先来结论:

  • 监听套接字的socket buffer只接受TCP连接请求过程中的syn和ack数据;
  • 已连接套接字的socket buffer主要存储的内容是两端传输的“正式数据”;
  • 两种套接字通过不同的四元组(其中客户端地址或端口不同)进行分辨

服务端连接过程详解

  1. /* 服务器 */
  2. lfd=socket(..,SOCK_STREAM,..);
  3. bind(lfd,srv_addr,srv_port);
  4. /* 五元组中三元组成型: {protocal,src_addr,src_port}*/
  5. listen(lfd);
  6. cfd=accept(lfd,client_addr,client_port); /* 生成已连接套接字 */
  7. /* 五元组成型: {protocal,src_addr,src_port,client_addr,client_port} */
  8. /* 客户端 */
  9. lfd=socket(..,SOCK_STREAM,..);
  10. bind(lfd,srv_addr,srv_port);
  11. connect(srv_addr,srv_port);
  12. /* 五元组成型: {protocal,src_addr,src_port,client_addr,client_port} */
  13. /* 发送数据 */

socket

操作系统会创建一个由文件系统管理的socket对象, 包含发送缓冲区、接收缓冲区和等待队列等成员。此时还未绑定端口号和ip地址。

bind

给指定socket对象绑定“addr:port”,形成三元组
{protocal,src_addr,src_port}

listen

主要流程及数据结构

该函数用于监控监听描述符状态,监听描述符五元组如下
{protocal,src_addr,src_port,*,*},此处*代表通配符;
监听套接字与已连接套接字的区别 - 图1
listen维护了两个队列:连接未完成队列syn_queue(未完成三次握手)、连接已完成队列accept_queue(完成三次握手)
当某一客户端发SYN请求报文时,服务器回复SYN+ACK报文后,将该TCP连接置于未完成队列尾部,直到收到客户端发来的ACK,才将其移入accept_queue;
accept_queue中的连接等待accept函数来消费。

注意事项

listen函数的backlog参数,用于设置accept_queue的最大长度。但其长度也被/proc/sys/net/core/somaxconn(128)硬限制,即len(accept_queue)=min(backlog,somaxconn);

  • 当accept_queue满时,内核停止接受新请求,并停止将syn_queue的对象移入accept_queue

/proc/sys/net/ipv4/tcp``_max_syn_backlog :用于设置sys_queue的最大长度

  • 当syn_queue满时,监听者阻塞不再接受新请求

    syn flood问题

    客户端调用connect()时伪造源地址,频繁发送SYN请求,导致服务器无法收到第三次握手(由于源地址伪造原因,服务器发送的SYN+ACK无法递达客户端)

由于无法收到客户端回应的ACK,超时时间唤醒后,服务器会再次发送SYN+ACK回应(服务器将数据拷入SEND_BUFFER,继而通过DMA拷入网卡【拷入网卡为异步IO】)。
若客户端发送数以万计的类似请求,会造成监听者崩溃,网卡阻塞。

解决方案:

  1. 缩小listen()维护的两个队列大小,减少重发ack+syn的次数
  2. 增大重发时间间隔
  3. 减少收到ACK的等待超时时间

    accept

    读取accept_queue中的第一项,并对此项生成一个用于后续连接的套接字描述符

当监听者发起accept系统调用时,若accept_queue为空,则监听者被阻塞。
若将套接字设置为非阻塞模式,accept会在得不到数据时返回EWOULDBLOCKEAGAIN错误。

同步连接与异步连接

通俗的来说,同步链接即一个进程/线程处理一个连接。 异步连接即一个线程/进程处理多个连接。

同步连接

从监听到某个客户端发起SYN请求后直到连接关闭之前,中间不会接受其他客户端连接请求
通常以同步连接方式处理请求时,监听者工作者常为同一个进程/线程

异步连接

异步连接则可以在建立连接和数据交互的任何一个阶段接收、处理其他连接请求。通常,监听者和工作者不是同一个进程时使用异步连接的方式。

TCP连接和套接字的关系

每个TCP连接的两端会关联如下信息:

  • 一个套接字
  • 与套接字绑定的文件描述符fd

我们通过分析服务器建立连接的流程来分析两者之间的关系;

  • accept之前

客户端与服务器“三次握手”完成后,新生成的tcp连接会存入accept_queue中,等待accept消费;此时该tcp连接,在服务端绑定的信息为:监听套接字,即与该套接字对应的**listen_fd**

  • accept之后

服务端创建了一个新的套接字,及其对应的描述符conn_fd,该tcp连接重新与新套接字建立连接;
经过**accept**后,这个tcp连接已与监听套接字无关。
监听套接字与已连接套接字的区别 - 图2
故由上可知,在accept之前便已经存在tcp通信了,只不过listen仅处理三次握手涉及的数据。若客户端与服务器之间需要传输数据,还需要通过accept创建新套接字。

closeshutdown区别

简单来说,close对文件描述符引用计数减一,不一定会关闭文件描述符; shutdown则直接掐断该文件描述符所有连接,从而引发四次挥手过程

shutdown可指定三种关闭方式:

  1. 关闭写。此时将无法向send buffer中再写数据,send buffer中已有的数据会一直发送直到完毕。
  2. 关闭读。此时将无法从recv buffer中再读数据,recv buffer中已有的数据只能被丢弃。
  3. 关闭读和写。此时无法读、无法写,send buffer中已有的数据会发送直到完毕,但recv buffer中已有的数据将被丢弃。

无论是shutdown()还是close(),每次调用它们,在真正进入四次挥手的过程中,它们都会发送一个FIN。

总结

监听进程调用listen,仅处理三次握手涉及的数据;三次握手成功后,调用accept创建新的套接字及对应的描述符,该tcp连接服务端关联的套接字由监听套接字转向已连接套接字,开始数据传输等工作。
即监听套接字与已连接套接字,在不同时段,共享过同一个TCP连接,以处理TCP连接中的不同工作。