网络编程的基石:POSIX API 与协议栈

目录

一、基本概念

1、POSIX(Portable Operating System Interface)

客户端和服务器相关API

客户端

服务端

 epoll相关函数

二、详细解释

1、连接过程

1)socket()

文件描述符(File Descriptor, fd)

传输控制块(Transmission Control Block, TCB)

2)bind()

bind() 函数的工作原理

查找套接字结构:

设置地址和端口:

更新 TCB:

 bind() 函数调用过程

总结

3)listen()

listen() 函数的工作原理

listen() 函数的调用过程

listen() 的作用

listen() 第二参数backlog 的作用

总结

4)accept()

accept 函数的工作原理

具体实现步骤

总结

5)三次握手的过程

客户端

服务器

三次握手的详细过程

三次握手过程的具体步骤

全连接队列和半连接队列

半连接队列(Syn Queue)

全连接队列(Accept Queue)

6)相关问题

1.TCP连接的生命周期

2.第三次握手的数据包在全连接队列中的查找过程

3.SYN泛洪攻击

4.防御SYN泛洪攻击

总结

2、数据传输

1)慢启动(Slow Start)

2)拥塞控制(Congestion Control)

3)滑动窗口(Sliding Window)

4)延迟确认(Delayed ACK)

5)超时重传(Timeout Retransmission)

6)总结

3、断开连接

1)调用 close 函数关闭连接和四次挥手

2)四次挥手的过程

3)状态转换

4)调用 close 函数

5)详细的状态解释

6)同时关闭的四次挥手过程

7)状态转换

4、tcp的p2p实现

1)P2P连接的基本概念

2)代码

节点代码(同时作为客户端和服务器)

解释

如何运行

3)总结


一、基本概念

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() 函数的工作原理
  1. 查找套接字结构:
    1. 当你调用 bind() 函数时,内核会根据传入的文件描述符(fd)查找对应的套接字结构(socket structure)。这个套接字结构包含了与该套接字相关的各种信息,包括指向 TCB 的指针。
      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      
  2. 设置地址和端口:
    1. bind() 函数将传入的IP地址和端口号设置到套接字结构中。在TCP协议中,这些信息最终会存储到对应的传输控制块(TCB)中。
    2. 具体来说,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
      
  3. 更新 TCB:
    1. 当套接字结构中的 IP 地址和端口号被设置后,这些信息也会反映到相应的 TCB 中。TCB 包含连接状态、IP地址、端口号等关键信息,用于管理和维护 TCP 连接。
    2. 内核会确保在调用 bind() 函数时,套接字的地址和端口号信息得到正确更新,从而在后续的连接建立和数据传输过程中使用。
 bind() 函数调用过程
  1. 应用程序调用 bind():
    1. 应用程序调用 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));
      
  2. 内核处理 bind() 请求:
    1. 内核接收到 bind() 请求后,根据文件描述符查找对应的套接字结构。
    2. 内核将传入的 sockaddr 结构体中的IP地址和端口号设置到套接字结构中。
    3. 内核更新套接字结构中的信息,同时确保这些信息反映到相应的 TCB 中。
  3. 确认地址和端口设置成功:
    1. 内核完成地址和端口的设置后,返回成功状态给应用程序,表示 bind() 操作成功。
总结
  1. 文件描述符(fd):标识套接字。
  2. 套接字结构(socket structure):包含套接字相关的信息,包括指向 TCB 的指针。
  3. 传输控制块(TCB):存储连接状态、IP地址、端口号等关键信息。

bind() 函数通过文件描述符查找套接字结构,然后将IP地址和端口号设置进去,最终在TCB中反映这些信息。这确保了在TCP连接建立和数据传输过程中使用正确的地址和端口。

3)listen()

listen() 函数将传输控制块(TCB)中的状态设置为 LISTEN 状态。以下是 listen() 函数的详细工作原理及其在 TCP 连接管理中的作用:

listen() 函数的工作原理
  1. 函数原型:
    int listen(int sockfd, int backlog);
    
    1. sockfd:由 socket() 创建并绑定了地址的套接字文件描述符。
    2. backlog:全连接队列的最大长度。
  2. 主要操作:
    1. 当你调用 listen() 函数时,内核会根据传入的文件描述符(fd)查找对应的套接字结构。
    2. 内核会将该套接字的状态设置为 LISTEN,并将其标记为一个被动套接字,表示它将接受传入的连接请求。
  3. 状态转换:
    1. 在 TCP 协议中,套接字的状态会从 CLOSED 转换为 LISTEN。这是 TCP 状态机中的一个重要状态,用于表示服务器套接字正在监听传入的连接请求。
