Bootstrap

TCP协议

TCP全称为"传输控制协议(Transmission Control Protocol").⼈如其名,要对数据的传输进⾏⼀个详细 的控制;

TCP协议段格式

• 源/⽬的端⼝号:表⽰数据是从哪个进程来,到哪个进程去;

• 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位紧急指针:标识哪部分数据是紧急数据;

• 40字节头部选项:暂时忽略;

确认应答

TCP将每个字节的数据都进⾏了编号.即为序列号.

每⼀个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下⼀次你从哪⾥开始发.

超时重传

• 主机A发送数据给B之后,可能因为⽹络拥堵等原因,数据⽆法到达主机B;

 • 如果主机A在⼀个特定时间间隔内没有收到B发来的确认应答,就会进⾏重发; 但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;

因此主机B会收到很多重复数据.那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉.

 这时候我们可以利⽤前⾯提到的序列号,就可以很容易做到去重的效果. 

那么,如果超时的时间如何确定?

• 最理想的情况下,找到⼀个最⼩的时间,保证"确认应答⼀定能在这个时间内返回".

 • 但是这个时间的⻓短,随着⽹络环境的不同,是有差异的.

 • 如果超时时间设的太⻓,会影响整体的重传效率;

 • 如果超时时间设的太短,有可能会频繁发送重复的包; 

TCP为了保证⽆论在任何环境下都能⽐较⾼性能的通信,因此会动态计算这个最⼤超时时间.

 • Linux中(BSDUnix和Windows也是如此),超时以500ms为⼀个单位进⾏控制,每次判定超时重发的 超时时间都是500ms的整数倍.

 • 如果重发⼀次之后,仍然得不到应答,等待2*500ms后再进⾏重传.

 • 如果仍然得不到应答,等待4*500ms进⾏重传.依次类推,以指数形式递增.

• 累计到⼀定的重传次数,TCP认为⽹络或者对端主机出现异常,强制关闭连接.

连接管理

在正常情况下,TCP要经过三次握⼿建⽴连接,四次挥⼿断开连接

下图是TCP状态转换的⼀个汇总:

• 较粗的虚线表⽰服务端的状态变化情况;

 • 较粗的实线表⽰客⼾端的状态变化情况;

 • CLOSED是⼀个假想的起始点,不是真实状态;

TIME_WAIT

想⼀想,为什么是TIME_WAIT的时间是2MSL?

 • MSL是TCP报⽂的最⼤⽣存时间,因此TIME_WAIT持续存在2MSL的话

 • 就能保证在两个传输⽅向上的尚未被接收或迟到的报⽂段都已经消失(否则服务器⽴刻重启,可能会 收到来⾃上⼀个进程的迟到的数据,但是这种数据很可能是错误的);

 • 同时也是在理论上保证最后⼀个报⽂可靠到达(假设最后⼀个ACK丢失,那么服务器会再重发⼀个 FIN.这时虽然客⼾端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK);

CLOSE_WAIT

⼀般⽽⾔,对于服务器上出现⼤量的CLOSE_WAIT状态,原因就是服务器没有正确的关闭socket,导致 四次挥⼿没有正确完成.这是⼀个BUG.只需要加上对应的close即可解决问题.

滑动窗⼝ 

刚才我们讨论了确认应答策略,对每⼀个发送的数据段,都要给⼀个ACK确认应答.收到ACK后再发送下 ⼀个数据段.这样做有⼀个⽐较⼤的缺点,就是性能较差.尤其是数据往返的时间较⻓的时候.

既然这样⼀发⼀收的⽅式性能较低,那么我们⼀次发送多条数据,就可以⼤⼤的提⾼性能(其实是将多个 段的等待时间重叠在⼀起了).

• 窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值.上图的窗⼝⼤⼩就是4000个字节 (四个段).

• 发送前四个段的时候,不需要等待任何ACK,直接发送;

• 收到第⼀个ACK后,滑动窗⼝向后移动,继续发送第五个段的数据;依次类推;

• 操作系统内核为了维护这个滑动窗⼝,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只 有确认应答过的数据,才能从缓冲区删掉;

• 窗⼝越⼤,则⽹络的吞吐率就越⾼;

那么如果出现了丢包,如何进⾏重传?这⾥分两种情况讨论.

 情况⼀:数据包已经抵达,ACK被丢了.

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进⾏确认;

 情况⼆:数据包就直接丢了.

• 当某⼀段报⽂段丢失之后,发送端会⼀直收到1001这样的ACK,就像是在提醒发送端"我想要的是 1001"⼀样;

 • 如果发送端主机连续三次收到了同样⼀个"1001"这样的应答,就会将对应的数据1001-2000重新 发送;

 • 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就 已经收到了,被放到了接收端操作系统内核的接收缓冲区中; 这种机制被称为"⾼速重发控制"(也叫"快重传").

流量控制

接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送 端继续发送,就会造成丢包,继⽽引起丢包重传等等⼀系列连锁反应.

因此TCP⽀持根据接收端的处理能⼒,来决定发送端的发送速度.这个机制就叫做流量控制(Flow Control);

• 接收端将⾃⼰可以接收的缓冲区⼤⼩放⼊TCP⾸部中的"窗⼝⼤⼩"字段,通过ACK端通知发送端;

