LV06-03-网络编程-09-网络属性

本文主要是网络编程——网络属性的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、网络属性

记得前边我们使用过setsockopt函数设置过允许地址重用,终于到了详细学习该函数的部分了。

1. getsockopt()函数

1.1 函数说明

linux下可以使用man 2 getsockopt命令查看该函数的帮助手册。

1
2
3
4
5
6
/* 需包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* 函数声明 */
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

【函数说明】该函数可以获取某个socket套接字的属性选项。

【函数参数】

  • sockfdint类型,表示需要获取选项的套接字。
  • levelint类型,选项所在的协议层。
点击查看 level 可取的值
SOL_SOCKET通用套接字选项
IPPROTO_IPIP选项
IPPROTO_TCPTCP选项
  • optnameint类型,表示要获取的选项名称。
点击查看 optname 常用可取的值

我们可以使用如下命令查看这些选项:

1
2
3
4
man 7 socket # 打开手册后查看 Socket options 部分
man 7 ip # 打开手册后查看 Socket options 部分
man 7 ipv6 # 打开手册后查看 Socket options 部分
man 7 tcp # 打开手册后查看 Socket options 部分

常用选项如下:

选项名称说明数据类型
SOL_SOCKET
SO_BROADCAST允许发送广播数据int
SO_DEBUG允许调试int
SO_DONTROUTE不查找路由int
SO_ERROR获得套接字错误int
SO_KEEPALIVE保持连接int
SO_LINGER延迟关闭连接struct linger
SO_OOBINLINE带外数据放入正常数据流 int
SO_RCVBUF接收缓冲区大小int
SO_SNDBUF发送缓冲区大小int
SO_RCVLOWAT接收缓冲区下限int
SO_SNDLOWAT发送缓冲区下限int
SO_RCVTIMEO接收超时struct timeval
SO_SNDTIMEO发送超时struct timeval
SO_REUSEADDR 允许重用本地地址和端口 int
SO_TYPE获得套接字类型int
SO_BSDCOMPAT与BSD系统兼容int
IPPROTO_IP
IP_HDRINCL在数据包中包含IP首部int
IP_OPTINOSIP首部选项int
IP_TOS服务类型
IP_TTL生存时间int
IPPRO_TCP
TCP_MAXSEGTCP最大数据段的大小int
TCP_NODELAY不使用Nagle算法int
  • optvalvoid类型指针变量,获取的套接字选项,需要根据选项名称的数据类型进行强制类型转换。
  • optlensocklen_t 类型指针变量,表示optval的长度,要注意需要传入的是一个地址。

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

【使用格式】一般使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

/* 至少应该有的语句 */
int sendBuff;
socklen_t optLen = sizeof(int);
int socket_fd = socket_fd = socket(AF_INET, SOCK_STREAM, 0);
getsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, (int *)&sendBuff, &optLen);

【注意事项】none

1.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int main(int argc, char *argv[])
{
int sendBuff;
socklen_t optLen = sizeof(int);
int socket_fd = socket_fd = socket(AF_INET, SOCK_STREAM, 0);
getsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, (int *)&sendBuff, &optLen);
printf("sendBuff length: %dKB\n", sendBuff / 1024);
return 0;
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
sendBuff length: 16KB

2. setsockopt()函数

2.1 函数说明

linux下可以使用man 2 setsockopt命令查看该函数的帮助手册。

1
2
3
4
5
6
/* 需包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* 函数声明 */
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

【函数说明】该函数可以设置某个socket套接字的属性选项。

【函数参数】

  • sockfdint类型,表示需要设置选项的套接字。
  • levelint类型,选项所在的协议层。
点击查看 level 可取的值
SOL_SOCKET通用套接字选项
IPPROTO_IPIP选项
IPPROTO_TCPTCP选项
  • optnameint类型,表示要设置的选项名称。
点击查看 optname 常用可取的值

我们可以使用如下命令查看这些选项:

1
2
3
4
man 7 socket # 打开手册后查看 Socket options 部分
man 7 ip # 打开手册后查看 Socket options 部分
man 7 ipv6 # 打开手册后查看 Socket options 部分
man 7 tcp # 打开手册后查看 Socket options 部分

常用选项如下:

