linux 网络协议api

前言
在前面的实验中,我们使用了socket中多个api:
客户端:
1、socket(); 2、bind() //可选 3、connnect() //udp也有 4、send() / recv(); 5、close()

服务器:
1、socket(); 2、bind(); 3、listen(); 4、accept(); 5、epoll_create epoll_ctl epoll_wait fcntl; 6、send()\recv() 7、close()

建立连接的过程

  • 关于socket 它直译为 插座, 网络io连接到 fd --tcp control block;;
    功能:1、分配一个文件描述符fd: 每个进程有一个 files_struct,其中 fdtable 包含一个位图(bitmap)管理 fd 使用情况:0 表示空闲,1 表示占用。位图中从小到大扫描,找到第一个为 0 的位,即找到一个未使用的fd;
    2、初始化tcb, 并进行alloc();

  • 关于bind
    1、通过fd找到 tcb。
    2、将用户传入的 IP 和端口写入 inet_sock 的 inet_saddr / inet_sport 字段。
    检查端口是否冲突(除非设置了 SO_REUSEADDR)。

  • 关于listen
    1、将TCP起始状态改为listen tcb->status = TCP_STATUS_LISTEN;
    image
    2、初始化两个队列:
    半连接队列(syn队列):存储 SYN_RCVD 状态的 request_sock。
    全连接队列(accept队列):存储 ESTABLISHED 状态的连接,长度受 min(backlog, net.core.somaxconn) 限制。

  • 补充三次握手
    image
    1、服务器处于LISTEN,客户端通过connect连接服务器,客户端TCP将发送一个SYN包;(客户端:CLOSED->SYN_SENT)
    2、收到SYN,服务器端存储到半连接队列,并回复一个ACK+SYN。(服务器:SYN_RCVD)
    3、客户端ACK服务器SYN。(客户端:SYN_SENT->ESTABLISHED),服务器接收到客户端ACK,将半连接的移动到全连接。(服务器:SYN_RECV->ESTABLISHED)

双方通过三次握手,实现全双工;都知道从一端到另一端的序列号SEQ从哪里开始,保证不重复、不乱序;;

  • 关键点:
    1、tcp的连接状态从半连接完成开始的;;协议栈会记录该连接;;
    2、第三次握手数据包,如何从半连接队列查找匹配的节点;半连接队列为了高效查找,使用了哈希表(listen_opt->syn_table),哈希键是客户端四元组(源IP、源端口、目的IP、目的端口)的哈希值。;
    3、早版本通过listen第二个参数backlog,限制半连接无限增长;它从旧版到新版分别可以表示syn队列长度、accept队列长度;现代更加关注,已经连接的情况即accept队列。
    image
    4、SYN泛洪:SYN Proxy(代理),压力转移到防火墙;限制单位时间内来自同一源IP的SYN包数量;丢弃超出阈值的SYN包,或对可疑源IP进行临时黑名单;

传输数据过程

  • 在内核中,网络层中每个发送的通信包裹数据的最大容量有限,它被设置在mtu中,可以通过命令行修改;假如客户端需要发送1400bit的数据,但是mtu == 1000; 网络层就会发两个包裹,数据大小分别是1000和400, 将应用层的一次发送与接收转换为多次发收。
    服务器在读取上,根据recv字节数的大小,存在多次读取的问题,从系统调用的角度,两者区别不大。如果接收缓冲区rmem相较于最大传输单元mtu比较小,比如2048B的rmem存了1000B数据,那分多次读取一次读100B,在第1次读取后,还有900B在缓冲区,假如又传来1000B,缓存区变成1900B,如果再有数据到来,缓冲区可能溢出(丢包或触发背压)。而如果rmem大就几乎不存在这个问题。实际中,接收缓冲区大小由 SO_RCVBUF 或内核参数 tcp_rmem 决定,且 TCP 有流量控制来防止溢出。

TCP为了平衡传输效率和可靠性;提出慢启动和拥塞控制的两个概念:
TCP 发送方维护两个窗口:
接收窗口(rwnd):对端通告的可用缓冲区大小,用于流量控制。
拥塞窗口(cwnd):发送方自己估计的网络容量,用于拥塞控制。

  • TCP 慢启动:慢启动通过渐进式增加发送速率,在避免拥塞的前提下快速逼近网络的承载能力。初始连接建立时,cwnd 初始化为一个很小的值(现代 Linux默认为10个MSS[Maximum Segment Size]))。慢启动阈值(ssthresh)初始化为一个较大值(如 rwnd 或 65535 字节)。 指数增长阶段,每收到一个 ACK,cwnd增加1个MSS(即每RTT[Round-Trip Time] 翻倍)。
    何时结束慢启动?
    有两种情况会退出慢启动,进入拥塞避免阶段:
    情况一:达到慢启动阈值(ssthresh)
    当 cwnd >= ssthresh 时,切换到拥塞避免模式,cwnd 改为线性增长(每收到cwnd个ack, cwnd增加 1 MSS)。
    情况二:发生丢包(检测到拥塞)
    超时重传(RTO):在 RTO 时间内未收到其 ACK,ssthresh = cwnd / 2,cwnd = 1 MSS,重新开始慢启动。
    快速重传(3 个重复 ACK):发送方连续收到 3 个(或以上)针对同一个数据包的重复 ACK。如接收方收到包 1,回复 ACK 2(期待收到包 2)。但包 2 丢失,接下来收到包 3、4、5。接收方每收到一个后续的乱序包(包 3、4、5),都会重复回复 ACK 2(即期待收到包 2 的确认)。ssthresh = cwnd / 2,cwnd = ssthresh + 3 MSS,然后进入快速恢复(Fast Recovery)阶段(实际上是拥塞避免的一种变体);

  • TCP 流量拥塞控制
    TCP 使用滑动窗口(接收窗口 rcv_wnd)来防止发送方淹没接收方。接收窗口的大小 ≈ 接收缓冲区剩余空间。
    当用户读取数据后,内核会更新 rcv_wnd 并通告给对端。
    如果用户读取慢(分多次),rcv_wnd 缩小,对端会暂停发送,直到窗口重新扩大。
    因此,缓冲区不会真正“溢出”——当剩余空间不足时,对端停止发送,新数据不会强行挤入。
    后果不是溢出,而是吞吐量下降:对端等待窗口更新,发送停滞,导致整体数据传输速率降低。在高延迟链路(如跨国网络)上,这种停滞影响尤其明显。

    TCP滑动窗口:核心作用是流量控制——告诉发送方“我还有多少缓冲区可用,你能发多少数据”。通告:接收方在每个 TCP 报文段的头部Window Size 字段)中携带当前的 rwnd 值,告知发送方。两个指针控制rmem中这三个区域|← 已接收已读取 →|← 已接收未读取 →|← 空闲缓冲区 →|。

