不可不知的Socket和TCP连接过程
当未完成连接队列满了,监听者被阻塞不再接收新的连接请求,并通过select()/poll()等待两个队列触发可写事件。当已完成连接队列满了,则监听者也不会接收新的连接请求,同时,正准备移入到已完成连接队列的动作被阻塞。在Linux 2.2以前,listen()函数有一个backlog的参数,用于设置这两个队列的最大总长度,从Linux 2.2开始,这个参数只表示已完成队列的最大长度,而/proc/sys/net/ipv4/tcp_max_syn_backlog则用于设置未完成队列的最大长度。/proc/sys/net/core/somaxconn则是硬限制已完成队列的最大长度,默认为128,如果backlog大于somaxconn,则backlog会被截断为等于该值。 当连接已完成队列中的某个连接被accept()后,表示TCP连接已经建立完成,这个连接将采用自己的socket buffer和客户端进行数据传输。这个socket buffer和监听套接字的socket buffer都是用来存储TCP收、发的数据,但它们的意义已经不再一样:监听套接字的socket buffer只接受TCP连接请求过程中的syn和ack数据;而已建立的TCP连接的socket buffer主要存储的内容是两端传输的"正式"数据,例如服务端构建的响应数据,客户端发起的Http请求数据。 netstat命令的Send-Q和Recv-Q列表示的就是socket buffer相关的内容,以下是man netstat的解释。 Recv-Q Established: The count of bytes not copied by the user program connected to this socket. Listening: Since Kernel 2.6.18 this column contains the current syn backlog.Send-Q Established: The count of bytes not acknowledged by the remote host. Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog. 对于监听状态的套接字,Recv-Q表示的是当前syn backlog,即已完成队列中当前的连接个数,Send-Q表示的是syn backlog的最大值,即已完成连接队列的最大连接限制个数; 对于已经建立的tcp连接,Recv-Q列表示的是recv buffer中还未被用户进程拷贝走的数据大小,Send-Q列表示的是远程主机还未返回ACK消息的数据大小。之所以区分已建立TCP连接的套接字和监听状态的套接字,就是因为这两种状态的套接字采用不同的socket buffer,其中监听套接字更注重队列的长度,而已建立TCP连接的套接字更注重收、发的数据大小。 [root@xuexi ~]# netstat -tnlActive Internet connections (only servers)Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN tcp6 0 0 :::80 :::* LISTEN tcp6 0 0 :::22 :::* LISTEN tcp6 0 0 ::1:25 :::* LISTEN[root@xuexi ~]# ss -tnlState Recv-Q Send-Q Local Address:Port Peer Address:PortLISTEN 0 128 *:22 *:* LISTEN 0 100 127.0.0.1:25 *:* LISTEN 0 128 :::80 :::* LISTEN 0 128 :::22 :::* LISTEN 0 100 ::1:25 :::* 注意,Listen状态下的套接字,netstat的Send-Q和ss命令的Send-Q列的值不一样,因为netstat根本就没写上已完成队列的最大长度。因此,判断队列中是否还有空闲位置接收新的tcp连接请求时,应该尽可能地使用ss命令而不是netstat。 2.3.2 syn flood的影响 此外,如果监听者发送SYN+ACK后,迟迟收不到客户端返回的ACK消息,监听者将被select()/poll()设置的超时时间唤醒,并对该客户端重新发送SYN+ACK消息,防止这个消息遗失在茫茫网络中。但是,这一重发就出问题了,如果客户端调用connect()时伪造源地址,那么监听者回复的SYN+ACK消息是一定到不了对方的主机的,也就是说,监听者会迟迟收不到ACK消息,于是重新发送SYN+ACK。但无论是监听者因为select()/poll()设置的超时时间一次次地被唤醒,还是一次次地将数据拷入send buffer,这期间都是需要CPU参与的,而且send buffer中的SYN+ACK还要再拷入网卡(这次是DMA拷贝,不需要CPU)。如果,这个客户端是个攻击者,源源不断地发送了数以千、万计的SYN,监听者几乎直接就崩溃了,网卡也会被阻塞的很严重。这就是所谓的syn flood攻击。 解决syn flood的方法有多种,例如,缩小listen()维护的两个队列的最大长度,减少重发syn+ack的次数,增大重发的时间间隔,减少收到ack的等待超时时间,使用syncookie等,但直接修改tcp选项的任何一种方法都不能很好兼顾性能和效率。所以在连接到达监听者线程之前对数据包进行过滤是极其重要的手段。 2.4 accept()函数 accpet()函数的作用是读取已完成连接队列中的第一项(读完就从队列中移除),并对此项生成一个用于后续连接的套接字描述符,假设使用connfd来表示。有了新的连接套接字,工作进程/线程(称其为工作者)就可以通过这个连接套接字和客户端进行数据传输,而前文所说的监听套接字(sockfd)则仍然被监听者监听。 例如,prefork模式的httpd,每个子进程既是监听者,又是工作者,每个客户端发起连接请求时,子进程在监听时将它接收进来,并释放对监听套接字的监听,使得其他子进程可以去监听这个套接字。多个来回后,终于是通过accpet()函数生成了新的连接套接字,于是这个子进程就可以通过这个套接字专心地和客户端建立交互,当然,中途可能会因为各种io等待而多次被阻塞或睡眠。这种效率真的很低,仅仅考虑从子进程收到SYN消息开始到最后生成新的连接套接字这几个阶段,这个子进程一次又一次地被阻塞。当然,可以将监听套接字设置为非阻塞IO模式,只是即使是非阻塞模式,它也要不断地去检查状态。 再考虑worker/event处理模式,每个子进程中都使用了一个专门的监听线程和N个工作线程。监听线程专门负责监听并建立新的连接套接字描述符,放入apache的套接字队列中。这样监听者和工作者就分开了,在监听的过程中,工作者可以仍然可以自由地工作。如果只从监听这一个角度来说,worker/event模式比prefork模式性能高的不是一点半点。 (编辑:东莞站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |