引言
本篇文章参考书目为《Linux高性能服务器编程》,在这个博客系列里面,我会努力梳理书本里面的主要内容。
TCP服务的特点
TCP协议是面向连接,字节流,可靠传输
在实际运用的过程中,通信双方是不是需要执行相同的次数读和写?答案是否定的。发送端TCP模块先把数据放入TCP缓冲区内,当TCP模块真正发送数据的时候,发送缓冲区那些等待的数据可能会被封装成一个或者多个TCP报文发出。大家一定要记住TCP是按照字节流发送的,所以一个TCP报文里面的上下文可能并不完整的关系,因为是按照字节流发出的。所以TCP模块发送的TCP报文数量和应用程序写端的次数没有特别多的关系。当接收端收到一个或者多个TCP报文之后,TCP模块会按照TCP报文的编号依次放入TCP缓冲区,并通知应用程序读取数据。接收端可以一次性全部读取,也可以分段读取,这取决于应用程序的缓冲区大小。因此应用程序执行的读操作次数和TCP模块接收到TCP报文的个数也没有固定的数量关系。所以通信双方不需要执行相同的次数读和写。
而UDP就不一样了,应用程序每执行一次写操作,UDP模块会把数据封装成一个报文直接发出去。接受方必须即使针对每一个UDP数据报进行读取,否则就会丢包。并且,如果用户没有指定足够的应用程序缓存来读取UDP数据,那么UDP数据就会被截断。
对于UDP来说没有缓冲区的概念,主要原因是UDP不用重新发送数据,所以不需要存储。
TCP头部

