赞
踩
博主个人社区:开发与算法学习社区
博主个人主页:Killing Vibe的博客
欢迎大家加入,一起交流学习~~
网络中的数据,是经过路由器之间,一跳一跳地,接力一样地,传送到目标主机上的。
这带来了两个问题:(站在网络层的视角上讨论)
✦ 网络传送是不可靠的
由于路可能不同,所以很难保证按照出发的顺序到达
✦ 网络传送是不安全的
这两个问题需要交给网络层以上(传输层、应用层)去解决
这篇文章博主将详细总结一下传输层的TCP协议
TCP,即Transmission Control Protocol,传输控制协议。人如其名,要对数据的传输进行一个详细的控制。
可靠的,有连接的,面向字节流的
什么是可靠性?
TCP对数据传输提供的管控机制,主要体现在两个方面:可靠和效率。
有了确认应答机制之后,现在还遗留两个问题:
答:
接下来就是围绕上述两个问题展开。
TCP将每个字节的数据都进行了编号。即为序列号。
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发。
下面围绕数据的编号以及确认的编号来讨论:
编号的规则:
ASN的填写规则:
填写的是要接受的下一个字节的数据(本次收到的数据的最后一个字节的下一个)
SN在发送TCP Segment 时,Header中是如何体现的?
注意:
1.TCP发送/接收的完整数据,一般称为segment(段)
2.TCP segment = header + payload
举个栗子:
因为一次可以发送多个字节的数据
byte[] data = {1,2,3,4,5}
tcp.write(data); //一次性发送了5个字节的数据
所以,SN直接填写本次发送的数据中第一个字节的数据即可。segment会携带payload长度 SN=a
TCP由于要进行发送,也要进行确认。所以实际上 TCP Segment有两种不同的角色:
TCP设计的时候,一个segment可以身兼两种不同的角色。
无论何时,一个segment 都视为send segment角色。
但当某个标志位被置位(开关被打开)时,segment具备了acknowledge segment的角色。
这个开关在TCP Header是通过ack标志位进行处理的:
- 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;
因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
那么,如果超时的时间如何确定?
- 最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。
- 但是这个时间的长短,随着网络环境的不同,是有差异的。
- 如果超时时间设的太长,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定
- 超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
- 如果仍然得不到应答,等待4*500ms 进行重传。依次类推,以指数形式递增。
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
TCP已经尽了自己最大的努力,接下来:
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
作为一台主机上的TCP,需要:
所以TCP就有了连接(Connection)的概念,以及连接管理(一条连接的医生= 开始创建+正式使用+销毁)
关于连接:
好了,有了如上的铺垫,接下来博主讲逐步讲解三次握手和四次挥手到底是怎么一回事:
✭ 握手阶段其实就是双方互相同步信息的阶段:
逻辑上需要四次,因为互相发同步信息(2次)都需要应答(2次)。
由于第二步和第三步是可以同时发生的,TCP也支持一个Segment同时起到(syn和ack)的作用,所以上述2和3可以合并,减少网络数据发送的次数,整体上提高性能。
✭ 同理,挥手阶段其实就是双方互相确认要断开连接的阶段:
情况一:
情况二:
情况三:
注意:
三次握手阶段是否可以携带payload?
原因在于不能确认连接一定建立成功,如果携带数据,则提升发送成本,如果失败,一切白发,所以协议设计时禁止携带数据。
syn序列号的变化:虽然不能携带数据,但也会消耗一个序列号:
这是一块重要的内容,博主直接分P详细总结并且画图逐步讲解了,链接如下:
- 调用write时,数据会先写入发送缓冲区中;
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 全双工
- 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次;
用Wireshark抓包工具,抓取传输协议为TCP的包:
以上就是三次握手的三个包。
下图为第一个包中的数据:(蓝色部分为TCP的部分)
现在可以根据这些数据来一一计算验证我们的TCP头:
验证如下:
16位源端口号:首先前面两个字节是 0xe542,换算成十进制就是 58690
16位目的端口号:然后就是后面的两个字节 0x22b8,换算过来就是 8888
32位SN: 0xb1e9b0e0 ,可以看出来ISN不是0。
32位ASN: 0x00000000,一开始发送的时候还没有ASN
4位首部长度: 0x8,说明协议首部长度为 8 * 4 = 32 字节(真实首部长度= 首部长度 * 4 ,像这里的8表示有8个32为bit,也就是8个4字节 = 32字节)
保留6位+标志位6位: 0x002 ,换算成二进制就是000000 000010,刚好对应的就是syn位被置为了1 .
后面的是一样计算的,博主就不一一列举了。
以下内容博主也进行了分P总结,这块内容比较难理解,最好就是死记硬背,具体可以参考博主的图解,更容易理解,链接如下:
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN。和正常关闭没有什么区别。
- 机器重启:和进程终止的情况相同。
- 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。
拓展:
进程终止和机器关机/重启:
一个进程的所有资源都是OS分配的(换言之,一个进程有哪些资源OS都是知道的),所以,即使进程内部没有关闭TCP连接,OS也会走进程资源释放流程,将TCP连接正常关闭,这个进程会作为主动关闭方,正常四次挥手
只要OS的代码能执行,连接就会正常关闭!!!
机器掉电/网线断开:
这种情况,OS的代码就不能执行了,因为机器掉电了。
此时这条连接的命运需要分开来讨论:
现在假设有两台主机 甲和乙。
为了解决上述这个问题:
例如QQ,在QQ断线之后,也会定期尝试重新连接。
关于RST:
- 首先要明确,粘包问题中的 “包” ,是指的应用层的数据包。
- 在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。
- 站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。
- 站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界
- 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
使用 netstat 命令:
由于输出较多,可以结合findstr 做结果过滤:
根据任务管理器,知道 pid为29632的进程占用了端口为8080的TCP连接:
以上就是TCP协议的万字总结,内容有点多,但每个知识点的细节博主都有举实例,帮助大家更好的理解,码字不易,有帮助的话大家可以点个赞收藏起来慢慢看,有什么问题可以私信博主,大家交流一下。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。