现代化改进:选择性确认(SACK):接收方在 ACK 中携带 SACK 选项,告诉发送方哪些包已收到、哪些丢了。发送方可以精确重传丢失的包,而不是重传从丢失点开始的所有包。这极大提高了快速重传的效率。

断开连接

  • 四次挥手(不存在 客户端服务器,只有主动方和被动方, 和握手区分):
    image
    对应close():工作流程:1、回收fd; 2、置位fin;

  • 几种特殊情况:
    1、ack后与fin;之前单独的 ACK 可能丢失或延迟到达。
    B 发送的 FIN 报文中通常会携带对 A 的 FIN 的确认(即 ACK 序号 = A 的 FIN 序号 + 1)。
    A 收到这个 FIN 后,发现其中包含了对自身 FIN 的确认,就不再需要等待那个丢失的 ACK 了。
    因此,A 先收到了 FIN(内含确认),

2、双方同时close:
TCP协议也允许这样的同时关闭(simultaneous close),下图给出同时关闭过程
image

实现p2p的问题
1、47.109.196.34 端永远绑定失败,bind: Cannot assign requested address
47.109.196.34 是公网 IP,它并不在 Linux 服务器的网卡上 —— 云服务器的公网 IP 通常是 NAT 映射,网卡实际绑定的是内网 IP(如 172.x.x.x)。所以 bind() 到 47.109.196.34 会报 Cannot assign requested address。

修复方案:监听绑定 0.0.0.0,出站连接不绑定本地 IP。
2、只实现了单向的链接
你的场景中 Windows 的 IP 10.16.74.205 是内网私有 IP,Linux 的 47.109.196.34 是公网 IP。TCP 同时打开要求双方能相互发送 SYN 包:但是,Linux ──SYN──→ 10.16.74.205 ← 公网无法路由到内网,SYN 到不了!

3、当前场景,TCP实现困难, TCP更适合双方都有公网 IP 或同一局域网,尝试UDP打洞,
为什么需要"打洞"?假设两台电脑都在 NAT(路由器)后面:

VM1 (192.168.134.130)          VM2 (192.168.134.132)
      │                               │
      │ 私有IP                         │ 私有IP
  ┌───┴───┐                       ┌───┴───┐
  │  NAT  │                       │  NAT  │
  └───┬───┘                       └───┬───┘
      │                               │
      └─────────── 公网 ──────────────┘

问题: VM1 直接往 VM2 的私有 IP 发包,如果中间有 NAT 且没有端口映射,包会被丢弃——NAT 不知道该把包转发给谁。打洞的本质是让两端的 NAT 都"记住"对方,从而允许后续 UDP 包通过。

实现原理和流程:

时间 →
VM1                              VM2
 │                                │
 │──sendto("PUNCH")──────────────→│  (VM1发出第1个打洞包)
 │                                │  VM2收到 → 在select()中读到"PUNCH"
 │                                │  但此时VM2的打洞循环也在发"PUNCH"
 │←──────────────sendto("PUNCH")──│  (VM2发出打洞包)
 │  VM1收到 → select()返回       │
 │  recvfrom读到"PUNCH"          │
 │  打洞成功 break!               │
 │                                │
 │──sendto("PUNCH")──────────────→│  (VM1继续发包,因为循环还在)
 │                                │  VM2在select()中读到"PUNCH"
 │                                │  打洞成功 break!
 │                                │
 ════════════ 双向通道已建立 ═══════════

双方创建 UDP socket,bind 到本地 8080
各发 10 个 "PUNCH" 包给对方(间隔 200ms)
收到对方的 punch 包后打洞成功
进入双向聊天循环,select() 同时监听 socket 和 stdin
输入 /quit 退出

4、编译问题:info: relocation truncated to fit: R_X86_64_PC32 against symbol 'begin' defined in COMMON section in /tmp/

solution: compile add flag '-mcmodel=medium'
$ gcc -o reactor reactor.c -mcmodel=medium

posted @ 2026-07-27 17:22  超级麋鹿  阅读(9)  评论(0)    收藏  举报