1.UDP
首先我们学习UDP和TCP协议 要从这三个问题入手
- 1.报头和有效载荷如何分离、有效载荷如何交付给上一层的协议?
- 2.认识报头
- 3.学习该协议周边的问题
UDP报头
UDP我们先从示意图来讲解,认识报头。
UDP协议首部有16位源端口号,16位目的端口号,16位UDP长度,16位UDP检验和。
16位的端口号,通过前面网络编程篇章,我们知道网络通信,必须要有源端口和目的端口号,2的16次方也就是 65536 所以端口号范围就是0到65535,这里不过多解释了。
16位UDP长度:UDP一次性最大能发送报文长度。16位 那么换算一下就是64KB。也就是说我们最大一次能发送数据就是64KB,对于现在的网络来说,64KB肯定不够用了。那超出的部分怎么办?
如果超过了64KB,那么就需要在应用层 手动的分包,多次发送,并在接收端手动拼装。
通过16位UDP长度,我们就可以将报文和数据分离,报头的长度是固定的,也就是8字节,那么报文长度减去8字节 不就是数据大小了吗?
16位UDP检验和:虽然UDP不保证可靠性,但是对方收到报文也要进行检验,如果收到的数据不全?对方拿着也没有用,还不如直接丢弃。
总结:通过认识报头,我们知道了UDP报文的报头分别是什么意思以及它们的作用。也解决了报头和数据如何分离的问题。
通过示意图还是不够直观的,为什么这么说?毕竟图片和代码是两个概念,很难联系在一起。为了大家能够理解的更深。下面先问大家一个问题
问题:UDP没有发送缓冲区,但是有接受缓冲区,既然有接受缓冲区那么就意味缓冲区里有着大量的UDP报文,既然是大量,那么OS要不要对这些报文进行管理?
明显是要管理的,那么如何管理? 先描述再组织!
所以针对UDP报文管理就转变成了特定的数据结构的增删改查 ,虽然我们没有见过UDP在Linux操作系统内核是如何实现的,但是通过刚才讲解我们也可写一个伪代码。
struct udp_header
{
uinnt32_t src_port;
uinnt32_t dst_port;
uinnt32_t length;
uinnt32_t check_code;
{
既然是自定义类型结构的变量,那么我们就可以在堆上开辟空间,在缓冲区利用链表进行增删改查
struct sk_buffer
{
char* start;
char* end;
char* pos;
int type;
......
struct sk_buffer* next;
}
UDP特点
UDP类似寄信,我们在现实过程中寄信,在寄信过程中不需要通知对方我给你寄信了,至于对方什么时候收到 ,我们不知道。人家回不回你的信也是一个问题(看人)!信内容是一次性发给对方的。总不可能你给人家寄信还分上下文吧?(也就是说我们数据是一次发给对方主机的,面向数据报)
- 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
- 面向数据报: 不能够灵活的控制读写数据的次数和数量;
基于UDP的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议
- BOOTP: 启动协议(用于无盘设备启动)
- DNS: 域名解析协议
2. TCP
TCP是非常重要的协议,没有之一! 我们学习TCP入手还是从刚才讲解UDP的那样一样。但是众所周知TCP是有接受缓冲区和发送缓冲区,而UDP是没有的,这就意味着TCP有很多细节。
2.1 理解TCP缓冲区
我们平时用的write read send recv 函数 本质是拷贝函数,为什么这么说?我们在应用层调用这些系统接口,其实是应用层的缓冲区中数据通过这4个函数拷贝到TCP的缓冲区。也就是说 我们平时在应用层定义的那个缓冲区不是TCP的缓冲区。这也是为什么这些函数叫做系统调用接口,这正好对应了 数据层层向下交付,应用层的数据交付给传输层,反之向上也是一样。说到这里你就会有种熟悉感,这不就是我们之前讲的文件系统一样的吗?
以前是数据 通过系统调用接口 拷贝到OS缓冲区中,OS在将数据写入到文件中。(文件也是有文件缓冲区的)
TCP全称为 "传输控制协议(Transmission Control Protocol"). 传输 我们知道,TCP是个协议我们也知道,控制如何理解?
控制 就是对传输的控制。比如什么时候发? 发多少? 出错了怎么办?
TCP是有接受\发送缓冲区的 发多少是由对方主机的缓冲区大小决定的,什么时候发由OS自主决定。 出错了TCP也有解决的办法。下面我们先从报文入手,认识报文。
2.2 TCP协议段格式
学习报头 我们先解决数据分离 和 向上数据交付。
通过16位的端口号我们就知道如何交付给那一个进程。16位源端口和目的端口号不多多说,前面UDP也讲过。
16位端口号的作用就是数据向上交付。
如何分离? 可是报头里没有报文的长度啊,但是有个4位首部长度。
这个4位首部长度是什么?
4位首部长度:也就是4个比特位,那么四位首部长度取值范围也就是0 到15 ,可是TCP报头就有20个字节。 这个15 不是明显不够? 4位首部长度是带单位的,就比如你的工资是2K?还是W? 而我们的首部长度的单位是4字节。所以首部长度真实大小是0到60字节 所以选项 最大的取值范围为40字节 (选项可以没有)
所以数据和报头分离的采取的是固定长度+自描述字段。(报头大小固定20个字节,那么首部长度-减去固定长度 就得到数据的偏移量。这样我们就找到数据的起始位置。)
16位窗口大小 :
先说作用:填写的是自己接受缓冲区的大小。
TCP要保证自己的可靠性,接受缓冲区的里面的内容 如果已经被打满了,上层不拿走数据,那么此时对方主机再发来数据,就要被覆盖。数据丢失,如何谈可靠性?所以就要有窗口大小。
注意:双方基于TCP协议通信的时候,互发消息都是完整的报文。且一定是携带的完整的TCP报头 所以双方在进行通信的时候通过16位的窗口大小告诉了对方自己的接受缓冲区的大小是多大,这样对面就知道该发多少数据过来了。
32位序号:
TCP有自己的发送缓冲区,而这个发送缓冲区我们可以看做是一个非常大的数组。从应用层拷贝到缓冲区的数据天然就有了序号了,数组的下标就是数据的序号。
那序号存在意义?
有没有想过我们发送大量报文。前面发送的可能比后面发送的数据后到?
如果数据乱序,本身就是一种不可靠。 所以序号存在的意义就是要让数据按序拷贝到对方的应用层。
围绕TCP的可靠性 TCP为了可靠性有了一些保证可靠的机制,比如应答机制。
32位确认序号:
对方主机收到数据会确认序号 会在对方序号+1 比如:对方发送的数据是1000 那么我们作为接受方会发送一个确认讯号 也就是1001 对方收到应答机制ACK 就意味着 我们收到1001之前的所有数据。
这里就有一个问题了 有了序号 为什么要确认序号? 我们确认直接复用序号不就可以了吗?
如果我们对序号确认,就发一个ACK 那当然没有问题,但是 如果我们是数据+应答?明显复用序号就不行了。 不要忘记了 TCP双方是对等,你给我发消息,我也要给你发消息。对方给我们发消息 我们刚好也要给对方发消息 而且要告诉对方我们收到他的消息,这种方式叫做捎带应答 ,最直观的就是提高了效率。
6个标记位:
为什么要有6个标记位? TCP报文可是有不同的类型,而6个标记对应的不同类型报文,比如TCP有建立连接的 断开连接的,正常的数据通信。而这些场景注定了要有一些符号来标识报文的不同。
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
对于ACK 不用多说 前面讲过 ,确认应答。 SYN/FIN 这个两个也不用多说,我们前面讲套接字的时候,调用connect 就是SYN建立连接,调用close 关闭文件描述符就是FIN。
接受缓冲区的是有16位窗口大小的。也就是说缓冲区的最大值是固定的,如果对方一直不读取,而我们要发数据,但是对方接受缓冲区满了 这时候就要用到PSH
TCP 虽然保证可靠性,但是TCP是允许建立连接失败的。
双方基于TCP协议通信的时候,建立连接是要3次握手,如果是A主动对B发起连接SYN 那么 B此时回一个SYN+ACK,这时A就知道了 对方B已经收到我们的请求连接,而这时A再发一个ACK 而A就已经认为自己已经和B建立好连接了 所以A就开始发数据了,但是有没有一种可能,B并没有收到ACK。对于B来说我们3次握手还没有完成,过了几毫秒这时B收到对方的数据,就会将RST置为1发送完整的报文给A。而A收到了RST,就知道了 连接还没有建立。所以重新发起连接请求。
URG 这个标记位我们用的并不多。它作用你可以认为是皇权特许,插位。 TCP为了保证数据的按序。但是有些数据比较急就需要对方优先处理。而此时配合16位紧急指针 就知道要处理紧急数据的位置。 紧急数据大小只有一个字节。毕竟TCP要保证可靠性,不能乱了主次。
16位紧急指针:就是紧急数据的偏移量
2.3 TCP可靠机制
确认应答机制
前面已经提到 可以看前面关于序号的内容。
超时重传机制
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
这个就是超时重传机制,但是上面的情况是对于B来说数据丢失,还有可能是B收到数据了,对于A来说ACK丢包了?
关于超时重传机制存在,对于应答丢失这种情况,那么对于对方主机B就会收到大量重复的报文,重复大量的报文,本身也是一种不可靠的行为。但是超时重传机制又不能不存在。没有超时重传机制TCP的可靠性就无法保证,有没有去重的办法?
前面讲的序号就是去重的报文的,因为在对方的接受缓冲区中之前发来的数据是一直存在的(上层没有拿走数据),根据序号就我们知道对方重新发来报文进行对比,序号一致那么就是重复报文。
那关于超时重传,超时多少重传?
- 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
所以超时重传机制,重传是动态的。随着次数的增多,后面再发送时间会增加,如果超时重传时间太长,那么对方网络的出了问题,这时就关闭我们已经和对方建立好的连接。
连接管理机制
基于TCP协议通信 双方在进行连接时,会进行三次握手,四次挥手。面试题!!!
那什么又是三次握手,四次挥手?
三次握手
基于上面这张图,我们一一讲解,首先我们先看到客户端应用层和服务端应用层 fd write 和read 等等这些函数,是不是很熟悉? 这就是我们前面套接字。
那么客户端和服务器如果是基于TCP 协议通信,此时主动的一方会发起建立连接的请求。(大部分都是客户端发起建立连接的请求 这里我们默认都是客户端发起请求。)
客户端发起请求,会发送一个SYN,(这就是一次了) 服务器收到SYN 就会回一个SYN +ACK (这就是两次了) , 然后 客户端收到 SYN +ACK 再发送一个ACK给服务端。(这就是第三次了)。
TCP为了保证可靠性 一来一回都会发送ACK。这就是3次握手
SYN_SENT 这个状态码叫做 同步发送 同理 SYN_RCVD 叫做 同步收到。 ESTABUSHED 说明本地连接已经建立好了。
这里要强调的有两个函数 connect 和 accept 。当我们客户端调用connect 时,也就是请求连接。会被阻塞,因为双方建立连接,是由双方的操作系统自主决定的,也就是说,当客户端收到SYN +ACK 此时 ESTABUSHED 被设置。如果 建立连接的期间 ESTABUSHED 没有被设置,那么connect 一直都是阻塞状态,直到收到SYN +ACK
同样的 accept 也是一样, 如果 本地连接没有建立好。那么上层调用accept 就会被阻塞,下层都没有连接拿上来能够用的,当然会被阻塞。
四次挥手
当我们建立好连接 就是数据 发送了 ,你给我发送一个数据,我给你一个应答。
如果双方都没有要发的数据了,那么此时双方就会断开连接。
断开连接 和 建立连接不一样, 毕竟 建立连接 是一方主动,一方被动。 断开连接 是要双方协商之后才会断开,就如同生活中,小明追小美一样,一方被动,一方主动。当他们结婚以后,再离婚,可不是说谁 我要离婚,就离婚了,而是 要经过小明和小美一致同意之后,才会离婚。
那么客户端如果没有数据要发了,调用 close 此时客户端就会发送一个FIN 给 服务端,(这就是一次)服务端收到之后 再发送ACK给 客户端(这是第二次) ,如果服务端没有要发的数据了,那么服务端就会给客户端发送FIN(这是第三次)客户端收到服务端的FIN 会发送ACK给服务端 (这是第四次)。
这就是传说中的4次握手。
状态码后面讲 ,我们先说为什么要三次握手,四次挥手。
首先为什么要握手三次?我先说原因 验证全双工,最小奇数次可以将连接管理的成本嫁接到客户端。
TCP 是平等的,你给我发,我也要给你发,所以当客户端请求服务端建立连接发送完整报头将SYN置为1 ,对方收到之后 回一个ACK,这就证明了 客户端给服务器发送消息没有问题,前面也说过,你来我往,你给我发没有问题,但是我给你发有没有问题也是需要验证的。所以客户端再发送一个ACK给服务端 如果服务端收到,这就证明了 服务端给客户端发消息也没有问题。
有人说一次可不可以?
我们设想一下 如果是一次SYN 那么客户端 就每发一次服务器收到就要建立连接。一台机器就可以给服务器建立大量的连接。连接再内核中也是一种数据结构,开辟这种结构体,是要耗费内存的,连接的管理也有成本的。一次握手就是SYN洪水。服务器压力会很大。
那两次?
如果是两次,也是优先给服务端建立连接动作,当服务器发送ACK 本地连接就建立好了,如果客户端不回消息,服务端会将这个连接维持一段时间,只是几台机器还好说,关键是服务器一对多,那么如果有大量连接异常,服务器维持成本就会很大。所以对于这种情况来说,最好是客户端做出让步,毕竟客户端只有自己一个连接。
三次就是最小寄数次 将连接的管理成本嫁接到了客户端。有人说5次 7次 可不可以? 这就有点画蛇添足了。一次就能做好的事情,为什么要返工再做一次或者是多次?
四次 其实3次握手也叫四次握手 因为 按照常理 客户端发SYN 服务端收到 发ACK 然后服务端也要和客户端建立连接,发送SYN 客户端收到之后,回一个ACK 这其实就是4次。 只不是 服务端那次被捎带应答了,也就是 SYN 和 ACK 被压缩成一次了。故而叫做三次握手。
那为什么断开连接是4次 不能三次吗?
这是因为 发送数据 双方都有可能发,所以断开连接必须两次。一来一回就是4次。三次只有极端情况才有可能,客户端想要断开,刚好服务器也没有要发的。而这种几率很小,毕竟服务器是被动的,那么就可以将FIN 和 ACK 压缩成一次。
理解TCP连接时的状态变化
双方建立连接时,是双方OS自主决定的,也就是说和accept没有任何关系。
listen第二参数是连接管理的最大长度,就是一个队列。如果服务器listen第二个参数设置太小 我们假设为9。 那么连接的最大长度就是10 假设我们已经连接了10个客户端,那么第11个客户端和服务器进行连接时,如果上层accept 没有拿走连接 还是10 个。那么 在服务端来说,连接并没有建立好,此时会将第11个客户端的请求建立的连接,状态还是为 SYN/ RCVD 而客户端则认为自己是建立好了连接 将自己的状态码设置为ESTABLISHED。
SYN/ RCVD要变为 ESTABLISHED 一定是条件满足了,条件不满足状态不会发送变化。
TCP保证可靠性,原来的全连接队列已经满了。那这个连接会放在那里?会放在半连接队列里。
对于服务端来说,处于 SYN/ RCVD的状态的连接,不会维护太长的时间。 半连接不重要直接跳过。
这里关于listen的参数就有两个问题了
一是为什么不能没有? 二是为什么不能太长。
先说为什么要有,就好比我们周末去比较火的饭店里吃饭,如果去时间比较晚了,你会发现在前面还有人排队。如果一个饭店没有排队机制,也就是饭店门口没有放座椅话,那么这些客人就会走,对于饭店来说,营业额就会大大降低。那么对于互联网来说也是一样,当双11或者双12时。流量访问巨大,那么对于服务器压力来说是比较大的,如果没有这个机制,人家客户连接不上你的服务器,还怎么买东西?如果我们有了这个管理连接的数据结构那么我们就可以最大化满足客户连接请求。当服务器的压力缓解了,立马就能调用accept。
既然有了为什么不能太长?这个就非常简单了,连接被打满一定是上层出了问题了。比如处理上层的计算任务,而这个任务又太大了,导致服务器的压力剧增。注意这是一种数据结构,也是要耗费内存开辟空间的,如果我们设置长度太多,服务器本来就忙不过来了,还要消耗一部分资源来维护这个连接队列,那么就得不偿失了。
基于刚才的情景,这就是传说中的连接建立不一致的问题。
这里我们以服务器主动断开的背景来讲。
当服务器主动断开连接时,会将自己的状态设置 FIN_WAIT1 此时作为客户端收到了客户端FIN报文,那么会将自己的状态设置为CLOSE_WAIT状态 收到之后给予应答ACK。服务端如果收到了ACK会将自己的状态设置为FIN_WAIT2 当客户端也要断开连接时,也会发送FIN,当对方收到了我们发送的FIN ,此时就会将状态设置为TIME_WAIT 当我们收到了服务端发来的ACK,此时4次挥手才算完成。也就是正常的断开连接。
理解TIME_WAIT状态
首先只有主动断开连接的一方才会有TIME_WAIT,从字面意思来看就是等待一段时间,也就是说连接并没有彻底的断开,IP 和 Port 正在被使用。
我先说理由:
1. 让通信双方历史数据得以消散。
2. 断开连接时,双方4次挥手具有较好的容错性。
为什么需要TIME_WAIT状态
- 确保数据完整性:确保所有重复的TCP段在网络中消失,避免新连接被旧的重复数据干扰。
- 防止端口耗尽:如果立即释放端口,可能会与其他服务冲突,因为TCP连接是基于四元组(源IP、源端口、目的IP、目的端口)来识别的。虽然这种概率很小
- 实现可靠传输:确保TCP连接的终止是可靠的,不会因为网络问题导致连接错误地被认为已经关闭。
那等多久?
- 2MSL等待:
TIME_WAIT
状态会持续一个称为“最大报文段生存时间”(Maximum Segment Lifetime,MSL)的两倍时间。MSL是任何TCP段在网络中生存的最大时间。这个等待时间确保了即使在网络延迟或TCP段被复制的情况下,所有的重复TCP段都能在这段时间内消失。 - 资源占用:在
TIME_WAIT
状态下,系统会为每个处于此状态的连接保留资源(如端口号),直到2MSL时间结束。这可能会导致端口耗尽,特别是在高并发的服务器上。 - 快速回收:在某些操作系统中,可以通过设置
SO_REUSEADDR
套接字选项来允许应用程序快速重用处于TIME_WAIT
状态的端口。可以让服务器立即重启,前面说过IP和Port正在被使用,而端口号只能被一个进程绑定,如果不设置,我们会绑定失败的。
注意:最大报文段生存时间 和 最大传送时长是两个不同概念, 报文在网络存活的时间就是MSL,而最大传送时长是 A端 到B 端 报文传送时间,最大传送时长的时间也就几毫秒,而最大报文段生存时间 这个没有定数,以Linux 为例 一般是60S 。也就是说这个是秒级的。当然这个也可改
流量控制
那我们怎么知道第一次发送数据时,不会打满对面的接受缓冲区? 也就是合理的发送数据?
不要忘记了 TCP在建立连接进行3次握手时,双方就已经告诉了自己各自16位窗口大小。所以在主动建立连接的一方第三次时,其实就可以发送数据了加捎带应答。
那如果双方在后序的发送数据往来时,一方的缓冲区被打满了,这时另一方基于流量控制的机制,那么就不会再发送数据了,为了提高效率,那如何知道对面的缓冲区已经被上层读取了?
作为发送数据的一方会定期发送一个窗口探测的报头,询问对方。 而接受的一方也是一样缓存区的数据被上层读走了,会立马向对方发送一个窗口更新的报头。
滑动窗口
背景知识
前面讲过TCP为了可靠性,发出的报文如果没有收到对应的ACK,那么这个报文还是会存在到自己的发送缓冲区。
其二TCP发送报文并不是我们想象中的发一个报文收一个ACK这样效率太过低下,而是可以串行的发送大量的报文。也就意味着,需要串行的收到大量的ACK,这个就好比 微信发送消息一样,你可以在对面没有回你消息前,一次性可以发送多条消息。
那么大家肯定就会有疑问了,那已经发出未收到ACK的报文是存放在发送缓冲区的那个位置?
首先TCP将发送缓冲区分成了3个区域,第一个区域为已发送已确认,第二区域为以发送未确认,第三个区域为待发送。
而这个第二个区域就是传送中的滑动窗口。而已发送未确认的报文是在滑动窗口这个区域里面,正是因为有了滑动窗口我们才可以发送大量的报文给对方主机。
那如何划分这些区域?前面讲过TCP缓冲区,就是一个数组,我们利用数组的下标充当指针,一个开始,一个结束,滑动窗口本质就是指针的右移,这个就是双指针算法。区域不就划分好了吗?
- 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
- 窗口越大, 则网络的吞吐率就越高;
既然是串行发送大量报文,那如果中间的报文丢了怎么办?就比如下面这种情况
情况1:数据包已经抵达,而ACK被丢了。
首先我们要明白确认序号的意义,如果中间的丢了这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认; 有了确认序号,如果我们收到了2000以后确认序号,那么1001 到2000 我们也是收到的。
情况2:如果是数据直接就丢了?
如上图所示1001——2000的数据丢了,我们串行发送大量报文,对于2000以后的报文,对应的ACK序号还是1001,如果我们收到的ACK重复了3次,会立马补发1001——2000的报文。而这个机制叫做快重传
这里提出了快重传想必大家会有疑问,有了快重传为什么还要有超时重传?
首先快重传存在的意义是提高效率,而快重传这个机制 触发是需要条件的,也就是上面说的3次重复的ACK。想一想如果到了后期数据没有这么多了,就一两条数据发送,能触发快重传吗?如果没有了超时重传,对于这种情况来说,后面的数据可能就收到不到了。
那这个滑动窗口 右移 如何移动?
首先窗口的移动只有右移,没有左移。窗口大小是动态的变化的,也就是说窗口大小不仅取决于对方接受窗口的能力,还要看自己的拥塞窗口(这个后面讲,暂时认为是对方的接受窗口)。
前面说,滑动窗口算法实现是基于双指针算法,如果左指针移动,右指针不动,那么这个滑动窗口必然是缩小的,如果左右指针都移动,那么滑动窗口范围要么变大要么变小(左移动宽度大于右的,那么就是缩小,反之变大),
如果一直都是左移动,右不变,那么滑动窗口就变为0了。(如果为0意味对方上层一直没有读取数据,而且缓冲区被打满。)
那如何现实滑动窗口?
既然是双指针算法,那我们肯定要定义两个指针,start 和 end 。 那如何确定这个两个指针的起始位置?
我们可以根据TCP的确认序号来确认, int start = 确认序号,end = 确认序号+ 对方窗口大小(暂时认为)。
所以到这里你明白了流量控制的实现是基于滑动窗口的。
滑动窗口会不会越界?
当然不会越界 TCP采取了类似环状的算法,也就是取模运算。
延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,为什么这么说,因为虽然对方主机收到了我们发送的数据,但是对方主机上层也就是应用层不一定马上就把数据拿上去了。所以缓冲区数据还是有,那么就意味着对方主机不能进行窗口更新。
所以延迟应答的作用就是提高传输的效率,本质就是在赌上层能够立即讲数据读走。
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
拥塞控制
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
- 此处引入一个概念程为拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
所以真正的滑动窗口的大小是取(滑动窗口的有效数据,拥塞窗口)最小值。
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
如上图所示,当TCP开始启动的时候, 慢启动阈值等于窗口最大值;初始值是16 当这个值为24时,出现了网络拥塞,此时用算法乘法减小(对半减)24变成12 同时拥塞窗口置回1;。 下次在慢开始时,如果这个值到了12了,那么后面就是线性方式增长。
3. 面向字节流
那什么是字节流?
一直都说TCP是面向字节流的,你可以把字节当成水一样。
其实我们上层发送的数据,不管是几个或者单个,到了下层也就是TCP。转化成二进制,几个报文都变成了二进制数据,那么也就意味着没有报文概念了,全部揉在一起,放在TCP的缓冲区中,通过滑动窗口一次发送大量的数据过去,那么对方收到之后就如同你家水龙头打开一样,水流就出来了。至于这个水流是由河水 还是 净化后的自来水,TCP各自的接受缓冲区不关心,真正关心数据来源以及如何区分这些水,那是上层来决定的。
所以我们上层为什么要自己定制协议,方便拿到数据之后,分析解析数据。
为什么TCP是面向字节流的?
这是因为TCP既有发送缓冲区,又有接受缓冲区,也就意味着读写可以不一一匹配。
由于缓冲区的存在,我们调用read 或者 write ,假设我们读取100字节,那么你可以一次性读100个字节,也可以100次读一个字节。对于write也是一样,但是UDP就不行。
4.粘包问题
由于TCP是面向字节流,上层的数据都揉在一起了,加上滑动窗口的存在,必定会有报文的数据一部分没有在滑动窗口内,那么发送过去,对方可能收到的就不是一个完整的报文。这个就是粘包问题。
如何解决?
确定好报文之间的边界
对于这个问题前面博客已经讲过了。这里总结一下吧
- 定长报文
- 特殊字符
- 自描述字段+定长报文
- 自描述字段+特殊字符。
注意:而这个东西也是属于我们自定义协议的一个部分,不要和序列化和反序列化搞混了。
5. TCP异常情况
进程被终止,那么TCP 建立的连接也是随进程一样释放了,因为文件的生命周期是随进程的。
所以机器重启这种情况也是和进程终止一样的。
那如果是网线掉了,或者机器掉电了?
对于这种情况来说:接收端认为自己的建立的连接还存在。但是如果有写入操作那么接受端就会发现建立的连接不存在了,这就是传说中的连接建立不一致的问题,那么接收端就会把标志位RST设置.
6.文件描述符与socket的关系
文件描述符其实就是个指针数组的指针,而这个数组里面的指针指向是一片内存区域的(比如文件缓冲区,TCP的缓冲区),那么在代码中,就是结构体套结构体,文件系统就是stuct *file 那么网络就是 struct *sock ,那么通过指针指向不同的结构体,那么就能执行不一样的代码,如果是本地指针指向的是 *file ,那么对于就是本地的增删改查的方法,如果指向的是*sock那么对应的就是网络中增删改查。
而这个两个结构体都内嵌相对应方法的结构体。
所以这就是C语言的多态。
这里只是简单的说了一说,感谢兴趣的读者可以去看内核的源码。
当然 应用层到数据链路层 每一层都有不同处理报文的方法,所以这里处理方式也是上面一样,指针移动,(这里指针而不是上面的指针,而是内嵌的结构体里面处理增删改查的方法指针,)所以在TCP缓冲区中,每一层数据其实不是向上交付而是通过指针的移动,让人感觉就是向上交付。
总结
- 校验和
- 序列号(按序到达
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答