本文主要是网络编程——网络属性的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
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> #include <sys/socket.h> int getsockopt (int sockfd, int level, int optname, void *optval, socklen_t *optlen) ;
【函数说明】 该函数可以获取 某个socket
套接字的属性选项。
【函数参数】
sockfd
:int
类型,表示需要获取选项的套接字。
level
:int
类型,选项所在的协议层。
点击查看 level 可取的值
SOL_SOCKET 通用套接字选项 IPPROTO_IP IP选项 IPPROTO_TCP TCP选项
optname
:int
类型,表示要获取的选项名称。
点击查看 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_OPTINOS IP首部选项 int IP_TOS 服务类型 IP_TTL 生存时间 int IPPRO_TCP TCP_MAXSEG TCP最大数据段的大小 int TCP_NODELAY 不使用Nagle算法 int
optval
:void
类型指针变量,获取的套接字选项,需要根据选项名称的数据类型进行强制类型转换。
optlen
:socklen_t
类型指针变量,表示optval
的长度,要注意需要传入的是一个地址。
【返回值】 int
类型,成功返回0
,失败返回-1
,并设置errno
表示错误类型。
【使用格式】 一般使用格式如下:
1 2 3 4 5 6 7 8 9 #include <sys/types.h> #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> #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 # 执行可执行程序
然后,终端会有以下信息显示:
2. setsockopt()
函数 2.1 函数说明 在linux
下可以使用man 2 setsockopt
命令查看该函数的帮助手册。
1 2 3 4 5 6 #include <sys/types.h> #include <sys/socket.h> int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ;
【函数说明】 该函数可以设置 某个socket
套接字的属性选项。
【函数参数】
sockfd
:int
类型,表示需要设置选项的套接字。
level
:int
类型,选项所在的协议层。
点击查看 level 可取的值
SOL_SOCKET 通用套接字选项 IPPROTO_IP IP选项 IPPROTO_TCP TCP选项
optname
:int
类型,表示要设置的选项名称。
点击查看 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_OPTINOS IP首部选项 int IP_TOS 服务类型 IP_TTL 生存时间 int IPPRO_TCP TCP_MAXSEG TCP最大数据段的大小 int TCP_NODELAY 不使用Nagle算法 int
optval
:void
类型指针变量,设置的套接字选项,需要根据选项名称的数据类型进行强制类型转换。
optlen
:socklen_t
类型,表示optval
的长度,要注意需要传入的是一个变量。
【返回值】 int
类型,成功返回0
,失败返回-1
,并设置errno
表示错误类型。
【使用格式】 一般使用格式如下:
1 2 3 4 5 6 7 8 #include <sys/types.h> #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> #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 # 执行可执行程序
然后,终端会有以下信息显示:
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) ;
【函数说明】 该函数可以用于获取与某个套接字关联的本地协议地址 。
【函数参数】
sockfd
:int
类型,表示已经创建的socket
套接字。
addr
:struct sockaddr
类型的结构体指针变量,指向一个struct sockaddr
类型变量,该结构体中含有要绑定的IP
地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in
类型,具体原因后边前边介绍bind
函数的时候已经说过了。
addrlen
:socklen_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> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> 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 ; } 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()
函数传出的信息相同)。
【函数参数】
sockfd
:int
类型,表示已经创建的socket
套接字。
addr
:struct sockaddr
类型的结构体指针变量,指向一个struct sockaddr
类型变量,该结构体中含有要绑定的IP
地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in
类型,具体原因后边前边介绍bind
函数的时候已经说过了。
addrlen
:socklen_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> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> char ipv4_addr[16 ] = {};int port = -1 ;struct sockaddr_in cin ; 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 2 3 4 5 struct timeval { time_t tv_sec; suseconds_t tv_usec; };
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 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> void usage (char *str) ; int main (int argc, char *argv[]) { int count = 0 ; int port = -1 ; if (argc != 3 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); if (port < 5000 ) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct timeval tout ; tout.tv_sec = 2 ; tout.tv_usec = 0 ; setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &tout, sizeof (tout)); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("bind" ); exit (-1 ); } if (listen(socket_fd, 5 ) < 0 ) { perror("listen" ); exit (-1 ); } printf ("Server starting....OK!\n" ); int newfd = -1 ; struct sockaddr_in cin ; socklen_t addrlen = sizeof (cin ); if ((newfd = accept(socket_fd, (struct sockaddr *)&cin , &addrlen)) < 0 ) { perror("accept" ); exit (-1 ); } 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); int ret = -1 ; char buf[BUFSIZ]; char replay[BUFSIZ]; while (1 ) { printf ("count=%d\n" , ++count); 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); strcat (replay, buf); ret = send(newfd, replay, strlen (replay), 0 ); if (ret < 0 ) { perror("send" ); exit (-1 ); } if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Client is exiting!\n" ); break ; } } close(newfd); close(socket_fd); return 0 ; } 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 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> void usage (char *str) ; int main (int argc, char *argv[]) { 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 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } struct sockaddr_in sinClient ; bzero(&sinClient, sizeof (sinClient)); sinClient.sin_family = AF_INET; sinClient.sin_port = htons(portClient); if (inet_pton(AF_INET, argv[1 ], (void *)&sinClient.sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sinClient, sizeof (sinClient)) < 0 ) { perror("bind" ); exit (-1 ); } if (connect(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("connect" ); exit (-1 ); } printf ("Client staring...OK!\n" ); int ret = -1 ; char buf[BUFSIZ]; char replay[BUFSIZ]; while (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); ret = recv(socket_fd, replay, BUFSIZ, 0 ); if (ret < 0 ) { perror("recv" ); exit (-1 ); } printf ("server replay:%s" , replay); if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Client is exiting!\n" ); break ; } } close(socket_fd); return 0 ; } 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 CC = gcc DEBUG = -g -O2 -Wall CFLAGS += $(DEBUG) TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} all : $(TARGET_LIST) %.o : %.c $(CC) $(CFLAGS) -c $< -o $@ .PHONY : all clean clean_o clean_outclean : clean_o clean_out @rm -vf $(TARGET_LIST) clean_o : @rm -vf *.o clean_out : @rm -vf *.out
2.2.4 测试结果 我们执行以下命令编译链接程序,生成两个可执行文件:
然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序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
然后我们就会看到如下现象:
由于我们设置了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
套接字选项 使用TCP
的keepalive
机制,其实在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
客户端连接正常,返回一个ACK
。server
端收到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. SO_KEEPALIVE
的使用 一般来说,我们可以通过设置网络属性来使用SO_KEEPALIVE
参数完成“心跳检测”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int keepAlive = 1 ; int keepIdle = 5 ; int keepInterval = 5 ; int keepCount = 3 ; 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)); }