环境:Linux C
一、协议介绍
TCP是面向连接的协议,提供可靠的数据传输;TCP协议的可靠传输基于三次握手、四次挥手以及确认重传机制实现。下面来具体展示下TCP的三次握手、四次挥手状态
大家都知道已经建立连接的TCP遇到网络丢包会有确认重传机制。在三次握手期间,如果A收到B的SYN+ACK,但是B没有收到A返回的ACK,此时B超时后会重传SYN+ACK,如果超过特定次数依然没有收到A的ACK,那么B向A发送RST包,关闭连接,避免A维护一个异常的连接。四次挥手也是类似,都是利用超时重传机制。
二、TCP网络编程
先展示一段代码
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> int main(int argc, char *argv[]) { int fd; struct sockaddr_in addr; struct timeval timeo = {3, 0}; //lingger参数1表示启用linger,5最多等待5秒或者socket发送队列中的消息发送完毕 //再执行close操作 struct linger linger_value = {1, 5}; socklen_t len = sizeof(timeo); //创建TCP连接,SOCK_CLOEXEC指明在执行fork等创建子进程函数时,子进程将继承的该fd描述符默认关闭 fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (argc == 4) timeo.tv_sec = atoi(argv[3]); //设置接受socket数据的等待时间,对read,recv,recvfrom等函数生效 setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, len); //设置该参数是为了保证在关闭前尽可能的将数据都发送出去 setsockopt(fd, SOL_SOCKET, SO_LINGER, &linger_value, (socklen_t)sizeof(linger_value)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(argv[1]); addr.sin_port = htons(atoi(argv[2])); //connect受SO_SNDTIMEO设置影响,最多等待timeo时间 if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { if (errno == EINPROGRESS) { fprintf(stderr, "timeout\n"); return -1; } perror("connect"); return -1; } printf("connected\n"); char *msg = "hello buddy !"; //这里指定MSG_NOSIGNAL表示当fd已经关闭时,不会触发SIGPIPE信号(该信号的默认操作是关闭程序 send(fd, msg, strlen(msg), MSG_NOSIGNAL); char buffer[1024] = {'\0'}; //这里的MSG_DONTWAIT就是要求recv函数非阻塞执行 ,同给socket设置SO_NONBLOCK效果一样 recv(fd, buffer, sizeof(buffer) - 1, MSG_NOSIGNAL | MSG_DONTWAIT); //向对端发送FIN报文,关闭该fd上所有的读写socket连接 shutdown(SHUT_RDWR, fd); //关闭文件描述符,如果执行了fork函数且没有设置SOCK_CLOEXEC,需要在子进程中也执行close关闭继承的文件描述符 close(fd); return 0; }
注:该代码是客户端的一部分代码,服务端与这个类似,区别就是服务端用bind、listen、accept函数替代了connect函数来实现对服务端口的监听、新连接的处理。
- 编写多进程程序时,如果创建文件描述符(打开文件、创建socket等)都需要设置SOCK_CLOEXEC标识,避免出现副作用。
- 给socket设置SO_LINGER选项、调用close之前执行shutdown命令一般用于服务端,这样可以最大限度地保证将数据发送到客户端并且可以有效防止,因为fork导致的子进程在处理socket时调用close关闭fd描述符,由于fd原有计数是2,所以fd并未实际关闭,也就不会触发向客户端发送FIN报文结束连接的动作。因而,服务端就可能出现大量的CLOSE_WAIT的连接。
- 如果系统收到FIN报文,会让recv、read等函数返回0表示连接已经关闭。此时send、recv等读写函数再对目标fd进行读写,根据TCP协议规定,会收到一个RST报文,如果再读写的话系统就会发送SIGPIPE信号给调用进程,而该信号默认动作是结束进程。因此,要么调用信号处理函数将SIGPIPE捕获处理,要么像代码中那样设置MSG_NOSIGNAL选项避免产生SIGPIPE信号。
- SO_SNDTIMEO除了对send、write等有效外,还对connect有效;SO_RCVTIMEO除了对recv、read等有效外还对accept有效。
- 如果系统中出现大量的close_wait状态的TCP连接,说明是程序在收到对端的FIN报文后没有关闭相关的socket(有可能是fork这类函数造成的),这需要检查结束连接处理部分的代码。
- 如果系统中出现大量的time_wait状态的TCP连接,这种连接是正常的。出现这种连接都是由于主动关闭socket造成的,解决方案就是改成客户端主动结束连接、或者降低time_wait的时间。