listen() 函数的调用过程
  1. 应用程序调用 listen():
    1. 应用程序调用 listen() 函数,并传入套接字文件描述符和待处理连接请求的最大队列长度(backlog)。
  2. 内核处理 listen() 请求:
    1. 内核接收到 listen() 请求后,根据文件描述符查找对应的套接字结构。
    2. 内核将该套接字的状态设置为 LISTEN,并更新相应的 TCB。
    3. TCB 中的状态字段被更新为 LISTEN,表示该套接字正在等待传入的连接请求。
  3. 队列管理:
    1. 内核会为该监听套接字维护一个连接请求队列。backlog 参数指定了该队列的最大长度。
    2. 当新的连接请求到达时,内核会将这些请求放入队列中,等待应用程序调用 accept() 函数处理。
listen() 的作用
  1. 将套接字状态设置为 LISTEN:表示该套接字正在监听传入的连接请求。
  2. 创建连接请求队列:为传入的连接请求维护一个队列,等待应用程序处理。
listen() 第二参数backlog 的作用
  1. 定义全连接队列长度:
    1. backlog 参数指定了全连接队列(Accept Queue)的最大长度,即服务器在 accept() 之前可以存储的完全建立的连接请求的数量。
    2. 如果全连接队列已满,新来的连接请求将被拒绝,客户端可能会收到错误或超时。
  2. 影响服务器并发处理能力:
    1. 较大的 backlog 值允许服务器处理更多的连接请求,有助于在高并发场景下提高性能。
    2. 较小的 backlog 值可能导致连接请求被拒绝,从而影响客户端的连接体验。
总结
  1. listen() 函数:将套接字的状态设置为 LISTEN,并创建一个连接请求队列。
  2. 传输控制块(TCB):在调用 listen() 时,TCB 中的状态字段被更新为 LISTEN,表示该套接字正在监听传入的连接请求。

4)accept()

accept 函数是用于从全连接队列中取出一个已经完成三次握手的连接,并为该连接分配一个新的文件描述符(fd)。这个新的文件描述符将用于后续的通信。具体过程包括分配文件描述符和建立文件描述符与传输控制块(TCB)之间的映射。

accept 函数的工作原理
  1. 函数原型
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
     
    1. sockfd:服务器监听套接字文件描述符。
    2. addr:指向存储客户端地址信息的结构体的指针。
    3. addrlen:指向地址结构体长度的指针。
  2. 工作流程
    1. 从全连接队列中取出连接:
      1. accept 函数从全连接队列中取出一个已经完成三次握手的连接请求。
      2. 如果全连接队列为空,accept 将阻塞直到有新的连接完成三次握手(除非套接字是非阻塞的)。
    2. 分配新的文件描述符:
      1. 为该连接分配一个新的文件描述符,这个文件描述符用于标识新的连接,并将其返回给调用者。
    3. 建立文件描述符与TCB的映射:
      1. 内核会为这个新的文件描述符创建一个新的套接字结构,并将其与相应的传输控制块(TCB)关联。
      2. TCB 中包含了连接的各种状态信息,如本地和远程的IP地址、端口号、序列号等。
具体实现步骤
  1. 调用 accept 函数:
    1. 应用程序调用 accept 函数,从全连接队列中取出一个连接请求,并分配一个新的文件描述符。
      int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
      
  2. 分配文件描述符:
    1. 内核为新的连接分配一个文件描述符(通常是最小的未使用的整数)。
    2. 这个新的文件描述符将被用于后续的读写操作。
  3. 创建套接字结构:
    1. 内核为新的文件描述符创建一个套接字结构,这个结构保存了新的连接的各种信息。
  4. 建立文件描述符与TCB的映射:
    1. 套接字结构中的某些字段会指向对应的传输控制块(TCB),这些TCB存储了TCP连接的状态信息。
    2. 内核将新的文件描述符与相应的TCB关联起来,以便在后续的通信中能够正确处理连接。
总结
  1. 分配文件描述符:accept 函数为新的连接分配一个新的文件描述符。
  2. 建立映射:内核为新的文件描述符创建套接字结构,并将其与相应的传输控制块(TCB)关联,以便在后续的通信中能够正确处理连接。
  3. 连接处理:通过新的文件描述符,应用程序可以进行读写操作,实现与客户端的通信。