选项名称说明数据类型
SOL_SOCKET
SO_BROADCAST允许发送广播数据int
SO_DEBUG允许调试int
SO_DONTROUTE不查找路由int
SO_ERROR获得套接字错误int
SO_KEEPALIVE保持连接int
SO_LINGER延迟关闭连接struct linger
SO_OOBINLINE带外数据放入正常数据流 int
SO_RCVBUF接收缓冲区大小int
SO_SNDBUF发送缓冲区大小int
SO_RCVLOWAT接收缓冲区下限int
SO_SNDLOWAT发送缓冲区下限int
SO_RCVTIMEO接收超时struct timeval
SO_SNDTIMEO发送超时struct timeval
SO_REUSEADDR 允许重用本地地址和端口 int
SO_TYPE获得套接字类型int
SO_BSDCOMPAT与BSD系统兼容int
IPPROTO_IP
IP_HDRINCL在数据包中包含IP首部int
IP_OPTINOSIP首部选项int
IP_TOS服务类型
IP_TTL生存时间int
IPPRO_TCP
TCP_MAXSEGTCP最大数据段的大小int
TCP_NODELAY不使用Nagle算法int
  • optvalvoid类型指针变量,设置的套接字选项,需要根据选项名称的数据类型进行强制类型转换。
  • optlensocklen_t 类型,表示optval的长度,要注意需要传入的是一个变量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

【使用格式】一般使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

/* 至少应该有的语句 */
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

【注意事项】none

2.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int main(int argc, char *argv[])
{
int b_reuse;
socklen_t optLen = sizeof(int);
int socket_fd = socket_fd = socket(AF_INET, SOCK_STREAM, 0);
getsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (int *)&b_reuse, &optLen);
printf("b_reuse: %d\n", b_reuse);
int a_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &a_reuse, sizeof (int));
getsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (int *)&b_reuse, &optLen);
printf("b_reuse: %d\n", b_reuse);
return 0;
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
b_reuse: 0
b_reuse: 1

3. getsockname()函数

3.1 函数说明

linux下可以使用man 2 getsockname命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/socket.h>

/* 函数声明 */
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

【函数说明】该函数可以用于获取与某个套接字关联的本地协议地址

【函数参数】

  • sockfdint类型,表示已经创建的socket套接字。

  • addrstruct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边前边介绍bind函数的时候已经说过了。

  • addrlensocklen_t类型指针变量,用于指定addr所指向的结构体对应的字节长度。

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

【使用格式】一般使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 需要包含的头文件 */
#include <stdio.h> /* perror scanf printf */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/

/* 至少应该有的语句 */

char ipv4_addr[16] = {};
int port = -1;

int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin;
socklen_t addrlen = sizeof(sin);

if( getsockname(scoket_fd, (struct sockaddr *)&sin, &addrlen) < 0 )
{
perror("[error]getsockname");
return -1;
}
/* 获取本地IP */
if (!inet_ntop(AF_INET, (void *)&sin.sin_addr, ipv4_addr, sizeof(sin)))
{
perror("[error]inet_ntop");
return -1;
}
/* 获取本地端口 */
port = ntohs(sin.sin_port);

【注意事项】none

3.2 使用实例

暂无

4. getpeername()函数

4.1 函数说明

linux下可以使用man 2 getpeername命令查看该函数的帮助手册。

1
2
3
4
5
6
/* 需包含的头文件 */
#include <sys/socket.h>

/* 函数声明 */
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

【函数说明】该函数可以用于获取与某个套接字关联的对端协议地址(如果在服务器端调用的话,可以获取客户端的IP和端口,与accept()函数传出的信息相同)。

【函数参数】

  • sockfdint类型,表示已经创建的socket套接字。

  • addrstruct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边前边介绍bind函数的时候已经说过了。

  • addrlensocklen_t类型指针变量,用于指定addr所指向的结构体对应的字节长度。

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

【使用格式】一般使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 需要包含的头文件 */
#include <stdio.h> /* perror scanf printf */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/

/* 至少应该有的语句 */
char ipv4_addr[16] = {};
int port = -1;
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(getpeername(accept_fd, (struct sockaddr *)&cin, &addrlen) < 0)
{
perror("[error]getsockname");
return -1;
}

if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
{
perror("[error]inet_ntop");
return -1;
}
port = ntohs(cin.sin_port);

【注意事项】none

4.2 使用实例

暂无

二、网络超时检测

1. 为什么要超时检测?

在网络通信中,很多操作会使得进程阻塞,比如前边TCP套接字中的accept()/connect()/recv()UDP套接字中的recvfrom()等,进程不能无限制阻塞吧,所以我们就需要进行超时检测,进行超时检测的必要性如下:

  • 避免进程在没有数据时无限制地阻塞。

  • 当设定的时间到时,进程从原操作返回继续运行。

2. socket属性设置超时

2.1 选项与结构体

我们上边学习了socket选项的获取和设置,在里边我们可以设置超时时间,此时相关的选项如下:

选项名称说明数据类型
SO_RCVTIMEO接收超时struct timeval
SO_SNDTIMEO发送超时struct timeval

