Don‘t cry over spilt milk.
"覆水难收"
参考资料:TCP/IP入门经典 (第五版)
TCP是协议栈中非常重要的一个部分,它和IP组成了整个协议栈的核心(从协议族的名字就可以看出来)。由于TCP内容较多,而且很多细节笔者也不是了解的很深入,所以本文只对最基本的概念和最简单的情况做一个介绍,更详细的内容请参考相关文档或书籍
一、简介
TCP(Transmission Control Protocol,传输控制协议),是协议栈的核心协议之一,提供一种面向连接的,可靠的字节流服务,位于协议栈的传输层
回顾UDP,UDP是把数据一整块的发送给网络层,由IP来管理数据的分片和传输,UDP并不确保传输的可靠性。然而TCP与UDP则完全不同,TCP在数据传输之前先在通信两端建立连接,然后传输数据,最后断开连接,并在数据传输过程中控制流量和传输的正确性。下面来看看TCP如何提供可靠性:
● 应用数据被分割成TCP认为最合适发送的数据块,避免IP分片(UDP则交给IP管理)
● 当TCP发出一个段后,它将启动一个定时器,等待目的端确认收到这个报文段,如果没有及时收到确认,它将重新发送这个报文段,这保证了数据完整正确的发送
● 当TCP收到另一端的数据时,它将发送一个确认
● TCP将保持它首部和数据的检验和,这样可以保证数据的完整性和正确性
● 有必要的话,TCP将会对收到的数据进行排序,将数据以正确的顺序发送给应用层
● 在IP数据报发生重复的情况下,TCP的接收端必须丢弃重复的数据
● TCP每一方都有固定大小的缓冲空间,接收端只允许另一端发送接收端缓冲区所能容纳的数据
二、TCP首部
TCP首部的数据格式如下
各字段含义如下:
● 端口号:标识应用程序,IP地址加上端口号组成了插口(socket,也称作套接字)
● 32位序号:用来标识从TCP发端向TCP收端的发送字节流,表示在这个报文段中的第一个数据字节。序号是一个32 bit的无符号数,序号到达232 - 1后又从0开始
● 32位确认号:确认序号包含发送确认的一端所期望收到的下一个序号,因此,确认序号应该是上一次成功接收到的数据字节序号加1,。只有ACK标志为1时确认序号才有效
● 4位首部长度:表示的含义跟IP首部中的首部长度字段一样
● 保留(6位):笔者目前还未了解
● 6个标志比特:
URG 紧急指针
ACK 确认序号有效
PSH 接收方应尽快将这个报文段交给应用层
RST 重建连接
SYN 同步序列号,用于建立一个连接
FIN 发送端完成发送任务
● 16位窗口大小:TCP通过在每一端声明窗口大小来进行流量控制,在后面会详细介绍
● 16位检验和:检验和覆盖了TCP首部和TCP数据,这是一个强制性的字段,其计算方法和UDP的计算方法相似
● 16位紧急指针:紧急指针是一个正的偏移量,和序号值相加得到紧急数据最后一个字节的序号,只有URG被置1时才有效
● 选项:最常见的可选字段是最长报文大小,又称为MSS(Maxinum Segment Size)。每个连接方通常在通信的第一个报文段中指明这个选项,指明本端所能接收的最大长度的报文段
数据部分是可选的,在许多情况下,TCP会发送一个不带数据的报文段,比如建立连接和断开连接时
三、TCP连接的建立与终止
由于TCP提供的是可靠的、面向连接的服务,所以在数据传输之前,通信双方需要建立一条连接。数据传输完成后,需要断开连接,下面来一一介绍
建立连接协议(三次握手)
为了建立一条TCP连接:
①主动打开:请求端(通常为客户端)发送一个带SYN标志以及初始序号(ISN)的报文段给服务器请求建立一个连接。这个SYN段为报文段1
②被动打开及确认:服务器发回包含服务器初始序号(ISN)的SYN报文段(报文段2)作为应答。同时,将确认序号设置为报文段1的ISN加1并置ACK位为1来确认已经收到客户的SYN段。一个SYN将占用一个序号
③最后确认:客户发送一个确认报文段(报文段3)来确认已经收到报文段2,该报文段将ACK位置1并将确认序号设置为报文段2的ISN加1
这三个报文段完成连接的建立,这个过程也称为三次握手
连接终止协议(四次挥手)
当数据传输结束后:
①主动关闭:请求端(通常为客户端)发送一个带FIN标志的报文段给服务器请求断开客户端方向的连接(表示我已经没有数据要发送给你了)
②被动关闭及确认:服务器发回一个带ACK标志的确认报文段,确认序号为收到的序号加1(好吧,你先关闭,但我还要向你发送数据)
③服务器端关闭:服务器发送一个带FIN标志的报文段给客户端请求断开服务器方向的连接(我的数据也发送完了,断开连接吧)
④终止连接:客户端发送一个带ACK标志的确认报文段表明已经收到服务器的FIN报文(收到请求)
第④步之后,由于最后一个带ACK标志的报文段可能丢失(前面的报文段也可能丢失,但是前面的报文段由其后一个报文段来确认,因此不需要特殊处理。最后一个报文段没有确认),所以客户端在发送完该报文段之后设置一个2MSL的定时器。如果在这个2MSL之内收到了服务器重发的FIN,则表明最后的ACK报文段丢失了,现在要重新发送。如果2MSL结束后没有收到,说明服务器已经收到确认,正常断开连接。并且,在2MSL内,服务器和客户端的正在使用的端口号和IP地址不能再被使用。
第①②步之后,请求主动关闭的一方就进入半关闭状态,在该状态下,请求端还可以接收来自另一方向上的数据。与其对应的是半打开状态:当已经建立连接的双方当中某一方断开连接而另一方还不知道的情况下,比如客户端突然断电,这时服务器就处于半打开状态,只要不打算发送数据,服务器就不会检测到客户端已经断开。
正常情况下,建立连接和断开连接时对应的状态如下:
特殊情况
当然,并不是所有的连接都能这样顺利进行,还有可能出现一些特殊的情况
● 同时打开:这个时候通信双方都主动请求连接,所以两端都执行了主动打开的两个步骤,一共交换了4个报文段。其状态变迁过程如下:
● 同时关闭:通信双方同时请求断开连接,由于单方向上的关闭需要交换2个报文段,所以一共需要交换4个报文段:
四、数据传输
建立好连接以后,通信双方就要进行数据传输了。对于不同类型的数据,应当使用不同的算法来传输数据。比如,我们登陆QQ时客户端需要与服务器验证账号密码,这时的数据只有很少的字节数;而当我们从网页上点击下载一部电影时,可能需要大块的数据传输很多次才能下载完成。TCP需要同时处理这两类数据,前者被称为交互数据,后者被称为成块数据。接下来我们就来了解一下这两种数据传输方式
TCP的交互数据流
当我们使用某些搜索引擎时,经常出现这样的情况,我们还没有完整的输入一个单词,只键入了一部分字符的时候,它就会自动弹出一些匹配信息。这是因为我们输入的每一个交互按键都会产生一个数据分组,也就是说事实上每次传送的是一个字节。搜索引擎在每次输入完一个字符后,就将该字符传送到服务器,服务器将已经输入的所有字符作为一个字符串进行模糊匹配,这样的输入方式称为交互式输入。
在前面将TCP如何提供可靠性的时候讲到:当TCP收到另一端发来的数据时,将会发送一个确认报文段来确认已经收到数据。然而如果每次刚收到数据就立即发送确认的话,可能会降低传输的效率,因为可能紧接着确认端又要发送一份数据,如果把确认信息包含在要发送的数据报文段内,那么不就可以提高一些效率了吗?但是确认和发送数据之间可能存在一定的时间差,也就是说“确认”需要等一下“数据”,绝大多数的实现采用200ms做为最大的时延,这样的确认方式称为经受时延的确认。
Nagle算法
前面讲到,交互式输入时TCP每次只传送一个字节的数据,然而当这样的小分组太多的时候,可能会造成网络拥堵。针对这个问题,TCP采用Nagle算法来解决:在一个TCP连接上,最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的分组。当已经产生很多个分组,但还没有收到前面某个分组的确认时,TCP将这些待发送的小分组合并为一个分组发送出去。这样做的优点就在于:只要收到一个确认,就能将所有待发送的数据全部发送出去(前提是不超过MSS),并且不用产生很多小分组来占用网络。
然而并不是所有情况都需要Nagle算法,比如我们在使用QQ的远程控制功能时,一端可以控制对方的电脑,这时控制方的每一个动作(包括鼠标移动)都要立即反馈给对方,这时就必须关闭Nagle算法。
TCP的成块数据流
滑动窗口协议
当我们从服务器上下载一个很大的文件时,可能需要发送很多的分组和很多的确认来传输文件。这时的分组一般都是“满长度”的,也就是要把分组“塞满”,这样才可以传的更快嘛。 正常情况下,发送端发送一个分组,接收方确认,发送端再发送下一个,。。。,然而,如果接收方的速度跟不上发送方的话,效率就可能非常低。那么是不是可以这样:发送方不需要等到接收方的确认,直接不停的发送,直到发送完成,然后等待所有确认。这样看起来可行,但是如果接收方“接收不下”这么多数据怎么办?
TCP提供了一种流量控制方法:接收方通告一个大小为S的窗口,并设置一个大小为S的缓冲区,用来保存已经收到但是还来不及处理的数据。当接收方处理完一块数据后,就发送该块数据的确认给发送方,当缓冲区没有满的时候,发送方可以继续发送数据;当缓冲区满了以后,发送方就要等待接收方处理数据,把缓冲区“誊出”空间来接收新数据,这种方法称作滑动窗口协议。
我们发现一个问题,发送方怎么知道接收方的缓冲区满没满呢?这就要用到序号和确认号了。假设窗口大小为2048,发送方发送的分组大小为500。某一时刻,发送方收到的确认号为1001,发送方将要发送的分组序号为2501,此时缓冲区的剩余空间为2500-1000=1500, 1500+500=2000<2048,即使再发送一个分组缓冲区也能“装下”,那么发送方将会继续发送分组;假设发送玩该分组后,还是没有收到确认,那么此时下一个分组的序号为3001,缓冲区内的数据大小为3001-1001=2000, 2000+500=2500>2048,这时如果再发送一个分组,缓冲区就“装不下了”,发送方就会停止发送,直到下一个确认到来。
滑动窗口协议的可视化表示如下:
紧急方式
考虑这样一种情况:我正在下载一个很大的文件,但是此时服务器因为某种原因需要重启,不能继续发送剩下的数据,那么服务器应该要给我发送一个通知告诉我不用等待了,并且缓冲区的数据也不用处理了,因为得不到完整的文件,那么这个通知应该在缓冲区的数据之前被接收方处理。TCP提供了“紧急方式”,通过将TCP首部中的URG比特置1,并且设置16 bit的紧急指针,来通知接收方“这是紧急数据,需要优先处理”,紧急指针加上序号字段得到紧急数据的最后一个字节的序号。
五、TCP的超时与重传
这篇文章只是简单介绍一下TCP,因此不会更深的介绍更详细的内容,比如:RTT的计算、快速重传算法等等(好吧,我承认我比较水)
TCP提供了可靠的传输,它使用的方法是对接收到的数据进行确认。然而数据和确认都可能在传输过程中丢失,TCP通过在发送时设置一个定时器来解决这种问题。当定时器超时时还没有收到确认,就重传该数据。
对于每个连接,TCP管理4个不同的定时器:
①重传定时器:当超时以后还没有收到另一端的确认时,就重传该数据
②坚持定时器:发送方使用一个坚持定时器来周期性地向接收方查询,以便观察接收方的窗口更新情况(因为接收方的确认可能丢失,而发送方需要知道接收方缓冲区的实时情况)
③保活定时器:当建立连接的双方都没有数据需要传送的时候,有可能出现半打开状态(前面讲过),此时就需要使用保活定时器来检测这种半打开状态。当一端的保活定时器超时后,就会向另一端发送一个探查报文段,如果另一端没有响应,那么发送端将会终止连接
④2MSL定时器:用于重传断开连接的确认,前面已经介绍过
六、TCP服务器的设计
TCP服务器的端口号
对于处于LISTEN状态的服务器进程(主进程/线程),将本地套接字设置为“*.port”的格式,表示可以通过服务器的任意接口来建立连接;将远端套接字设置为“*.*”格式,表示可以接收来自任何IP地址和任意端口的连接请求。
限定的本地IP地址
将服务器进程绑定到指定的接口,客户端只能通过指定的IP地址访问服务器
限定的远端IP地址
服务器只能跟指定的客户端建立连接并传输数据
呼入的连接请求队列
设置等待队列来处理已经完成三次握手但还没有被应用层接受的客户连接,一般将队列长度设置为5
总结:TCP是一个可靠的传输层协议,但也因为提供可靠性,TCP要比UDP复杂很多,以后慢慢的补充吧