UDP是一种没有复杂控制,提供面向无连接通信服务的一种协议,它将部分控制转移给应用程序处理,自己只提供作为传输层协议的最基本的功能,与UDP不同,TCP充分实现了数据传输时各种控制的功能,可以在丢包是进行重发,可以对次序乱掉的分包进行顺序控制,这些功能在UDP中都没有,此外,TCP是一种面向有连接的协议,只有在通信对端存在时才会发送数据,从而可以控制通信流量的浪费
其中可靠传输是TCP诞生的初衷,为了实现可靠传输,TCP引入了一系列的机制,例如检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等
TCP中没有表示包长度和数据长度的字段。可由IP层获知TCP的包长度由TCP的包长可知数据的长度
源端口号:表示发送端端口号,字段长16位
目的端口号:表示接收端端口号,字段长16位
序列号:字段长32位,指发送数据的位置。每发送一次数据,就累加一次该数据字节数的大小,序列号不会从0或1开始,而是在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机
确认应答号:字段长32位,在确认应答报文中,确认应答序号才有意义
数据偏移: 该字段表示TCP所传输的数据应该从TCP包的哪个位开始计算,也可以把它看作TCP首部的长度,该字段长4位,单位为4字节。不包括选项字段的话,TCP的首部为20字节长,因此数据偏移字段可以设置为5,反之如果该字段值为5,说明从TCP包的最开始到20字节为止都是TCP首部,余下部分为TCP数据
控制位:字段长8位
CWR:CWR和后面的ECE标志都用于IP首部的ECN字段。ECE标志为1时,则通知对方已将拥塞窗口缩小
ECE:
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
窗口大小:16位,用于通知从相同TCP首部的确认应答号所指位置开始能够接收的数据大小(8位字节)。TCP不允许发送超过此处所示大小的数据,如果窗口位0,表示可以发送窗口探测
校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。
在TCP中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知,这个消息叫做"确认应答"。
以打电话为例,说话人说了一段话后,接收人会回答"嗯,好的",这里的"嗯,好的"就相当于是确认应答,说话人收到这个确认应答后,就可以继续讲话
由于网络传输会出现"后发先至"的情况,因此在TCP中引入了序列号的机制,序列号是按顺序给发送数据的每一个字节都标上号码的编号。接收端查询接收数据TCP首部的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去。
【正常的数据传输】
TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端,反之,数据丢失的可能性很大,在一定时间内没有收到确认应答,发送端就可以认为数据已经丢失,并进行重发。
由于丢包是无差别的,丢的包有可能是数据包,也有可能是确认应答包
【数据包丢失的情况】
【确认应答丢失的情况】
此时在接收端已经接收到了数据,但由于返回给发送端的确认应答丢失,导致发送端会误以为自己发送的数据丢失,进而进行重发,这就导致接收端会收到重复的数据,TCP协议能根据数据包的序号丢弃重复的包,保证应用层读到的数据是不重复的
【超时重传的时间如何确定?】
- 最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。
- 但是这个时间的长短,随着网络环境的不同,是有差异的。
- 如果超时时间设的太长,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
- 数据被重发之后若还是收不到确认应答,则进行再次发送。此时等待确认应答的时间将会以2倍、4倍的指数函数延长
此外,数据不会被无限、反复的重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接并通知应用通信异常终止
TCP面向有连接的通信传输。面向有连接是指在数据通信开始之前先做号通信两端之间的准备工作
在数据通信之前,通过TCP首部发送一个SYN包作为建立连接的请求等待确认应答
【建立连接的过程】
共经历了四次交互的过程,完成了建立连接的过程,但为什么叫做"三次握手"呢?
因为在服务端在收到了客户端的SYN后,将SYN(和客户端建立连接的报文)和ACK(对客户端发来的SYN的确认报文)通过一个数据报发送给了客户端
那么是如何做到将SYN和ACK合并成一个数据报的呢?
将TCP首部中控制位字段中的SYN位和ACK位都置为1
为什么要将SYN和ACK合并成一个报文发送呢?
原因很简单,每次数据报文的传输都要经历一系列的封装和分用,合并成一个数据报发送节省代价
为什么要进行三次握手?
三次握手起到的是"投石问路"作用,是一种保证可靠传输的机制,在正式通信之前先确定通信链路是否畅通,如果通信链路不畅通,后续传输过程大概率会丢包
同时三次握手能够让通信双方协商一些重要的参数,比如MSS、序号的初始值等
为什么不是两次握手或者四次握手呢
四次可以但没必要,降低了效率
两次是不够的,三次握手是为了验证通信双方的发送能力和接收能力是否正常
验证通信双发的发送和接受能力和两个人进行组队开黑之前验证双方的耳机和麦克风是否正常的过程是类似的
如果只进行两次握手,对于服务端来说是无法知道自己的发送能力和客户端的接收能力是否正常的
【注意】:
TCP中发送第一个SYN包的一方叫做客户端,接收这个的一方叫做服务端
在通信结束时会进行断开连接的处理(FIN包)
TCP的首部用于控制的字段来管理TCP的连接。一个连接的建立和断开,正常过程至少需要来回发送7个数据包才能完成
与三次握手不同的是,三次握手的过程只能是客户端主动发起,对于四次挥手来说,通信双方都能主动发起
【断开连接的过程】
与三次握手不同的是,四次挥手的中间两次的交互过程不一定能进行合并
主要是因为主机B返回ACK和FIN的时机是不同的,主机B返回ACK是操作系统内核来完成的,内核在收到来自A的FIN后,会立即返回一个ACK,而B要返回的FIN是由代码控制的,当用户在代码中调用了socket.close方法之后,才会触发FIN,这就导致B发送的FIN和ACK之间有不可忽视的时间间隔
这里之所以说不一定能合并是因为TCP中还有延时应答和捎带应答的机制,在后面具体叙述
而再三次握手的时候,B返回的SYN和ACK都是内核收到A的SYN之后,立即返回的
TCP建立连接是没有历史包袱的,立即就能完成,而断开连接,很可能再A->B发送FIN的时候,B还有数据每读完,B一般不会立即断开,要把未处理的数据都处理完了再说,B何时发送FIN就是代码层次的了
【服务器状态转化】
bug分析:
一般而言,对于服务器上出现大量的 CLOSE_WAIT 状态,原因就是服务器没有正确的关闭 socket,导致四次挥手没有正确完成。这是一个 BUG。只需要加上对应的 close 即可解决问题。
【客户端状态转化】
TIME_WAIT的意义:
为了防止最后一个ACK丢失,如果最后一个ACK丢失了,在TIME_WAIT的状态下还可以重传,而如果连接释放了,就无法重传了
为什么TIME_WAIT的时间时2MSL
MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话
就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);
同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN。这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK);
滑动窗口是一个用来提高效率的机制,TCP在保证可靠性的前提下,尽可能的提高效率。
TCP以1个段为单位,每发一个段进行一次确认应答的处理,但这样的传输方式有一个缺点,就是,包的往返时间越长通信性能就越低
为了解决这个问题,TCP引入了滑动窗口的这个机制,确认应答不再是以每个分段,而是以更大的单位进行确认,转发时间会被大幅度的缩短,也就是说,发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)。
- 发送前四个段的时候,不需要等待任何ACK,直接发送;
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推; + 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
- 窗口越大,则网络的吞吐率就越高;
在收到确认应答后,将窗口滑动到确认应答中的序列号的位置,这样就可以顺序的将多个段同时发送提供通信性能。
【情况一】:ACK包丢失,这种情况其实数据已经到到达了对端,是不需要进行重发的,然而在没使用窗口控制的时候,没有收到收到确认应答的数据都会被重发,而使用了窗口控制,某些确认应答即使丢失也无需重发
【情况二】:数据包丢失,接收端主机如果收到一个自己应该接收的序号以外的数据,会针对当前为止收到数据返回确认应答
- 当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 “我想要的是 1001” 一样;
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 “高速重发控制”(也叫 “快重传”)
滑动窗口能够提高效率,指的是相比于没有滑动窗口,普通的确认应答,但是和无可靠性的传输(UDP)相比,效率还是要差一些的
滑动窗口的窗口越大,发送速率就越快,流量控制就是在针对发送速率进行制约
整体的传输速率由发送速率和接受速率共同决定,当发送速率大于接受速率时,这是即使继续提高发送速率,也不能提高整体的效率了,反而会因为接收方丢包,触发更多的重传,反而降低了速率。
为了防止这种现象的发生,TCP提供一种机制可以让发送端根据接收端的实际接受能力控制发送的数据量,这就是所谓的流控制
具体操作是,接收端主机向发送端主机通知自己可以接收数据的大小,于是发送端会发送不超过这个限度的数据,该大小就被称作窗口大小,在滑动窗口机制中所说的窗口大小就是由接收端主机决定的。
在TCP首部中,专门有一个字段用来通知窗口大小,接受主机将自己可以接收的缓冲区大小放入这个字段中通知给发送端,这个字段的值越大,说明网络的吞吐量越高。
不过,接收端的这个缓冲区一旦面临数据溢出时,窗口大小的值也会随之被设置为一个更小的值通知给发送端,从而控制数据发送量。
如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
TCP首部中,有一个16位窗口字段,就是存放了窗口 大小信息;那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移 M 位;
流量控制,是站在接收方的角度,来控制发送速率,但是 整体的传输,其实不仅由发送方和接受方,还有中间一系列用来转发的设备,因此控制发送方的发送速率,不能只考虑率接收端的接受能力,也要考虑中间设备的转发能力
对于接收端的处理能力,可以用接收缓冲区的剩余空间来衡量,那么对于中间设备的转发能力,我们该如何衡量呢?
TCP采取的办法是,在通信一开始时就会通过一个叫做慢启动的算法得出的数值,对发送数据量进行控制
像上面这样的拥塞窗口增长速度,是指数级别的。“慢启动” 只是指初使时慢,但是增长速度非常快。
流量控制和拥塞控制都是对滑动窗口发送速率进行制约的机制
延时应答是一个用来提高效率的机制,如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。
- 假设接收端缓冲区为1M。一次收到了500K的数据;如果立刻应答,返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;
- 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M;
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么?肯定也不是;
- 数量限制:每隔N个包就应答一次;
- 时间限制:超过最大延迟时间就应答一次;
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms;
基于延时应答的策略,为了提高传输效率的机制
正常情况下,ACK是收到请求后,由内核立即返回的,而响应数据,则是由应用层代码发送的,正常情况下,ACK和响应数据的发送时机是不同的,也就不能将ACK和响应报文合并一起发送,但是由于有了延时应答的机制,也就是说ACK延时一段时间后可能就和返回响应的数据的返回时机相同了。
也就是说,没有启用延时应答就无法实现捎带应答,延时应答是能够提高网络利用率从而降低计算机处理负荷的一种较优的机制
面向字节流,指的是在应用层读取载荷数据的时候,是按照"字节流"的方式来读取的,TCP数据报,本身仍然是一个一个"数据报"的方式来传输的,因此在应用程序中,是感知不到从哪到哪是一个数据包的
面向字节流最核心的问题:粘包问题
如果一个TCP连接,里面只传一个应用层数据报,这个时候,不存在粘包问题(短连接)
如果一个TCP连接,传输多个应用层数据报,这个时候就容易区分不清,从哪到哪是一个完整的应用层数据(长连接)
- 首先要明确,粘包问题中的 “包” ,是指的应用层的数据包。
- 在TCP的协议头中,没有如同UDP一样的 "报文长度"这样的字段,但是有一个序号这样的字 段。
- 站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。
- 站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
【对于UDP协议来说,是否也存在 “粘包问题” 呢?】
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
- 站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况。
归根结底就是一句话,明确两个包之间的边界
- 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可;
按照程序关机,会先杀死所有的用户进程(同时也就包括TCP程序),进程终止会释放文件描述符(相当于调用了close),调用了close就会触发FIN,开启四次挥手的过程。和正常关闭没有什么区别。如果还没完成四次挥手,就已经关机了,对端重传了FIN若干次后,没有响应,也就放弃了
和主机重启一样
突然拔电源,主机突然掉电是来不及进行四次挥手的
【接受方掉电】
对方尝试发送数据,发现没有ACK,会尝试重传,重传几次如果还没有ACK,发送方会尝试重新建立连接,如果无法重新建立连接,就认为网络上出现了问题,就放弃了
【发送方掉电】
接受方在等待发送方发送的数据,由于发送方此时已经无了,等待的数据肯定是发不过来的,但是接收方无法判断当前数据包是发送方处在正常工作的情况下,但还没发,还是发送方已经出现了问题。为了解决这个问题,接收方会给发送方定期的发送一个"心跳包"。
心跳包:接收方给发送方发送一个特殊的报文"ping",对方返回一个特殊的报文"pong",如果发送的ping后收到对应的pong,则认为对方是正常工作的,如果发送的ping没有收到pong则认为对方挂了
【补充】:
TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果 对方不在,也会把连接释放。
另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。例如QQ,在QQ断线之后,也会定期尝试重新连接
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时,数据会先写入发送缓冲区中; 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适 的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区; 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既 可以读数据,也可以写数据。这个概念叫做 全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
- 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次;
- 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接
- 不可靠传输:没有任何安全机制,发送端发送数据报以后,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
- 面向数据报:应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并; 用UDP传输100个字节的数据:如果发送端一次发送100个字节,那么接收端也必须一次接收100个字节;而不能循环接收10次,每次接收10个字节。
- 只有接收缓冲区,没有发送缓冲区:UDP没有真正意义上的 发送缓冲区。发送的数据会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃;UDP的socket既能读,也能写,这个概念叫做
全双工- 一个UDP能传输的数据最大长度是64K(包含UDP首 部)
这里所说的面向字节流和面向数据报都是站在应用层的角度来看的
面向字节流和面向数据报主要影响的是应用层代码的写法,而在传输层中,数据仍然是以报文的方式来传输的,因此传输数据报和面向数据包、面向字节流是两码事
在应用层代码里,参考TCP的策略来实现,