linux TCP状态转换 半关闭 shutdown netstat 端口复用 setsockopt
黑色为异常的信息;
红色线为客户端;
绿色虚线为服务器端;
报文段寿命:
2MSL(Maximum Segment Lifetime)
主动断开连接的一方,最后进出入一个 TIME_WAIT状态,这个状态会持续: 2msl
msl:官方建议:2分钟,实际是 30s
为什么四次挥手的同意客户端的断开(第二次挥手)和服务器的请求断开(第三次挥手)为什么分开发送?
因为同意客户端断开,但服务器端不一定断开,第二次挥手和第三次挥手期间还可以发送数据,可能相隔一段时间;
为什么用2MSL?(双方都可以主动申请断开连接)
因为第三次挥手的后,第四次挥手的信息服务端 可能 没有接收到第四次挥手的ACK信息,未收到ACK后,再次发送FIN,直到收到ACK为止,2次MSL为了确保收到第四次挥手的ACK。
当TCP连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK后,连接的主动关闭方必须处于 TIME_WAIT 状态并持续 2MSL时间
这样就能够让TCP连接的主动关闭方在它发送的ACK丢失的情况下重新发送最终的ACK
主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN 。
事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
半关闭:
当TCP连接中 A 向 B 发送 FIN请求关闭,另一端 B 回应 ACK 之后(A端进入 FIN_WAIT 状态),并没有立即发送 FIN 给 A,,A处于半连接状态(半开关),此时A可以接收B发送的数据,但是A已经不能再向B发送数据。
从程序的角度看,可以使用 API 来控制实现半连接状态:
#include <sys/socket.h> int shutdown(int sockfd, int how); /* sockfd : 需要关闭的socket的描述符 how : 允许为 shutdown 操作选择以下几种方式: SHUT_RD(0): 关闭 sockfd上的读功能, 此选项不允许sockfd进行读操作 该套接字不再接收数据, 任何当前在套接字接收缓冲区的数据将被无声的丢弃掉 SHUT_WR(1): 关闭sockfd的写功能, 此选项不允许sockfd进行写操作, 进程不能在对此套接字发出写操作 SHUT_RDWR(2): 关闭sockfd的读写功能, 相当于调用 shutdown 两次, 首先是 SHUT_RD, 然后是 SHUT_WR */
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。 shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
1. 如果有多个进程共享一个套接字, close 每被调用一次,计数减1, 直到计数为0时,也就是所有进程都调用了 close,套接字将被释放。
2. 在多进程中如果一个进程调用了 shutdown(sfd,SHUT_RDWR)后,其他的进程将无法进行通信。但如果一个进程 close(sfd) 将不会影响到其他进程。
客户端:
1 #include <stdio.h> 2 #include <arpa/inet.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <string.h> 6 7 int main() { 8 9 // 创建socket 10 int fd = socket(PF_INET, SOCK_STREAM, 0); 11 if(fd == -1) { 12 perror("socket"); 13 return -1; 14 } 15 16 struct sockaddr_in seraddr; 17 inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr); 18 seraddr.sin_family = AF_INET; 19 seraddr.sin_port = htons(9999); 20 21 // 连接服务器 22 int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr)); 23 24 if(ret == -1){ 25 perror("connect"); 26 return -1; 27 } 28 29 while(1) { 30 char sendBuf[1024] = {0}; 31 fgets(sendBuf, sizeof(sendBuf), stdin); 32 33 write(fd, sendBuf, strlen(sendBuf) + 1); 34 35 // 接收 36 int len = read(fd, sendBuf, sizeof(sendBuf)); 37 if(len == -1) { 38 perror("read"); 39 return -1; 40 }else if(len > 0) { 41 printf("read buf = %s\n", sendBuf); 42 } else { 43 printf("服务器已经断开连接...\n"); 44 break; 45 } 46 } 47 48 close(fd); 49 50 return 0; 51 }
服务器端:
1 #include <stdio.h> 2 #include <ctype.h> 3 #include <arpa/inet.h> 4 #include <unistd.h> 5 #include <stdlib.h> 6 #include <string.h> 7 8 int main(int argc, char *argv[]) { 9 10 // 创建socket 11 int lfd = socket(PF_INET, SOCK_STREAM, 0); 12 13 if(lfd == -1) { 14 perror("socket"); 15 return -1; 16 } 17 18 struct sockaddr_in saddr; 19 saddr.sin_family = AF_INET; 20 saddr.sin_addr.s_addr = INADDR_ANY; 21 saddr.sin_port = htons(9999); 22 23 //int optval = 1; 24 //setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); 25 //端口复用,断开服务器,可以重新启动 server 服务端,正常显示****** 26 int optval = 1; 27 setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); 28 29 // 绑定 30 int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr)); 31 if(ret == -1) { 32 perror("bind"); 33 return -1; 34 } 35 36 // 监听 37 ret = listen(lfd, 8); 38 if(ret == -1) { 39 perror("listen"); 40 return -1; 41 } 42 43 // 接收客户端连接 44 struct sockaddr_in cliaddr; 45 socklen_t len = sizeof(cliaddr); 46 int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len); 47 if(cfd == -1) { 48 perror("accpet"); 49 return -1; 50 } 51 52 // 获取客户端信息 53 char cliIp[16]; 54 inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp)); 55 unsigned short cliPort = ntohs(cliaddr.sin_port); 56 57 // 输出客户端的信息 58 printf("client's ip is %s, and port is %d\n", cliIp, cliPort ); 59 60 // 接收客户端发来的数据 61 char recvBuf[1024] = {0}; 62 while(1) { 63 int len = recv(cfd, recvBuf, sizeof(recvBuf), 0); 64 if(len == -1) { 65 perror("recv"); 66 return -1; 67 } else if(len == 0) { 68 printf("客户端已经断开连接...\n"); 69 break; 70 } else if(len > 0) { 71 printf("read buf = %s\n", recvBuf); 72 } 73 74 // 小写转大写 75 for(int i = 0; i < len; ++i) { 76 recvBuf[i] = toupper(recvBuf[i]); 77 } 78 79 printf("after buf = %s\n", recvBuf); 80 81 // 大写字符串发给客户端 82 ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0); 83 if(ret == -1) { 84 perror("send"); 85 return -1; 86 } 87 } 88 89 close(cfd); 90 close(lfd); 91 92 return 0; 93 }
查看网络相关信息的命令:
netstat:
参数:
-a 所有的 socket
-p 显示正在使用 socket 的程序的名称
-n 直接使用 IP地址,而不通过域名服务器
监听的socket 和 通信的 socket(established)
此时先关闭服务器端:
关闭客户端:
关闭服务器端和客户端后,重新启动服务器端,显示端口号被使用
需要等待 2MSL(60秒),后端口号被释放,方可重新使用。这期间此端口号不能使用,因此需要端口复用。
端口复用:
端口复用最常用的用途是:
⚪ 防止服务器重启时之前绑定的端口还未释放
⚪ 程序突然退出而系统没有释放端口
#include <sys/types.h> #include <sys/socket.h> //设置套接字的属性(不仅仅能设置端口复用) int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 参数: - sockfd : 要操作的文件描述符 - level : 级别 - SQL_SOCKET(端口复用的级别) - optname : 选项的名称 - SO_REUSEADDR - SO_REUSEPORT - optval : 端口复用的值(整型) - 1 : 可以复用 - 0 : 不可以复用 - optlen : optval参数的大小 端口复用,设置的时机是在服务器绑定端口之前 setsockopt(); bind();
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)