文章目录
传输层负责数据能够从发送端传输到接收端。
再谈端口号
端口号 (Port) 标识了一个主机上进行通信的不同的应用程序;

在 TCP/IP 协议中,用 “源 IP”, “源端口号”, “目的 IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信 (可以通过 netstat -n 查看);

端口号范围划分
0 - 1023: 知名端口号,HTTP, FTP, SSH 等这些广为使用的应用层协议,他们的端口号都是固定的。
1024 - 65535: 操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。
认识知名端口号
有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号:
ssh 服务器,使用 22 端口
ftp 服务器,使用 21 端口
telnet 服务器,使用 23 端口
http 服务器,使用 80 端口
https 服务器,使用 443
执行下面的命令,可以看到知名端口号
cat /etc/services
我们自己写一个程序使用端口号时,要避开这些知名端口号。
两个问题
1、一个进程是否可以 bind 多个端口号?
可以!
2、一个端口号是否可以被多个进程 bind?
不行!
UDP 协议
UDP 协议端格式

内核中的UDP报头:

为什么udp不考虑黏包问题?(即udp如何分离报头和有效载荷)
16 位 UDP 长度,表示整个数据报 :UDP 首部(报头) + UDP数据(有效载荷)的最大长度,报头中的16 位 UDP 长度是由发送方填写udp报文总长度的,当接收方拿到udp报文后底层会先检验是否有8个字节,如果8个字节都没有会直接将该报文丢弃,否则会继续拿着16 位 UDP 长度检验有效载荷是否完整,也就是将16 位 UDP 长度减去读取到的有效载荷长度是否等于8,如果校验和出错,就也会将该报文丢弃。如何检验和正确说明udp报文完整,可以继续向上交付。
所以对于程序员来说我们不考虑udp的黏包问题,因为系统底层已经自动帮我们解决了。
报头最后的16位校验和是用来检验数据是否乱序,因为长度是对的不一定数据的对的。
UDP 的特点
UDP 传输的过程类似于寄信.
无连接: 知道对端的 IP 和端口号就直接进行传输,不需要建立连接;
不可靠: 没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量;
面向数据报
应用层交给 UDP 多长的报文,UDP 原样发送,既不会拆分,也不会合并;
用 UDP 传输 100 个字节的数据:
如果发送端调用一次 sendto, 发送 100 个字节,那么接收端也必须调用对应的一次 recvfrom, 接收 100 个字节;而不能循环调用 10 次 recvfrom, 每次接收 10 个字节;
UDP 的缓冲区
UDP 没有真正意义上的发送缓冲区(因为没必要,udp不需要将用户发送的数据缓存起来,tcp要将数据缓存起来一是为了一次发送更多数据,二是将数据保存起来以便发送失败后再次发送)。调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
UDP 具有接收缓冲区(当上层来不及处理多个udp报文时能将报文缓存起来,而不是直接丢弃,UDP 接收缓冲区只能一定程度上缓解操作系统收报文的压力,当缓冲区满了后如果发送方还在发,那udp会直接将报文丢弃)。但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致;如果缓冲区满了,再到达的 UDP 数据就会被丢弃;
UDP 的 socket 既能读,也能写,这个概念叫做 全双工。
UDP 使用注意事项
我们注意到,UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部).
然而 64K 在当今的互联网环境下,是一个非常小的数字.
如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
基于 UDP 的应用层协议(应用场景)
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议 (用于无盘设备启动)
DNS: 域名解析协议
当然,也包括你自己写 UDP 程序时自定义的应用层协议;
理解报文
目前关于报文我们只有抽象的概念,这里就又要用到我们对抽象事物的看待过程:先描述,再组织,对于计算机来说,再网络协议栈内部的各种层中一定存在很多报文,计算机要将他们管理起来就需要先描述再组织。
在linux内核中,描述报文的是sk_buff结构体,该结构体内部没有ip、端口号等网络概念,因为该结构体是描述报文在系统中的存在,也就是一个报文的内存空间布局。

