客户端和服务器之间通过tcp协议来通信。需要使用很多POSIX api

客户端:

        socket() bind() conect  send recv close

服务器:

        socket bind listen accept recv send close

使用到的的io多路复用是epoll。对应的接口是epoll create、epoll ctl、epoll wait。fcntl

一、建立连接

使用到的有 socket bind listen。

1.socket:

        内部由两部分组成。分配int型的fd(套接字描述符)。tcp控制块。

它的原型定义在 <sys/socket.h> 头文件中

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

(1) domain —— 协议族/地址族

指定套接字使用的通信域(地址格式)。常见的取值有:

常量 含义
AF_INET IPv4 网络通信,地址结构为 struct sockaddr_in
AF_INET6 IPv6 网络通信,地址结构为 struct sockaddr_in6

总之就是设置协议族。

(2) type —— 套接字类型

指定通信的语义/服务类型。常见取值:

常量 含义
SOCK_STREAM 流式套接字,提供可靠的、面向连接的字节流服务。底层通常使用 TCP。保证数据顺序、无丢失、无重复。
SOCK_DGRAM 数据报套接字,提供无连接的、不可靠的消息服务。底层通常使用 UDP。数据可能乱序、丢失、重复,但保留了消息边界。

就是设置用哪个协议进行通信。代码中使用的是tcp。

(3)protocol —— 协议号

指定具体的传输协议,大多数情况下设为 0,表示让系统自动选择合适的默认协议

设置完之后,会返回一个int型的fd。也就是套接字描述符。socket() 本身只是创建一个通信端点,此时它还未绑定地址、未连接

总结

维度 内容
主要作用 创建一个套接字对象,返回文件描述符,用来后续接收客户端的链接。
三个参数 domain(协议族)、type(服务类型)、protocol(具体协议,多数为0)
核心产出 一个通信端点,可以是 TCP、UDP、原始包、本地通信等不同形态。
后续配合 bindlistenacceptconnectsend/recv 等系统调用。

2.bind:

        将套接字与一个本地地址(IP + 端口或文件路径)绑定,使得系统知道该套接字从哪个地址接收数据。将socketfd绑定在本地上的端口。记事把服务员绑定在饭店门口

函数原型:

        

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

把sockfd绑定在 本地addr上。

3.listen:

        就是让sockfd正式上岗。

函数原型:

        

#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数

  • sockfd:由 socket() 返回,并已经通过 bind() 绑定了本地地址的套接字描述符。指定让哪一个sockfd开始工作

  • backlog:定义已完成连接队列的最大长度。

返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno

1. sockfd —— 待转换的套接字

  • 这个套接字必须是 SOCK_STREAM 或 SOCK_SEQPACKET 类型。

  • 在调用 listen() 之前,通常已经调用了 bind()

2. backlog —— 连接队列大小

backlog 定义了套接字等待连接队列的最大长度。对于 TCP 而言,系统通常维护两个队列:

  • 未完成连接队列(SYN_RCVD 状态)半连接状态:客户端发送 SYN,服务端收到后等待客户端 ACK,这个队列长度由系统全局限制决定,backlog 对它影响较小。

  • 已完成连接队列(ESTABLISHED 状态):三次握手已完成,等待 accept() 取走的连接。backlog 主要限制这个队列的大小。此时内核里面已经建立好连接了。accpet只是把链接拿走而已。并不是accept接收了连接。

当已完成连接队列满时,新的连接请求的处理行为依赖于系统:Linux 会忽略新的 SYN,客户端会重试;某些系统可能直接拒绝连接(发送 RST)。

listen() 与 accept() 的关系

  • listen() 是准备工作:告诉内核开始接受连接,但并不阻塞。

  • accept() 是实际从已完成队列中取出一个连接(若队列为空则阻塞,除非设置为非阻塞)。

可以把 listen() 理解为“启动电话交换机的接线员”,accept() 是“接听一个来电”。listen是真正干活的。把链接处理完,accpet只是拿走而已。

4.tcp三次握手,调用那些函数?

一、客户端(主动打开连接)

客户端调用 connect() 函数,该函数会:

  1. 立即向服务端发送 SYN 报文(第一次握手)。

  2. 等待服务端回复 SYN+ACK(第二次握手)。

  3. 收到后,内核自动回复 ACK(第三次握手)。

  4. 当三次握手完成,connect() 返回(成功返回 0,失败返回 -1)。

