posix API与网络协议栈

248 阅读5分钟

posix API与网络协议栈

scoket

socket包含两部分:fd、tcb(tcp control block)

其中,fd属于文件系统,可在用户态进行操控;而tcb属于内核协议栈

三次握手

20230117220414

服务端API

  1. socekt():创建一个tcb和fd(会将socketfd转换为listenfd)
    • fd最小为3,因为0、1、2号fd分别被stdin、stdout、stderr占了
    • 一个连接包括客户端socket和服务端socket,每个socket包括一个fd和一个tcb
  2. bind(): 为tcb绑定ip和端口
    • 两个特殊ip:0.0.0.0和127.0.0.1
    • 绑定0.0.0.0:socekt绑定多张网卡 (一张网卡一个ip,一个机器可能有多个网卡)
    • 绑定127.0.0.1:本地ip才能连接
  3. listen():指定全连接队列(或全连接+半连接队列的长度),把tcb 置为 listen 的状态。当客户端发送SYN包准备建立连接,服务端会init/add new tcb作为一个新的节点push in syn队列,此时服务端的状态由LISTEN转为SYN_RCVD, 并且服务端会发送SYN + ACK给客户端
  4. accept():当收到客户端的ACK后,此时会把syn队列中的tcb节点pop出来,再push in accept队列中,服务端状态由SYN_RCVD转为ESTABLISHED,三次握手完成。后续数据传输会从accept队列中取出tcb节点进行数据传输。并且accept为每个节点分配一个socket(conn_fd)
  5. recv/read(): 从读缓冲区读数据
  6. send/write(): 往写缓冲区写数据
  7. close(): 将FIN置为1,发送空包给对方请求关闭连接

客户端API

  1. socket():创建client_fd
  2. bind(): 可选,非必须
  3. connect():发起三次握手,若client_fd是阻塞的,则等待连接完成后返回;若client_fd是非阻塞的,则立即返回,如果连接成功,则返回可读/可写结果
  4. recv/read(): 从读缓冲区读数据
  5. send/write(): 往写缓冲区写数据
  6. close(): 将FIN置为1,发送空包给对方请求关闭连接

api小总结:

  1. socket和bind()都是本地操作
  2. listen()和accept()参与了三次握手,全连接和半连接队列示意图:
20230117221535
  1. 服务器的tcb通过bind()绑定本地(ip+port)、通过connect知道客户端(ip+port)

send相关问题

首先需注意不能通过send()>0来判断数据发送方成功了,因为send()只负责写协议栈,不负责发送数据。

无法send的情况

  1. 本地sendbuff满了
  2. 对方recvbuff空间不足,无法接收发送端的数据,此时内核协议栈会保留一个push的标志位,当有足够空间时,推给发送方进行发送

多次send时的粘包问题

比如我一次向sendbuff写1k的数据,共发送三次,但协议栈每次发送1.5k的数据,两次就发完了,就导致发送方发送了三次,但接收方只接收了两次,产生粘包现象

解决方法:

  • 包的协议头加包长度
  • 包尾加分隔符,如http就是加\r\n\r\n作为包结束标志

然而,这两种方法实现的前提都要求包是顺序到达的,就是说不能有中间包丢失,对此,tcp有一种延迟ack的超时重传机制:比如有序列号为1,2,3,4,5的5个包需要发送,接收端设置一个定时器(如设置200ms),每接收到一个包,就将定时器重置,当定时器到期后,发送ACK消息。

接收端:2包到达,重置定时器;1包到达,重置定时器;5包到达,重置定时器;3包到达,重复定时器;然后定时器过期,发送对2包的确认,也就是需要重传3,4,5包(乱序到达,接收方有重排机制恢复原有顺序)

这种延迟ack的方法有两个不好的点:

  • 实时性差:确认事件长,所以游戏都用UDP
  • 重传多,占用宽带:需要重传丢包以后的后续包,所以下载用UDP快 不过这种延迟ACK是可以关闭的,也有算法对次提出了改进,利用NACK只需要重传丢失的包即可

四次挥手

挥手只分主动方和被动方

20230118192429

五个状态:

  1. 主动方调用close(),发送一个fin为1的空包,然后进入fin_wait 1状态
  2. 被动方recv()返回0,发送对这个包的确认,进入close_wait()状态
    • 如果服务器出现大量的fin_wait_2,说明服务器没及时close(),因为业务逻辑太多了,服务器需要很长时间去处理
    • 解决办法:要么先close()再处理业务逻辑;要么将这些数据放到一个队列,交给线程池去处理(应用层和传输层解耦)。
  3. 主动方接收到ack,进入fin_wait_2状态
  4. 被动方调用close(),进入last_ack状态
  5. 主动方recv()返回0,发送对这个包的确认,进入time_wait状态
    • 这个状态会维持2MSL,因为怕这个ack丢失了,被动方会超时重传fin包,但由于主动方关闭连接了,一直无法响应,那服务器就一直关闭不了

close后的socket回收:

  • 被动方:发送fin包:fd被回收;收到ack包:tcb被回收
  • 主动方:发送fin包:fd被回收;time_wait到期:tcb被回收