文章目录
TCP协议
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制;
TCP协议格式
源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去。和UDP的作用是一样的。
TCP可以通过目的端口号解决向上交的问题,那么它如何解决有效数据和报头进行分离呢?
因为TCP报头中的选项字段大小是不确定的,可能存在,也可能不存在,所以TCP不能通过固定报头的长度来进行有效载荷的分离,它的报头中存在一个4位的首部长度,它表示的就是TCP的报头长度,它的单位为4字节,也就是说TCP的报头大小的范围就是0~60字节,因为除了选项之外其他的字段长度都是固定的,一共20字节,又因为首部长度的单位为4字节,也就限制了选项的长度,一定是4的整数倍个字节,最多40字节。所以如果没有选项的话,那么首部长度的大小就是5。
TCP的报头同样的,在内核中也是结构化的字段。
TCP是需要连接的,对于连接好的客服端和服务器来说,这一对连接客服端和服务器都存在自己的发送缓冲区和接受缓冲区的,因为TCP是全双工的,双方的地位是对等的。不管是服务器还是客服端,在OS中一定会存在很多的TCP连接,所以OS一定要对这些连接进行管理,所以建立连接的本质就是在OS底层创建连接结构体,用合适的数据结构把他们管理起来,因此对于一对连接来说,在双方的OS中都存在对应的连接结构体,对于每一对连接,双方OS都存在对应的接受和发送缓冲区。
确认应答机制
对于TCP请求放发送的报文,每一条在对方收到之后,都会回复一个应答报文(ACK)。只要有应答,发送方就能100%保证上一条消息对方已经收到,但是对于发送应答的一方,无法保证自己的应答对方是否收到。这就是确认应答机制。应答报文,不携带任何数据,只有一个TCP报头。
但是这种一条一条的发送确定性是保证了,但是效率太慢了,所以在实际中一般可以同时发送多条报文,这就需要被请求的一方在收到报文之后,每一条都需要确认应答。
这样虽然可以让发送时间进行重叠,提高效率,但是同时也会伴随一些新的问题的产生。因为网络的状态是不确定的,所以从客服端出来的报文顺序是正确的,但是到服务器之后,谁先到后到不太确定,可能是乱序,这怎么办呢?
TCP的报头中存在序号字段,客服端在发送的时候会把每一个报文的序号填好,这样到达服务器之后,是乱序的话,根据序号对他们进行排序就可以了。然后分别对他们进行应答即可。
为什么会存在32位的序号和32为的确认序号?两个不可以合并?
如果对于单方的请求和对方的回应,使用一个就够了,但是TCP是全双工的,在客户端给服务器发消息的同时,服务器也可能像客户端发消息,这时就必须要两个。
此时服务器给客服端发送的报文就可以合二为一,这个报文即是数据,也是就历史报文的应答,这就是捎带应答。
所以因为序号和确认序号可能被同时使用,所以必须分开。确认序号 = 序号 + 1 。
确认序号表示确认序号之前的所有数据都已经被收到了!!这样规定可以允许少量的应答丢失。
因此TCP保证可靠性不仅仅保证保证可靠性,还会有许多提高效率的设定。这些动作都是在内核中由OS完成的,用户毫不知情。
标记位
为什么会存在标记位??
对于一个主机来说,它有可能正在建立连接,有可能连接已经建立完成准备发送数据,所以对于一个主机来说它一定存在各种各样不同的TCP报文,所以报文是要有类型的,所以说才存在标记位。
URG: 紧急指针是否有效
16位紧急指针表示的是数据在有效载荷中的偏移量,只有一个字节,它存在的意义就是会存在一些情况需要出现提前处理出现插队的情况,此时就需要设计紧急指针了,接收方拿到数据之后,直接根据数据执行相应的方法就可以了。紧急任务一般为(终止或暂停上传行为和服务检测),ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
在数据需要被尽快交付的场景使用,也使用于指令输入。RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
TCP的保证可靠的运输,但是在运输前需要完成三次握手,但是三次握手是不一定成功的,有可能当客服端像服务器发送了请求,服务器收到后给了客户端回应,客户端收到服务器的回应只会给了服务器应答,但是最后一次应答丢失了,所以就会存在,客户端认为链接建立好了,但是服务器认为还没有建立好,这样客户端正常给服务器发送消息,服务器认为它还没有建立好连接就发送消息,就会把RST标记位置1,让客服端重新请求三次握手。因此三次握手本质也就是在赌对方把最后一个ACK应答收到了。如果对方没收到,就会出现链接建立不一致的问题,需要重新请求连接。SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了
流量控制
对于一对建立好的TCP连接,双方都会存在自己的接受缓冲区和发送缓冲区,因为内存是有限的,所以双方的缓冲区的大小一定是有限制的。而且对于接收端来说,处理数据的速度也是有限制的。如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制。
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据,进行阻塞, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.或者是发送端定期的去询问接收端,实际中是两种方法一起在起作用。
所以网络通信也可以理解为一个生产消费者模型,当生产者把空间生产满了,就需要进行阻塞,等待消费者来进行数据的消费,这本质就是同步。
报头汇总的16 位窗口大小就是控制流量控制的字段,因为报文是发送对方的,所以大小一定要填写自己的接收缓冲区剩余大小。
16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位。
超时重传
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
主机A未收到B发来的确认应答, 也可能是因为ACK丢失了。
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 这时就需要用到32位的序号了,对与相同的报文他们的序号一定是一样的,所以序号除了用来排序之外,还可以用来去重。
那么问题就来了,超时重传的时间设置多少比较合适?
设置的太短,会出现过于频繁的出现重传,如果太久效率不行,但是因为网络的状态是浮动的,所以这个时间的设置也一定是要浮动的。最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返,TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间,Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍,如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传,如果仍然得不到应答, 等待 4500ms 进行重传. 依次类推, 以指数形式递增,累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
既然存在超时重传,那么就要求发送方在发送完数据之后,在一段时间内要把数据存放起来。
保存在哪里?
暂时保存在发送缓冲区的滑动窗口中,收到应答是根据滑动窗口移除报文。
连接管理机制
三次握手
TCP在进行建立连接时时需要进行三次握手的。所谓的三次握手也就是请求方发送SYN,接收方回应SYN和ACK然后请求方收到之后再次回复ACK。
那么为什么是三次握手,不是一次握手和二次握手呢??
首先建立连接是需要有成本的,不管是客户端还是服务器,都是需要成本的,如果是一次握手,单台主机就可以通过某种手段大量的向服务器发送请求,这样服务器直接就建立连接的话,单台主机就可能使服务器瘫痪,二次握手也是这样,收到客户端的的请求就建立连接,也会导致这样的问题,这样大量的向服务器发送SYN的问题为iSYN洪水问题。如果是三次握手,就保证了一定是客户端先建立连接,然后才是服务器建立连接,这样就不容易出现上述的问题,当然上述的问题对于三次握手依然存在,但是想要单台主机就做到就比较困难了。
为什么要进行三次握手??
- 可以最小成本的验证全双工
不管是服请求方还是接收方,在完成三次握手之后吗,都已经发送了一次数据和接受了一次数据,所以可以最小成本的验证全双工。 - 奇数次握手,可以保证是客户端先建立连接,服务器后建立连接。
- 其实是4次握手,中间合并为捎带应答了。
可以理解为客服端像服务器请求连接SYN,服务器回应ACK,然后服务器请求连接SYN,客户端回应ACK,中间服务器的应答和请求合并为捎带应答,因为客户端在请求的时候,服务器一定是在等待客户端请求连接的,一旦收到客户端的请求就立马回应,所以服务端的请求和回应大概率实在同时发生的,所以可以捎带应答。这样双方都可以确保对方收到了我们的连接请求。
TCP在通信之前进行三次握手,其实也是在协商其实序号,还有窗口的大小。
四次挥手
TCP在进行断开连接时时需要进行四次挥手的。
我们会发现四次挥手服务器并没有把对客户端大应答和请求进行捎带应答,这是为什么呢??
因为服务器对客户端的应答和对客户端的请求断开大概率是不一起进行的,当客户端对服务器请求连接断开是,表明客户端对服务器的数据发送完了,但是服务器可能还没有对数据处理完毕,或者说数据还在网络中,因此服务器大概率是不能直接就也进行断开请求,因此是四次挥手并不是三次挥手。
系统中也存在系统调用,允许我们只关闭读端或者写端。
对上面四次挥手的状态观察,可以发现如果对于服务器来说,客户端关闭了连接,而服务器不关闭,也就是不调用close,就会一直处于CLOSE_WAIT状态,这样连接就一直不会被清除,会不断地消耗系统资源,Linux在进行开发网络应用时越来越卡可能就是这个导致的。
并且我们还会发现,对于主动断开连接的一方,在四次挥手完之后会处于TIME_WAIT状态,这是一个等待状态,因为虽然已经断开连接了,但是在网络中还会存在尚未到达的对方的报文,所以需要等待报文从网络中进行消散。那么这个等待的时间一般是多久呢?
一般为等待两个MSL,MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的)同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重LAST_ACK);
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
因为TIME_WAIT的存在,所以导致了我们在短时间内会绑定同一个端口号绑定失败的问题。因为它处于TIME_WAIT状态时,虽然进程不在了,但是OS还会在底层维护一段时间它的TCP连接,所以端口还在被使用就会出现这种情况。如何解决?
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符。
Linux内核协议栈为一个tcp连接管理使用两个队列:
- 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
- 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
全连接队列的长度会受到 listen 第二个参数的影响.全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.大小为 listen 的第二个参数 + 1.
滑动窗口
滑动窗口是为了解决同时发送大量的数据暂时不要ACK,从而提高效率提出的解决方案。
滑动窗口其实就是发送缓冲区的一个区域。
缓冲区就可以理解为一个字符数组,里面都是一个一个的字符,而序号就是数据在数组的下标,确认序号就是对方下一次要从哪里开始发,而确认序号 = 序号 + 1 所以序号就是数据的结尾的下标。
滑动窗口就可以理解为一个start = 确认序号, end = start + 窗口大小。
所以本质窗口的滑动就是数组下标的移动。那么滑动窗口的大小有谁来决定?
滑动窗口的大小应该由对方的接收能力来决定,一般来说对方报头的窗口大小就是影响因素之一。
最开始滑动窗口也是有大小的,三次握手也是在协商窗口大小。
滑动窗口的大小会动态变化的,本质也是在进行流量控制。如果对方接受能力好,速度就快一点,接受能力不好,速度就慢一点。
现在假设滑动窗口是4个报文,然后把4个报文发送给对方,假设第一个报文丢了,那么后三个报文的确认序号就都是第一个报文的起始序号,当发送方收到同样的确认序号后,就会对对应报文进行重发,这种机制为快重传,如果四个报文都收到了,第一个应答丢了,那么其他报文的应答还是正常的,发送方收到后面几个报文的应答,也会知道第一个报文收到了,因为确认序号的含义就是该序号之前的数据全部收到了!
滑动窗口不会滑出去吗?
我们可以把缓冲区想象成一个环形的结构。所以就不用担心这个问题了。
报文丢失怎么办?
报文丢失无非就是窗口最左侧的数据丢失,或者中间,或者最右边的丢失,对于最左侧的上面已经解释过了是没问题的,而中间和右侧的报文丢失,最终都会转化为最左侧的报文丢失问题,所以是不怕报文丢失的。
快重传和超时重传
快重传是连续收到3个及以上相同确认序号的报文,才进行的快重传。快重传提高效率的重传策略,而超时重传是保底策略,两者是需要相互配合的。
拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题,因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的,TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;所以TCP的可靠性还考虑了网络状况。
这是就又存在一个窗口,拥塞窗口,它其实就是第一个数字,发送开始的时候, 定义拥塞窗口大小为1;每次收到一个ACK应答, 拥塞窗口加1;所以实际滑动窗口的大小也是受拥塞窗口大小影响的,每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际的滑动窗口的大小。
这种每次收到一个应答都把窗口+1,其实是2^n次幂在增加的,后面是增长非常快的,我们要的就是这样的效果。“慢启动” 只是指初使时慢, 但是增长速度非常快。但是也不能增长的太快,因为窗口毕竟是有上限的嘛。
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍,所以需要引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长,在不考虑流量控制的前提下,拥塞窗口的大小变化就是下图:
当TCP开始启动的时候, 慢启动阈值等于窗口最大值;在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
拥塞窗口一直在变化本质也是一直在探测网络的状况。
延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小,所以当主机收到一个报文之后,可以等这个报文处理了再进行应答,这样可以提高返回窗口的大小。窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;一般延迟应答的方案就两种:
- 数量限制: 每隔N个包就应答一次
- 时间限制: 超过最大延迟时间就应答一次
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
面向字节流
TCP是面向字节流的,什么叫做面向字节流呢?
TCP中对于我们发送的数据,都是以字节为单位的,如果发送的字节数太长, 会被拆分成多个TCP的数据包发出,如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出
去,接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区,因为所以接受方读到数据后读到的也不一定是一个完整的报文,这也是需要我们自己在上层进行分析的,
由于缓冲区的存在, TCP程序的读和写不需要一一匹配
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节,同样读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次。
我们上面说可以把缓冲区理解成一个字符数组,所以数组里面都是一个字符都是以字节为单位的,当我们读取一步分数据时候,数据就会向左侧流动,就想流水一样,有数据就往后面方,前面的数据用掉了数据就往前面流动,所以为字节流。
粘包问题
由于TCP是面向字节流的,如果不分清每个报文和每个报文的边界的话,我们就无法拿出一个准确的报文,这就是粘包问题。
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.
- 使用特殊字符
- 使用定长的报文
- 报头+自描述字段
UDP不存在这个问题,因为使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。这就是面向数据报的。