我们以udp为例,udp报头长度8字节,自顶向下封装本质就是sk_buff中的data指针不断向向上移动,每次移动8字节,当移动到指向tcp协议头时会把data强转为ip的报头结构体,然后填写tcp属性,不断重复上述过程就完成了封装过程。整个封装过程报文结构体始终都没有发生改变。
解包同理,data指针不断向下移动。
socket和文件系统的关系
在struct_file内部有一个 void * 类型的privata_data指针,该指针就会指向底层的socket结构体:

socket结构体内的sock字段会指向sk_buff 链表的头和尾(sk_receive_queue、sk_write_queue),而这个由sk_buff组成的链表就是tcp/udp的发送/接受缓冲区。
所以对于tcp/udp的发送、接受这两个缓冲区不是一块连续内存,而是一个 sk_buff 链表。
TCP 协议
TCP 全称为 “传输控制协议 (Transmission Control Protocol)"。人如其名,要对数据的传输进行一个详细的控制。
TCP 协议段格式

内核中的tcp报头:

tcp如何分离报头和有效载荷?
tcp报头中有一个4位首部长度的字段,它用来表示tcp完整报头的长度(报头+选项),4位首部长度只能表示0-15,要完整表示整个报头的长度还需带单位:4字节,所以4位首部长度可以表示报头长度0-60字节,所以tcp选项最长可以有40个字节。
这样我们拿到tcp报头长度,当接受到报文后我们也知道了报文长度,就可以计算出有效载荷长度,从而将报头和有效载荷分离。
这里有的读者可能会疑问,为什么tcp报头没有报文总长度?因为tcp不需要!tcp不关心数据是否发送、接受完整,不关注报文是否完整。
序号:
当发送方要发送的信息较大时,就会把消息拆分成多个报文同时发送,但是对于接受方来说,它接收报文的接收顺序是不一定和发送方的发送顺序一致的,比如消息一共200字节,发送方拆分为1-100和101-200分两个报文发送,接受方可能先收到101-200的报文,再收到1-100报文,这时对于接受方来说它收到的消息就是乱序的,乱序也是一种不可靠,所以tcp为了解决这个问题,就会对发送的报文进行编号,只要tcp对每条消息报文做编号,就能保证消息的有序的,因为接受方可以根据编号将报文拼接成完整的消息,这里的编号就是报头内的序号。
确认序号:
我们知道tcp是需要对消息做应答的,而消息接受方给发送方发送的应答就包含确认序号,但确认序号不是对单个报文做应答,而是对历史所有报文做应答,确认序号ack_seq表示该序号之前的报文我已全部收到,例如发送方发送了0-100 101-200 201-300 301-400 401-500,接受方只实际接受到了0-100 101-200 401-500,即使接受方实际接受到了401-500,但接受方也只会应答确认序号ack_seq 201,表示201之前的报文已全部收到,告诉发送方下次发送从ack_seq 201开始发送。
细节补充:
1、上面的发送消息本身包含完整的报头和有效数据,而应答只包含报头,因为序号和确认序号就在报头里,所以tcp中发送的消息本身就包含序号和确认序号。
2、在tcp真实应用场景中,其实很少发只包含报头的应答,因为对端主机如果要回消息的话除了发送一次只包含报头的应答,还要再发一次消息,所以一般都是发送捎带应答,即应答本身包含有效载荷,该应答报文既是对对上一个报文可靠性的确认,也是一个数据报文,正因为存在捎带应答的报文,所以tcp报头会同时存在序号和确认序号,因为捎带应答的报文即是对历史报文的确认(确认型号),也是一个独立的数据报文,也需要对方对其做应答(序号)。
16位窗口大小:
用来让发送方知道对端的接受能力,接受能力本质就是对端接受缓冲区的剩余空间,这样就能避免发送方一直发数据导致对端主机来不及接受,以至于丢弃大量报文,浪费资源。这种通过窗口大小调整发送方发送速率的机制叫做流量控制。
细节补充:
1、流量控制一般是双向的,如果是单向发送,则只做单向流量控制。
2、窗口中填写的是自己的接受缓冲区大小。
3、流量控制不仅仅是保证可靠性,也是一种保证效率的手段,例如当自己接受缓冲区很大时,16位窗口填写的空间很大就相当于变心通知对方多发消息。
六个标准位:
1、标准位本质就是结构体字段中的比特位,为1表示该标准位被设置,为0表示该标准位无效。
2、标准位本质用于区分报文类型,我们以tcp服务器为例,它可能会收到来自客户端建立连接的请求,也可能收到来自客户端断开连接的请求,也可能收到客户端发送的正常数据报文,也可能收到客户端的应答报文,这些不同类型的报文需要服务端做出不同的动作,而要做出不同的动作就需要先识别报文的类型,故标准位就是用来区分报文类型的。
3、
ACK: 标记该报文是否是一个应答报文。因为在tcp中存在大量捎带报文,故大部分报文ACK都会被置为1。
SYN: 标记该报文是否是一个请求建立连接报文,我们把SYN置1的报文称为同步报文段。
FIN: 标记该报文是否是一个断开建立连接报文,我们把FIN置1的报文称为结束报文段。
PSH(push): 提示接收端应用程序立刻从 TCP 缓冲区把数据读走。(本质就是尝试唤醒对端进程)
RST(reset): 用来处理连接异常时让对方进行连接重置;我们把携带 RST 标识的称为复位报文段。
细节:
1、tcp通过三次握手建立连接,不能100%保证建立成功,因为最后一个ACK是没有应答的,所以最后一个ACK不保证可靠性。
2、对于请求建立连接的客户端来说,只要发出ack或接受到syn+ack就是一次握手,而不是发出ack后等到对方收到才算一次握手,因为最后一个ack无法100%确定对方一定收到了的。
3、所以当客户端发送最后一个ack完成后,在客户端视角就成功建立连接了,于是客户端就开始向服务端发送数据,而对于服务端来说,客户端发送最后一个ack是可能丢包的,也就是服务端没收到ack,在服务端视角,它就认为三次握手没有完成,还没建立连接成功,如果此时服务端收到了来自客户端的消息,服务端就会发送RST置1的应答,通知客户端重新三次握手建立连接。
URG: 紧急指针是否有效
紧急指针使用场景:在tcp中,接受缓冲区是按序到达的,这是一种保证可靠性的手段,当有时我们会有让报文插队的需求,例如用户通过tcp上传视频时,若用户想停止上传了,客户端就会发送终止报文,如果此时服务端接收到该终止报文还是按照按序到达,那么就会浪费系统资源,因为会把接受缓冲区的数据全部上传完才会处理终止报文,而如果我们报终止报文URG置1后,并在报头16位紧急指针中添加终止上传操作,此时服务端接受到该URG置1报文后,就会让该报文插队优先处理,从而实现操作快速响应的效果。
TCP可靠性-确认应答机制
我们先理解一下什么是可靠,本质就是发送的消息成功被对方接受到了,并且接受的消息的完整的。
我们用现实生活的场景举个例子,当两个人站A B的很远进行互相喊话时,当A说“吃饭了没?”后是不确定另B是否听到了的,只有当B回应“还没呢”并且A成功接收到了B的回应后,A才能100%确定他说的“吃饭了没”B一定听到了,这时A听到了B的回应,A继续喊话“没吃来我家吃”,而对于B来说,B是不确定他说的“还没呢”A是否听到了,只有当B听到了“没吃来我家吃”时才能100%确定他说的“还没呢”A听到了,这时就会出现一个问题,当双方进行通话时,最新的一条消息是永远无法100%确定对方一定收到了的,所以这里有三个结论:
1、这个世界不存在100%可靠的协议。
2、只要收到应答,上一条发送的报文(历史报文),就能100%确认被对方收到。
3、tcp的确认应答机制是对历史报文100%可靠的保证。
对于tcp来说,当A对B发消息时,B是需要对A做应答的,但是A不需要对应答做应答,否则就没完没了了,当A收到B的应答后,就能保证A->B的可靠性。
tcp的确认应答机制细节:
1、消息 != 应答,消息是包含用户有效信息的报文,而应答只是单纯的应答。
2、tcp需要保证消息的可靠性,故消息接受方必须对消息做应答,但不需要保证应答的可靠性,故不需要对应答做应答。
3、应答方是不确定应答对方是否收到了的,但消息发送方是能确定消息对方是否收到了的(通过是否收到应答确认)。
4、收到应答,本质为了保证历史消息的可靠性。
5、确认应答机制本质是对被应答的报文保证100%可靠。
6、tcp的确认应答是OS自动完成的,属于通信细节,上层用户不参与。
但是上述的tcp常规通信模式效率是十分低下的,因为发一次应答一次是串行进行的,所以实际上tcp是一次性发送多条消息,然后接受方同时对多条消息挨个做应答。
有关序号和确认序号的细节:
TCP 将每个字节的数据都进行了编号. 即为序列号.