序号:在第一次发送TCP报文的时候会初始化一个ISN,后面的所有数据报的序号都是ISN+第n个字节,比如1025 - 2038 那么就是ISN+1025 。当然互相通信的双方ISN是不一样的,所以传递过来的序号也是不一样的。
ACK:上面也说了,序号是不一样的,所以每个机器只认识自己的那个序号,所以当我们发送ACK的时候,应该把对方发来的TCP序号+1,就是我们要发送的ACK,这个样子发过去的ACK对方可以完全识别。
首部长度:4 * 15
标志位:URG 紧急指针 ACK 确认号是否有效 PSH 提醒接收端应用程序应该立即从TCP缓冲区中读取走数据。 RST 要求对方重新建立连接 SYN 请求建立一个连接 FIN 结束连接
16位窗口:TCP控制流量的一个方法,告诉对方本端的TCP接收缓冲区可以容纳多少字节的数据,这个样子对方就可以控制数据的发送速率
TCP连接的建立和关闭
我们这里不主要讨论三次握手和四次挥手,讨论一些其中的问题。
TCP状态转移
服务器通过listen的系统调用进入LISTEN状态,被动等待用户连接,当收到用户连接的请求,会把连接放入内核等待队列里面,并向客户端发送带SYN(同步连接)标志的确认报文段。此时此链接处于SYN_RCVD状态,如果成功接收到了客户端的确认报文,那么就转移到了ESTABLISHED状态。这个状态可以互相通信。
当用户断开连接的时候,用户会先发送带有FIN标志的报文,并且进入FIN_WAIT_1的状态,当用户收到服务器的ACK标志的报文的时候就会进入FIN_WAIT_2的状态。这个状态下面服务器还可以和客户端进行发送数据,处于半关闭状态。当服务器的数据发送完,服务器发送带FIN标志的报文,此时客户端接受到这个报文之后状态变成TIME_WAIT。
当然,当客户端发来断开请求的时候,服务器进入CLOSE_WAIT状态,这个时候服务器可以发送数据,但是客户端不可以,这个时候也是我们所说的半关闭状态。当服务器发出FIN标志的报文的时候,就进入LAST_ACK,当接收到客户端的ACK时候,服务器就会进入CLOSED状态。
当然了,如果不是为了接收数据而处于长时间的半关闭状态也不是一件好事情,此时客户端连接由内核来接管,称为孤儿进程。所以为了解决这个问题,我们内核里面有两个参数,一个是能接管的孤儿连接数量,一个是孤儿连接在内核里面的时间。
TIME_WAIT状态
这个状态存在的原因我们上面也已经说了,当客户端接受到服务器发出的FIN标志的报文时候,就会进入这个状态。这个状态存在两个原因:
1、可靠的终止TCP连接:
如果客户端最后发送的ACK报文丢失了,那么服务器会重新发送一个FIN报文给客户端来重复这个过程。(一定不是客户端再重新发一个,而是服务器再发一个)
2、保证让迟来的TCP报文有足够的时间被识别并丢弃。当一个TCP连接处于TIME_WAIT时,无门无法使用该端口建立一个新的连接,因为一旦使用,那么很可能会受到来自原来连接携带应用数据的TCP报文段,这显然是不对的,所以在TIME_WAIT状态的时候不会让其他应用程序使用这个端口。另外一个TCP报文最大生存时间是MSL,那么坚持2MSL的时间的TIME_WAIT可以确保网络上的两个传输方向的报文都已经消失。
所以我们写代码的时候也会遇到一个类似的问题,当我们关闭测试的端口,然后立马重新运行程序,这个时候就会报错,因为端口被占用了(处于TIME_WAIT)。而我们常常会设置socket选项SO_REUSEADDR来强制进程立即使用正在处于TIME_WAIT状态的端口。
复位报文段
复位报文段主要就是通知对方关闭连接或者重新建立连接。因为复位报文段的接受通告窗口时0,所以可以预见:收到复位报文段的一端应该关闭连接或者重新连接,而不能回应这个复位报文段。
TCP交互数据流
TCP报文段所携带的应用程序数据按照长度分为:交互数据流和成块数据。交互数据仅仅包含很少的字节,使用交互数据流对应用程序的实时性要求很高。成块数据的长度通常为TCP报文的最大长度,使用成块数据对于传输效率的要求很高。
对于交互数据流,一般来说,客户端发送的确认报文段不携带任何的数据,而服务器每次发送确认报文段都会携带应用程序数据,服务器这种处理方式是延时确认。它不马上确认上一次收到的数据,而是隔一段时间看本端有没有数据需要发出,如果有那么那么就和确认报文一起发出。这样子可以大大减少TCP报文的数量。由于用户的输入速率明显小于客户端程序的处理速率,所以客户端的确认报文段总是不携带任何程序数据。而前文也提到我们在关闭连接的时候可能会延迟收到一些数据,因为可能发生了延迟确认。
当然,这个只在局域网里面有用,但是在广域网里面会有很多的延迟。所以Nagle算法要求一个TCP通信双方在任意时刻最多只可以发一个未被确认的TCP报文段,当TCP报文段的确认没有到达之前不可以发其他的报文段。另一个方面,在等待接受确认报文段的同时,收集需要发送的微量数据,之后和确认报文段一起发出,这样子就极大的减少了微小TCP报文段的数量。该算法的另一优点:自适应性,到达的越快,发送的越快。
TCP成块数据流
当传输大量数据块的时候,发送方会连续发送多个TCP报文段,接收方可以一次性确认所有报文段。那么发送方收到上一次的确认后,可以发送多少个报文段呢?这个是取决于接受的窗口大小。
TCP超时重传
TCP服务器必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护了一个重传定时器,该定时器在TCP报文第一次发送的时候就启动。如果超时时间之内没有收到对方的应答,那么TCP模块将重传TCP报文段,并且重置定时器。那么我们应该怎么样设置这个超时重传的时间呢?假设执行了5次超时重传,那么时间就是0.2s 0.4s 0.8s 1.6s 3.2s,当这5次均失败之后,IP和ARP就会接管,因为这个时候就要检测网络是否可以到达。Linux两个重要的内核参数,一个是在底层IP接管之前最少重传的次数,一个是连接放弃前TCP最多可以执行的重传次数。
拥塞控制
我们先介绍几个概念,SWND发送窗口,CWND拥塞窗口,RWND接受窗口,SMSS TCP报文的最大长度。发送端需要合理选择SWND窗口,如果太小会阻塞信息传递,如果太大会使网络堵塞。
所以实际就是SWND = min(CWND, RWND)
慢启动和拥塞避免
最开始我们先设定一个窗口的大小(1W,比较的小),然后随着数据不断的发送成功,这个窗口逐渐的变大,成指数增长,为了避免过一会窗口就很大很大,所以我们设定了一个阈值,当超过这个阈值就变成线性增长,速率变慢,减缓窗口的扩大。(这个让速度变慢的操作就是拥塞避免)
当拥塞发生的时候,有两个判断依据:
1、超时传输
2、接收到重复的确认报文段。
第一种情况仍然使用慢启动和拥塞避免,第二种情况使用快速恢复和快速重传。
如果是第一种情况,我们就要缓慢的减小窗口的大小。
快速重传和快速恢复
当收到重复的数据报,我们需要判断是不是真的网络阻塞了,具体说是不是TCP报文真的丢失了,还是说只是因为网络原因阻塞了。具体做法是:发送端如果连续收到三个重复的确认报文,就确认是网络拥塞了。
重传过程如下:
1、当收到第三个重复的确认报文,重新设置CWND,并重新传送丢失的报文。
2、每一次收到1个重复的确认的时候,(重新)设置CWND = CWND + SMSS(相当于多了一个可以发送的报文段),此时发送端可以发送TCP报文段。
3、当收到新数据确认之后,重新设置CWND。
总结
本篇文章到这里就结束了!!!希望可以帮助大家理解本书的内容~~~