通过这些步骤,accept 函数完成了从全连接队列中取出连接请求、分配文件描述符并建立文件描述符与TCB映射的过程,使得服务器能够处理多个客户端连接。

5)三次握手的过程

三次握手(Three-Way Handshake)是TCP协议中建立连接的过程,它确保客户端和服务器都准备好进行数据传输,并且能够可靠地进行通信。三次握手的具体过程发生在以下函数调用中:

客户端
  1. socket():创建一个新的套接字。
  2. connect():发起连接请求,触发三次握手。
服务器
  1. socket():创建一个新的套接字。
  2. bind():绑定套接字到一个地址和端口。
  3. listen():将套接字设置为监听模式,准备接受连接请求。
  4. accept():接受一个连接请求,完成三次握手。
三次握手的详细过程
  1. 客户端角度
    1. 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));
      
  2. 服务器角度
    1. 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);
      
    2. ACK:客户端收到服务器的 SYN-ACK 包后,回复一个 ACK(确认序列编号)包,完成三次握手。这时,客户端的 connect() 函数返回,表示连接已经建立。
  3. 最后一步(服务器)
    1. accept():服务器调用 accept() 函数,从全连接队列中取出已经完成三次握手的连接,返回一个新的套接字用于与客户端通信。
      int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
      
三次握手过程的具体步骤
  • 客户端 -> 服务器:客户端调用 connect(),发送 SYN 包,进入 SYN_SENT 状态。
  • 服务器 -> 客户端:服务器收到 SYN 包,放入半连接队列,发送 SYN-ACK 包,进入 SYN_RCVD 状态。
  • 客户端 -> 服务器:客户端收到 SYN-ACK 包,发送 ACK 包,进入 ESTABLISHED 状态,connect() 返回。
  • 服务器:服务器收到 ACK 包,将连接从半连接队列移到全连接队列,进入 ESTABLISHED 状态。服务器调用 accept(),从全连接队列取出连接,accept() 返回新的套接字。
全连接队列和半连接队列
半连接队列(Syn Queue)
  • 作用:存储那些已经发送了SYN(同步)包,但还未完成三次握手的连接请求。这个队列中的连接称为半连接。
  • 工作流程
    1. 客户端发送 SYN 包:客户端发起连接时,首先发送一个 SYN 包给服务器。
    2. 服务器接收 SYN 包:服务器接收到 SYN 包后,将此连接请求加入半连接队列,并返回一个 SYN-ACK 包给客户端。
    3. 等待 ACK 包:服务器等待客户端的 ACK 包,以完成三次握手。这段时间内,这个连接仍然在半连接队列中。
  • 存储位置:半连接队列通常存储在内核中。
全连接队列(Accept Queue)
  • 作用:存储那些已经完成三次握手的连接请求。这个队列中的连接称为全连接,等待应用程序调用 accept() 函数进行处理。
  • 工作流程
    1. 客户端发送 ACK 包:客户端接收到服务器的 SYN-ACK 包后,发送一个 ACK 包给服务器,完成三次握手。
    2. 服务器接收 ACK 包:服务器接收到 ACK 包后,将此连接从半连接队列移到全连接队列。
    3. 等待 accept():连接进入全连接队列后,服务器应用程序可以调用 accept() 函数来处理这个连接。
  • 存储位置:全连接队列通常也是存储在内核中。

6)相关问题

1.TCP连接的生命周期

TCP连接的生命周期从客户端发起连接请求(即发送第一个SYN包)开始,到连接关闭(即收到FIN包并完成连接释放)结束。具体流程如下:

  1. 连接建立

    • 客户端调用connect()函数,发送SYN包,进入SYN_SENT状态。
    • 服务器接收SYN包,放入半连接队列,发送SYN-ACK包,进入SYN_RCVD状态。
    • 客户端接收SYN-ACK包,发送ACK包,进入ESTABLISHED状态。
    • 服务器接收ACK包,将连接从半连接队列移到全连接队列,进入ESTABLISHED状态。
  2. 数据传输

    • 在ESTABLISHED状态下,双方可以进行数据传输,通过send()和recv()函数进行通信。
  3. 连接关闭

    • 一方调用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.第三次握手的数据包在全连接队列中的查找过程
  1. 半连接队列(Syn Queue)

    • 在三次握手的前两次(SYN -> SYN-ACK)期间,服务器将收到的SYN包放入半连接队列。
  2. 查找匹配节点

    • 当服务器收到客户端的ACK包(第三次握手),它会在半连接队列中查找与该ACK包匹配的SYN-ACK节点。
    • 查找匹配节点的依据包括源IP地址、源端口号、目标IP地址、目标端口号和序列号等。
    • 如果找到匹配的节点,服务器会将该连接从半连接队列移到全连接队列。
  3. 全连接队列(Accept Queue)

    • 服务器将完成三次握手的连接放入全连接队列,等待应用程序调用accept()函数处理。
