网络基础(二)——传输层

1、再谈端口号

端口号(Port)标识了一个主机上进行通信的不同的应用程序;

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

1.1、端口号范围划分

0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。

1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。

1.2、认识知名端口号

有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:

ssh服务器, 使用22端口

ftp服务器, 使用21端口

telnet服务器, 使用23端口

http服务器, 使用80端口

https服务器, 使用443端口

执行下面的命令, 可以看到知名端口号:

cat /etc/services

我们自己写一个程序使用端口号时, 要避开这些知名端口号。

1.3、两个问题

  1. 一个进程是否可以bind多个端口号?

  2. 一个端口号是否可以被多个进程bind?

这两个问题我们之前谈udp服务器的时候已经说过了,这里再重复一遍:

一个进程可以绑定多个端口号,但是一个端口号不可以被多个进程绑定,如果被多个进程绑定就会让端口号无法决定该将信息发送给哪个进程!

1.4、两个指令

netstat

netstat是一个用来查看网络状态的重要工具。

语法:netstat [选项]

功能:查看网络状态

常用选项:

n 拒绝显示别名,能显示数字的全部转化成数字

l 仅列出有在 Listen (监听) 的服務状态

p 显示建立相关链接的程序名

t (tcp)仅显示tcp相关选项

u (udp)仅显示udp相关选项

a (all)显示所有选项,默认不显示LISTEN相关

pidof

在查看服务器的进程id时非常方便。

语法:pidof [进程名]

功能:通过进程名, 查看进程id


2、UDP协议

2.1、UDP协议端格式

16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;

如果校验和出错, 就会直接丢弃。

2.2、UDP的特点

UDP传输的过程类似于寄信。

1、无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;

2、不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;

3、面向数据报: 不能够灵活的控制读写数据的次数和数量;

2.3、面向数据报

应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;

用UDP传输100个字节的数据:

如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;

2.4、UDP的缓冲区

UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;

UDP具有接收缓冲区.。但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;

UDP的socket既能读, 也能写, 这个概念叫做全双工。

2.5、UDP使用注意事项

我们注意到, UDP协议首部中有一个16位的最大长度。 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。

然而64K在当今的互联网环境下, 是一个非常小的数字。

如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装。

2.6、基于UDP的应用层协议

NFS: 网络文件系统

TFTP: 简单文件传输协议

DHCP: 动态主机配置协议

BOOTP: 启动协议(用于无盘设备启动)

DNS: 域名解析协议

当然, 也包括你自己写UDP程序时自定义的应用层协议;


3、TCP协议

TCP全称为 "传输控制协议(Transmission Control Protocol")。人如其名, 要对数据的传输进行一个详细的控制;

3.1、TCP协议段格式

接下来我们来一个个的解释这些字段的含义:

源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;

32位序号/32位确认号: 后面详细讲;(TCP将每个字节的数据都进行了编号,即为序列号。每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。确认序列号的填充的就是收到的报文的序号+1。)

**4为首部长度:**表示该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字节头部选项: 暂时忽略;

接下来我们对重要的字段进行详细的解释。

16位窗口大小

我们知道,TCP和UDP最大的区别就是TCP是可靠性保证的传输,那么TCP凭什么保证可靠性呢?最基本的一个特点:确认应答(ACK)机制

这里先简单介绍一下确认应答机制,这个机制就是我们服务端在给客户端发送消息的时候,服务端收到了会给客户端一个确认收到的通知

当然要保证可靠性只靠这个也是不现实的,下面我们看其他问题。

TCP协议规定双方都有自己的发送缓冲区和接受缓冲区,那么如果接收方的接收缓冲区要满了,我们就得让发送方发的慢一点,依据是什么呢?这里就要解释上面没说的16位窗口大小的作用, 这个窗口填写的就是自己的接收缓冲区中的剩余空间的大小。根据确认应答机制,服务端在返回确认报文的时候会将自己的接受缓冲区的大小填进报头的16为窗口中,所以,这里也就规定了,client和server基于tcp协议进行通信的时候,发送的是完整的tcp报文!