二、服务端(被动打开连接)

服务端在握手前必须先准备好监听套接字,调用顺序为:

  1. socket() – 创建套接字。

  2. bind() – 绑定地址和端口。

  3. listen() – 将套接字转为被动监听状态,并分配连接队列。

上述三个函数完成之后,内核开始能够处理发往该端口的 SYN 请求。

当客户端 SYN 到达时,内核自动进行以下操作(不涉及用户函数调用)这是tcp自动完成的,不涉及到各种函数:

  • 回复 SYN+ACK(第二次握手)。

  • 将连接放入“未完成连接队列”(SYN_RCVD)。

当收到客户端的 ACK(第三次握手)后,连接转入“已完成连接队列”(ESTABLISHED),并等待应用程序取走。

服务端通过调用 accept() 函数从已完成队列中取出一个已经完成三次握手的连接:

  • 如果队列为空,accept() 会阻塞(同步模式)或返回 -1(非阻塞模式)。

  • accept() 返回一个新的已连接套接字描述符,用于与该客户端通信。

图标流程如下:

        客户端                                              服务端
------                                                           ------
socket()                                                    socket()
                                                                 bind()
                                                                     listen()     --> 进入监听状态
connect()           -------- SYN (1) --------->           (内核接收并处理)
(内核发SYN)      <-------- SYN+ACK (2) -------    内核自动发SYN+ACK
(内核收ACK)      -------- ACK (3) --------->         连接进入已完成队列
connect() 返回                          accept()  <--          从队列取出连接,返回
                                                               ( accept返回后即可通信)

总结

角色 核心函数 作用
客户端 connect() 触发三次握手的开始(发送 SYN 并等待完成)。
服务端 listen() 使内核准备好接收连接请求(不参与握手报文,是前置条件)。
服务端 accept() 取出已经完成三次握手的连接(被动等待,不发送握手包)。

三次握手中实际发送报文的动作完全由内核自动完成,应用程序只需调用上述函数来同步或等待握手的完成。

tcp生命周期从什么时候开始?

        TCP 连接的生命周期从第一次握手的 SYN 发出开始

第三次握手后,如何从半连接队列查找匹配的节点?

        在 TCP 三次握手中,第三次握手是客户端发送的 ACK 报文。服务端收到这个 ACK 后,根据报文的 五元组(源 IP、源端口、目的 IP、目的端口、协议)从 半连接队列(syn queue) 中查找对应的半连接节点(request_sock),然后将其移入全连接队列(accept queue),等待应用程序调用 accept() 取出。

如何避免syn泛红?

启用SYN Cookies。不存储半连接状态,而是将连接信息编码成Cookie随SYN/ACK发回,只有当客户端回复正确的ACK且通过Cookie验证后,才为其分配资源。此机制确保了即便半连接队列满了,仍能为合法请求提供服务。也就是半连接队列里都是真正的请求连接。

减轻连接压力。限制一个监听(listen())端口上已完成握手但尚未被应用程序accept()的连接数。适当增加此值有助于应对突发的高并发请求。也就是限制全连接队列的大小。

二、tcp数据传输

        传输过程由内核自动完成。主要使用的有拥塞控制,超时重传,延迟确认,滑动窗口,慢启动。

三、tcp断开连接

        断开连接时,tcp有好多种状态。

状态 含义
ESTABLISHED 正常数据传输状态
FIN_WAIT_1 主动关闭方,发完 FIN,等待 ACK
FIN_WAIT_2 主动关闭方,收到 FIN 的 ACK,等待对方 FIN
CLOSE_WAIT 被动关闭方,收到对方 FIN,回复 ACK
LAST_ACK 被动关闭方,自己发 FIN,等待对方最后 ACK
TIME_WAIT 主动关闭方,收到对方 FIN,回复最后 ACK,等待 2MSL
CLOSED 完全断开

场景一:正常单方关闭(最常用)如果a发起断开,b接收:

  1. 第一次挥手A 业务结束,调用 close(),向 B 发送 FIN 报文,A 进入 FIN_WAIT_1。含义:A 不会再发数据给 B

  2. 第二次挥手B 收到 FIN,立即回复 ACK 确认,B 进入 CLOSE_WAIT。含义:B 知道 A 不发数据了,但 B 还可以继续给 A 发剩余数据(半关闭状态)。A 收到这个 ACK → 进入 FIN_WAIT_2

  3. 第三次挥手B 所有数据发送完毕,业务调用 close(),向 A 发 FIN,B 进入 LAST_ACK。含义:B 也不会再发数据给 A

  4. 第四次挥手A 收到 B 的 FIN,回复 ACK,A 进入 TIME_WAIT;B 收到最后这个 ACK → 直接进入 CLOSED。A 等待 2MSL 确保报文全部消散,再进入 CLOSED

