赞
踩
TCP 和 UDP -> 传输层的协议
UDP | TCP | |
---|---|---|
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠 | 可靠的 |
连接的对象个数 | 一对一、一对多、多对一、多对多 | 支持一对一 |
传输的方式 | 面向数据报 | 面向字节流 |
首部开销 | 8个字节 | 最少20个字节 |
适用场景 | 实时应用(视频会议,直播) | 可靠性高的应用(文件传输) |
TCP通信的流程:
服务器端(被动接受连接的角色)
创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符(读:接收数据;写:发送数据)
将这个监听文件描述符和本地的 IP 和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个 IP和端口
设置监听,监听的 fd开始工作
阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个用于和客户端通信的套接字(fd)
通信
- 接收数据
- 发送数据
通信结束,断开连接
客户端
创建一个用于通信的套接字(fd)
连接服务器,需要指定连接的服务器的 IP 和 端口
连接成功了,客户端可以直接和服务器通信
接收数据
发送数据
通信结束,断开连接
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> //包含了这个头文件,上面两个就可以省略 int socket(int domain, int type, int protocol); - 功能:创建一个套接字 - 参数: - domain:协议族 AF_INET : ipv4 AF_INET6 : ipv6 AF_UNIX, AF_LOCAL :本地套接字通信(本地进程间通信) - type:通信过程中使用的协议类型 SOCK_STREAM :流式协议 SOCK_DGRAM :报式协议 - protocol:具体的一个协议。 一般写 0: type = SOCK_STREAM :流式协议默认使用 TCP type = SOCK_DGRAM :报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败: -1 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //socket命名 - 功能:绑定,将 fd 和本地的 IP + 端口 进行绑定 - 参数: sockfd :通过 socket函数得到的文件描述符 addr :需要绑定的 socket地址,这个地址封装了 ip和端口号的信息 addrlen :第二个参数结构体占的内存大小 int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn -功能:监听这个 socket上的连接 -参数: sockfd :通过 socket()函数得到的文件描述符 backlog :未连接的和已经连接的和的最大值, eg. 5 (不能超过/proc/sys/net/core/somaxconn 中最大值) 底层维护了两个队列,分别为已连接和未连接的socket连接队列 当调用accept()后已连接的socket队列就会减一,速度很快,所以backlog值不用过大。 -返回值: success, zero is returned. error, -1 is returned, and errno is set appropriately. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); -功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 -参数: sockfd :用于监听的文件描述符 (在底层将客户端的地址信息(ip, port)读取出来) addr :传出参数,记录了连接成功后客户端的地址信息(ip,port) addrlen :指定第二个参数的对应的内存大小 -返回值: 成功: 用于通信的文件描述符 失败: -1 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); -功能:客户端连接服务器 -参数: sockfd: 用于和客户端通信的文件描述符 addr: 客户端要连接的服务器的地址信息 addrlen: 第二个参数的内存大小 -返回值:成功0,失败-1 ssize_t write(int fd,const void* buf, size_t count);//写数据 ssize_t read(int fd,void* buf, size_t count);//读数据
案例:实现TCP通信 服务端/客户端 并实现手动从客户端输入并从服务端返回相同内容(“回射服务器”)
(但是只能接受一个客户端通信)
// TCP通信 客户端 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main() { // 1. 创建套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); // 2. 连接服务器端 struct sockaddr_in serverAddr; serverAddr.sin_port = htons(9999); serverAddr.sin_family = AF_INET; inet_pton(AF_INET, "192.168.241.128", &serverAddr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if(ret == -1) { perror("connect"); exit(-1); } // 3. 通信 char recvBuf[1024] = {0}; char data[1024] = {0}; while(1) { // char * data = "hello, i am client"; memset(data, 0, 1024); // 将data内容清空 // 获取标准输入的数据 fgets(data, 1024, stdin); // 发送给客户端数据 write(fd, data, strlen(data)); // 获取服务器端的数据 memset(recvBuf, 0, 1024); // 将recvBuf内容清空 int len = read(fd, recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) printf("recv server data: %s\n", recvBuf); else if(len == 0) { // 表示服务器端断开连接 printf("server closed..."); break; } } // 关闭连接 close(fd); return 0; }
// TCP 通信的服务器端 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main() { // 1. 创建用于监听的套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("soclet"); exit(-1); } // 2. 绑定 对应的IP地址和端口号port struct sockaddr_in saddr; saddr.sin_family = PF_INET; // inet_pton(AF_INET, "192.168.241.128", &saddr.sin_addr.s_addr); // 计算机有多个网卡 无线网卡、以太网卡... // IP地址也都不同 // saddr.sin_addr.s_addr = 0; 表示所有网卡都绑定 客户端连接任何IP都可以访问到主机 saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0 saddr.sin_port = htons(9999); int ret = bind(lfd, (struct sockaddr *) &saddr, sizeof(saddr)); if(ret == -1) { perror("bind"); exit(-1); } // 3. 监听是否有客户端连接 ret = listen(lfd, 8); if(ret == -1) { perror("listen"); exit(-1); } // 4. 接收客户端连接 struct sockaddr_in clientaddr; socklen_t sock_len = sizeof(clientaddr); int cfd = accept(lfd, (struct sockaddr *) &clientaddr, &sock_len); if(cfd == -1) { perror("accept"); exit(-1); } // 5. 输出客户端信息 char clientIP[16]; inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf("client ip is %s, port is %d\n", clientIP, clientPort); // 5. // 获取客户端的数据 char recvBuf[1024] = {0}; while(1) { memset(recvBuf, 0, 1024); int len = read(cfd, recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) printf("recv client data: %s\n", recvBuf); else if(len == 0) { // 表示客户端断开连接 printf("client closed...\n"); break; } // char * data = "hello, i am server"; // 发送给客户端数据 write(cfd, recvBuf, strlen(recvBuf)); } // 关闭文件描述符 close(cfd); close(lfd); return 0; }
三次握手的目的:保证双方互相建立了连接
三次握手发生在客户端连接的时候,当调用connect()时,底层会通过TCP协议进行三次握手。
16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。
32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。
32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度。假设主机 A 和主机 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。
只有收到对方的报文段 ACK/FIN 其中一个标志位置1,自己发送时ack = 收到的 TCP 报文段的序号值 + 1
只有ACK置为1,对应的ack序号才有效。
4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示 15,所以 TCP 头部最长是60 字节。
6 位标志位包含如下几项:
URG 标志,表示紧急指针(urgent pointer)是否有效。
ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。
16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
第一次握手:
客户端将SYN标志置为1
生成一个随机的32位序号seq = J
(这个序号后面可以显示携带数据的大小 eg. seq = 1001(100) -> seq = 1101)
第二次握手
第三次握手
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。
窗口理解为缓冲区的大小。
滑动窗口的大小会随着发送数据和接收数据而变化。
通信的双方都有发送缓冲区和接收数据的缓冲区。
# mss:Maximum Segment Size(一条数据(报文段)的最大数据量)
# win:滑动窗口的大小 单位:字节
1. 客户端向服务器发起连接,客户端的滑动窗口是4096B,一次发送的报文段最大数据量是1460B
2. 服务器端接受连接情况,告诉客户端 服务器端的窗口大小是6144B,一次发送的报文段最大数据量是1024B
3. 第三次握手...
4-9. 客户端连续给服务器端发送了6KB的数据,每次报文段大小为1024B
10. 服务器端告诉客户端:发送的6K数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了2KB,窗口大小是2KB
11. 服务器端告诉客户端:发送的6K数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了4KB,窗口大小是4KB
12. 客户端给服务器端发送了1024B的数据
13. 第一次挥手,客户端主动请求和服务器断开连接 FIN = 1,并且给服务器端发送了1KB的数据
14. 第二次挥手,服务器端回复ACK 8194, (1) 确认断开连接的请求 (2) 告诉客户端已经接受到刚才发的2KB的数据 (3) 滑动窗口大小是2KB
15-16. 通知客户端滑动窗口的大小。
17. 第三次挥手,服务器端给客户端发送FIN = 1,请求断开连接。
18. 第四次挥手,客户端确认服务器端的断开请求。
注意:
第一次握手,发送方不能发送数据(不包括TCP文件头);
第二次握手,接收方不能发送数据(只能完成三次握手后);
第三次握手,发送方就可以发送数据了
主动发送断开请求FIN的一方,在收到对方确认后,就不能再主动发送数据。但是可以再接收数据。
四次挥手:发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行4次挥手。
客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就发起。
因为在TCP连接的时候,采用三次握手建立的连接是双向的,在断开的时候也需要双向断开。
要实现TCP通信服务器处理并发的任务,使用多线程/多进程解决。
思路:
// 服务器端 #define _DEFAULT_SOURCE #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <errno.h> void recycleChild(int arg) { while(1) { int ret = waitpid(-1, NULL, WNOHANG); if(ret == -1) { // 所有的子进程都回收完了 break; } else if(ret == 0) { // 还有子进程活着 break; } else if(ret > 0) { // 一个子进程被回收了 printf("子进程 %d 被回收了\n", ret); } } } int main() { // 注册信号捕捉 struct sigaction act; act.sa_flags = 0; sigemptyset(&act.sa_mask); act.sa_handler = recycleChild; sigaction(SIGCHLD, &act, NULL); // 创建socket int lfd = socket(PF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket"); exit(-1); } // 绑定 struct sockaddr_in seraddr; seraddr.sin_port = htons(9999); seraddr.sin_family = AF_INET; seraddr.sin_addr.s_addr = INADDR_ANY; int ret = bind(lfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if(ret == -1) { perror("bind"); exit(-1); } // 监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen"); exit(-1); } // 循环等待客户端连接 while(1) { // 接受连接 struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); /* 主进程可以考虑close(cfd), 因为linux系统默认最多只能开1024个文件, 即使更改后也有4096的最高上限数量,我之前在进行65535个端口扫描时发现不及时close是会溢出报错的 */ int cfd = accept(lfd, (struct sockaddr *)&cliaddr,&len); /* 如果某个子进程退出,那么主进程会收到信号SIGCHLD调用回调函数,触发软中断, 当执行完回调函数后,回到accept()(之前主进程被阻塞的位置上),accept()会报错,返回EINTR错误 */ if(cfd == -1) { if(errno == EINTR) continue; // 如果返回EINTR错误,重新进行循环 perror("accept"); exit(-1); } // 每一个连接进来的客户端,就创建一个子进程和客户端通信 pid_t pid = fork(); if(pid > 0) continue; else if(pid == 0) { // 子进程 // 获取客户端的信息 char cliIP[16]; inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP)); unsigned short cliPort = ntohs(cliaddr.sin_port); printf("client ip is: %s, port is: %d\n", cliIP, cliPort); // 接收客户端发来的数据 char recvBuf[1024] = {0}; while(1) { int len = read(cfd, &recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) { printf("recv clinet data: %s\n", recvBuf); // strlen在计数的时候是结束符'\0'为止,但不包含结束符。 write(cfd, recvBuf, strlen(recvBuf) + 1); // +1 把结束符也发送出去 } else if(len == 0) { printf("client closed...\n"); break; } } close(cfd); exit(0); } } close(lfd); return 0; }
客户端:同之前的TCP通信(回射服务器)
// TCP通信 客户端 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main() { // 1. 创建套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); // 2. 连接服务器端 struct sockaddr_in serverAddr; serverAddr.sin_port = htons(9999); serverAddr.sin_family = AF_INET; inet_pton(AF_INET, "192.168.241.128", &serverAddr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if(ret == -1) { perror("connect"); exit(-1); } // 3. 通信 char recvBuf[1024] = {0}; char data[1024] = {0}; int i = 0; while(1) { memset(data, 0, 1024); // 将data内容清空 // 获取标准输入的数据 //fgets(data, 1024, stdin); sprintf(data, "%d\n", i++); // 发送给客户端数据 // 发送的时候不会将 \n 发送出去 因为strlen在计数的时候是结束符'\0'为止,但不包含结束符。 // 所以write(fd, data, strlen(data) + 1); 要+1 write(fd, data, strlen(data) + 1); sleep(1); // 获取服务器端的数据 memset(recvBuf, 0, 1024); // 将recvBuf内容清空 int len = read(fd, recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0){ printf("recv server data: %s\n", recvBuf); } else if(len == 0) { // 表示服务器端断开连接 printf("server closed...\n"); break; } } // 关闭连接 close(fd); return 0; }
注意:
发送数据:发送的时候不会将 \n 发送出去,因为strlen在计数的时候是结束符’\0’为止,但不包含结束符
所以write(fd, data, strlen(data) + 1); 要+1
我们设置信号捕捉函数来捕捉SIGCHLD信号,用于子进程结束通信退出,回收子进程资源。
如果某个子进程退出,那么主进程会收到信号SIGCHLD调用回调函数,触发软中断,
当执行完回调函数后,回到accept()(之前主进程被阻塞的位置上),accept()会报错,返回EINTR错误
解决办法:可以设置 if(errno == EINTR) continue; // 如果返回EINTR错误,重新进行循环
主进程可以考虑close(cfd), 因为linux系统默认最多只能开1024个文件,
即使更改后也有4096的最高上限数量,之前在进行65535个端口扫描时发现不及时close是会溢出报错的
从多进程转为多线程会有很多问题:
这会造成如下问题:
我们在pthread_create()只能向目标线程运行函数传递一个参数(void*) arg,但是我们可能会需要很多参数,包括与客户端通信的fd,线程tid,客户端的socket地址结构体等等…因此需要建立一个结构体讲这些参数封装一起传过去。
我们在while()循环中创建结构体,这个结构体是创建在主线程的栈中,在子线程运行过程中如果主线程退出while()循环释放资源会造成错误。
即使我们可以在堆中创建结构体(malloc()),但是我们需要手动释放资源;并且如果线程数过大,我们需要申请许多线程,所需的资源巨大。
结构体变量不能直接简单赋值,需要将其对应的内部变量分别进行赋值;或者使用memcpy()函数
pthread_t tid 在创建结构体变量之后,调用pthread_create()之前还没有初始化
解决方案: 直接将pinfo->tid的地址传进 pthread_create(),不另外设置
服务器端:
#include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <strings.h> #include <sys/types.h> #include <pthread.h> struct sockInfo{ int fd; // 通信的文件描述符 pthread_t tid; // 线程号 struct sockaddr_in addr; //客户端的socket地址结构体 }; struct sockInfo sockinfos[128]; // 同时最大连接线程数128 void * working(void* arg) { // 和客户端通信 // 获取客户端的信息 struct sockInfo* pinfo = (struct sockInfo*) arg; char cliIP[16]; inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIP, sizeof(cliIP)); unsigned short cliPort = ntohs(pinfo->addr.sin_port); printf("client ip is: %s, port is: %d\n", cliIP, cliPort); // 接收客户端发来的数据 char recvBuf[1024]; while(1) { int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf)); if(len == -1) { perror("read"); exit(-1); } else if(len > 0) { printf("recv clinet data: %s\n", recvBuf); // strlen在计数的时候是结束符'\0'为止,但不包含结束符。 write(pinfo->fd, recvBuf, strlen(recvBuf) + 1); // +1 把结束符也发送出去 } else if(len == 0) { printf("client closed...\n"); break; } } close(pinfo->fd); pinfo->fd = -1; return NULL; } int main() { // 创建socket int lfd = socket(PF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket"); exit(-1); } // 绑定 struct sockaddr_in seraddr; seraddr.sin_port = htons(9999); seraddr.sin_family = AF_INET; seraddr.sin_addr.s_addr = INADDR_ANY; int ret = bind(lfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if(ret == -1) { perror("bind"); exit(-1); } // 监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen"); exit(-1); } // 初始化数据 int max = sizeof(sockinfos) / sizeof(sockinfos[0]); for(int i = 0; i < max; i++) { bzero(&sockinfos[i], sizeof(sockinfos[i])); sockinfos[i].fd = -1; // -1表示该sockinfos[i]空闲 否则占用中 sockinfos[i].tid = -1; } // 循环等待客户端连接 // 一旦一个客户端连接进来,就创建一个子线程创建连接 while(1) { // 接受连接 struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); int cfd = accept(lfd, (struct sockaddr*) &cliaddr, &len); // 创建子线程 struct sockInfo * pinfo; for(int i = 0; i < max; ++i) { // 从数组中找到一个可用的sockinfos[i] if(sockinfos[i].fd == -1) { pinfo = &sockinfos[i]; break; } if(i == max - 1) { // 当前sockinfos[i]都被占用 需等待... sleep(1); printf("当前连接链路被占用 需等待..."); i = -1; } } pinfo->fd = cfd; memcpy(&pinfo->addr, &cliaddr, len); // 创建子线程 pthread_create(&pinfo->tid, NULL, working, pinfo); // 设置线程分离 自主释放资源 pthread_detach(pinfo->tid); } close(lfd); return 0; }
注意:
在第二次挥手和第三次挥手期间,服务器端(被动关闭方)是可以继续发送数据给对方,直到都发送完毕再进行第三次挥手申请断开连接。
半连接队列(SYN队列),服务端收到第一次握手后,会将sock加入到这个队列中,队列内的sock都处于SYN_RCVD 状态。
全连接队列(ACCEPT队列),在服务端收到第三次握手后,会将半连接队列的sock取出,放到全连接队列中。队列里的sock都处于 ESTABLISHED状态。这里面的连接,就等着服务端执行accept()后被取出了
2MSL(Maximum Segment Lifetime)
主动断开连接的一方, 最后进出入一个 TIME_WAIT状态, 这个状态会持续: 2msl
msl: 官方建议: 2分钟, 实际是30s
当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。
这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。
主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
半关闭
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 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 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
查看网络相关信息的命令:netstat
boyangcao@MyLinux:~$ netstat # 参数 -a: 所有的socket -p: 显示正在使用socket的程序的名称 -n: 直接使用IP地址,而不通过域名服务器 -l: 正在监听的服务器socket -t: 使用tcp协议传输的socket -d: 使用udp协议传输的socket boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999 # client server 都打开并处于连接状态 (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户) tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 4720/./server tcp 0 0 127.0.0.1:9999 127.0.0.1:50722 ESTABLISHED 4720/./server tcp 0 0 127.0.0.1:50722 127.0.0.1:9999 ESTABLISHED 4721/./client boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999 # 关闭 server tcp 0 0 127.0.0.1:9999 127.0.0.1:50722 FIN_WAIT2 - tcp 1 0 127.0.0.1:50722 127.0.0.1:9999 CLOSE_WAIT 4721/./client boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999 # 关闭 client (之前绑定的端口9999还未释放 处于TIME_WAIT状态) tcp 0 0 127.0.0.1:9999 127.0.0.1:50722 TIME_WAIT -
端口复用最常用的用途是:
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了上面两个头文件 // 设置套接字的属性(不仅仅能设置端口复用) int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 参数: - sockfd: 要操作的文件描述符 - level: 级别 - SOL_SOCKET(端口复用的级别) - optname: 选项的名称 - SO_REUSEADDR: 允许重用本地地址 - SO_REUSEPORT: 允许重用本地端口 - optval: 端口复用的值(整型) - 1: 可以复用 - 0: 不可以复用 - optlen: optval参数的大小 端口复用设置的时机:在服务器绑定端口之前 eg. int optval = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); // 绑定 int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
现在的结果:
boyangcao@MyLinux:~/Linux/Lesson34$ netstat -anp | grep -i 9999、
# 关闭了server后在2MSL时间内再次打开Server,端口可以复用
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 4728/./server
tcp 0 0 127.0.0.1:9999 127.0.0.1:50722 TIME_WAIT -
tcp 0 0 127.0.0.1:50724 127.0.0.1:9999 ESTABLISHED 4731/./client
tcp 0 0 127.0.0.1:9999 127.0.0.1:50724 ESTABLISHED 4728/./server
注意:我很疑惑为什么这里客户端设置等待键盘输入信息,然后先关闭服务器,在关闭客户端就能看到服务器进程处于time_wait状态。
但是把客户端设置成一直发送消息的状态,先关闭服务端,客户端read检测到返回值为0自动关闭,就不能看到服务器对应进程处于time_wait状态??
补充:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
- sockfd: 用于通信的fd
- buf: 传出参数,接受的数据放入到此数组中
- len: buf数组长度
- flags: (具体看man 2 recv) 此处为 0
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。