在TCP进行三次握手,或者四次挥手的过程中,通信的服务器和客户端内部会发送状态上的变化,发生的状态变化在程序中是看不到的,这个状态的变化也不需要程序猿去维护,但是在某些情况下进行程序的调试会去查看相关的状态信息,先来看三次握手过程中的状态转换。

- 在第一次握手之前,服务器端必须先启动,并且已经开始了监听
- - 服务器端先调用了 listen() 函数, 开始监听
- - 服务器启动监听前后的状态变化: 没有状态 ---> LISTEN
当服务器监听启动之后,由客户端发起的三次握手过程中状态转换如下:
第一次握手:
第二次握手:
第三次握手:
三次握手完成之后,客户端和服务器都变成了同一种状态,这种状态叫:ESTABLISHED,表示双向连接已经建立, 可以通信了。在通过过程中,正常的通信状态就是 ESTABLISHED。
关于四次挥手对于客户端和服务器哪段先断开连接没有要求,根据实际情况处理即可。下面根据上图中的实例描述一下四次挥手过程中TCP的状态转换(上图中主动断开连接的一方是客户端):
第一次挥手:
第二次挥手:
第三次挥手:
第四次挥手:
在下图中同样是描述TCP通信过程中的客户端和服务器端的状态转,看起来比较乱,其实只需要看两条主线:红色实线和绿色虚线。关于黑色的实线对应的是一些特殊情况下的状态切换,在此不做任何分析。
因为三次握手是由客户端发起的,据此分析红色的实线表示的客户端的状态,绿色虚线表示的是服务器端的状态。

客户端:
服务器端:
在TCP通信的时候,当主动断开连接的一方接收到被动断开连接的一方发送的FIN和最终的ACK后(第三次挥手完成),连接的主动关闭方必须处于TIME_WAIT状态并持续2MSL(Maximum Segment Lifetime)时间,这样就能够让TCP连接的主动关闭方在它发送的ACK丢失的情况下重新发送最终的ACK。
一倍报文寿命(MSL)大概时长为30s,因此两倍报文寿命一般在1分钟作用。
主动关闭方重新发送的最终ACK,是因为被动关闭方重传了它的FIN。事实上,被动关闭方总是重传FIN直到它收到一个最终的ACK。
- $ netstat 参数
- $ netstat -apn | grep 关键字
TCP连接只有一方发送了FIN,另一方没有发出FIN包,仍然可以在一个方向上正常发送数据,这中状态可以称之为半关闭或者半连接。当四次挥手完成两次的时候,就相当于实现了半关闭,在程序中只需要在某一端直接调用 close() 函数即可。套接字通信默认是双工的,也就是双向通信,如果进行了半关闭就变成了单工,数据只能单向流动了。比如下面的这个例子:
服务器端:
客户端:
按照上述流程做了半关闭之后,从双工变成了单工,数据单向流动的方向: 客户端 —–> 服务器端。
- // 专门处理半关闭的函数
- #include
- // 可以有选择的关闭读/写, close()函数只能关闭写操作
- int shutdown(int sockfd, int how);
在网络通信中,一个端口只能被一个进程使用,不能多个进程共用同一个端口。我们在进行套接字通信的时候,如果按顺序执行如下操作:先启动服务器程序,再启动客户端程序,然后关闭服务器进程,再退出客户端进程,最后再启动服务器进程,就会出如下的错误提示信息:bind error: Address already in use
- # 第二次启动服务器进程
- $ ./server
- bind error: Address already in use
-
- $ netstat -apn|grep 9999
- (Not all processes could be identified, non-owned process info
- will not be shown, you would have to be root to see it all.)
- tcp 0 0 127.0.0.1:9999 127.0.0.1:50178 TIME_WAIT -
-
通过netstat查看TCP状态,发现上一个服务器进程其实还没有真正退出。因为服务器进程是主动断开连接的进程, 最后状态变成了 TIME_WAIT状态,这个进程会等待2msl(大约1分钟)才会退出,如果该进程不退出,其绑定的端口就不会释放,再次启动新的进程还是使用这个未释放的端口,端口被重复使用,就是提示bind error: Address already in use这个错误信息。
如果想要解决上述问题,就必须要设置端口复用,使用的函数原型如下:
- // 这个函数是一个多功能函数, 可以设置套接字选项
- int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
这个函数应该添加到服务器端代码中,具体应该放到什么位置呢?答:在绑定之前设置端口复用
参考代码如下:
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- // server
- int main(int argc, const char* argv[])
- {
- // 创建监听的套接字
- int lfd = socket(AF_INET, SOCK_STREAM, 0);
- if(lfd == -1)
- {
- perror("socket error");
- exit(1);
- }
-
- // 绑定
- struct sockaddr_in serv_addr;
- memset(&serv_addr, 0, sizeof(serv_addr));
- serv_addr.sin_family = AF_INET;
- serv_addr.sin_port = htons(9999);
- serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地多有的IP
- // 127.0.0.1
- // inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
-
- // 设置端口复用
- int opt = 1;
- setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
- // 绑定端口
- int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
- if(ret == -1)
- {
- perror("bind error");
- exit(1);
- }
-
- // 监听
- ret = listen(lfd, 64);
- if(ret == -1)
- {
- perror("listen error");
- exit(1);
- }
-
- fd_set reads, tmp;
- FD_ZERO(&reads);
- FD_SET(lfd, &reads);
-
- int maxfd = lfd;
-
- while(1)
- {
- tmp = reads;
- int ret = select(maxfd+1, &tmp, NULL, NULL, NULL);
- if(ret == -1)
- {
- perror("select");
- exit(0);
- }
-
- if(FD_ISSET(lfd, &tmp))
- {
- int cfd = accept(lfd, NULL, NULL);
- FD_SET(cfd, &reads);
- maxfd = cfd > maxfd ? cfd : maxfd;
- }
- for(int i=lfd+1; i<=maxfd; ++i)
- {
- if(FD_ISSET(i, &tmp))
- {
- char buf[1024];
- int len = read(i, buf, sizeof(buf));
- if(len > 0)
- {
- printf("client say: %s\n", buf);
- write(i, buf, len);
- }
- else if(len == 0)
- {
- printf("客户端断开了连接\n");
- FD_CLR(i, &reads);
- close(i);
- }
- else
- {
- perror("read");
- exit(0);
- }
- }
- }
- }
-
- return 0;
- }