• 窗⼝⼤⼩字段越⼤,说明⽹络的吞吐量越⾼;

• 接收端⼀旦发现⾃⼰的缓冲区快满了,就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;

• 发送端接受到这个窗⼝之后,就会减慢⾃⼰的发送速度;

• 如果接收端缓冲区满了,就会将窗⼝置为0;这时发送⽅不再发送数据,但是需要定期发送⼀个窗⼝探 测数据段,使接收端把窗⼝⼤⼩告诉发送端.

接收端如何把窗⼝⼤⼩告诉发送端呢?回忆我们的TCP⾸部中,有⼀个16位窗⼝字段,就是存放了窗⼝⼤⼩信息;

那么问题来了,16位数字最⼤表⽰65535,那么TCP窗⼝最⼤就是65535字节么?

 实际上,TCP⾸部40字节选项中还包含了⼀个窗⼝扩⼤因⼦M,实际窗⼝⼤⼩是窗⼝字段的值左移M位;

拥塞控制

虽然TCP有了滑动窗⼝这个⼤杀器,能够⾼效可靠的发送⼤量的数据.但是如果在刚开始阶段就发送⼤量 的数据,仍然可能引发问题.

因为⽹络上有很多的计算机,可能当前的⽹络状态就已经⽐较拥堵.在不清楚当前⽹络状态下,贸然发送 ⼤量的数据,是很有可能引起雪上加霜的.

TCP引⼊慢启动机制,先发少量的数据,探探路,摸清当前的⽹络拥堵状态,再决定按照多⼤的速度传输 数据;

• 此处引⼊⼀个概念程为拥塞窗⼝

• 发送开始的时候,定义拥塞窗⼝⼤⼩为1;

• 每次收到⼀个ACK应答,拥塞窗⼝加1;

• 每次发送数据包的时候,将拥塞窗⼝和接收端主机反馈的窗⼝⼤⼩做⽐较,取较⼩的值作为实际发送的窗⼝; 像上⾯这样的拥塞窗⼝增⻓速度,是指数级别的."慢启动"只是指初使时慢,但是增⻓速度⾮常快.

• 为了不增⻓的那么快,因此不能使拥塞窗⼝单纯的加倍.

• 此处引⼊⼀个叫做慢启动的阈值 • 当拥塞窗⼝超过这个阈值的时候,不再按照指数⽅式增⻓,⽽是按照线性⽅式增⻓

• 当TCP开始启动的时候,慢启动阈值等于窗⼝最⼤值;

• 在每次超时重发的时候,慢启动阈值会变成原来的⼀半,同时拥塞窗⼝置回1;

少量的丢包,我们仅仅是触发超时重传;⼤量的丢包,我们就认为⽹络拥塞;

当TCP通信开始后,⽹络吞吐量会逐渐上升;随着⽹络发⽣拥堵,吞吐量会⽴刻下降;

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对⽅,但是⼜要避免给⽹络造成太⼤压⼒的折 中⽅案.

TCP拥塞控制这样的过程,就好像热恋的感觉

延迟应答

如果接收数据的主机⽴刻返回ACK应答,这时候返回的窗⼝可能⽐较⼩.

• 假设接收端缓冲区为1M.⼀次收到了500K的数据;如果⽴刻应答,返回的窗⼝就是500K;

• 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了;

• 在这种情况下,接收端处理还远没有达到⾃⼰的极限,即使窗⼝再放⼤⼀些,也能处理过来;

• 如果接收端稍微等⼀会再应答,⽐如等待200ms再应答,那么这个时候返回的窗⼝⼤⼩就是1M; ⼀定要记得,窗⼝越⼤,⽹络吞吐量就越⼤,传输效率就越⾼.我们的⽬标是在保证⽹络不拥塞的情况下 尽量提⾼传输效率;

 那么所有的包都可以延迟应答么?肯定也不是;

• 数量限制:每隔N个包就应答⼀次;

• 时间限制:超过最⼤延迟时间就应答⼀次;

具体的数量和超时时间,依操作系统不同也有差异;⼀般N取2,超时时间取200ms;

捎带应答

在延迟应答的基础上,我们发现,很多情况下,客⼾端服务器在应⽤层也是"⼀发⼀收"的.意味着客⼾端 给服务器说了"How are you",服务器也会给客⼾端回⼀个"Fine,thank you";

那么这个时候ACK就可以搭顺⻛⻋,和服务器回应的"Fine,thank you"⼀起回给客⼾端

⾯向字节流

创建⼀个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报⽂,要么不收.不会出 现"半个"的情况. 

异常情况

 • 进程终⽌:进程终⽌会释放⽂件描述符,仍然可以发送FIN.和正常关闭没有什么区别.

 • 机器重启:和进程终⽌的情况相同.

 • 机器掉电/⽹线断开:接收端认为连接还在,⼀旦接收端有写⼊操作,接收端发现连接已经不在了,就 会进⾏reset.即使没有写⼊操作,TCP⾃⼰也内置了⼀个保活定时器,会定期询问对⽅是否还在.如果 对⽅不在,也会把连接释放. 另外,应⽤层的某些协议,也有⼀些这样的检测机制.例如HTTP⻓连接中,也会定期检测对⽅的状态.例 如QQ,在QQ断线之后,也会定期尝试重新连接.

;