目录
1.确认应答(ACK)机制
2. 超时重传机制
3. 流量控制机制
4.滑动窗口
4.1 滑动窗口在哪里呢?
4.2 滑动窗口的本质
4.3 滑动窗口可能出现的丢包情况
5.拥塞窗口
6. 延迟应答
7.捎带应答
8.面向字节流
8.1 粘包问题
9.TCP异常情况
10. listen()函数的第二个参数
10.1 回答几个问题
10.2 第二个参数的含义
11. TCP的小结
TCP将每个字节的数据都进行了编号,即为序列号。
首先需要重新认识一下TCP的发送缓冲区,可以将tcp的发送缓冲区看做一个char sendbuffer[NUM]的数组。这样就完成了TCP协议对于数据的编号处理。
发送数据的时候,最后一个字节的数组下标成为报文的序号。
确认序号的意思也就是,确认序号之前的数据我已经拿到了,该从确认序号所在的字节开始向我发送数据。
主机A给主机B发消息,如果发送的报文数据丢失了。
站在B的角度,他无从知道主机A是否给自己发过消息,也就对数据丢包不会有任何反应。站在A的角度,他一直在等B主机的应答报文,可是总不能一直等下去吧?所以当A的一条报文发出去之后,如果迟迟没有应答,就会在一个特定的时间间隔后,重新发送这条报文。
那如果是应答的报文丢失了呢?
站在A的角度,他无法确认是自己发送的报文数据丢失了,还是应答报文数据丢失了。他都会进行超时重传。可在B的角度上,他就会一直接收到重复的报文数据!
但是就算B收到了重复的报文,可以通过序号进行去重,一定程度上也是提高了可靠性。
超时时间如何设定呢?
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包;
Linux中,超时以500ms为单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
接收端处理数据的速度有限.,如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制。
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端。
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度。
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据。
不发送数据就是真的不进行询问了吗?
对于客户端而言,虽然不会发数据,但会不断向服务端发送窗口探测(询问服务端是否有空间接受数据),服务端的ACK应答报文会携带窗口大小给用户端。
对于服务端而言,如果数据进行了向上交付,有了空余空间,也会向客户端发送窗口更新通知。告诉客户端可以发送消息了。
在进行流量控制的时候,第一次通信时我们如何得知对方的接受能力?
其实在三次握手的时候,由于还没有建立连接,是不能携带有效载荷的。但是由于进行客户端和服务端之间互相发送报文,是可以传入窗口大小的。
如果主机A和主机B的消息是一条一条发送的。即A向B发送第一条消息后,等到B的应答报文后才继续发送第二条数据,会导致性能较低。那么如果我们可以一次性发送多条数据,就可以大大提高性能(将多个端的等待时间重叠在一起)。
在发送缓冲区。滑动窗口可以将发送缓冲区分为三个部分。一、已发送数据并且已经收到应答。二、滑动窗口内的数据(可以直接发送并且暂时不需要应答)。三、尚未发送的数据。
发送方可以一次性向对方推送数据的上限。(滑动窗口的上限,理应与对方的接受能力挂钩)。滑动窗口的左右区间即是下标,
下标的更新
win_start=收到应答报文中的确认序号。
win_end=win_start+收到应答报文中的窗口大小。
滑动窗口的一定往右移动吗?
不一定。当数据并没有被向上传达时,滑动窗口其实是在减少的。
滑动窗口可以为0吗?
可以的。一直没有数据向上传达,滑动窗口可能为0。
情况一:数据包已经抵达,但是ACK丢了。
无所谓。只要数据发送到了主机B,B端就会更新确认序号。如果是前面发送的ACK丢失,主机A可以根据主机B后续的应答中获得确认序号。收到确认序号为6001的报文,就说明前面的数据都送到了。如果是后续的丢了,无非也就是再发送一边重复的数据,有历史送达的报文数据帮以去重。
情况二:数据包就直接丢失了
当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK。当连续三次收到了同样一个“1001”这样的应答,就会将对应的数据1001-2000重新发送。如果接收端收到了1001,再次返回的应答中的确认序号就成为了7001.
这种由于连续收到同一个确认序号,而使发送发重新发送该序号数据的机制,称为“高速重发控制”,也称快重传。
快重传和超时重传不是对立的,而是协作的。
如果在刚开始阶段就发送大量的数据, 可能引发问题.因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 才导致发送的报文大量丢失。在不清楚当前网络状态下, 贸然发送大量的数据,会导致服务端更快的崩溃。
拥塞窗口:单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值。
当发送开始的时候,或者遇到网络拥塞的情况时,定义拥塞窗口大小为1。每次收到一个ACK应答,拥塞窗口大小*2,每次发送数据包时,将拥塞窗口和接受端的滑动窗口做对比,取较小值作为实际发送的窗口。
然而指数级的增长,一定是没有必要的,发送到一定次数后,如果没有出现网络拥塞,就会完全取决于对方的接受能力。一开始使用指数级的增长,只是为了让通信快速回复到没有网络拥塞的正常水平。
所以还需要引入一个叫做慢启动的阈值。当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式进行增长。
当TCP开始启动的时候,慢启动的阈值等于窗口最大值。
每次当超时重发时,慢启动阈值会变为原来的一半,同时拥塞窗口置回1.
因为对方的接收缓冲区存在滑动窗口。就有可能允许发送方一次推送更多的数据,这样单次的IO操作可以提升效率。也就是说接收方的应答中如果可以给发送方同步一个更大的接收能力,就有可能可以提高效率。
那如何同步更大的接收能力?
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口大小可能比较小。但如果我们等一等,如果这段时间中接收方进行了向上递交数据,就会导致窗口大小变大,从而可能提高效率!
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
那么所有的包都可以延迟应答吗?
数量限制: 每隔N个包就应答一次。
时间限制: 超过最大延迟时间就应答一次。
将确认应答和发送的数据这两条报文二合一。
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区。调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去。
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 是全双工。
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;完全取决于应用层想怎么对数据进行处理。
在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段。
站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中。站在应用层的角度, 看到的只是一串连续的字节数据。就无从知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
由于缓存区的数据怎么读取是应用层决定的,那也就可能出现想读取一个完整的、有效的报文时,却多读或者少读的情况。
这个问题是通过协议解决的!可以使用定长、或者自定义分隔符的方式保证我们接收到的数据是合法的!这个问题是在应用层解决的。
进程终止
进程终止会释放文件描述符,依然可以发送FIN,和正常关闭没有什么区别。依然保证四次挥手,然后链接正常释放
机器重启
和进程终止情况相同。
发送端网线断开/发送端机器停电
没有机会进行四次挥手,立马识别网络情况变化。本地直接杀掉连接。接收端依旧认为连接存在,但是如果发送端长时间不发送消息,接收端会触发“包活”机制。会定期询问对方是否还存在,如果对方不在,也会释放连接。
接收端重启
发送方依然认为连接存在,会给接收端发送消息。但是接收端认为连接不存在,会向发送端发出携带将RST标记位置一的报文。
accept()要不要参与三次握手?
不用!accept()函数它需要从底层直接获取已经建立好的连接。
如果不调用accept()能建立连接成功吗?
因为不需要参与三次握手,所以是可以的。
如果来不及调用accept(),并且对端还来了大量的连接,难道所有连接都应该先建立好连接吗?
不。就像在满员的餐厅吃饭,需要排队拿号一样。需要对这些等待的人进行管理,才能保证一旦有客人离开,立即让外面的人进来,保证资源是100%利用的。
所以服务器本身要维护一个连接队列。这个队列不能没有,不能太长。和listen()的第二个参数有关。
以 listen(listensock,1) 为例,在上层建立套接字,绑定套接字,监听套接字,但是不accept
当第一个客户端申请连接时,使用 netstat -nltp查看服务端的网络连接状态。发现对应的连接状态为ESTABLISHED。
当第二个客户端申请连接时,使用 netstat -nltp查看服务端的网络连接状态。发现对应的连接状态为ESTABLISHED。
当第三个客户端申请连接时,使用 netstat -nltp查看服务端的网络连接状态。发现对应的连接状态为SYN_RECV(表示三次握手没有完成!)。当在SYN_RECV状态等待一定时间后,如果连接还没有完成,那就会释放掉这个连接!
至于状态的变化,这是因为Linux内核协议栈为一个tcp连接管理提供了两个队列以供使用。
1.半连接队列:用来保存处于SYN_SENT和SUN_RECV状态的请求。2.全连接队列:accept队列。用来保存ESTABLISHED状态
而全队列的长度会受到listen第二个参数的影响。全连接队列满了的时候,就无法继续让当前连接的状态进入ESTABLISHED状态了。这个队列的长度是listen的第二个参数+1
TCP由于既要保证可靠性,又要尽可能的提高性能。所以很复杂。
可靠性:
校验和
序列号(按序到达)
确认应答
超时重发
连接管理
流量控制
拥塞控制
提高性能 :
滑动窗口
快速重传
延迟应答
捎带应答