3.SYN泛洪攻击

SYN泛洪(SYN Flood)是一种常见的拒绝服务(DoS)攻击,攻击者发送大量的SYN包,消耗服务器的资源,使其无法处理正常的连接请求。攻击过程如下:

  1. 发送大量SYN包

    • 攻击者发送大量伪造的SYN包到目标服务器,每个包都请求建立新的连接。
  2. 占用半连接队列

    • 服务器收到这些SYN包后,将它们放入半连接队列,并返回SYN-ACK包。
    • 攻击者通常不会回应SYN-ACK包,使得这些半连接一直占用队列。
  3. 资源耗尽

    • 半连接队列被大量伪造的SYN请求占满,服务器无法处理新的合法连接请求。
4.防御SYN泛洪攻击
  1. SYN Cookies

    • 服务器在发送SYN-ACK包时,不立即分配资源,而是通过计算哈希值生成一个特殊的序列号(Cookie)。客户端回应ACK包时,服务器根据序列号验证连接的合法性。
  2. 增加半连接队列大小

    • 增加半连接队列的大小,使其能够容纳更多的SYN请求,减少被攻击时的影响。
  3. 缩短SYN-ACK超时

    • 缩短服务器等待客户端ACK包的时间,使得未完成的连接能够更快地被清理。
  4. 过滤伪造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)总结

  1. 慢启动:逐步增加数据发送速率,防止网络拥塞。
  2. 拥塞控制:包括慢启动、拥塞避免、快速重传和快速恢复,确保网络的高效利用和拥塞的及时处理。
  3. 滑动窗口:控制数据流量,确保发送方不会超过接收方的处理能力。
  4. 延迟确认:减少ACK包数量,降低网络负载。
  5. 超时重传:处理数据包丢失,通过定时器和RTO机制确保数据可靠传输。

3、断开连接

1)调用 close 函数关闭连接和四次挥手

在TCP连接的关闭过程中,涉及到四次挥手(Four-Way Handshake),以及连接状态的转换。与三次握手不同,四次挥手的过程不区分客户端和服务器,而是区分主动关闭方和被动关闭方。

2)四次挥手的过程

  1. 主动关闭方发送FIN包

    • 主动关闭方调用 closeshutdown 函数,发送一个FIN(Finish)包,表示不再发送数据。
    • 进入 FIN_WAIT_1 状态。
  2. 被动关闭方接收FIN包,发送ACK包

    • 被动关闭方接收到FIN包后,发送一个ACK(确认)包,确认FIN包的接收。
    • 被动关闭方进入 CLOSE_WAIT 状态。
    • 主动关闭方接收到ACK包后,进入 FIN_WAIT_2 状态。
  3. 被动关闭方发送FIN包

    • 被动关闭方处理完所有未发送的数据后,调用 closeshutdown 函数,发送一个FIN包。
    • 被动关闭方进入 LAST_ACK 状态。
  4. 主动关闭方接收FIN包,发送ACK包

    • 主动关闭方接收到FIN包后,发送一个ACK包,确认FIN包的接收。
    • 主动关闭方进入 TIME_WAIT 状态,等待2倍的最大报文段生存时间(2MSL),以确保被动关闭方接收到ACK包。
    • 被动关闭方接收到ACK包后,进入 CLOSED 状态。
    • 主动关闭方在 TIME_WAIT 状态等待超时后,进入 CLOSED 状态。

3)状态转换

  • 主动关闭方状态转换

    1. ESTABLISHED -> FIN_WAIT_1(发送FIN包)
    2. FIN_WAIT_1 -> FIN_WAIT_2(接收到ACK包)
    3. FIN_WAIT_2 -> TIME_WAIT(接收到FIN包,发送ACK包)
    4. TIME_WAIT -> CLOSED(等待2MSL)
  • 被动关闭方状态转换

    1. ESTABLISHED -> CLOSE_WAIT(接收到FIN包,发送ACK包)
    2. CLOSE_WAIT -> LAST_ACK(发送FIN包)
    3. LAST_ACK -> CLOSED(接收到ACK包)

4)调用 close 函数

