以下都是自己的学习总结,有不足也有错误的地方,谨供参考。
TCP协议特点
---面向字节流,有连接,可靠,全双工
面向字节流
面向字节流指的是服务器和客户端之间的数据传输,使用的字节流传输,获取socket对象内置的InputStream和OutputStream,调用read和write方法读取对端放来的数据和写入要发送给对端的数据;
有连接
有连接指的是客户端和服务器要绑定,连接通俗来说就是双方保存对端的信息,比如IP地址和端口号;
可靠
可靠是TCP最大的特点,也是最核心的特点,可靠不代表会百分百的将数据安全完整的传输到对端,网络是复杂的,很多情况都是不可预测的,我们只能尽可能地保证高可靠,TCP为实现可靠传输,实现了很多机制,后面我会分享TCP是如何这些机制完成可靠传输的;
TCP协议段格式
要学习一个协议,就要看懂它的协议段格式
源端口号:发送数据的进程在主机上的端口号
目的端口号:接受数据的进程在主机上的端口号
4位首部长度:首部长度指的就是这个TCP数据段的报头长度(TCP长度=报头+载荷(数据)),4个bit,最大就是0xF,难道报头最大长度就是15字节吗?实际不是,基本单位是4个字节,15*4=60字节,报头最大长度其实是60字节;
6位保留位:没有任何含义,就是保留着,先不用,为的是以后有余地给TCP拓展功能;UDP传输速度非常快,但是一次传输的数据量十分有限,就是吃了没有办法拓展的亏,TCP在设计的时候就考虑了这一点,所以保留了6位空白;
16位检验和:在传输之前会根据TCP的内容计算一次检验和,在接收到数据之后会在计算一次检验和,将这两次获得的检验和做对比,就能知道在数据传输的过程过数据有没有发生改变,前后检验和不同的要舍弃掉;
选项:上面说到TCP报头最大是60字节,计算可得选项就是40字节,选项是翻译的,原文是optional,意思是可选择的,可以选择要不要这个选项,我们暂时不深入了解;
其余没有说到的,会在我接下来要讲的关于TCP协议的十大核心机制会涉及到,将其融入到其中来了解会更加深刻,更加容易理解;
TCP协议的十大核心机制
说是十大核心机制,但是不是代表TCP只有这十个核心的机制,只是在学习和工作种这是个相对比较重要,就像面向对象有三大特点,封装,继承,多态,但是不代表面向对象就只有三个特点,像反射,实例化也是很重要的;
1.确认应答机制
确认应答机制是TCP保证可靠传输的一个非常重要的机制,就像我们现实生活中,我们给一个人发消息,我们如何确认对方收到了,就是对方可以针对你说的给你一个回答,在TCP种也是如此,如何确认对方已经收到了你发送的数据了,就是对方可以根据你发送的数据,给你反馈一个应答,这个不携带任何业务数据,只是一个应答的数据报,我们称之为“应答报文”,相关的英语单词是acknowledge,取其前三个字母,ACK,就是上面协议种的那6个标志位中(后面统称“标志位”)的第二个,如果该TCP包头中,该位置是1,就代表这个TCP数据报就是一个应答报文,不携带任何业务数据,只是接收方在收到发送方发来的数据之后,给发送方一个反馈,为的是让发送方确实这一部分数据,接收方被已经收到了;
TCP连接双方的数据来往不是一点而已,通常会有非常多的数据,但是我们也不是一股脑的全部发送过去,这不现实,事实上,要发送的数据,TCP会给他们每一个字节都分配一个序号,前后递增,而这个起始序号是在TCP连接过程中确认的,有可能是1,也有可能是一个不常见的数字。
此后我们每次发送一些数据,有可能是一次1000字节,也有可能是其他数量,上面段协议中的32位序号就是保存了这1000个字节的第一个字节的序号,接收方根据收到的数据段,就能知道该数据段携带的数据是多大,通过检验和检验数据没有发生错误,就会立刻给发送方一个应答报文,也叫ack报文,也可以叫确认应答报文,该数据段不携带任何业务数据,最关键是该应带报文的报头的确认序号会保存一个序号,这个序号就是就是发送方发来的数据的最后一个字节的序号+1,当发送方收到这样一个ack报文,就可以确认该确认序号前的收据,已经被接收方顺利接到了;
通过32确认序号,站在发送者的角度,这个序号之前的数据,对方已经接收到了;站在接收者的角度,这个序号之前的数据我已经收到了,我要跟发送者索取此后的数据;
2.超时重传
网络是复杂的,一个数据的发送可能要经过很多的路由器,交换机的转发,才能到达,而一个路由器和交换机的转发能力也是有限的,一旦承受不了,新接收到的数据就会被直接舍弃,这种数据无法顺利达到目的ip和目的端口的现象,我们称之为“丢包”,丢包是很容易发生,虽然有确认应答,我们可以确认数据到了,这是远远不够的,如果发生了丢包,又该怎么办呢?这就牵扯到TCP的第二个核心机制了,超时重传。
丢包有两种可能:一种是数据段丢了,另外一种就是ack丢了。
-
如果是数据段丢了
当发送方将一个数据段发送出去之后,等待一定的时间之后,如果没有收到ack报文,就会重新发送该数据段的数据。
-
如果是ack丢了
站在发送者的角度,我没有收到ack,我就要重新发送一次数据,直到我收到ack;而站在接收者的角度,我收到了重复的数据,我就要能够分辨出我收到了重复的数据,我要要重复的舍弃,实际也是如此,当接收到重复的数据之后,就会直接舍弃;
检测数据重复的原理
无论是TCP协议,还是UDP协议,都是操作系统内核的协议,在接收到操作系统内核中有一个接收缓冲区,该数据结构类似优先级阻塞队列,所以会按照接收到数据的字节编号先后排序,可以保证读取的时候是按顺序读取的,这是非常重要的,也会检测该数据是否存在队列中或者曾经存在过,来决定是否舍弃;
网络是复杂的,不可预测的,当我们批量发送数据,由于数据发送的过程是需要时间的,所以经常会发生先发后至的情况,先发送的数据在后发的数据之后才达到,在接受方的数据缓存区,就会按照序号进行排列,对于先发后至,会空出地方,等待该数据的达到;下面就会空出1000个字节的数据,等待序号1001-2000的数据达到;
实际上丢包只是一个概率性问题,假设10%的丢包率,连续两次丢包的概率也只是1%,实际的丢包率也不会这么高,正常情况下,如果超时重复触发了很多次,那么就要考虑是不是欠网费了,为了避免出发超时重传带来的消耗,超时重传的等待ack时间会随着重传的次数增加而变长,是一个动态变化的股过程,这个时间取决与系统的具体实现;
一旦重传的次数达到一定的阈值,系统会认为是不是断开连接,发送方会触发“重置连接”,向接收方发送一个”复位报文“,RST标志位变成1,所以复位报文也叫RST报文,进行重置连接,也是重置连接失败,通信双方直接断开连接,删除保存的信息;
超时重传也是保障TCP可靠传输的核心机制,是确认应答的重要补充。
3.连接管理
TCP一个重要的特点就是有连接,深入了解TCP的建立连接和断开连接也是非常重要的。
- 建立连接:通信双方保存对方的信息,要经历的过程俗称”三次握手”;
- 断开连接:通信双发删除保存的对方的信息,要经历的过程俗称”四次挥手“;
无论是建立连接还是断开连接,期间发送的TCP数据段报文都不携带任何业务数据逻辑,也不会发送任何跟数据有关的,就像上面的应答报文一样;
建立连接(三次握手)
客户端一般指主动发起请求一方,服务器一般指接收请求的一方
- 第一次握手:SYN是synchronize的缩写,同步的意思,客户端处于SYN_SENT状态,意思就是客户端处在发送请求同步的状态,关联标志位SYN,所以该报文也叫同步报文,客户端发出同步报文,尝试与服务器建立连接;
- 第二次握手:服务器收到客户端发的申请建立连接的请求,进入SYN_RCVD状态,RCVD是received,就是处在收到同步的状态,按正常逻辑,服务器应该立即给客户端先发送一个ack应答报文,再给客户端发送同步报文,但是是由于该过程是系统内核处理的,两个报文的发送之间不会有任务业务逻辑,所以内核将这个报文合并,同时是标志位SYN,ACK都为1,意为这是一个同步报文,也是一个应答报文;
- 第三次握手:客户端收到服务器发来的同步应答报文就进入ESTABLISHED状态,去已经建立的意思,意思就是客户端正处在连接成立成功的状态,此后就会给服务器发送一个应答报文,服务器收到应答报文之后也随即进入连接建立成功状态。
以上就是建立连接的三次握手过程,图也只是一个简图,下来我们来看官方的一个三次握手的过程:
我只能说一个比较重要的状态,LISTEN状态,是服务器才会出现的一个状态,服务器再绑定端口号之后,就会进入listen状态,意思也很明确,就是等待随时有客户端的连接,我觉得可以叫等待连接状态;
通过了解三次握手,除了要简单知道它的过程之外,重要的是三次握手的重要意义
为了更好的了解三次握手的过程,我举个例子:打电话
A给B打电话,在电话接通之后,A会说:“喂,听得到我说话吗?”(第一次握手),B听到A说的话之后就会:“我听得到你说话,你听得到我说话?”(第二次握手),这时候B就可以确认:A的发送能力和自己的接收能力是好的,但是B不能确认自己的发送能力和A的接收能力是否可以?A也不能确认自己的发送能力和接收能力和B的接收能力和发送能力是否可以?所以A接到B的回复的时候,就回复B:“我听到你的说话”(第三次握手),这是A就可以确认:A的发送能力和接收能力是可以的,也可以确认B的发送能力和接收能力是可以的。在B收到A的第三次挥手的回复之后,B就可以在第一次确定的基础上在确定:B的发送能力和A的接收能力可以的,至此,双方都确认双方的接收,发送能力是OK的。
三次握手的重要意义
- 1.投石问路,检验网络通路是否顺畅;
- 2.检验通信双方的发送能力和接收能力是否正常;
- 3.通信双发确认一些必要的参数。
前面说到的确认序号就是在三次握手过程中确定的,每次连接确定的起始序号都有可能不一样,往往差异是非常大的,为的就是避免之前的连接发送的数据报在网络中经过传输又到了目的ip,网路是复杂的,这种情况也是可以可能会出现的,就比如原来内核以为是丢包了,但是其实有可能是网络延迟的原因,使得数据报晚到了,但是这个过程中原来的连接已经被新的连接取代,在服务器收到这样一个“前朝的”数据段之后,根据序号就可以判断这个数据报不是本次传输应该出现的,就会舍弃。
断开连接(四次挥手)
在建立连接成功之后,通信双方发就会进行正常的数据的业务来往,在处理完全部数据之后,就回来到断开连接的过程。断开连接有可能是客户端发起的,也有可能是服务器发起的,但实际是差不多的,这里以客户端主动发起为例子。
与三次握手相同,四次挥手期间的网络数据段都是不携带任何业务逻辑的。
四次挥手的简图
- 第一次挥手:客户端进入第一次等待结束状态,主动发送FIN报文,原单词就是finish(完成,结束)。
- 第二次挥手:服务器收到客户端发来的断开连接,结束报文,进入等待关闭CLOASE_WAIT状态,就立即给客户端一个应答报文,让客户端确保发送的结束报文被接收到,防止重复发出结束报文。
- 这里涉及到一个非常常见的问题:如果服务器出现大量的CLOSE_WAIT是因为什么呢?
- 原因就是服务器忘记调用close,或者根本没有close,到底就是代码不健壮。
- 第三次挥手:服务器处在LAST_ACK状态,可以理解成等待最后一个ack报文,等待发送出去结束报文收到的回复。
- 第四次挥手:客户端在收到服务器发来的结束报文之后,就进入TIME_WAIT状态,随后给服务器发送ack应答报文。
经过以上正常的四次挥手的过程,客户端和服务器就彻底删除通信双方的信息,彻底断开连接。
断开连接的两个问题
- 1.服务器的发出的两个报文能否合并?
理论是可以的,要看服务器收到客户端发来的结束报文,和本身自己调用close方法之间的代码量,特殊情况下,如果两者之间的代码量特别少,可以让ack晚点发出,等待close方法的调用之后,再合并两个报文,实现三次挥手断开连接。
- 2.客户端的TIME_WAIT状态有什么含义?
该状态是为了防止服务器没有收到客户端发出来的ack应答报文,导致服务器无法正常断开连接,再服务器发出FIN报文之后,等待一定时间之后没有收到客户端发来的ack应答报文,就会触发超时重传,客户端的TIME_WAIT就是为了应对服务器可能再次发来的结束报文请求。直到服务器收到ack,不会再重传FIN,客户端才能正常结束,断开连接。
4.滑动窗口机制
有确认应答机制和超时重传机制保障TCP传输的可靠性,有高可靠性的同时TCP就牺牲了传输速度,滑动窗口机制就是为了尽快能提高TCP传输速度,更像是高可靠性的补充机制。
如果只是发送一个数据段,直到收到ack应答报文之后,再接着发送下一个数据段,就会浪费很多时间,所以滑动窗口就是同一个时间内同时发送很多个数据报,重叠等待ack的时间,从而达到减少等到时间。
对于第二个图做点解释,比如第一次发送的时候,就发送序号为1-1000,1001-2000,2001-3000,3001-4000四个数据报,服务器收到之后就会以此发送确认序号为1001,2001,3001,4001的确认应答报文,客户端收到1001,站在客户端就知道1001之前的数据发送成功了,所以就可以发送4001-5000字节数据的,收到2001之后就可以发送5001-6000字节数据,像一个框框一样往后走;
在该机制中传输数据的过程发送丢包是不可避免的,丢包也是分成两种情况
- 1.如果是ack丢了
由于服务器收到一个数据段,就会发送一个确认序号为数据报载荷最后一个字节的序号+1的确认应答报文,客户端收到该报文就能直到,该确认序号之前的字节数据都已经发送成功,所以即使ack丢了,只要的ack没丢,客户端就能确定前面没有收到对应的ack的已经到了服务器,不用在重传。所以ack丢了是没有关系的。
- 2.如果是数据报丢了
如果是数据段丢了,没有收到ack确认应答报文,客户端就会触发超时重传,但是在该机制下,客户端是如何确定哪些数据段丢了呢?还是跟确认序号有关,假设发送序号为1-1000,1001-2000,2001-3000,3001-4000四个数据报,1001-2000的数据报丢了,服务器收到1-1000,就会发送确认序号为1001的ack确认应答报文,随后像后移动,客户端发送序号为4001-5000的数据,接着由于1001-2000数据段丢了,服务器就到了2001-3000和3001-4000以及4001-5000数据段,前面说到服务器内核有一个数据缓存区,收到以上数据之后,内核就会发现本应该放1001-2000的地方是空白的,这时候服务器发送的不是确认序号为2001,也不是3001,更不是4001,5001,而是1001,服务器就是要跟客户端索取1001之后的数据段,由于服务器连着三次分别收到2001-3000和3001-4000,4001-5000的数据段,所以服务器会连续发送三次确认序号为1001的确认应答报文,直到客户端意识到服务器一直在索要1001之后的数据,就知道1001-2000数据没有发送成功,就触发重传,假设这一次发送被服务器收到了,猜猜看服务器会发送确认序号是多少的ack确认应答报文?没错,会发送确认序号为5001的确认应答报文。
上述说到的重复索要次数和是可以配置的,为了是避免因为数据晚到就立即触发重传,直到真的要重传的时候,客户端就会触发快速重传,注意是快速重传,不是超时重传,快速重传只会和滑动窗口搭配使用。
5.流量控制
流量控制就是接收方根据目前自己的处理能力,反向制约发送方的发送数据报的速度,是跟滑动窗口大搭配的机制。
滑动窗口的大小是可以改变的,接收方会将自己的接收缓冲区的空白的区域的大小,作为在ack确认应答报文中的报头中的16位窗口大小中保存,等到发送者从这个ack报文中获取到的16位窗口大小的数据作为自己滑动窗口的大小,从而实现控制流量,控制发送方的发送速度。
报头中窗口大小只有16位,那最大就只有65535,就差不多64KB,对于目前操作系统内核的处理速度来说,这个窗口就有可能太小了。所以在报头中的选项中,有一个特殊属性变量叫做窗口拓展因子,实际的窗口大小不止只是报头中那个窗口大小,还有左移上该窗口拓展因子。比如窗口拓展因子是4,实际的窗口大小最大就是256KB。
如果窗口大小是0的话,发送方就会停止发送数据段,那谁来告诉发送方什么时候可以继续发送呢?实际上是发送方自己,发送方会周期性的发送窗口大小探测报文,从而获取窗口大小。
6.拥塞控制
我们直到数据的发送要经过很多的路由器和交换机,每个交换机或者路由器的转发能力又是有上限的,一旦窗口大小设置不当,或者有些路由器或者交换机等的数据转发能力不够,就会使得无法转发的数据段被直接社舍弃,出现丢包现象,实际上无论怎么拓展某个机器的处理能力都是有上限的,都有可能某个机器的转发能力比较低导致丢包,类似于木桶效应,装水量取决于最短的一根木板,网络上的最大转发能力就取决于那个最小的转发或者处理数据的某一个节点,有可能是客户端,有可能是接收方,也有可能是路由器。
所以根据网络的处理能力来动态调整窗口大小是非常有必要的,所以就要通过实验的方法,将整个网络不同的节点看作一个整体,来找到一个合适的窗口大小,既能兼顾到相对较高的发送速度,也能保证非常高的传输可靠。
窗口大小变化的过程:
- 1.慢启动,刚开始的窗口很小
- 2.如果没有发生丢包现象,就指数增长
- 3.增长到一定的阈值之后,还没有出现丢包,就线性增长。
- 4.线性增长到开始出现丢包现象了,就要开始降低窗口大小,对于降低窗口大小,有两种方案,一种是直接将到慢启动的窗口大小在,这种已经被舍弃了,相对第二种方案比较低效,第二种方案就是降低一半,毕竟这个时候的窗口的大小是肯定不会发生丢包想象的,此后再线性增长。
流量控制和拥塞控制实际上都是再调整窗口大小,不同的是,流量控制是可以算出来的,拥塞控制是根据网络情况动态变化,根据木桶效应,那个得到的窗口大小小就把哪个作为窗口大小,能够避免发生丢包现象。
7.延迟应答
延迟应答指的是ack确认应答报文,延迟一下再将该报文发送出去。上面有说到发送方会根据ack的保存到窗口大小来调整自己的发送速度,所以这个窗口大小就会影响到传输效率,延迟应答,就是为让接收方能够在这个延迟时间内处理掉接收缓冲区中的一些数据,以增加接收缓冲区的空白大小,从而ack在返回的时候会返回一个更大的窗口,达到提高传输目的地目的,所以延迟应答也是TCP为了提高传输速度而提出的一个机制。
延迟应答的时间通常由两种参数来决定:按照一定的时间来作为延迟时间,按照收到的数据量来延时,数据量少就延迟短一点时间,数据量多就延迟多一点时间,两种参数结合得到延迟时间。
8.捎带应答
有时候服务器也会主动发送一些业务数据,所以在返回的ack上,就会带上这样的数据,也是TCP为了提高数据传输的效率而做出的一个机制。
9.面向字节流
面向字节流是TCP协议的一个重要特点,在使用字节流传输数据的过程中会出现“粘包问题”,举个例子:第一个发送了一个“hello”,第二次发送了一个”cat“,第三次发送了”dog“,如果没有隔开的话,在数据缓存区中就会出现”hellocatdog”,会导致无法正确解读接收到数据,比如有可能读到一个"hel“之类的不争取的字符串,更何况实际上保存的是二进制,就会更加能以分辨了,这就叫做”粘包问题“。
所以不同的数据段就应该由明确的界限,这也关乎到我们的编程习惯,在TCP网络编程的时候,要习惯主动机上空白符(协议规定的)。除了使用分隔符可以是不同的数据报有明确界限,也可以给数据报加上载荷的长度,还是上面的例子,在hello前面加上一个5,在cat前面加上一个3,相同意思。
- 1.使用分隔符(空白符)
- 2.加上载荷的长度
10.异常情况
-
1.进程崩溃
进程是应用层面的,而建立连接和断开连接是在系统内核实现的,即使进程崩溃,系统用内核依旧可以调用close方法,正常完成四次回收过程。
-
2.关机(正常主动关机)
不知道你们有没有发现,有时候关机的时候系统会提醒你有些进程还没有保存,是否要取消关机,如果选择取消的话,就不会关机了,进程也就结束;如果选择了关机,系统也会调用close方法,区尝试正常四次挥手来断开连接这个过程,但是也有可能没有挥完就已经关机,但是由于关机就会释放内存上的数据,而建立连接时通信双方保存的对方的信息也是保存在内存上,所以也能够正常删除保存的对端的信息,释放资源。
-
3.断电(台式机,并非笔记本)
有可能是发送方断电,也有可能是接收方断电
如果是发送方断电,接收方就会一直接收不到来自发送发的数据段,就会尝试给发送方发送一个”心跳包“,周期时间比较长,有可能是分钟级别的,根据不同的系统规定的,如果接收方收不到发送方的关于”心跳包“的ack确认应答报文,就说明发送方已经单方面断开连接了,接收方就会自动删除保存的发送方的信息,从而也能达到断开连接。由于系统实现的触发心跳包的周期比较长,在实际开发的过程中,往往会在应用层自己实现一个类似于”心跳包”的功能,往往周期能够达到毫秒,纳秒级别,能更快的发现对方已经断开连接。
如果是接收方断电,那么发送发就会一直收不到ack,就会触发超时重传,超时重传也收不到ack,就会尝试”重置连接“,也还是不能收到ack,这时候发送方就能断定接收方已经断开连接了,然后发送方也是自动删除保存接收方的信息,从而也能达到断开连接。
-
4.网线断了
本质上还是第三种情况。
最后
说到这里,已经接近末尾了,回头再看TCP协议段格式还有一些没有谈论到,这里我就粗略谈一下,因为对我们来说也不是很重要,想仔细了解的话就去查看RFC-TCP标准文档,是官方的,更详细。
标志位:URG报文和16位紧急指针是控制TCP自身工作特殊机制的;
标志位:PUH报文,意思是推的试意思,催促对方尽快给自己的一个回复。