每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
超时重传机制
重新理解丢包问题:
1、对于报文是否被对端收到,发送方是可以100%确定的,只要收到应答,说明报文100%被收到。
2、但对于报文是否丢包,发送方是无法确定的,因为发送的数据丢包和对发发送的应答丢包都会导致发送方无法收到应答,所以我们约定只要没收到应答,都把该数据报文当做丢包处理,所以判定是否丢包不是被检验出来的,而是我们约定出来的。

超时重传就是人为规定一个时间,发送数据报文后在这个时间内没有收到应答,就会判定超时(不是丢包!),从而重新发送报文。
细节:
1、如果实际报文没丢,而是应答丢了,超时重传后接收端就会收到重复数据,这是一种不可靠,所以我们可以用序号来去重,来保证可靠性。
那么, 如果超时的时间如何确定?
最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”.
但是这个时间的长短,随着网络环境的不同,是有差异的.
如果超时时间设的太长,会影响整体的重传效率;
如果超时时间设的太短,有可能会频繁发送重复的包;
TCP 为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.
Linux 中 (BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍.
如果重发一次之后,仍然得不到应答,等待 2500ms 后再进行重传.
如果仍然得不到应答,等待 4500ms 进行重传。依次类推,以指数形式递增.
累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接.
连接管理机制
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接。

为什么有三次握手建立连接?
1、双方同意建立连接。三次握手本质是四次握手,因为服务端是“添狗”,它会无条件接受来自客户端的连接请求,所以syn+ack会合二为一成为一条捎带应答。也就是是说客户端和服务端都对对方发送了syn,并且都对对方的syn做了应答,从而建立了双方要进行通信的共识。
2、外界同意建立连接。验证全双工,也就是验证服务端、客户端都能收发消息,本质是在验证网络是否通畅。(建立连接不仅要双方同意,也需要外界同意)
为什么有四次挥手断开连接?
1、客户端发送fin表示我要发的数据都已经发完了,在逻辑上关闭了客户端到服务端的通道,但是此时服务端还可以向客户端发数据,所以服务端的fin+ack一般不是捎带应答。
2、一端close(fd)关闭套接字后便会向对方发送fin,但此时该套接字还有可能收到来自对方发来的信息,所以关闭套接字触发fin除了调用close彻底关闭外还可以调shutdown,它有三个选项可以选择,关闭读端、关闭写端和关闭读写端。
细节:
1、accept不参与3次握手,只获取已经建立好的连接,也就是是说服务器只是监听状态,但没有accept,客户端也能通过connect与服务端三次握手建立连接,也就说明3次握手不需要程序员关心,是由双方操作系统自动完成的。
2、我上方示意图,当客户端发送fin后服务端会处在CLOSE_WAIT状态,当服务端忘记关闭fd,就会导致客户端关闭链接后服务端一直处于CLOSE_WAIT状态,当服务器出现大量CLOSE_WAIT,就说明出现了fd泄露问题!
3、当服务端也关闭套接字,服务端就会从CLOSE_WAIT状态变为LAST_ACK,并且发送fin给客户端,客户端收到fin后就会变为time_wait状态,并发送最后一个ack,这里引出了一个问题,对于客户端来说发送最后一个ack后不就相当于四次挥手完成了吗,为什么还会有time_wait状态?下面小编来详细解释一下。
为什么会存在time_wait?
客户端从TIME_WAIT 到 CLOSED 会等待2MSL(Max Segment Life),MSL是报文最大生存时间,time_wait是为了防止如下场景出现,如果没有time_wait,当客户端发送最后一个ack后即closed,而后客户端所在主机由马上新起了一个客户端,恰好该客户端又和同一个服务端进行通信,并且端口号恰好和上一个客户端端口号一样,恰好服务端给上一个客户端发送的陈旧报文在路由器中阻塞住了,当新起了一个客户端后陈旧报文才姗姗来迟,到达新起的客户端,此时陈旧报文就会对新连接产生干扰,time_wait就是为了防止此类巧合发生。
time_wait等待2MSL作用如下:
1、强制要求新套接字更换端口号。
2、等待陈旧报文从网络中消散。
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
• 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过 ACK 端通知发送端;
• 窗口大小字段越大, 说明网络的吞吐量越高;
• 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
• 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
• 如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.

细节:
1、第一次主机A给主机B发送数据之前是怎么知道主机B的窗口大小的?答案就在三次握手,三次握手时双方已经交换过报文了,即知道了对方的窗口(接受缓冲区)大小。
2、我们知道报文中的窗口大小是16位,那TCP窗口最大最大就是 65535 字节么(64kb)? 实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位(左移一位是2倍,左移两位是4倍,左移三位是8倍)。
滑动窗口
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个 ACK 确认应答. 收到 ACK 后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。

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

1、滑动窗口大小指的是暂时无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是 4000 个字节(四个段).
发送前四个段的时候, 不需要等待任何 ACK, 直接发送;
收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
窗口越大, 则网络的吞吐率就越高;

细节一:
1、滑动窗口在发送缓冲区内,属于发送缓冲区的一部分。
2、滑动窗口把发送缓冲区划分为三部分:滑动窗口内,直接发,暂时不要应答,滑动窗口左侧,已发送,已应答(该部分数据逻辑上已经无效,可以被覆盖),滑动窗口右侧,待发送。
3、滑动窗口大小是由对方ACK的报文中16位窗口大小(win)决定的,即对方的接受能力,所以流量控制,本质是由滑动窗口实现的。
4、因为我们之前介绍过,缓冲区可以简单理解成一个char类型的数组,所以滑动窗口本质是由两个数组下标start和end维护的,start = 确认序号,end = 确认序号 + win。
细节二:
1、滑动窗口只能向右滑动,因为要发送的数据在右边。
2、滑动窗口的大小是会变化的,根据对方的接受能力即接受缓冲区大小决定。
丢包详解
我们先把丢包大致分为两种情况,应答丢和数据包丢,下面我们来分类讨论:
情况一:应答丢
应答丢不要紧,因为应答丢失我们可以根据后续的ack确认,由于应答发送方要保证确认序号之前的报文已全部收到,如下图所以一共丢了三个ack,但是主机A收到了确认序号6001,说明主机B已经接收到了6001之前的所有数据包,主机A下一次就可以从6001开始继续发送数据。

情况二:数据包丢

当某一段报文段丢失之后, 发送端会一直收到 1001 这样的 ACK, 就像是在提醒发送端 “我想要的是 1001” 一样,如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送,这个时候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因为 2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中,这种机制被称为 “高速重发控制”(也叫 “快重传”)。快重传可以提高速率上限,我们之前讲的超时重传是用来兜底的。
对于数据包丢又分为三种情况,下面来一一讲解:

1、最左侧报文丢失
此时后续ack都是1001,所以滑动窗口左侧不会移动,这是理论概念:(为了支持重传,发出的数据,不能立即删除,而应该被暂时保存起来,以方便后续确认或者重传)的实际体现,滑动窗口左侧不移动表示窗口左侧报文报文还不可被覆盖,后续还可以重传。
2、中间报文丢失
此时滑动窗口左侧会向右移动到中间报文丢失的位置,此时就又变成最左侧报文丢失了。
3、最右侧报文丢失
同理也可以变成最左侧报文丢失。
所以滑动窗口的数据包丢包问题本质都是最左侧报文丢失,这是由tcp的确认序号机制决定的,确认序号机制可以用来保证滑动窗口滑动的连续性,即滑动窗口不能跳过没有确认的报文。
总结
1、是什么?
滑动窗口是发送缓冲区中一小段可以暂时不用应答、可以直接发送的数据区域。
2、为什么?
滑动窗口是流量控制,重传机制的底层实现。
最后两个小问题:
1、滑动窗口不会越界,因为逻辑上是一个环形队列。
2、滑动窗口发送数据要把窗口内的数据还划分出几个小块发送,而不是把整个滑动窗口的数据打包一起发送?这个问题答案在链路层,我们后续讲到链路层小编在解释。
拥塞控制
我们之前介绍的各种可靠性机制本质都是用来保证端到端的可靠性,而下面介绍的拥塞控制是用来保证网络的可靠性,但是对于网络瘫痪、软件崩溃、OS异常等软硬件问题tcp是解决不了的,这些问题需要人工介入,tcp只能处理软硬件正常,只是网络拥堵的问题。
何时OS会判断网络拥塞呢?当出现少量丢包时,OS只会认为是偶然丢包,重传即可,但如果出现大量丢包,OS会判定网络拥塞,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能雪上加霜的,所以OS不会重传,而是等一等或减少数据发送。(丢包率:1%-1.5%正常,)
当识别到网络拥塞后,TCP进入 慢启动 机制(本质由拥塞窗口实现), 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据。

此处引入一个概念称为拥塞窗口,本质是一个整型变量,用来动态衡量网络的拥塞程度,当发送方发送的数据大于拥塞窗口就可能引发网络拥塞,反之则不会。
所以对于发送方,不能只考虑对端的接收能力,还要考虑网络的拥塞程度,故纠正滑动窗口的概念:
滑动窗口 = min(对端缓冲区剩余空间大小,拥塞窗口)
下面来介绍拥塞控制算法是如何实现的:
1、发送开始的时候, 定义拥塞窗口大小为 1;
2、每次收到一个 ACK 应答, 拥塞窗口加 1;
3、每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快,因为拥堵情况经过探测后发现不那么拥堵了,主要矛盾就变成了尽快恢复正常通信。利用指数增长是tcp可靠性和效率之间的平衡点!
• 指数增长到一定数量级后为了避免再次拥堵, 因此不能继续使拥塞窗口单纯的加倍.
• 此处引入一个叫做慢启动的阈值(ssthresh)
• 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
• 在每次线性增长触发网络拥塞后, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1;
当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
我们前面介绍的每次线性增长触发网络拥塞时的拥塞窗口值是当前网络拥塞的临界值,这个值可以作为以后拥塞窗口的参考值,但是网络是一直在动态变化的,所以后续的线性增长策略是在探测当前真实的网络拥塞的临界值。

拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
延迟应答
用来提高tcp效率。
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.
• 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
• 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
• 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
• 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
• 数量限制: 每隔 N 个包就应答一次;
• 时间限制: 超过最大延迟时间就应答一次(防止超时重传);
具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms;

捎带应答
捎带应答小编之前已经渗透过了,这里只想补充一点,在tcp三次握手时,前两次syn和syn+ack是不能携带有效载荷的,但最后一个ack是可以携带有效载荷的,这是数据级别的捎带应答,而syn+ack也是一种捎带应答,是报文级别的。
面向字节流
tcp是面向字节流的,字节流中可能有多个报文,报文可能完整也可能不完整,对于tcp来说,它并不关心字节流中的报文具体是什么,所以处理字节流中的报文信息需要应用层自定义协议来解决。
TCP 异常情况
进程终止:
进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别(故如果不主动断开连接,进程直接挂掉系统也会自动触发四次挥手).
机器重启:
和进程终止的情况相同,因为机器重启之前会先杀掉已启动的进程。
发送端机器掉电/网线断开:
因为机器掉电/网线断开是外部时间,发送端不会触发四次挥手,接收端也认为连接还在, 所以TCP 自己内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放(保活机制实际应该由上层决定,因为保活机制和应用场景强相关tcp并不清楚用户建立tcp的目的是什么)。
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接.
TCP 小结
为什么 TCP 这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
• 校验和
• 序列号(按序到达)
• 确认应答
• 超时重发
• 连接管理
• 流量控制
• 拥塞控制
提高性能:
• 滑动窗口
• 快速重传
• 延迟应答
• 捎带应答
其他:
• 定时器(超时重传定时器, 保活定时器, TIME_WAIT 定时器等)
TCP/UDP 对比
我们说了 TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的优点和缺点, 不能简单, 绝对的进行比较
• TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
• UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传输等. 另外 UDP 可以用于广播;
归根结底, TCP 和 UDP 都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.

810

被折叠的 条评论
为什么被折叠?



