老规矩,带着问题阅读:
三次握手中服务端做了什么?
为什么要将accept()单独一个线程而不是和读写的io线程共用一个线程池?
当调用send()返回后数据就一定到对方或者在网线中传输了呢?
1. TCP通信流程
服务端我们首先会创建一个监听套接字,然后给这个套接字绑定一个ip和端口,这一步对应的方法就是bind(),之后就是调用listen()来监听端口,端口是和应用程序对应的,网卡收到一个数据包的时候后需要知道这个包是给哪个程序用的,当然一个应用程序可以监听多个端口。之后客户端发起连接内核会分配一个随机端口,然后tcp在经历三次握手成功后,客户端会创建一个套接字由connect()方法返回,而服务端的accept()方法也会返回一个套接字,之后双方都会基于这个套接字进行读写操作。所以服务端会维护两种类型的套接字,一种用于监听,另一种用于和客户端进行读写。
而在linux内核中,socket其实是一个文件,挂载于SocketFS文件类型下,有点类似于/proc,不过该文件不能像磁盘上的文件一样进行正常的访问和读写。既然是文件,就会有inode来表示索引,有具体的地方存储数据不管是磁盘还是内存,而socket的数据是存储在内存中的,每个报文的数据是存放在一个叫 sk_buff 的结构体里,要访问文件我们一般会对应一个文件描述符,每个文件描述符都会有一个id。
2. 三次握手
linux内核中会维护两个队列,这两个队列的长度都是有限制且可以配置的,当客户端发起connect()请求后,服务端收到syn包后将该信息放入sync队列,之后客户端回复ack后从sync队列取出,放到accept队列,之后服务端调用accept()方法会从accept队列取出生成socket。
如果客户端发起sync请求,但是不回复ack,将导致sync队列满载,之后会拒接新的连接。如果客户端发起ack请求后,服务端一直不调用,或者调用accept队列太慢,将导致accept队列满载,accept队列满了则收到ack后无法从syn队列移出去,导致syn队列也会堆积,最终拒绝连接。所以服务端一般会将accept单独起一个线程执行,避免accept太慢导致数据丢弃。当然accept()方法也有阻塞和非阻塞两种,当accept队列为空的时候阻塞方法会一直等待,非阻塞方法会直接返回一个错误码。
3. 消息发送
连接建立好后,客户端和服务端都有一个socket套接字,双方都可以通过各自的套接字进行发送和接收消息,socket里面维护了两个队列,一个发送队列,一个接收队列。
发送的时候数据在用户空间的内存中,当调用send()或者write()方法的时候,会将待发送的数据按照MSS进行拆分,然后将拆分好的数据包拷贝到内核空间的发送队列,这个队列里面存放的是所有已经发送的数据包,对应的数据结构就是sk_buff,每一个数据包也就是sk_buff都有一个序号,以及一个状态,只有当服务端返回ack的时候,才会把状态改为发送成功,并且会将这个ack报文的序号之前的报文都确认掉,如果长期没有确认,会重新调用tcp_push继续发送,如果发送队列慢了,则从用户空间拷贝到内核空间的操作就会阻塞,并触发清理队列中已确认发送成功的数据包。tcp层会将数据包加上ip头然后发给ip层处理,ip层将数据包加入到一个qdisc队列,网卡驱动程序检测到qdisc队列有数据就会调用DMA Engine将sk_buff拷贝到网卡并发送出去,网卡驱动通过ringbuffer来指向内核中的数据,所以qdisc的长度也会影响到网络发送的吞吐量。
关于mss分段:mtu是数据链路层的最大传输单元,一般为1500字节,而一个ip包的最大长度为65535,所以ip层在发送数据前会根据mtu分片,这样一个tcp包本来对应一个ip包,分片后将对应多个ip包,每个包都有一个ip头,在接收端需要等到所有的ip包到达后,才能确定这个tcp收到然后才发送ack,这种方式无疑是低效的,所以tcp层会尽量阻止ip层进行分片,他会在从用户空间拷贝的时候就会按照mtu进行拆分,将一个数据包拆分成多个数据包。但是链路中mtu是会改变的,为了完全避免ip层进行分片,可以在ip层设置一个df标记,如果一定要分片就回一个icmp报文。
由于tcp发送的时候会进行各种分片和合并,所以接收方会出现粘包现象,需要应用层进行处理。
4. 消息接收
当服务端网卡收到一个报文后,网卡驱动调用DMA engine将数据包通过ringbuffer拷贝到内核缓冲区中,拷贝成功后,发起中断通知中断处理程序,这时候ip层会处理该数据包,之后交给tcp层,最终到达tcp层的recv buffer(接收队列),这时候就会返回ack给客户端,并没有等到客户端调用read将数据从内核拷贝到用户空间,所以应用层也应该有相关的确认机制。如果recv buffer设置的太小,或者应用层一直不来取,那么也将阻塞数据接收,从而影响到滑动窗口大小,导致吞吐量降低。
tcp在收到数据包后会获取序号,并且看是否应该正好放入接收队列,如果此时收到一个大序号的报文,会将该报文缓存直到接收队列中之前的报文已经插入。
另外如果网卡支持多队列,可以将多个队列绑定到不同的cpu上,这样网卡收到报文后,不同的队列就会通过中断触发不同的cpu,从而可以提高吞吐量