核心:两次单向关闭 + 两次 ACK 确认 = 四次挥手

场景二:双方同时调用 close ()(同时断开)

逐步拆解状态变化

  1. 初始A、B:ESTABLISHED

  2. 双方同时发 FIN

  • A 发 FIN → A:FIN_WAIT_1
  • B 发 FIN → B:FIN_WAIT_1
  1. 双方各自收到对方的 FIN
  • A(FIN_WAIT_1)收到 B 的 FIN:立即应答 ACK,A 从 FIN_WAIT_1CLOSING
  • B(FIN_WAIT_1)收到 A 的 FIN:立即应答 ACK,B 从 FIN_WAIT_1CLOSING
  1. 双方收到自己那条 FIN 的 ACK
  • A 在 CLOSING 收到 B 发来的、确认 A-FIN 的 ACK→ A:CLOSINGTIME_WAIT
  • B 在 CLOSING 收到 A 发来的、确认 B-FIN 的 ACK→ B:CLOSINGTIME_WAIT
  1. 收尾两端都维持 TIME_WAIT 2MSL,确保网络中残留报文消失、最后 ACK 可靠送达,之后双双进入 CLOSED

4. 核心特点

  1. 全程没有FIN_WAIT_2CLOSE_WAITLAST_ACK
  2. 报文依旧是 4 个:FIN、FIN、ACK、ACK,本质还是四次挥手
  3. CLOSING 状态专属场景:只有「双方同时主动断连」才会出现
  4. 两端都是主动关闭方,都需要走 TIME_WAIT

场景三:A 先发 FIN,A 处于 FIN_WAIT1,对方 FIN 先到、对方 ACK 后到

步骤 1:A 主动发 FIN

  • A 调用 close,关闭上行写入,发 FIN
  • A 状态:ESTABLISHEDFIN_WAIT_1
  • 此时 A 的预期:等待 B 回复「ACK」确认这个 FIN

步骤 2:B 的 FIN 率先抵达 A(异常乱序点)

此时 A 还停留在 FIN_WAIT_1还没收到 B 的 ACK,反而收到了 B 发来的 FIN。

TCP 标准行为:

当主机处于 FIN_WAIT_1 状态,收到对端 FIN:

  1. 立即发送 ACK 确认对方的 FIN
  2. 状态直接切换为 CLOSING

步骤 3:A 进入 CLOSING 状态

  • CLOSING 含义:我发过 FIN、对方也发了 FIN,双方都要关闭,只差收到我自己 FIN 的 ACK
  • 此时连接属于「双向同时关闭中」的中间态

步骤 4:迟到的 ACK 终于到达 A

这个 ACK 是:B 用来确认 A 最开始发的那条 FIN 的应答。A 现在处于 CLOSING,收到这个迟到 ACK:

  • 无冲突、不丢弃、不报错
  • 满足了「收到自身 FIN 的 ACK」条件
  • A 状态:CLOSINGTIME_WAIT

步骤 5:最终收尾

  • A:TIME_WAIT 保持 2MSL,防止最后报文丢失、旧报文残留
  • B:收到 A 的两次 ACK 后,正常从 LAST_ACK 进入 CLOSED

4. 该场景核心规则

  1. FIN_WAIT_1 收到 对端 FIN → 切 CLOSING
  2. CLOSING 收到 自己 FIN 的 ACK → 切 TIME_WAIT
  3. 报文乱序(FIN 比 ACK 先到)是 TCP 设计内兼容的,不会崩连接、不会死锁
  4. 区别:
    • 正常单关闭:FIN_WAIT1 → FIN_WAIT2 → TIME_WAIT
    • 本场景:FIN_WAIT1 → CLOSING → TIME_WAIT

总之就是:

A 在 FIN_WAIT1 没等到 ACK、先等到对方 FIN,立刻回 ACK 切 CLOSING;后续迟到的 ACK 到达,再从 CLOSING 转入 TIME_WAIT,正常关闭。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