当应用程序调用 close 函数时,以下过程会发生:

  1. 回收文件描述符

    • close 函数首先回收文件描述符,使其不再指向任何打开的文件或套接字。
    • 释放文件描述符占用的系统资源。
  2. 发送FIN包

    • 对于TCP套接字,close 函数会触发TCP协议发送FIN包,开始四次挥手过程。

5)详细的状态解释

  • FIN_WAIT_1

    • 主动关闭方发送FIN包后进入此状态,等待对方的ACK包。
  • FIN_WAIT_2

    • 主动关闭方接收到ACK包后进入此状态,等待对方的FIN包。
  • TIME_WAIT

    • 主动关闭方接收到对方的FIN包后进入此状态,发送ACK包,并等待2MSL,以确保对方接收到ACK包,防止旧连接的数据包影响新连接。
  • CLOSE_WAIT

    • 被动关闭方接收到FIN包并发送ACK包后进入此状态,等待应用程序处理完所有未发送的数据并调用 close 函数。
  • LAST_ACK

    • 被动关闭方调用 close 函数发送FIN包后进入此状态,等待对方的ACK包。
  • CLOSED

    • 双方都完成关闭过程,连接彻底断开,所有资源释放。

在TCP连接中,如果双方几乎同时调用close函数,会发生一种被称为"同时关闭"(simultaneous close)的情况。这种情况实际上也是TCP协议允许的,并且被设计为能够正确处理。具体过程如下:

6)同时关闭的四次挥手过程

  1. 双方同时发送FIN包

    • 主动关闭方A和主动关闭方B几乎同时调用close,并发送FIN包。
    • 双方进入FIN_WAIT_1状态。
  2. 双方接收对方的FIN包,并发送ACK包

    • 双方接收到对方的FIN包,进入CLOSING状态。
    • 双方发送ACK包。
  3. 双方接收对方的ACK包

    • 双方接收到对方的ACK包,进入TIME_WAIT状态。
  4. 等待2MSL,进入CLOSED状态

    • 双方在TIME_WAIT状态等待2倍的最大报文段生存时间(2MSL),以确保对方接收到ACK包并避免旧连接的数据包干扰新连接。
    • 超时后,双方进入CLOSED状态,完成连接关闭。

7)状态转换

  • 双方状态转换
    1. ESTABLISHED -> FIN_WAIT_1(发送FIN包)
    2. FIN_WAIT_1 -> CLOSING(接收到对方的FIN包,发送ACK包)
    3. CLOSING -> TIME_WAIT(接收到对方的ACK包)
    4. TIME_WAIT -> CLOSED(等待2MSL)

4、tcp的p2p实现

TCP的P2P(Peer-to-Peer)连接是指两台计算机(或节点)直接相互通信,而不是通过一个中介服务器。P2P连接通常用于文件共享、即时通信和分布式计算等应用。

在一个P2P系统中,每个节点既可以作为客户端发送请求,也可以作为服务器接收请求。因此,P2P通信需要处理两种情况:发起连接和接受连接。

1)P2P连接的基本概念

  1. 节点(Node)

    • 在P2P网络中,每个设备或程序都是一个节点。
    • 节点既可以充当客户端(发起连接),也可以充当服务器(接受连接)。
  2. 连接建立

    • 两个节点都需要知道对方的IP地址和端口号。
    • 每个节点都需要监听一个端口,以接受来自其他节点的连接请求。
  3. 数据传输

    • 节点之间可以通过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;
}
解释
  1. 服务器线程

    • 使用 pthread_create 创建一个新线程来监听连接请求。
    • server_thread 函数负责创建服务器套接字,绑定端口并监听连接。当有连接请求时,接受连接并处理数据。
  2. 客户端连接

    • 主线程调用 connect_to_peer 函数,使用套接字连接到对等节点(由命令行参数提供的IP地址)。
    • 连接成功后,发送和接收数据。
  3. 主程序

    • 检查命令行参数,确保提供了对等节点的IP地址。
    • 创建服务器线程,并连接到对等节点。
    • 等待服务器线程结束。
如何运行

假设有两台计算机A和B:

  1. 在A上运行:
    ./p2p_program <B的IP地址>
    
  2. 在B上运行:
    ./p2p_program <A的IP地址>
    

3)总结

  • P2P连接:每个节点既作为客户端发起连接请求,也作为服务器接受连接请求。
  • TCP通信:通过创建套接字、绑定端口、监听连接、接受连接和发送/接收数据实现。
  • 多线程:使用多线程来同时处理发起连接和接受连接的操作。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值