目录
1、POSIX(Portable Operating System Interface)
传输控制块(Transmission Control Block, TCB)
5)超时重传(Timeout Retransmission)
一、基本概念
1、POSIX(Portable Operating System Interface)
POSIX(Portable Operating System Interface)是一个定义了一系列API(应用程序编程接口)的标准,旨在提高不同操作系统之间的兼容性和可移植性。POSIX API涵盖了文件和目录操作、进程控制、线程管理、信号处理、内存管理、网络通信等多方面的功能。
客户端和服务器相关API
客户端
- 使用socket()函数创建一个套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
- 使用connect()函数连接到服务器
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
- 使用send()和recv()函数进行数据通信
send(sockfd, message, strlen(message), 0); recv(sockfd, buffer, sizeof(buffer), 0);
- 使用close()函数关闭套接字
close(sockfd);
服务端
- 使用socket()函数创建一个套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
- 使用bind()函数绑定套接字到特定的IP地址和端口号
struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); bind(server_fd, (struct sockaddr *)&address, sizeof(address));
- 使用listen()函数使套接字进入监听状态
listen(server_fd, 3);
- 使用accept()函数接受客户端连接
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
- 使用send()和recv()函数进行数据通信
recv(new_socket, buffer, sizeof(buffer), 0); send(new_socket, response, strlen(response), 0);
- 使用close()函数关闭套接字
close(new_socket); close(server_fd);
epoll相关函数
- epoll_create():创建一个 epoll 实例
int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); }
- epoll_ctl():控制 epoll 实例,注册、修改或删除感兴趣的事件
struct epoll_event event; event.events = EPOLLIN; // 监控读事件 event.data.fd = fd; // 需要监控的文件描述符 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1) { perror("epoll_ctl"); exit(EXIT_FAILURE); }
- epoll_wait():等待事件的发生
struct epoll_event events[MAX_EVENTS]; int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int i = 0; i < nfds; ++i) { if (events[i].events & EPOLLIN) { // 处理可读事件 } }
二、详细解释
1、连接过程
1)socket()
在网络编程中,socket 的实现确实涉及多个部分,其中主要包括文件描述符(File Descriptor, fd)和传输控制块(Transmission Control Block, TCB)。以下是对这两个部分及其相互作用的详细解释:
文件描述符(File Descriptor, fd)
- 文件描述符分配:
- 当你调用 socket() 函数创建一个新的套接字时,操作系统会分配一个文件描述符。文件描述符是一个整数,用于唯一标识该套接字。
- 文件描述符在内核中作为索引,用于查找与该套接字相关的数据结构和状态信息。
传输控制块(Transmission Control Block, TCB)
- 传输控制块的作用:
- TCB 是一个数据结构,用于存储与 TCP 连接相关的所有状态信息。对于每个 TCP 连接,操作系统内核会维护一个 TCB。
- TCB 包含的信息包括:
- 本地和远程 IP 地址
- 本地和远程端口号
- 连接状态(如 LISTEN、SYN_SENT、ESTABLISHED 等)
- 发送和接收缓冲区
- 序列号和确认号
- 拥塞控制和流量控制参数
2)bind()
bind() 函数通过文件描述符(fd)找到对应的套接字结构,然后将IP地址和端口号设置进去,从而在传输控制块(TCB)中反映这些信息。下面是详细的解释:
bind() 函数的工作原理
-
查找套接字结构:
- 当你调用 bind() 函数时,内核会根据传入的文件描述符(fd)查找对应的套接字结构(socket structure)。这个套接字结构包含了与该套接字相关的各种信息,包括指向 TCB 的指针。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 当你调用 bind() 函数时,内核会根据传入的文件描述符(fd)查找对应的套接字结构(socket structure)。这个套接字结构包含了与该套接字相关的各种信息,包括指向 TCB 的指针。
-
设置地址和端口:
- bind() 函数将传入的IP地址和端口号设置到套接字结构中。在TCP协议中,这些信息最终会存储到对应的传输控制块(TCB)中。
- 具体来说,bind() 函数将 sockaddr 结构体中的 sin_addr 和 sin_port 字段的值设置到套接字结构中。
struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; // IP address address.sin_port = htons(PORT); // Port number
-
更新 TCB:
- 当套接字结构中的 IP 地址和端口号被设置后,这些信息也会反映到相应的 TCB 中。TCB 包含连接状态、IP地址、端口号等关键信息,用于管理和维护 TCP 连接。
- 内核会确保在调用 bind() 函数时,套接字的地址和端口号信息得到正确更新,从而在后续的连接建立和数据传输过程中使用。
bind() 函数调用过程
- 应用程序调用 bind():
- 应用程序调用 bind() 函数,并传入文件描述符、IP地址和端口号。
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); bind(sockfd, (struct sockaddr *)&address, sizeof(address));
- 应用程序调用 bind() 函数,并传入文件描述符、IP地址和端口号。
- 内核处理 bind() 请求:
- 内核接收到 bind() 请求后,根据文件描述符查找对应的套接字结构。
- 内核将传入的 sockaddr 结构体中的IP地址和端口号设置到套接字结构中。
- 内核更新套接字结构中的信息,同时确保这些信息反映到相应的 TCB 中。
- 确认地址和端口设置成功:
- 内核完成地址和端口的设置后,返回成功状态给应用程序,表示 bind() 操作成功。
总结
- 文件描述符(fd):标识套接字。
- 套接字结构(socket structure):包含套接字相关的信息,包括指向 TCB 的指针。
- 传输控制块(TCB):存储连接状态、IP地址、端口号等关键信息。
bind() 函数通过文件描述符查找套接字结构,然后将IP地址和端口号设置进去,最终在TCB中反映这些信息。这确保了在TCP连接建立和数据传输过程中使用正确的地址和端口。
3)listen()
listen() 函数将传输控制块(TCB)中的状态设置为 LISTEN 状态。以下是 listen() 函数的详细工作原理及其在 TCP 连接管理中的作用:
listen() 函数的工作原理
- 函数原型:
int listen(int sockfd, int backlog);
- sockfd:由 socket() 创建并绑定了地址的套接字文件描述符。
- backlog:全连接队列的最大长度。
- 主要操作:
- 当你调用 listen() 函数时,内核会根据传入的文件描述符(fd)查找对应的套接字结构。
- 内核会将该套接字的状态设置为 LISTEN,并将其标记为一个被动套接字,表示它将接受传入的连接请求。
- 状态转换:
- 在 TCP 协议中,套接字的状态会从 CLOSED 转换为 LISTEN。这是 TCP 状态机中的一个重要状态,用于表示服务器套接字正在监听传入的连接请求。
listen() 函数的调用过程
- 应用程序调用 listen():
- 应用程序调用
listen()
函数,并传入套接字文件描述符和待处理连接请求的最大队列长度(backlog
)。
- 应用程序调用
- 内核处理 listen() 请求:
- 内核接收到 listen() 请求后,根据文件描述符查找对应的套接字结构。
- 内核将该套接字的状态设置为 LISTEN,并更新相应的 TCB。
- TCB 中的状态字段被更新为 LISTEN,表示该套接字正在等待传入的连接请求。
- 队列管理:
- 内核会为该监听套接字维护一个连接请求队列。backlog 参数指定了该队列的最大长度。
- 当新的连接请求到达时,内核会将这些请求放入队列中,等待应用程序调用 accept() 函数处理。
listen() 的作用
- 将套接字状态设置为 LISTEN:表示该套接字正在监听传入的连接请求。
- 创建连接请求队列:为传入的连接请求维护一个队列,等待应用程序处理。
listen() 第二参数backlog 的作用
- 定义全连接队列长度:
- backlog 参数指定了全连接队列(Accept Queue)的最大长度,即服务器在 accept() 之前可以存储的完全建立的连接请求的数量。
- 如果全连接队列已满,新来的连接请求将被拒绝,客户端可能会收到错误或超时。
- 影响服务器并发处理能力:
- 较大的 backlog 值允许服务器处理更多的连接请求,有助于在高并发场景下提高性能。
- 较小的 backlog 值可能导致连接请求被拒绝,从而影响客户端的连接体验。
总结
- listen() 函数:将套接字的状态设置为 LISTEN,并创建一个连接请求队列。
- 传输控制块(TCB):在调用 listen() 时,TCB 中的状态字段被更新为 LISTEN,表示该套接字正在监听传入的连接请求。
4)accept()
accept 函数是用于从全连接队列中取出一个已经完成三次握手的连接,并为该连接分配一个新的文件描述符(fd)。这个新的文件描述符将用于后续的通信。具体过程包括分配文件描述符和建立文件描述符与传输控制块(TCB)之间的映射。
accept 函数的工作原理
- 函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:服务器监听套接字文件描述符。
- addr:指向存储客户端地址信息的结构体的指针。
- addrlen:指向地址结构体长度的指针。
- 工作流程
- 从全连接队列中取出连接:
- accept 函数从全连接队列中取出一个已经完成三次握手的连接请求。
- 如果全连接队列为空,accept 将阻塞直到有新的连接完成三次握手(除非套接字是非阻塞的)。
- 分配新的文件描述符:
- 为该连接分配一个新的文件描述符,这个文件描述符用于标识新的连接,并将其返回给调用者。
- 建立文件描述符与TCB的映射:
- 内核会为这个新的文件描述符创建一个新的套接字结构,并将其与相应的传输控制块(TCB)关联。
- TCB 中包含了连接的各种状态信息,如本地和远程的IP地址、端口号、序列号等。
- 从全连接队列中取出连接:
具体实现步骤
- 调用 accept 函数:
- 应用程序调用 accept 函数,从全连接队列中取出一个连接请求,并分配一个新的文件描述符。
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
- 应用程序调用 accept 函数,从全连接队列中取出一个连接请求,并分配一个新的文件描述符。
- 分配文件描述符:
- 内核为新的连接分配一个文件描述符(通常是最小的未使用的整数)。
- 这个新的文件描述符将被用于后续的读写操作。
- 创建套接字结构:
- 内核为新的文件描述符创建一个套接字结构,这个结构保存了新的连接的各种信息。
- 建立文件描述符与TCB的映射:
- 套接字结构中的某些字段会指向对应的传输控制块(TCB),这些TCB存储了TCP连接的状态信息。
- 内核将新的文件描述符与相应的TCB关联起来,以便在后续的通信中能够正确处理连接。
总结
- 分配文件描述符:accept 函数为新的连接分配一个新的文件描述符。
- 建立映射:内核为新的文件描述符创建套接字结构,并将其与相应的传输控制块(TCB)关联,以便在后续的通信中能够正确处理连接。
- 连接处理:通过新的文件描述符,应用程序可以进行读写操作,实现与客户端的通信。
通过这些步骤,accept 函数完成了从全连接队列中取出连接请求、分配文件描述符并建立文件描述符与TCB映射的过程,使得服务器能够处理多个客户端连接。
5)三次握手的过程
三次握手(Three-Way Handshake)是TCP协议中建立连接的过程,它确保客户端和服务器都准备好进行数据传输,并且能够可靠地进行通信。三次握手的具体过程发生在以下函数调用中:
客户端
- socket():创建一个新的套接字。
- connect():发起连接请求,触发三次握手。
服务器
- socket():创建一个新的套接字。
- bind():绑定套接字到一个地址和端口。
- listen():将套接字设置为监听模式,准备接受连接请求。
- accept():接受一个连接请求,完成三次握手。
三次握手的详细过程
- 客户端角度
- SYN:客户端调用 connect() 函数,向服务器发送一个 SYN(同步序列编号)包。
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
- SYN:客户端调用 connect() 函数,向服务器发送一个 SYN(同步序列编号)包。
- 服务器角度
- SYN-ACK:服务器在调用 listen() 函数后,等待连接请求。当接收到客户端的 SYN 包时,服务器将此请求放入半连接队列,并回复一个 SYN-ACK(同步序列编号-确认序列编号)包。
int server_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); bind(server_fd, (struct sockaddr *)&address, sizeof(address)); listen(server_fd, 3);
- ACK:客户端收到服务器的 SYN-ACK 包后,回复一个 ACK(确认序列编号)包,完成三次握手。这时,客户端的 connect() 函数返回,表示连接已经建立。
- SYN-ACK:服务器在调用 listen() 函数后,等待连接请求。当接收到客户端的 SYN 包时,服务器将此请求放入半连接队列,并回复一个 SYN-ACK(同步序列编号-确认序列编号)包。
- 最后一步(服务器)
- accept():服务器调用 accept() 函数,从全连接队列中取出已经完成三次握手的连接,返回一个新的套接字用于与客户端通信。
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
- accept():服务器调用 accept() 函数,从全连接队列中取出已经完成三次握手的连接,返回一个新的套接字用于与客户端通信。
三次握手过程的具体步骤
- 客户端 -> 服务器:客户端调用
connect()
,发送 SYN 包,进入 SYN_SENT 状态。 - 服务器 -> 客户端:服务器收到 SYN 包,放入半连接队列,发送 SYN-ACK 包,进入 SYN_RCVD 状态。
- 客户端 -> 服务器:客户端收到 SYN-ACK 包,发送 ACK 包,进入 ESTABLISHED 状态,
connect()
返回。 - 服务器:服务器收到 ACK 包,将连接从半连接队列移到全连接队列,进入 ESTABLISHED 状态。服务器调用
accept()
,从全连接队列取出连接,accept()
返回新的套接字。
全连接队列和半连接队列
半连接队列(Syn Queue)
- 作用:存储那些已经发送了SYN(同步)包,但还未完成三次握手的连接请求。这个队列中的连接称为半连接。
- 工作流程:
- 客户端发送 SYN 包:客户端发起连接时,首先发送一个 SYN 包给服务器。
- 服务器接收 SYN 包:服务器接收到 SYN 包后,将此连接请求加入半连接队列,并返回一个 SYN-ACK 包给客户端。
- 等待 ACK 包:服务器等待客户端的 ACK 包,以完成三次握手。这段时间内,这个连接仍然在半连接队列中。
- 存储位置:半连接队列通常存储在内核中。
全连接队列(Accept Queue)
- 作用:存储那些已经完成三次握手的连接请求。这个队列中的连接称为全连接,等待应用程序调用
accept()
函数进行处理。 - 工作流程:
- 客户端发送 ACK 包:客户端接收到服务器的 SYN-ACK 包后,发送一个 ACK 包给服务器,完成三次握手。
- 服务器接收 ACK 包:服务器接收到 ACK 包后,将此连接从半连接队列移到全连接队列。
- 等待 accept():连接进入全连接队列后,服务器应用程序可以调用
accept()
函数来处理这个连接。
- 存储位置:全连接队列通常也是存储在内核中。
6)相关问题
1.TCP连接的生命周期
TCP连接的生命周期从客户端发起连接请求(即发送第一个SYN包)开始,到连接关闭(即收到FIN包并完成连接释放)结束。具体流程如下:
-
连接建立:
- 客户端调用
connect()
函数,发送SYN包,进入SYN_SENT状态。 - 服务器接收SYN包,放入半连接队列,发送SYN-ACK包,进入SYN_RCVD状态。
- 客户端接收SYN-ACK包,发送ACK包,进入ESTABLISHED状态。
- 服务器接收ACK包,将连接从半连接队列移到全连接队列,进入ESTABLISHED状态。
- 客户端调用
-
数据传输:
- 在ESTABLISHED状态下,双方可以进行数据传输,通过send()和recv()函数进行通信。
-
连接关闭:
- 一方调用close()或shutdown()函数,发送FIN包,进入FIN_WAIT_1状态。
- 对方接收FIN包,发送ACK包,进入CLOSE_WAIT状态。
- 发送FIN包的一方接收ACK包,进入FIN_WAIT_2状态。
- 对方调用close()或shutdown()函数,发送FIN包,进入LAST_ACK状态。
- 发送FIN包的一方接收FIN包,发送ACK包,进入TIME_WAIT状态。
- 对方接收ACK包,进入CLOSED状态。
- 发送ACK包的一方在TIME_WAIT状态等待2倍的最大报文段生存时间(2MSL),然后进入CLOSED状态。
2.第三次握手的数据包在全连接队列中的查找过程
-
半连接队列(Syn Queue):
- 在三次握手的前两次(SYN -> SYN-ACK)期间,服务器将收到的SYN包放入半连接队列。
-
查找匹配节点:
- 当服务器收到客户端的ACK包(第三次握手),它会在半连接队列中查找与该ACK包匹配的SYN-ACK节点。
- 查找匹配节点的依据包括源IP地址、源端口号、目标IP地址、目标端口号和序列号等。
- 如果找到匹配的节点,服务器会将该连接从半连接队列移到全连接队列。
-
全连接队列(Accept Queue):
- 服务器将完成三次握手的连接放入全连接队列,等待应用程序调用
accept()
函数处理。
- 服务器将完成三次握手的连接放入全连接队列,等待应用程序调用
3.SYN泛洪攻击
SYN泛洪(SYN Flood)是一种常见的拒绝服务(DoS)攻击,攻击者发送大量的SYN包,消耗服务器的资源,使其无法处理正常的连接请求。攻击过程如下:
-
发送大量SYN包:
- 攻击者发送大量伪造的SYN包到目标服务器,每个包都请求建立新的连接。
-
占用半连接队列:
- 服务器收到这些SYN包后,将它们放入半连接队列,并返回SYN-ACK包。
- 攻击者通常不会回应SYN-ACK包,使得这些半连接一直占用队列。
-
资源耗尽:
- 半连接队列被大量伪造的SYN请求占满,服务器无法处理新的合法连接请求。
4.防御SYN泛洪攻击
-
SYN Cookies:
- 服务器在发送SYN-ACK包时,不立即分配资源,而是通过计算哈希值生成一个特殊的序列号(Cookie)。客户端回应ACK包时,服务器根据序列号验证连接的合法性。
-
增加半连接队列大小:
- 增加半连接队列的大小,使其能够容纳更多的SYN请求,减少被攻击时的影响。
-
缩短SYN-ACK超时:
- 缩短服务器等待客户端ACK包的时间,使得未完成的连接能够更快地被清理。
-
过滤伪造IP地址:
- 使用防火墙或其他网络设备过滤掉伪造的IP地址,减少伪造SYN包的数量。
总结
- TCP连接的生命周期:从客户端发起SYN包开始,到连接关闭结束。
- 第三次握手的匹配:服务器在半连接队列中查找与ACK包匹配的SYN-ACK节点,并将其移到全连接队列。
- SYN泛洪攻击:通过发送大量伪造的SYN包,占用服务器资源,使其无法处理正常连接请求。
- 防御措施:包括使用SYN Cookies、增加半连接队列大小、缩短SYN-ACK超时、过滤伪造IP地址等。
2、数据传输
在TCP数据传输过程中,有一些关键机制和算法用于确保数据的可靠性、有效性和效率。这些机制包括慢启动、拥塞控制、滑动窗口、延迟确认和超时重传。以下是对这些概念的详细解释:
1)慢启动(Slow Start)
慢启动是TCP拥塞控制的一部分,用于防止网络突然拥塞。其主要目的是逐步增加发送数据的速率,以便找到网络的最佳传输速度。
-
工作原理:
- 初始状态下,TCP连接的拥塞窗口(cwnd)设为一个较小的值(通常为1个MSS,最大报文段大小)。
- 每次收到一个确认(ACK)包,拥塞窗口加倍(指数增长)。
- 当拥塞窗口达到慢启动阈值(ssthresh)时,慢启动阶段结束,进入拥塞避免阶段。
-
示例:
- 初始cwnd = 1,发送一个数据包,收到ACK后,cwnd = 2。
- 发送两个数据包,收到两个ACK后,cwnd = 4,依此类推,直到达到阈值。
2)拥塞控制(Congestion Control)
拥塞控制包括多个阶段和算法,用于检测和避免网络拥塞。
-
慢启动(Slow Start):
- 前面已经介绍过。
-
拥塞避免(Congestion Avoidance):
- 当cwnd达到阈值时,增长速率减慢,每个RTT(往返时间)内,cwnd增加一个MSS(线性增长)。
-
快速重传(Fast Retransmit)和快速恢复(Fast Recovery):
- 快速重传:当收到三个重复的ACK时,认为数据包丢失,立即重传丢失的数据包。
- 快速恢复:重传丢失的数据包后,不进入慢启动阶段,而是将cwnd减半,并继续线性增长。
3)滑动窗口(Sliding Window)
滑动窗口机制用于控制数据流量,确保发送方不会发送超过接收方处理能力的数据。
-
发送窗口和接收窗口:
- 发送窗口:发送方根据接收方的接收窗口(rwnd)和自身的拥塞窗口(cwnd)来确定发送数据的最大量。
- 接收窗口:接收方告诉发送方自己能接收的数据量,避免接收方缓存溢出。
-
工作原理:
- 发送方根据窗口大小发送数据,接收方接收到数据后发送ACK,同时更新接收窗口大小。
- 发送方根据ACK滑动窗口,继续发送新的数据。
4)延迟确认(Delayed ACK)
延迟确认机制用于减少ACK包的数量,从而减少网络负载。
-
工作原理:
- 接收方在接收到数据包后,不立即发送ACK,而是等待一段时间(通常200ms)。
- 在等待期间,如果接收方有数据要发送,可以将ACK与数据一起发送(捎带ACK),否则在等待时间到期后发送ACK。
-
优点:
- 减少网络中的ACK包数量,降低网络负载。
-
缺点:
- 增加了RTT,可能会降低传输效率。
5)超时重传(Timeout Retransmission)
超时重传机制用于处理数据包丢失的情况。
-
工作原理:
- 发送方发送数据包后,启动一个定时器,如果在规定时间内没有收到ACK,认为数据包丢失,重新发送数据包。
-
RTO(重传超时时间):
- RTO的设置非常重要,它根据网络条件动态调整。
- 通常使用RTT的估计值来计算RTO,常见算法包括加权移动平均(Exponential Weighted Moving Average, EWMA)等。
-
示例:
- 发送方发送数据包,启动定时器。如果在RTO时间内未收到ACK,重传该数据包并重启定时器。
6)总结
- 慢启动:逐步增加数据发送速率,防止网络拥塞。
- 拥塞控制:包括慢启动、拥塞避免、快速重传和快速恢复,确保网络的高效利用和拥塞的及时处理。
- 滑动窗口:控制数据流量,确保发送方不会超过接收方的处理能力。
- 延迟确认:减少ACK包数量,降低网络负载。
- 超时重传:处理数据包丢失,通过定时器和RTO机制确保数据可靠传输。
3、断开连接
1)调用 close 函数关闭连接和四次挥手
在TCP连接的关闭过程中,涉及到四次挥手(Four-Way Handshake),以及连接状态的转换。与三次握手不同,四次挥手的过程不区分客户端和服务器,而是区分主动关闭方和被动关闭方。
2)四次挥手的过程
-
主动关闭方发送FIN包:
- 主动关闭方调用
close
或shutdown
函数,发送一个FIN(Finish)包,表示不再发送数据。 - 进入
FIN_WAIT_1
状态。
- 主动关闭方调用
-
被动关闭方接收FIN包,发送ACK包:
- 被动关闭方接收到FIN包后,发送一个ACK(确认)包,确认FIN包的接收。
- 被动关闭方进入
CLOSE_WAIT
状态。 - 主动关闭方接收到ACK包后,进入
FIN_WAIT_2
状态。
-
被动关闭方发送FIN包:
- 被动关闭方处理完所有未发送的数据后,调用
close
或shutdown
函数,发送一个FIN包。 - 被动关闭方进入
LAST_ACK
状态。
- 被动关闭方处理完所有未发送的数据后,调用
-
主动关闭方接收FIN包,发送ACK包:
- 主动关闭方接收到FIN包后,发送一个ACK包,确认FIN包的接收。
- 主动关闭方进入
TIME_WAIT
状态,等待2倍的最大报文段生存时间(2MSL),以确保被动关闭方接收到ACK包。 - 被动关闭方接收到ACK包后,进入
CLOSED
状态。 - 主动关闭方在
TIME_WAIT
状态等待超时后,进入CLOSED
状态。
3)状态转换
-
主动关闭方状态转换:
ESTABLISHED
->FIN_WAIT_1
(发送FIN包)FIN_WAIT_1
->FIN_WAIT_2
(接收到ACK包)FIN_WAIT_2
->TIME_WAIT
(接收到FIN包,发送ACK包)TIME_WAIT
->CLOSED
(等待2MSL)
-
被动关闭方状态转换:
ESTABLISHED
->CLOSE_WAIT
(接收到FIN包,发送ACK包)CLOSE_WAIT
->LAST_ACK
(发送FIN包)LAST_ACK
->CLOSED
(接收到ACK包)
4)调用 close
函数
当应用程序调用 close
函数时,以下过程会发生:
-
回收文件描述符:
close
函数首先回收文件描述符,使其不再指向任何打开的文件或套接字。- 释放文件描述符占用的系统资源。
-
发送FIN包:
- 对于TCP套接字,
close
函数会触发TCP协议发送FIN包,开始四次挥手过程。
- 对于TCP套接字,
5)详细的状态解释
-
FIN_WAIT_1:
- 主动关闭方发送FIN包后进入此状态,等待对方的ACK包。
-
FIN_WAIT_2:
- 主动关闭方接收到ACK包后进入此状态,等待对方的FIN包。
-
TIME_WAIT:
- 主动关闭方接收到对方的FIN包后进入此状态,发送ACK包,并等待2MSL,以确保对方接收到ACK包,防止旧连接的数据包影响新连接。
-
CLOSE_WAIT:
- 被动关闭方接收到FIN包并发送ACK包后进入此状态,等待应用程序处理完所有未发送的数据并调用
close
函数。
- 被动关闭方接收到FIN包并发送ACK包后进入此状态,等待应用程序处理完所有未发送的数据并调用
-
LAST_ACK:
- 被动关闭方调用
close
函数发送FIN包后进入此状态,等待对方的ACK包。
- 被动关闭方调用
-
CLOSED:
- 双方都完成关闭过程,连接彻底断开,所有资源释放。
在TCP连接中,如果双方几乎同时调用close
函数,会发生一种被称为"同时关闭"(simultaneous close)的情况。这种情况实际上也是TCP协议允许的,并且被设计为能够正确处理。具体过程如下:
6)同时关闭的四次挥手过程
-
双方同时发送FIN包:
- 主动关闭方A和主动关闭方B几乎同时调用
close
,并发送FIN包。 - 双方进入
FIN_WAIT_1
状态。
- 主动关闭方A和主动关闭方B几乎同时调用
-
双方接收对方的FIN包,并发送ACK包:
- 双方接收到对方的FIN包,进入
CLOSING
状态。 - 双方发送ACK包。
- 双方接收到对方的FIN包,进入
-
双方接收对方的ACK包:
- 双方接收到对方的ACK包,进入
TIME_WAIT
状态。
- 双方接收到对方的ACK包,进入
-
等待2MSL,进入CLOSED状态:
- 双方在
TIME_WAIT
状态等待2倍的最大报文段生存时间(2MSL),以确保对方接收到ACK包并避免旧连接的数据包干扰新连接。 - 超时后,双方进入
CLOSED
状态,完成连接关闭。
- 双方在
7)状态转换
- 双方状态转换:
ESTABLISHED
->FIN_WAIT_1
(发送FIN包)FIN_WAIT_1
->CLOSING
(接收到对方的FIN包,发送ACK包)CLOSING
->TIME_WAIT
(接收到对方的ACK包)TIME_WAIT
->CLOSED
(等待2MSL)
4、tcp的p2p实现
TCP的P2P(Peer-to-Peer)连接是指两台计算机(或节点)直接相互通信,而不是通过一个中介服务器。P2P连接通常用于文件共享、即时通信和分布式计算等应用。
在一个P2P系统中,每个节点既可以作为客户端发送请求,也可以作为服务器接收请求。因此,P2P通信需要处理两种情况:发起连接和接受连接。
1)P2P连接的基本概念
-
节点(Node):
- 在P2P网络中,每个设备或程序都是一个节点。
- 节点既可以充当客户端(发起连接),也可以充当服务器(接受连接)。
-
连接建立:
- 两个节点都需要知道对方的IP地址和端口号。
- 每个节点都需要监听一个端口,以接受来自其他节点的连接请求。
-
数据传输:
- 节点之间可以通过TCP连接传输数据,就像在客户端-服务器模型中一样。
2)代码
每个节点既作为客户端发起连接请求,也作为服务器接受连接请求。
节点代码(同时作为客户端和服务器)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#define SERVER_PORT 8080
#define PEER_PORT 9090
#define BUFFER_SIZE 1024
// 用于接受连接的线程函数
void* server_thread(void* arg) {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// 创建服务器套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置端口复用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(SERVER_PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Listening on port %d\n", SERVER_PORT);
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Accepted connection\n");
// 处理连接
char buffer[BUFFER_SIZE] = {0};
read(new_socket, buffer, BUFFER_SIZE);
printf("Received: %s\n", buffer);
send(new_socket, "Hello from server", strlen("Hello from server"), 0);
close(new_socket);
close(server_fd);
return NULL;
}
// 用于发起连接的函数
void connect_to_peer(const char* peer_ip) {
int sockfd;
struct sockaddr_in serv_addr;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation error");
return;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PEER_PORT);
if (inet_pton(AF_INET, peer_ip, &serv_addr.sin_addr) <= 0) {
perror("Invalid address");
return;
}
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
return;
}
printf("Connected to peer\n");
// 发送数据
send(sockfd, "Hello from client", strlen("Hello from client"), 0);
char buffer[BUFFER_SIZE] = {0};
read(sockfd, buffer, BUFFER_SIZE);
printf("Received: %s\n", buffer);
close(sockfd);
}
int main(int argc, char const *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <peer_ip>\n", argv[0]);
return 1;
}
pthread_t tid;
// 启动服务器线程
pthread_create(&tid, NULL, server_thread, NULL);
// 连接到对等节点
connect_to_peer(argv[1]);
// 等待服务器线程结束
pthread_join(tid, NULL);
return 0;
}
解释
-
服务器线程:
- 使用
pthread_create
创建一个新线程来监听连接请求。 server_thread
函数负责创建服务器套接字,绑定端口并监听连接。当有连接请求时,接受连接并处理数据。
- 使用
-
客户端连接:
- 主线程调用
connect_to_peer
函数,使用套接字连接到对等节点(由命令行参数提供的IP地址)。 - 连接成功后,发送和接收数据。
- 主线程调用
-
主程序:
- 检查命令行参数,确保提供了对等节点的IP地址。
- 创建服务器线程,并连接到对等节点。
- 等待服务器线程结束。
如何运行
假设有两台计算机A和B:
- 在A上运行:
./p2p_program <B的IP地址>
- 在B上运行:
./p2p_program <A的IP地址>
3)总结
- P2P连接:每个节点既作为客户端发起连接请求,也作为服务器接受连接请求。
- TCP通信:通过创建套接字、绑定端口、监听连接、接受连接和发送/接收数据实现。
- 多线程:使用多线程来同时处理发起连接和接受连接的操作。