32位序号/32位确认号

理解完这一点,我们再回到确认应答机制,我们仔细想一想这样的机制是否存在一定的问题,比如我们该如何知道服务器应答的是我们发送的哪一条数据?还有最重要的一点,我们又如何保证我们发送数据的顺序性?对于我们传输层来说,数据的乱序,本身就是不可靠的一种,所以TCP协议也有两个字段来解决这个问题,就是32位序号/32位确认号。

TCP将每个字节的数据都进行了编号(本质就是数组下标),即为序列号。

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。确认序列号的填充的就是收到的报文的序号+1。

这里有人就会提一个问题,如果发送的数据一定会有应答,那为什么要分出序号和确认序号,直接合并成一个,客户端用序号决定发送顺序,服务器用序号来回答,不也可以吗?这里就要对之前说的确认应答机制进行一个重新解释,TCP最基本最原始的通信过程,应答的过程并不是单纯的应答,而是捎带应答,也就是服务器可以在给客户端发送数据的同时应答一下之前客户端发送的数据。这样可以提高网络数据传输的效率。

6位标志位

tcp在开始通信的时候建立连接、正常数据通信和断开连接都需要发送tcp报文,所以tcp收到的报文一定是有各种类型的,不同的类型,就决定了服务器不同的动作,6位标志位的作用就是:区分tcp报文的类型。

这里还涉及到另一个保证tcp可靠性的机制,就是超时重传机制。


主机 A 发送数据给 B 之后 , 可能因为网络拥堵等原因 , 数据无法到达主机 B;
如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答 , 就会进行重发 ;
但是 , 主机 A 未收到 B 发来的确认应答 , 也可能是因为 ACK 丢失了 ;

因此主机 B 会收到很多重复数据。 那么 TCP 协议需要能够识别出那些包是重复的包 , 并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。
那么超时的时间该如何规定呢?

最理想的情况下 , 找到一个最小的时间 , 保证 " 确认应答一定能在这个时间内返回 "。
但是这个时间的长短 , 随着网络环境的不同 , 是有差异的。
如果超时时间设的太长 , 会影响整体的重传效率 ;
如果超时时间设的太短 , 有可能会频繁发送重复的包 ;
TCP 为了保证无论在任何环境下都能比较高性能的通信 , 因此会动态计算这个最大超时时间。
Linux 中 (BSD Unix 和 Windows 也是如此 ), 超时以 500ms 为一个单位进行控制 , 每次判定超时重发的超时时间都是500ms 的整数倍。
如果重发一次之后 , 仍然得不到应答 , 等待 2*500ms 后再进行重传。
如果仍然得不到应答 , 等待 4*500ms 进行重传 . 依次类推 , 以指数形式递增。
累计到一定的重传次数 , TCP 认为网络或者对端主机出现异常 , 强制关闭连接。

3.2、连接管理机制

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


服务端状态转化 :

