协议段格式
• 源/⽬的端⼝号:表⽰数据是从哪个进程来,到哪个进程去;
• 32位序号/32位确认号:后⾯详细讲;
• 4位TCP报头⻓度:表⽰该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最⼤⻓度是15*4=60
• 6位标志位:
◦ URG:紧急指针是否有效
◦ ACK:确认号是否有效
◦ PSH:提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛
◦ RST:对⽅要求重新建⽴连接;我们把携带RST标识的称为复位报⽂段
◦ SYN:请求建⽴连接;我们把携带SYN标识的称为同步报⽂段
◦ FIN:通知对⽅,本端要关闭了,我们称携带FIN标识的为结束报⽂段
• 16位窗⼝⼤⼩:后⾯再说
• 16位校验和:发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含TCP⾸部,也包含TCP数据部分.
• 16位紧急指针:标识哪部分数据是紧急数据;
接下来我们来谈谈TCP常见的几个特性
1.确认应答-------用来确保可靠性最核心的机制
TCP将每个字节的数据都进⾏了编号.即为序列号.
每⼀个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下⼀次你从哪⾥开始发.
2.超时重传 --------确认应答的补充
没收到ack 一种情况就是丢包
如果一切顺利,通过应答报文就可以告诉发送方,当前数据是不是成功收到
这个情况下,就要超时重传了
发送方发了个数据,要等; 等的时间里收到了ack(数据报在网络上传输,需要时间)
等了好久,ack还没等到,此时发送方就认为数据的传输出现丢包
当认为丢包之后,就会把刚才的数据包在传输一次(重传)
等待的过程有一个时间阈值(上线),就是(超时)
没收到ack 另一种情况就是ack丢了
因此接收方会收到很多重复数据.那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉
接收方如何判定这个数据是否是"重复数据" 核心判定依据,数据的序号 ---序列号
a.数据还在接收缓冲区里没有被read走
此时,就拿着新收到的数据的序号,和缓冲区中的所有数据的序号对一下,看看有没有一样的
有一样的就是重复了,就可以把新收到的数据丢弃了
b.数据在接收缓冲区中,已经被应用程序read走了,此时新来的数据序号直接无法在接收缓冲区查到
超时是会重传,重传也不是无限的重传.......重传过程也是有一定的策略~~
1.重传次数是有上限的,重传到一定程度,还没有ack ,就尝试重置连接,如果重置也失败,就直接放弃连接
2.重传的超时时间阈值也不是固定不变的,随着重传次数的增加,而增大(重传频率越来越低)
经历重传之后还是丢包,大概率是网络问题,再怎么重传也白费力气,重传还是要重传,但是可以少传几次
数据丢了,还是ack丢了,发送方角度看起来,就是区分不了,都是ack没收到
3.连接管理
1)建立连接(三次握手)
SYN称为同步报文, 意思就是向另外一放申请连接, ACK为应答报文, 意思为同意建立连接)
三次握手本质是就是检测客户端和服务器各自的发送能力和接收能力是否正常.
建⽴连接的意义:
1. 投⽯问路,确认当前通信路径是否畅通.
2. 协商参数,通信双⽅共同确认⼀些通信中的必备参数数值.其中一个信息挺关键的,TCP通信的序号,初始值
TCP一次通信过程中,序号不是从0或者1开始计算的
而是选择一个比较大的数字,以这个数字开头来继续计算
即使同一个客户端和服务器,每次连接没开始的序号都不同
2)断开连接(四次挥手)
断开连接的本质:把对方信息从数据结构中给删除掉
四次挥手能否像三次握手一样,把中间两次交互合并?
有时候能合并,有时候不能合并,不像三次握手一样,100%合并
在实际通信过程中,ack 和第二个fin时间间隔比较长,此时就无法进行合并,就分两次进行传输
四次挥手和三次握手有什么区别?
相似
都是通信双方各自给对方发起一个syn/fin,各自给对方返回ack
数据传输的顺序,syn/ack/syn/ack fin/ack/fin/ack
不同
三次握手中间两次一定能合并,四次挥手则不一定
三次握手,必须是客户端主动, 四次挥手 ,客户端/服务器都可以主动
确认应答,超时重传,连接管理----->可靠传输,可靠传输也付出了代价 传输效率
4.滑动窗口 ----->TCP中非常有特点的机制
确认应答机制下,每次发送方收到一个ack才会发下一个数据,导致大量的时间都消耗在等待ack上了
滑动窗口提出就是为了解决上述问题的,滑动窗口就可以在保证可靠传输的基础上,提高效率(这里的提高效率其实是降低损失,而不是增加速度)
• 窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值.上图的窗⼝⼤⼩就是4000个字节(四个段).
• 发送前四个段的时候,不需要等待任何ACK,直接发送;
• 收到第⼀个ACK后,滑动窗⼝向后移动,继续发送第五个段的数据;依次类推;
• 操作系统内核为了维护这个滑动窗⼝,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只
有确认应答过的数据,才能从缓冲区删掉;
• 窗⼝越⼤,则⽹络的吞吐率就越⾼;
那么如果出现了丢包,如何进⾏重传?这⾥分两种情况讨论.
情况⼀:数据包已经抵达,ACK被丢了
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进⾏确认;
情况⼆:数据包就直接丢了.
• 当某⼀段报⽂段丢失之后,发送端会⼀直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001"⼀样;
• 如果发送端主机连续三次收到了同样⼀个"1001"这样的应答,就会将对应的数据1001-2000重新发送;
• 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
确认应答 超时重传 滑动窗口 快速重传 并不冲突 ,而且是同时存在
滑动窗口中当然也有确认应答,只不过把等待的策略稍作调整,转成批量的
批量的前提,短时间发了很多数据,如果发的数据很少,此时滑动窗口滑不起来,退化成确认应答
如果当前传输过程是按照滑动窗口(短时间传输大量数据) 就按照快速重传保证可靠性.此时判定丢包的标准就是看连续多少个ack索要数据
如果当前传输过程不是按照滑动窗口(没有传很多数据),此时仍按照之前的超时重传保证可靠性,此时判断丢包就是达到超时时间还没有ack到达
5.流量控制(流控)
通过滑动窗口可以提高传输效率
窗口大小越大,更多的数据复用同一块时间等待,效率就越高(批量传多少数据不需要等待ack,此时数据的量就称为"窗口大小")
接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继⽽引起丢包重传等等⼀系列连锁反应.
因此TCP⽀持根据接收端的处理能⼒,来决定发送端的发送速度.这个机制就叫做流量控制(Flow
Control);
• 接收端将⾃⼰可以接收的缓冲区⼤⼩放⼊TCP⾸部中的"窗⼝⼤⼩"字段,通过ACK端通知发送端;
• 窗⼝⼤⼩字段越⼤,说明⽹络的吞吐量越⾼;
• 接收端⼀旦发现⾃⼰的缓冲区快满了,就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;
• 发送端接受到这个窗⼝之后,就会减慢⾃⼰的发送速度;
• 如果接收端缓冲区满了,就会将窗⼝置为0;这时发送⽅不再发送数据,但是需要定期发送⼀个窗⼝探测数据段,使接收端把窗⼝⼤⼩告诉发送端.
接收端如何把窗⼝⼤⼩告诉发送端呢?回忆我们的TCP⾸部中,有⼀个16位窗⼝字段,就是存放了窗⼝⼤⼩信息;
那么问题来了,16位数字最⼤表⽰65535,那么TCP窗⼝最⼤就是65535字节么?
实际上,TCP⾸部40字节选项中还包含了⼀个窗⼝扩⼤因⼦M,实际窗⼝⼤⼩是窗⼝字段的值左移M位;
6.拥塞控制 限制发送方发送数据的速率
流量控制是站在接收方的角度来制约发送方速率的
总的原则是,流量控制和拥塞控制,谁产生的窗口大小更小,谁就说了算
这个拥塞控制具体是怎么这个窗口大小给试出来de?
1.慢启动,刚开始传输得数据,速率是比较小的,采用的窗口大小也就比较小
此时,网络的拥堵情况未知,如果一上来就搞很大,可能就让本来不富裕的网络带宽,就雪上加霜了
2.如果上述传输的数据,没有出现丢包,,说明网络还是畅通的,就增加窗口大小,此时,增大方式是按指数来增长的
由于使用慢启动,开始的时候,窗口大小非常小,也有可能网络上就是很畅通,通过指数增长可以让上述的窗口大小快速变大,这样就可以保证传输的效率
3.指数增长,不会一直持续保持的,可能会增长太快,一下就导致网络拥堵
这里引入一个阈值,当拥塞窗口达到阈值之后,此时,指数增长就成了线性增长
线性增长能使当下的窗口持久保持在一个比较高的速率,并且也不容易一下就造成丢包
4.线性增长也是一直在增长,积累一段时间之后,传输的速率可能过快,此时还是会引起丢包
一旦出现丢包.就把拥塞窗口重置成一个较小的值,回到最初的 慢启动 过程(又要重新指数增长)
并且这里也会根据刚才丢包时窗口大小,重新设置指数增长到线性增长的阈值
7. 延时应答
也是基于滑动窗口,是要尽可能的再提高一点效率
结合滑动窗口以及流量控制,能够通过延时应答ack的方式.把反馈的窗口大小,搞大一些
接收收到数据之后,不会立即返回ack,而是稍等一下,等一会再返回ack.等了这一会,相当于给接收方的应用程序这里,腾出来更多的时间,来消费这里的数据
核心就在于允许的范围内,让窗口尽可能的大
那么所有的包都可以延迟应答么?肯定也不是;
• 数量限制:每隔N个包就应答⼀次;
• 时间限制:超过最⼤延迟时间就应答⼀次;
具体的数量和超时时间,依操作系统不同也有差异;⼀般N取2,超时时间取200ms;
8.捎带应答 基于延时应答,引入的机制,能够提高传输效率
修改窗口大小,确实是提升效率的有效途径
捎带应答,就是走另一条路,尽可能的把能合并的数据包进行合并,从而起到提高效率的效果
9.⾯向字节流
创建⼀个TCP的socket,同时在内核中创建⼀个发送缓冲区和⼀个接收缓冲区;
• 调⽤write时,数据会先写⼊发送缓冲区中;
• 如果发送的字节数太⻓,会被拆分成多个TCP的数据包发出;
• 如果发送的字节数太短,就会先在缓冲区⾥等待,等到缓冲区⻓度差不多了,或者其他合适的时机发送出去;
• 接收数据的时候,数据也是从⽹卡驱动程序到达内核的接收缓冲区;
• 然后应⽤程序可以调⽤read从接收缓冲区拿数据;
• 另⼀⽅⾯,TCP的⼀个连接,既有发送缓冲区,也有接收缓冲区,那么对于这⼀个连接,既可以读数据,也可以写数据.这个概念叫做全双⼯
由于缓冲区的存在,TCP程序的读和写不需要⼀⼀匹配,例如:
• 写100个字节数据时,可以调⽤⼀次write写100个字节,也可以调⽤100次write,每次写⼀个字节;
• 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以⼀次read100个字节,也可以⼀次read⼀个字节,重复100次;
粘包问题
• ⾸先要明确,粘包问题中的"包",是指的应⽤层的数据包
• 在TCP的协议头中,没有如同UDP⼀样的"报⽂⻓度"这样的字段,但是有⼀个序号这样的字段.
• 站在传输层的⻆度,TCP是⼀个⼀个报⽂过来的.按照序号排好序放在缓冲区中.
• 站在应⽤层的⻆度,看到的只是⼀串连续的字节数据
• 那么应⽤程序看到了这么⼀连串的字节数据,就不知道从哪个部分开始到哪个部分,是⼀个完整的应⽤层数据包.
那么如何避免粘包问题呢?归根结底就是⼀句话,明确两个包之间的边界.
• 对于定⻓的包,保证每次都按固定⼤⼩读取即可;例如上⾯的Request结构,是固定⼤⼩的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
• 对于变⻓的包,可以在包头的位置,约定⼀个包总⻓度的字段,从⽽就知道了包的结束位置;
• 对于变⻓的包,还可以在包和包之间使⽤明确的分隔符(应⽤层协议,是程序猿⾃⼰来定的,只要保证分隔符不和正⽂冲突即可);
思考:对于UDP协议来说,是否也存在"粘包问题"呢?
• 对于UDP,如果还没有上层交付数据,UDP的报⽂⻓度仍然在.同时,UDP是⼀个⼀个把数据交付给应⽤层.就有很明确的数据边界.
• 站在应⽤层的站在应⽤层的⻆度,使⽤UDP的时候,要么收到完整的UDP报⽂,要么不收.不会出现"半个"的情况.
10.异常情况
1)其中有一方出现了 ,进程崩溃
进程无论是正常结束还是异常崩溃,都会触发到回收文件资源,关闭文件这样的效果(系统自动完成)就会触发 四次挥手
TCP连接的生命周期,可以比进程更长一些,虽然进程已经退出了,但是TCP连接还在,仍然可以继续四次挥手
虽然说是异常崩溃,实际上和正常的四次挥手结束,没啥区别,进程不在了,是通过系统中仍然持有的连接信息,完成后续的挥手过程的
3)其中一方出现断电(直接拔电源,也是关机,更突然性的关机)
如果直接断电,机器瞬间关机,此时,肯定来不及发送fin
a.断电是接收方,发送方就会突然发现,没有ack了,就要重传,重传几次之后还是,还是不行
TCP就会尝试"复位"连接(相当于清除原来的TCP中的各种临时数据,重新开始)
需要用到一个TCP中的"复位报文段"
b.断电是发送方?接收方本来就是在阻塞等待发送方的消息,迟迟没来消息,咋办
这种情况下,接收方需要区分出,发送方是挂了,还是好着暂时没法
TCP中也是如此,接收方一段时间之后,没有收到对方的消息,就会触发"心跳包"来询问对方的情况
如果对端没心跳了,此时本端也就会尝试复位并且单方面释放连接了
心跳包:也是不携带应用层数据的特殊数据包
1.周期性
2.没有心跳,视为是对端挂了
4)网线断开
⼩结
为什么TCP这么复杂?因为要保证可靠性,同时⼜尽可能的提⾼性能.
可靠性:
• 校验和
• 序列号(按序到达)
• 确认应答
• 超时重发
• 连接管理
• 流量控制
• 拥塞控制
提⾼性能:
• 滑动窗口
• 快速重传
• 延迟应答
• 捎带应答
其他:
• 定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)