posix api

posix api是一套标准的操作系统接口规范,用于统一linux,unix系统中的进程,文件,网络等接口。

tcp与tcb的关系

在这里插入图片描述

fd操作的API

socket

socket()主要做了两件事情:

  • 分配一个文件描述符 fd,返回给用户空间作为访问句柄。
  • 分配并初始化一个 TCB(传输控制块),即struct tcp_sock,这个数据结构是未来完整描述和控制一条 TCP 连接的核心。刚创建时,它只标记了协议等基本信息,五元组等连接标识尚未确定,会在后续的 bind()connect()accept() 中逐步填充。

在内核中,文件描述符fd采用bitmap进行存储,比如当我们创建了要给数字为5的文件描述符,则bit位为5的索引的位置记为1,当我们对这个fd进行close()操作的时候,这个位置记录为0,并释放相应的资源。

bind

bind()函数将本地的源IP,源port写入到TCB控制块的结构体中。

在服务端,bind()函数是必须的,用于固定端口以及IP地址。

在客户端,bind()函数是可选的,如果不调用,connect() 时内核会自动选择合适的源IP和临时端口,再写入 TCB

建立连接

connect

connect()的作用是:

  • 客户端主动发起 TCP 三次握手
  • 发送携带初始序列号(ISN)的 SYN(我想和你建立 TCP 连接)报文
  • 通过 TCP 四元组与服务端建立连接关系
  • 并等待服务端返回 SYN+ACK

listen

listen()主要实现了如下功能:

  • 将 socket 状态设置为 LISTEN
  • 使该 socket 进入被动监听模式
  • 初始化连接请求队列(半连接队列与全连接队列)
  • 告诉 TCP 协议栈:
    后续允许接收客户端连接请求

半连接队列(SYN Queue)

对端发送syn信息的连接请求,syn queue存储的是处于 SYN_RECV 状态的 request_sock(半连接请求控制块),作用是暂存尚未完成 TCP 三次握手的连接请求。

半连接控制块中包含了如下信息:

源IP
目标IP
源端口
目标端口
ISN序号
MSS
窗口参数
TCP option
等...

全连接队列(Accept Queue)

存储的是已经完成三次握手的完整 TCP socket/TCB,作用是暂存等待应用层 accept() 获取的已建立连接。

建立一个完整的TCB控制块的过程中,通过源IP,目标IP,源端口,目标端口来定位到半连接队列syn queue中的半连接请求控制块,并将其moveAccept queue中,完成一个完整的TCB的创建,这时完成了TCP连接。

syn泛洪

SYN 泛洪就是:攻击者不断发送 SYN,但不完成三次握手,让服务器的“半连接队列”被占满,从而无法服务正常连接。

SYN Cookie解决syn泛洪

SYN Cookie 通过将连接状态编码进 SYN-ACK 的序列号中,使服务器在收到 SYN 时不分配半连接资源,而在收到合法 ACK 后再恢复连接状态,从而避免 SYN Flood 攻击导致的资源耗尽。

accept

  • 从**全连接队列(accept queue)**中取出一个已完成三次握手的连接(该连接已拥有自己的 TCB,五元组齐备,状态为 ESTABLISHED)。

  • 为这个连接分配一个新的文件描述符 fd,返回给用户进程,后续的读写操作都通过这个新 fd 进行。

    而原来的监听 fd(listen_fd)仍然保持在 TCP_LISTEN 状态,继续接收新的连接请求,其 TCB 和相关队列不变。

三次握手

TCP连接过程

在这里插入图片描述

三次握手中发送的Sequence number 是随机的,告知对方要发送的数据的编号。保证数据不丢失,不乱序。

状态转移图

在这里插入图片描述

状态的变化

在这里插入图片描述

发送数据

我们在用户态调用send() 时,内核通常会把用户态 buffer 中的数据拷贝到该 socket 的发送缓冲区(wmem);之后 TCP 协议栈根据拥塞控制、流量控制、MSS、网卡队列等条件异步地把数据发送到网络。对端内核收到 TCP 数据后,会按序重组并缓存到该 socket 的接收缓冲区(rmem);应用调用recv()时,再把接收缓冲区中的数据拷贝到用户态 buffer。

在这里插入图片描述

需要注意的是:

send() 成功返回,并不代表数据已经到达对端,也不代表对端应用已经 recv() 到了。它通常只表示:数据已经成功从用户态拷贝进本机内核发送缓冲区,或者至少部分数据被接收进内核缓冲区。

另外,TCP 是字节流协议,不保留消息边界。比如你调用两次:

send(fd, "hello", 5, 0); 
send(fd, "world", 5, 0); 

对端可能一次 recv() 到:

helloworld 

也可能分多次收到:

hel 
lowor
ld 

所以不能认为一次 send() 对应一次 recv()。

send() 不是“发送完成”,而是“交给内核发送”;recv() 也不是“从网络直接读”,而是“从内核接收缓冲区读”。

如何查看最大传输单元mtu:
在这里插入图片描述

断开连接

close

close(sockfd)先让 fd 失效;如果这是 socket 的最后一个引用,内核默认会把剩余数据发完,然后发送 FIN,进入 TCP 关闭流程,最后释放资源。

close()函数主要做了这几件事情:

  • 回收文件描述符fd

  • 如果发送缓冲区还有数据,TCP 会尽量把它们发出去。

  • 发送FIN报文,FIN 表示:本端不会再发送数据了。

  • 本端 TCP 进入关闭状态机
    例如主动关闭方会进入:

    ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
    

四次挥手

在这里插入图片描述

TIME_WAIT存在的解释

  • 一是防止旧连接残留报文在新连接中造成数据污染,通过等待2MSL(Maximum Segment Lifetime × 2)的事件来让就旧连接的“所有残留报文”在网络中自然消失
  • 二是保证最后一个 ACK 丢失时可以重传,从而确保 TCP 四次挥手的可靠完成。

两边同时发起close

在这里插入图片描述
在 TCP simultaneous close 场景下,由于双方几乎同时发送 FIN 并完成确认,没有明确的主动关闭方,因此双方都可能进入 TIME_WAIT 状态,这是 TCP 协议允许的正常现象。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