1、[CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态 , 等待客户端连接 ;
2、[LISTEN -> SYN_RCVD] 一旦监听到连接请求 ( 同步报文段 ), 就将该连接放入内核等待队列中 , 并向客户端 发送SYN 确认报文。
3、 [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文 , 就进入 ESTABLISHED 状态 , 可以进行 读写数据了。
4、[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接 ( 调用 close), 服务器会收到结束报文段 , 服务器 返回确认报文段并进入CLOSE_WAIT;
5、[CLOSE_WAIT -> LAST_ACK] 进入 CLOSE_WAIT 后说明服务器准备关闭连接 ( 需要处理完之前的数据 ); 当 服务器真正调用close 关闭连接时 , 会向客户端发送 FIN, 此时服务器 LAST_ACK 状态 , 等待最后一个 ACK到来 ( 这个 ACK 是客户端确认收到了 FIN)
6、[LAST_ACK -> CLOSED] 服务器收到了对 FIN 的 ACK, 彻底关闭连接
客户端状态转化 :
1、[CLOSED -> SYN_SENT] 客户端调用 connect, 发送同步报文段 ;
2、[SYN_SENT -> ESTABLISHED] connect 调用成功 , 则进入 ESTABLISHED 状态 , 开始读写数据 ;
3、[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用 close 时 , 向服务器发送结束报文段 , 同时进入 FIN_WAIT_1;
4、[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认 , 则进入 FIN_WAIT_2, 开始等待服 务器的结束报文段;
5、[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段 , 进入 TIME_WAIT, 并发出 LAST_ACK;
6、[TIME_WAIT -> CLOSED] 客户端要等待一个 2MSL(Max Segment Life, 报文最大生存时间 ) 的时间 , 才会 进入CLOSED 状态 .
首先我们解释一下为什么是三次握手:

  1. 全双工协议,连接建立的核心要务:首先要验证双方的通信信道是连通的。所以三次握手是验证双方通信信道的最小次数。
  1. 连接建立异常的情况下,一定已经已经建立的连接是在client端,也就是最后维护连接的要是client端。所以三次握手是需要奇数次。
    对于第一点,三次握手是最小次数是因为,client要验证能收,能发。server也要验证能收,能发,所以三次握手是验证双方通信信道的最小次数。其中前两次是验证client能收,能发,后两次是验证server能收,能发。
    第二点, 建立的连接一定会消耗时间和空间,而且谁最后发ACK,谁就先建连接,所以如果最后的ACK丢了,client认为已经建立好了连接,而server不这么认为。换句话说,谁最后发送ACK,谁就先维护连接。所以如果最后是偶数次,也就是最后发ACK是server端,换句话说也就是如果最后的ACK丢了,那么这条连接就维护在server端,可是服务器是一对多的,所以如果很多是client连接server,而且很多的client最后的ACK都连接失败了,那么server就会有大量的临时连接被建立起来,而且还不做事情,还占了操作系统大量资源,那么就会导致操作系统短期之内无法建立或者接收新的内容。虽然最后还是有异常连接,但是不影响,因为最后的异常连接维护在client,是无影响的,不像在server端是一对多的,如果异常连接太多就会导致服务器出问题,有风险。这样维护在client端也是没风险的!因为client端不会维护太多的资源,所以通过风险转移,保证了连接建立。
    那么为什么是四次挥手呢?
  1. 信道都没有问题
  1. tcp是全双工,可以双向通信,所以在断开连接的时候要经过双方认可,至此才能关闭连接。
    当我们在连接断开的时候,我们已经不需要验证双方通信信道是否通常了,因为前面我们一直在通信,所以我们在连接断开连接这个事情上,我们更重要的是在功能上通知对方,换句话说,如果client给server发送给FIN,server收到了,那么server就确认了已经把连接断开的信息给了server,如果server给client发送了ACK,client收到了,那么client就确认了,自身可以释放对应的缓冲区资源,而且server也保证的知道了这条消息,换言之这两次挥手在功能上就已经可以确认了client已经单向的向server断开连接,所以再两次挥手就可以实现server单向的向client断开。所以我们就可以通过四次挥手在功能上完成双方信道的关闭。

理解TIME_WAIT状态

当client收到一次FIN,然后对server进行响应,但是ACK在途中丢了,但是client还是处于TIME_WAIT状态,虽然server不会对client进行响应,但是server因为给client发送了FIN,超时重传的时间没有收到响应,那么就会进行超时重传,也就是说client第二次收到FIN就说明了client对server第一次FIN的响应是丢了的。

这时可能有人会问:那么如果第二次的FIN也丢了呢?因为client此时处于TIME_WAIT状态,如果第二次的FIN丢了,也就意味着client到server的信道出问题了,server到client的信道也出问题了,那么也就说明了client和server之间的网络出问题了,那么客户端就等TIME_WAIT时间退出就行了,因为此时client没有收到FIN,那么ACK也就不需要重传了。server经过多次尝试也就断开了。

这时可能又会有人说:这样不还是server经过多次尝试,然后尝试无果最后再断开么?这样就和上面的为什么不会立马进入CLOSED状态相同了么?因为这样做是最好的,而且这种情况虽然可能会发生,但是发生的概率很少。

  1. 第一个理由是:等待TIME_WAIT可以通过等待,较大概率的保证给最后一个ACK被对面收到。
  2. 第二个是:保证双方通信信道上面的正常数据在网络中尽可能的消散。

解释一下第二个理由:有没有可能你在断开连接的时候,历史上还有一些数据没有发送给对方呢?完全有可能,虽然tcp是按序到达的,但实际上网络中被对方先收到的可能就会出现断开连接的报文,比正常的报文先到,这是完全有可能出现的。所以我们等上TIME_WAIT的时间会保证双方通信信道上面的正常数据在网络中尽可能的消散。也就是我们在等的期间,双方的连接依旧维持着,那么曾经发送的历史数据可能在网络中还存在,所以给它一段时间,让它把历史数据消散掉。至于为什么要将历史数据消散,可以理解成为了防止历史上的数据对下次新通信产生影响。

通过上面所说,我们可以知道,对于我们来说,不是建立连接和断开连接就一定保证可靠性。所以现在再强调一下,我们前面说过,tcp是保证数据通信的可靠性,它是尽量的保证在连接建立之后,连接断开之前,在我们正常通信期间,通过确认应答机制,保证我们能够100%可靠,但是建立连接和断开连接,我们不能做到100%。

TIME_WAIT状态的等待时间

TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime) 的时间后才能回到CLOSED状态。(MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s)
想一想 , 为什么是 TIME_WAIT 的时间是 2MSL?

MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到 来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这 时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);

理解CLOSE_WAIT状态

我们知道,在四次挥手的时候是通过用户层的close(client_fd);close(server_fd);实现的。

那么如果在client段没有关闭这个描述符,server端会存在处于CLOSE_WAIT状态的连接,因为server没有进行第三、四次挥手,这样的问题很大,因为server只有处于CLOSED状态才会释放连接资源,也就意味着有很多处于CLOSE_WAIT状态的话,就表示有大量的连接占着资源,系统的资源就会很少,所以如果后期我们发现server端存在大量的CLOSE_WAIT状态的连接,那么就需要排查一下自己的代码是否有bug,没有即时close(fd);

这里有一个问题:为什么主动断开连接的一方在四次挥手之后不会立马进入CLOSED状态?

在这四次挥手的时候,前三次的:FIN->ACK、FIN丢失了都没事,都会进行超时重传,但是最后一次挥手的ACK丢了就会有很大的问题。

因为我们前面说了,当client发送完ACK后,client的状态不会立马从TIME_WAIT状态到CLOSED状态,因为如果当client发送完ACK后立马处于CLOSED状态的话,也就意味着client的连接立马关闭,那么万一最后一次挥手的ACK丢了呢?

server给client发的FIN丢了没事,因为client没有收到,所以会进行超时重传,client对server的响应丢了,而且立马进入了CLOSED就会完蛋。因为对应server来说,不管哪个丢了,都会进行重传,但是因为client进入了CLOSED状态,那么server再怎么重传,client都不会有响应,一旦没有响应,server经过无数次尝试之后,最后也一定会断开连接。但是!在这个server重复的同时,也会短暂的维护一条废弃的连接,这样对于server来讲,也是不友好的,因为我们争取让断开连接的一方,也就是client方来维护这个成本。

所以只要双方没有断开连接,一切都还有余地。

3.3、滑动窗口

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

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

什么是滑动窗口呢?

描述的是,发送方不用等待ACK一次所能发送的数据最大量。这里滑动窗口的大小是与TCP的窗口大小(对方的接收能力)是相关的。

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

滑动窗口和窗口大小对比
  • 窗口大小表示的是对方的接收能力,衡量的是对方接收缓冲区剩余空间的大小
  • 滑动窗口是在自己的发送缓冲区中限定一块区域可以直接发送,暂时不用ACK。

此时看来窗口大小和滑动窗口的大小近似是相等的。

滑动窗口一定会整体右移么?

先说结论,不一定会!因为我们的发送方的滑动窗口大小是表示可以直接给对方发送的大小,当我们发送过去之后,如果对方的应用层拿了数据,而且是发多少拿多少,那么则会整体右移,但是如果我们发过去了,接收方响应了ACK,但是没有来得及处理,那么也就意味着对方的窗口大小为0了,也就是对方的接受能力为0,那么发送方的滑动窗口大小就滑不动了,此时的滑动窗口就不会在整体右移。

所以滑动窗口变宽变窄,是与接收方接受能力有关。所以是不一定!!右方还有可能一直不变。

**注:**滑动窗口实际上就像是数组,在动的就是数组指针。
那么如果出现了丢包 , 如何进行重传 ? 这里分两种情况讨论
情况一 : 数据包已经抵达, ACK被丢了
这种情况下 , 部分 ACK 丢了并不要紧 , 因为可以通过后续的 ACK 进行确认 ;
情况二 : 数据包就直接丢了。

1、当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001" 一样。

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

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

快重传 VS 超时重传

这里说一下为什么有快重传还要有超时重传,这是因为快重传的条件是发送端主机连续三次收到了同样一个应答,才会触发快重传,那么如果没有触发条件,快重传就是不可以的,所以这里其实超时重传是保底策略,而快重传是提高效率的。

3.4、流量控制

接收端处理数据的速度是有限的。 如果发送端发的太快 , 导致接收端的缓冲区被打满 , 这个时候如果发送端继续发送 , 就会造成丢包, 继而引起丢包重传等等一系列连锁反应。
因此 TCP 支持根据接收端的处理能力 , 来决定发送端的发送速度。 这个机制就叫做 流量控制 (Flow Control) ;

**·**接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 " 窗口大小 " 字段 , 通过 ACK 端通知发送端 ;
**·**窗口大小字段越大 , 说明网络的吞吐量越高 ;
**·**接收端一旦发现自己的缓冲区快满了 , 就会将窗口大小设置成一个更小的值通知给发送端 ;
**·**发送端接受到这个窗口之后 , 就会减慢自己的发送速度 ;
**·**如果接收端缓冲区满了 , 就会将窗口置为 0; 这时发送方不再发送数据 , 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。


接收端如何把窗口大小告诉发送端呢 ? 回忆我们的 TCP 首部中 , 有一个 16 位窗口字段 , 就是存放了窗口大小信息 ; 那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么 ?
实际上 , TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位 ;

3.5、拥塞控制

虽然 TCP 有了滑动窗口这个大杀器 , 能够高效可靠的发送大量的数据 . 但是如果在刚开始阶段就发送大量的数据 , 仍然可能引发问题。
因为网络上有很多的计算机 , 可能当前的网络状态就已经比较拥堵。 在不清楚当前网络状态下 , 贸然发送大量的数据 , 是很有可能引起雪上加霜的。
TCP 引入 慢启动 机制 , 先发少量的数据 , 探探路 , 摸清当前的网络拥堵状态 , 再决定按照多大的速度传输数据 ;

**·**此处引入一个概念程为 拥塞窗口
**·**发送开始的时候 , 定义拥塞窗口大小为 1;
**·**每次收到一个 ACK 应答 , 拥塞窗口加 1;
**·**每次发送数据包的时候 , 将拥塞窗口和接收端主机反馈的窗口大小做比较 , 取较小的值作为 实际发送的窗 口;
像上面这样的拥塞窗口增长速度 , 是指数级别的。 " 慢启动 " 只是指初使时慢 , 但是增长速度非常快。
**·**为了不增长的那么快 , 因此不能使拥塞窗口单纯的加倍。
**·**此处引入一个叫做慢启动的阈值。
**·**当拥塞窗口超过这个阈值的时候 , 不再按照指数方式增长 , 而是按照线性方式增长。

**·**当 TCP 开始启动的时候 , 慢启动阈值等于窗口最大值 ;
**·**在每次超时重发的时候 , 慢启动阈值会变成原来的一半 , 同时拥塞窗口置回 1;
少量的丢包 , 我们仅仅是触发超时重传 ; 大量的丢包 , 我们就认为网络拥塞 ;
当 TCP 通信开始后 , 网络吞吐量会逐渐上升 ; 随着网络发生拥堵 , 吞吐量会立刻下降 ;
拥塞控制 , 归根结底是 TCP 协议想尽可能快的把数据传输给对方 , 但是又要避免给网络造成太大压力的折中方案。

3.6、延迟应答

如果接收数据的主机立刻返回 ACK 应答 , 这时候返回的窗口可能比较小。

**·**假设接收端缓冲区为 1M. 一次收到了 500K 的数据 ; 如果立刻应答 , 返回的窗口就是 500K;
**·**但实际上可能处理端处理的速度很快 , 10ms 之内就把 500K 数据从缓冲区消费掉了 ;
**·**在这种情况下 , 接收端处理还远没有达到自己的极限 , 即使窗口再放大一些 , 也能处理过来 ;
**·**如果接收端稍微等一会再应答 , 比如等待 200ms 再应答 , 那么这个时候返回的窗口大小就是1M;
一定要记得 , 窗口越大 , 网络吞吐量就越大 , 传输效率就越高 . 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么 ? 肯定也不是 ;
**·**数量限制 : 每隔 N 个包就应答一次 ;
**·**时间限制 : 超过最大延迟时间就应答一次 ;
具体的数量和超时时间 , 依操作系统不同也有差异 ; 一般 N 取 2, 超时时间取 200ms;

3.7、面向字节流

创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区 ;

**·**调用 write 时 , 数据会先写入发送缓冲区中 ;
**·**如果发送的字节数太长 , 会被拆分成多个 TCP 的数据包发出 ;
**·**如果发送的字节数太短 , 就会先在缓冲区里等待 , 等到缓冲区长度差不多了 , 或者其他合适的时机发送出去;
**·**接收数据的时候 , 数据也是从网卡驱动程序到达内核的接收缓冲区 ;
**·**然后应用程序可以调用 read 从接收缓冲区拿数据 ;
**·**另一方面 , TCP 的一个连接 , 既有发送缓冲区 , 也有接收缓冲区 , 那么对于这一个连接 , 既可以读数据 , 也可以写数据。 这个概念叫做 全双工
由于缓冲区的存在 , TCP 程序的读和写不需要一一匹配 , 例如:
**·**写 100 个字节数据时 , 可以调用一次 write 写 100 个字节 , 也可以调用 100 次 write, 每次写一个字节 ;
**·**读 100 个字节数据时 , 也完全不需要考虑写的时候是怎么写的 , 既可以一次 read 100 个字节 , 也可以一次read一个字节 , 重复 100 次 ;

3.8、粘包问题

**·**首先要明确 , 粘包问题中的 " 包 " , 是指的应用层的数据包。
**·**在 TCP 的协议头中 , 没有如同 UDP 一样的 " 报文长度 " 这样的字段 , 但是有一个序号这样的字段。
**·**站在传输层的角度 , TCP 是一个一个报文过来的 . 按照序号排好序放在缓冲区中。
**·**站在应用层的角度 , 看到的只是一串连续的字节数据。
**·**那么应用程序看到了这么一连串的字节数据 , 就不知道从哪个部分开始到哪个部分 , 是一个完整的应用层数据包。
那么如何避免粘包问题呢 ? 归根结底就是一句话 , 明确两个包之间的边界。
**·**对于定长的包 , 保证每次都按固定大小读取即可 ; 例如上面的 Request 结构 , 是固定大小的 , 那么就从缓冲区从头开始按sizeof(Request) 依次读取即可 ;
**·**对于变长的包 , 可以在包头的位置 , 约定一个包总长度的字段 , 从而就知道了包的结束位置 ;
**·**对于变长的包 , 还可以在包和包之间使用明确的分隔符 ( 应用层协议 , 是程序猿自己来定的 , 只要保证分隔符不和正文冲突即可);
思考 : 对于 UDP 协议来说 , 是否也存在 " 粘包问题 " 呢?
**·**对于UDP, 如果还没有上层交付数据 , UDP 的报文长度仍然在。 同时 , UDP 是一个一个把数据交付给应用层。 就有很明确的数据边界。
**·**站在应用层的站在应用层的角度 , 使用 UDP 的时候 , 要么收到完整的 UDP 报文 , 要么不收。 不会出现 " 半个" 的情况。

3.9、TCP异常情况

进程终止 : 进程终止会释放文件描述符 , 仍然可以发送 FIN。 和正常关闭没有什么区别。
机器重启 : 和进程终止的情况相同。
机器掉电 / 网线断开 : 接收端认为连接还在 , 一旦接收端有写入操作 , 接收端发现连接已经不在了 , 就会进行 reset。 即使没有写入操作, TCP 自己也内置了一个保活定时器 , 会定期询问对方是否在。 如果对方不在 , 也会把连接释放。
另外 , 应用层的某些协议 , 也有一些这样的检测机制。 例如 HTTP 长连接中 , 也会定期检测对方的状态。 例如 QQ, 在 QQ断线之后, 也会定期尝试重新连接。

3.10、TCP小结

为什么 TCP 这么复杂 ? 因为要保证可靠性 , 同时又尽可能的提高性能。
可靠性 :

· 校验和
· 序列号(按序到达)
· 确认应答
· 超时重发
· 连接管理
· 流量控制
· 拥塞控制
提高性能 :
· 滑动窗口
· 快速重传
· 延迟应答
· 捎带应答
其他 :
· 定时器(超时重传定时器, 保活定时器, TIME_WAIT定 时器等)

3.11、基于TCP应用层协议

· HTTP
· HTTPS
· SSH
· Telnet
· FTP
· SMTP
当然 , 也包括你自己写 TCP 程序时自定义的应用层协议 ;

4、TCP/UDP对比

我们说了 TCP 是可靠连接 , 那么是不是 TCP 一定就优于 UDP 呢 ? TCP 和 UDP 之间的优点和缺点 , 不能简单 , 绝对的进行比较

· TCP 用于可靠传输的情况 , 应用于文件传输 , 重要状态更新等场景 ;
**·**UDP 用于对高速传输和实时性要求较高的通信领域 , 例如 , 早期的 QQ, 视频传输等 . 另外 UDP 可以用于广播;
归根结底 , TCP 和 UDP 都是程序员的工具 , 什么时机用 , 具体怎么用 , 还是要根据具体的需求场景去判定。

相关推荐
AI青年志1 分钟前
【服务器】linux服务器管理员查看用户使用内存情况
linux·运维·服务器
zquwei18 分钟前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
Aimin202232 分钟前
路由器做WPAD、VPN、透明代理中之间一个
网络
dessler39 分钟前
Docker-run命令详细讲解
linux·运维·后端·docker
群联云防护小杜1 小时前
如何给负载均衡平台做好安全防御
运维·服务器·网络·网络协议·安全·负载均衡
PyAIGCMaster1 小时前
ubuntu装P104驱动
linux·运维·ubuntu
奈何不吃鱼1 小时前
【Linux】ubuntu依赖安装的各种问题汇总
linux·运维·服务器
icy、泡芙1 小时前
T527-----音频调试
linux·驱动开发·音视频
aherhuo1 小时前
kubevirt网络
linux·云原生·容器·kubernetes
爱码小白1 小时前
网络编程(王铭东老师)笔记
服务器·网络·笔记