我们在网络编程时,一般过程是创建套接字,然后绑定端口,然后开始监听,那么这个所谓的监听数量为什么一般很小,但是能接收很多连接,恩,后来才知道不在一个过程上,下面先从listen函数看起。
listen(socket, backlog);
socket backlog定义内核监听队列的最大长度。
内核为任何一个给定的监听套接字维护两个队列:
1) 未完成连接队列。每个这样的SYN分节对应其中一项:已由某个客户端发送到服务器,服务器等待完成三次握手过程。这些套接字处于SYN_RECV状态。
2) 已完成连接状态队列。处于ESTABLISHED状态。
查看listen监听队列内容 netstat -nt | grep 端口号
syncookies功能,它控制着系统内核ipv4参数修改是否生效。如果启动,那么内核参数修改无效。
$cat /proc/sys/net/ipv4/tcp_syncookies 查看是否开启
$echo 0 > /proc/sys/net/ipv4/tcp_syncookies 通过echo禁用syncookies
$cat /proc/sys/net/ipv4/tcp_max_syn_backlog
查看内核半连接状态的socket上限是多少
$echo 2 > /proc/sys/net/ipv4/tcp_max_syn_backlog 修改该内核半连接socket上限为2
$echo 2 > /proc/sys/net/core/somaxconn 通过echo修改该内核完全连接socket上限为2
通过上面的查看套接字状态可以发现处于连接状态的总是该参数值+1。
如果backlog设置的小,就按backlog来,设置的大,按/proc/sys/net/core/somaxconn为准。
服务器完成套接字创建,绑定端口后,内核中维持了 SYN 和 ACCEPT两个队列。
建立连接时,客户端向服务器发送SYN包,服务器将其加入到SYN队列中,并返回 SYN+ACK 包给客户端,一段时间后,客户端发来针对服务器的SYN包的ACK网络分组时,内核从SYN队列中取出连接,放到ACCEPT队列中。
而调用accpet()函数时,实际就是从ACCEPT队列中拿出套接字而已。
两个队列都是有上限的,这个过程中,前两步的大小和运作速度都由内核决定,但是调用accept()取连接,却是用户可控的。但如果取的连接不及时,导致两个队列都满了,那么服务器将无法接收新连接。
所以backlog在此处表示的是最大完整的连接数。
发送数据过程:
1. 调用send()发送
2. 内核调用tcp_sendmsg()
3. 复制按MSS分组的数据到内核态,复制到内核中的sk_buff结构来存放,同时把这些分片组成队列,放到这个TCP连接对应的tcp_write_queue发送队列中。
4. 缓存不足,send_timed时间内等待有空闲的缓存,超时或有空闲的缓存时继续复制MSS分组到内核态,,内核态为这个TCP连接分配的内核缓存有限(/proc/sys/net/core/wmem_default),当没有多余的内核态缓存来复制用户态的待发送数据时,就需要调用sk_stream_wait_memory来等待滑动窗口移动,释放一些内存,即那些已被对方确认收到的数据。
5. 调用tcp_push方法, 按Nagle、慢启动等算法调用IP层来发送tcp_write_queue队列中的报文.
6. 发送方法返回(内核试图将消息发给对方,不确保发送到网络或发送给对方成功)
IP报文中IP报的长度字段为一个16位的字段,所以IP包最大为65535.
若TCP层在以太网中试图发送一个大于1500字节的消息,调用IP网络层方法发送消息时,IP层会自动的获取所在局域网的MTU值,并按照所在网络的MTU大小来分片。接收方根据收到的多个IP包的头部,将发送方IP分片出的IP包重组为一个消息。这种效率很低,因为其中任何一个片丢了,整个都要重发。
为了避免IP层的分片,TCP协议定义了一个新的概念:最大报文段长度为MSS。它定义了一个TCP连接上,一个主机期望对端主机发送单个报文的最大长度。TCP3次握手建立连接后,连接双方告知自己期望接收到的MSS大小。
但这个MSS值也不是固定的,因为这个值由对方主机告知,但是在发送过程中,如果对方主机太远,也就是说要通过许多中间网络才可达,而这些中间网络中有些的MTU值比较小,还是会导致中间路由器出现IP层的分片。
为了再防止中间路由分片,通过IP头部的DF标志位,这个标志位告诉IP报文所经过的所有IP层代码:不要对这个报文分片。
慢启动:
就是对方通告的窗口大小只表示对方接收TCP分组的能力,不表示中间网络能够处理分组的能力。所以,发送方请悠着点发,确保网络非常通畅了后,再按照对方通告窗口来敞开了发。
拥塞窗口就是下面的cwnd,它用来帮助慢启动的实现。连接刚建立时,拥塞窗口的大小远小于发送窗口,它实际上是一个MSS。每收到一个ACK,拥塞窗口扩大一个MSS大小,当然,拥塞窗口最大只能到对方通告的接收窗口大小。当然,为了避免指数式增长,拥塞窗口大小的增长会更慢一些,是线性的平滑的增长过程。
下面来看一下接收方法:
1.首先是PC上的网卡接收到网线传来的报文。通过软中断内核并且解析其为TCP报文,然后TCP模块决定如何处理这个TCP报文。
2.用户进程调用read,recv等获取TCP消息,则是将网卡上的消息拷贝到用户进程里的内存里。
接收阙值:SO_RCVLOWAT 1
内核在处理接收到的TCP报文时使用了4个队列容器,分别为receive, out_of_order, prequeue, backlog队列。假设此时共有报文S1-S2, S2-S3, S3-S4.
1.网卡接收到报文并判断为TCP协议,内核调用tcp_v4_rcv方法,此时TCP连接上需要接收的下一个报文是S1,正好,所以tcp_v4_rcv方法直接将其插入到receive队列。(recieve队列中是已经去除了TCP头部,排好序放入的,用户进程可直接按序读取的队列)
2.接着,按道理应该收到S2-S3报文,但是我们收到的是S3-S4,此时进入out_of_order队列。
3.只要out_of_order不为空,那么只要往receive加入一个报文,就检查一遍out_of_order,如果刚好是下一个需要的,将其加入到receive队列中。
4.上面都是TCP报文到达网卡后怎么往内核分配的队列中放,此时有用户进程来从socket读取数据,用户调用read()或recv(),内核调用tcp_recvmsg方法,这个方法先锁住socket,因为socket是可以被多进程同时使用的,同时内核中断也会操作它。所以需要锁住来拿。至于拿走数据后socket删除拿走数据还是留下,则取决于系统调用函数的参数设置。
5.拷贝读到的数据到用户内存。
6.如果此时进程整在处理数据,但是有网卡上有报文来了,则加入到backlog队列中。
7.用户代码开始执行,此时recv返回从内核读到的字节数。
这个过程中,receive,out_of_order,backlog都用到了,那还有prequeue呢。。。
假如,,我是说假如这四个队列都没有数据,但是进程过来读了。。先锁住socket,获得最低接收阙值,结果队列都为空,已拷贝字节为0,进程休眠。此时,网卡接收到报文S1-S2,插入prequeue队列,激活休眠的用户进程。来read报文。