其中,struct timeval结构体其实我们前边已经学习过了,我们可以使用man struct timeval命令查看帮助手册,使用该命令后输入以下搜索标记:

1
/struct timeval <enter>

然后我们便会搜索到该结构体的成员说明:

1
2
3
4
5
struct timeval
{
time_t tv_sec; /* Seconds */
suseconds_t tv_usec; /* Microseconds */
};

2.2 使用实例

2.2.1 server服务器端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——socket超时检测(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
int count = 0;
/* 1.参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 2.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror ("socket");
exit(-1);
}
/* 3.socket属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 3.2 设置超时时间 */
struct timeval tout;
tout.tv_sec = 2; /* 设置2秒时间超时 */
tout.tv_usec = 0;
setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &tout, sizeof(tout)); /* 设置接收超时 */

/* 4.将套接字与指定端口号和IP进行绑定 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 4.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*5.调用listen()把主动套接字变成被动套接字 */
if (listen(socket_fd, 5) < 0)
{
perror("listen");
exit(-1);
}
printf ("Server starting....OK!\n");
/*6.阻塞等待客户端连接请求 */
int newfd = -1;
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
{
perror("accept");
exit(-1);
}
/* 7.打印成功连接的客户端的信息(此处只能打印一个) */
char ipv4_addr[16];
if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
{
perror ("inet_ntop");
exit(-1);
}

printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);

