先来结论:
- 监听套接字的socket buffer只接受TCP连接请求过程中的syn和ack数据;
- 已连接套接字的socket buffer主要存储的内容是两端传输的“正式数据”;
- 两种套接字通过不同的四元组(其中客户端地址或端口不同)进行分辨
服务端连接过程详解
/* 服务器 */
lfd=socket(..,SOCK_STREAM,..);
bind(lfd,srv_addr,srv_port);
/* 五元组中三元组成型: {protocal,src_addr,src_port}*/
listen(lfd);
cfd=accept(lfd,client_addr,client_port); /* 生成已连接套接字 */
/* 五元组成型: {protocal,src_addr,src_port,client_addr,client_port} */
/* 客户端 */
lfd=socket(..,SOCK_STREAM,..);
bind(lfd,srv_addr,srv_port);
connect(srv_addr,srv_port);
/* 五元组成型: {protocal,src_addr,src_port,client_addr,client_port} */
/* 发送数据 */
socket
操作系统会创建一个由文件系统管理的socket对象, 包含发送缓冲区、接收缓冲区和等待队列等成员。此时还未绑定端口号和ip地址。
bind
给指定socket对象绑定“addr:port”,形成三元组{protocal,src_addr,src_port}
listen
主要流程及数据结构
该函数用于监控监听描述符状态,监听描述符五元组如下{protocal,src_addr,src_port,*,*}
,此处*
代表通配符;
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】)。
若客户端发送数以万计的类似请求,会造成监听者崩溃,网卡阻塞。
解决方案:
- 缩小listen()维护的两个队列大小,减少重发ack+syn的次数
- 增大重发时间间隔
- 减少收到ACK的等待超时时间
accept
读取accept_queue中的第一项,并对此项生成一个用于后续连接的套接字描述符
当监听者发起accept
系统调用时,若accept_queue
为空,则监听者被阻塞。
若将套接字设置为非阻塞模式,accept
会在得不到数据时返回EWOULDBLOCK
或EAGAIN
错误。
同步连接与异步连接
通俗的来说,同步链接即一个进程/线程处理一个连接。 异步连接即一个线程/进程处理多个连接。
同步连接
从监听到某个客户端发起SYN请求后直到连接关闭之前,中间不会接受其他客户端连接请求
通常以同步连接方式处理请求时,监听者工作者常为同一个进程/线程
异步连接
异步连接则可以在建立连接和数据交互的任何一个阶段接收、处理其他连接请求。通常,监听者和工作者不是同一个进程时使用异步连接的方式。
TCP连接和套接字的关系
每个TCP连接的两端会关联如下信息:
- 一个套接字
- 与套接字绑定的文件描述符fd
我们通过分析服务器建立连接的流程来分析两者之间的关系;
accept
之前
客户端与服务器“三次握手”完成后,新生成的tcp连接会存入accept_queue
中,等待accept
消费;此时该tcp连接,在服务端绑定的信息为:监听套接字,即与该套接字对应的**listen_fd**
accept
之后
服务端创建了一个新的套接字,及其对应的描述符conn_fd
,该tcp连接重新与新套接字建立连接;
经过**accept**
后,这个tcp连接已与监听套接字无关。
故由上可知,在accept
之前便已经存在tcp通信了,只不过listen
仅处理三次握手涉及的数据。若客户端与服务器之间需要传输数据,还需要通过accept
创建新套接字。
close
与shutdown
区别
简单来说,close对文件描述符引用计数减一,不一定会关闭文件描述符; shutdown则直接掐断该文件描述符所有连接,从而引发四次挥手过程
shutdown可指定三种关闭方式:
- 关闭写。此时将无法向send buffer中再写数据,send buffer中已有的数据会一直发送直到完毕。
- 关闭读。此时将无法从recv buffer中再读数据,recv buffer中已有的数据只能被丢弃。
- 关闭读和写。此时无法读、无法写,send buffer中已有的数据会发送直到完毕,但recv buffer中已有的数据将被丢弃。
无论是shutdown()还是close(),每次调用它们,在真正进入四次挥手的过程中,它们都会发送一个FIN。
总结
监听进程调用listen,仅处理三次握手涉及的数据;三次握手成功后,调用accept创建新的套接字及对应的描述符,该tcp连接服务端关联的套接字由监听套接字转向已连接套接字,开始数据传输等工作。
即监听套接字与已连接套接字,在不同时段,共享过同一个TCP连接,以处理TCP连接中的不同工作。