/* 8.数据读写 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
while(1)
{
printf("count=%d\n", ++count);
/* 8.1接收来自客户端的数据 */
bzero(buf, BUFSIZ);
bzero(replay, BUFSIZ);
do{
ret = read(newfd, buf, BUFSIZ - 1);
}while(ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
continue;
}
if(!ret) break; /* 对方已经关闭 */
printf("Receive data: %s", buf);
/* 8.2对客户端做出应答 */
strcat(replay, buf);
ret = send(newfd, replay, strlen(replay), 0);
if(ret < 0)
{
perror("send");
exit(-1);
}
/* 8.3判断是否需要退出 */
if(!strncasecmp(buf, "quit", strlen("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 9.关闭文件描述符 */
close(newfd);
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

2.2.2 client客户端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——socket超时检测(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */

#include <sys/types.h> /* socket connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h> /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
int port = -1;
int portClient = -1;
if (argc != 4)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
portClient = atoi(argv[3]);
if (port < 5000 || portClient < 5000 || (port == portClient))
{
usage(argv[0]);
exit(-1);
}

/* 2.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 3.socket属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 4.连接服务器 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 5.设置客户端发送接收数据的端口 */
/* 5.1客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 5.2绑定固定的IP 和 端口号 */
struct sockaddr_in sinClient;
bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
sinClient.sin_family = AF_INET; /* 协议族 */
sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 5.3绑定 */
if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
{
perror("bind");
exit(-1);
}

/* 6.连接 */
if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("connect");
exit(-1);
}
printf("Client staring...OK!\n");
/* 7.数据读写 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
while (1)
{
/* 7.1发送数据 */
bzero (buf, BUFSIZ);
bzero (replay, BUFSIZ);
printf(">");
if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
{
continue;
}
do{
ret = write(socket_fd, buf, strlen(buf));
}while (ret < 0 && EINTR == errno);
/* 7.2接收服务器的回馈数据 */
ret = recv(socket_fd, replay, BUFSIZ, 0);
if(ret < 0)
{
perror("recv");
exit(-1);
}
printf("server replay:%s", replay);
/* 7.3判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 8.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

2.2.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

点击查看 Makefile 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

clean_out :
@rm -vf *.out

2.2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

1
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

1
2
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall server.c -o server

对于服务器端,我们执行以下命令启动服务器进程:

1
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

1
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后我们就会看到如下现象:

image-20220705141116835

由于我们设置了2s超时,所以在2s内客户端没有连接的话,accept()就会返回,并且显示错误信息,当我们2s内连接客户端后,没有数据从客户端发送到服务器时,每2s就会超时一次,而原本会一直阻塞在recv()函数。设置了超时时候,依然可以正常接收数据。

3. 使用函数自身参数

一般来说,我们一般会使用select()poll()或者epoll_wait()来查询某个socket文件描述符是否可读或者可写,而这函数一般是带有超时时间的,前边介绍这些函数的时候也有过说明,例如:

1
2
3
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

4. 定时器中接收

还有一种方式就是在定时器中接收数据,因为定时器定时时间到了之后会产生SIGALRM信号,这样我们便可以自定义信号处理函数来接收数据,防止进程一直阻塞。

三、TCP客户端掉线了?

对于面向连接的TCP协议的 socket套接字,在实际应用中通常都要检测对端是否处于连接中,连接端口分两种情况:

  • (1)连接正常关闭,调用close()/shutdown()连接正常关闭,send()recv()等函数立马返回错误,select()函数返回SOCK_ERR;
  • (2)连接的对端异常关闭,比如网络断掉、突然断电等。

对于第一种情况,正常关闭的,就没啥好说的了,关于第二条,我们如何判断呢?我们了解一个概念吗,就是心跳检测,就像心跳一样客户端每隔几秒钟发送一个数据包(心跳包)给服务器,告诉服务器,客户端还在线。如果服务器在规定时间内没有收到客户端发来的心跳包,则认为客户端已经掉线。

1. 客户端异常掉线的判断

1.1 应用层自定义心跳包程序

自己编写心跳包程序,就是自己的程序加入一条线程,定时向对端发送数据包,查看是否有ACK应答数据包发回,根据ACK的返回情况来管理连接,如果在一定时间内没有收到对方的回应,即认为对方已经掉线。。此方法比较通用,一般使用业务层心跳处理,灵活可控,但会改变现有的协议。

1.2 TCP中使用SO_KEEPALIVE套接字选项

使用TCPkeepalive机制,其实在UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测。keepalive基本原理如下:

TCP内嵌有心跳包,以服务端为例,当server检测到超过一定时间tcp_keepalive_time,一般在linux中是7200s,也就是2h,我们使用以下命令可以查该时间:

1
cat /proc/sys/net/ipv4/tcp_keepalive_time

当没有数据传输的时候,会向client客户端发送一个keepalive packet,此时client端一般会有三种反应:

  • (1)client客户端连接正常,返回一个ACKserver端收到ACK后重置计时器,在2小时后再发送探测。如果2小时内连接上有数据传输,那么在该时间的基础上向后推延2小时发送探测包;
  • (2)client客户端异常关闭,或网络断开。client无响应,server收不到ACK,在一定时间tcp_keepalive_intvl(一般默认是75s)后重发keepalive packet,并且重发一定次数tcp_keepalive_probes(一般默认是9);
1
2
3
4
# 查看 tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
# 查看 tcp_keepalive_probes
cat /proc/sys/net/ipv4/tcp_keepalive_probes
  • (3)客户端曾异常关闭,但后来已经重启。server收到的探测响应是一个复位,server会端终止连接。

我们还可以使用如下命令查看上边三个值的设置情况:

1
sudo sysctl -a | grep keepalive

默认情况下应该会显示如下信息:

1
2
3
4
[sudo] hk 的密码: 
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200

【注意事项】

(1)根据 MSDN 的文档,如果为 socket 设置了KEEPALIVE选项,TCP/IP 栈在检测到对方掉线后, 任何在该 socket 上进行的调用(发送/接受调用)就会立刻返回,错误号是 WSAENETRESET ;同时,此后的任何在该socket句柄的调用会立刻失败,并返回WSAENOTCONN错误。

(2)SO_KEEPALIVE设置空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测。对于判断网络断开时间太长,对于需要及时响应的程序不太适应。当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口,关联完成端口的socket可能会忽略掉该套接字选项。

1.3 内核检测

  • 2.6内核里面的网卡驱动中,使能1s的周期性检查定时器

  • 网卡硬件或者我们通过GPIO,插拔网线时候产生中断,处理相应中断,这样的话可以立即检测到。

2. SO_KEEPALIVE的使用

一般来说,我们可以通过设置网络属性来使用SO_KEEPALIVE参数完成“心跳检测”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 主函数中应有的部分 */
int keepAlive = 1; /* 设定KeepAlive */
int keepIdle = 5; /* 开始首次KeepAlive探测前的TCP空闭时间 */
int keepInterval = 5; /* 两次KeepAlive探测间的时间间隔 */
int keepCount = 3; /* 判定断开前的KeepAlive探测次数 */
setKeepAlive(socket_fd, keepAlive, keepIdle, keepInterval, keepCount); /* 设置参数 */

/* 自定义函数 */
void setKeepAlive(int socket_fd, int attr_on, socklen_t idle_time, socklen_t interval, socklen_t cnt)
{
setsockopt(socket_fd, SOL_SOCKET, SO_KEEPALIVE, (const char *) &attr_on, sizeof(attr_on));
setsockopt(socket_fd, SOL_TCP, TCP_KEEPIDLE, (const char *) &idle_time, sizeof(idle_time));
setsockopt(socket_fd, SOL_TCP, TCP_KEEPINTVL, (const char *) &interval, sizeof(interval));
setsockopt(socket_fd, SOL_TCP, TCP_KEEPCNT, (const char *) &cnt, sizeof(cnt));
}