【Linux】UDP、TCP协议

  • Post author:
  • Post category:linux



目录


前言


1.UDP协议


1.1. UDP协议段格式


1.2. UDP的特点


1.3. UDP的缓冲区


2. TCP协议


2.1. TCP报文格式


2.2. TCP的确认应答机制


2.3. 流量控制


2.4. 标志位


2.4.1. ACK、SYN


2.4.2. RST(reset)


2.4.3. PSH(push)


2.4.4. URG


2.4.5. FIN


2.5. TCP三次握手


2.5.1. 握手过程


2.5.2. 常见面试题


2.6. TCP四次挥手


2.6.1. 挥手过程


2.6.2. TIME_WAIT


2.6.3. CLOSE_WAIT


2.7.超时重传


2.8. 滑动窗口


2.8. 拥塞控制


2.9. 延迟应答


2.10. 面向字节流


2.11.粘包问题


2.12. 总结


前言

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

IP+端口号,用来表明互联网中的唯一一台主机上的唯一一个进程。


端口号的划分:

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

  • 1024 – 65535(端口号为16位): 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.


知名端口号:


  • ssh服务器, 使用22端口


  • ftp服务器, 使用21端口


  • telnet服务器, 使用23端口


  • http服务器, 使用80端口


  • https服务器, 使用443端口

执行

cat /etc/services

命令, 可以看到知名端口号 。

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

网络中常用的命令:


netstat

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

语法: netstat [选项] 功能:查看网络状态 常用选项:


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


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


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


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


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


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


pidof

在查看服务器的进程id时非常方便. 语法: pidof [进程名] 功能:通过进程名, 查看进程id

1.UDP协议

1.1. UDP协议段格式

16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度,即UDP一次最多能发送2^16字节的数据,即最大长度为64K。 如果校验和出错, 就会直接丢弃;

UDP报头大小为8字节。

1.2. UDP的特点

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

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

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

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


面向数据报:

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

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

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

1.3. UDP的缓冲区


  • UDP没有真正意义上的 发送缓冲区

    . 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;


  • UDP具有接收缓冲区

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


UDP是全双工协议,即UDP的socket既能读, 也能写。


全双工:允许数据在两个方向上同时传输和接收。


半双工:允许数据在两个方向上传输,但是同一时间数据只能在一个方向上传输,实际上是切换的单工。


单工:只允许甲方向乙方传送信息,而乙方不能向甲方传送 。

2. TCP协议

2.1. TCP报文格式

  • TCP首部标准长度是20个字节

  • 4位首部长度(数据偏移),

    基本单位是:4字节!

    (若这里填充的是0101,表示首部长度为5*4=20字节),所以TCP头部最大长度是15 * 4 = 60

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

  • 32位序号/32位确认号: 见下文

  • 6位标志位: URG: 紧急指针是否有效 ACK: 确认号是否有效 PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走 RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段 SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段 FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

  • 16位窗口大小: 见下文

  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.

  • 16位紧急指针: 标识哪部分数据是紧急数据;

2.2. TCP的确认应答机制

前面我们已经说过,TCP是可靠的,而TCP的可靠性最核心的机制是:基于序号的确认应答机制!

TCP常规可靠性-确认应答的工作方式:

上面的client-server是一条数据一个确认的方式传输,事实上可能并不会这样,client可能会发送多条数据:

可靠性不仅仅是保证数据被对方收到,还要保证数据按序到达!



32位序号可以保证数据的按序到达

TCP是如何确认的收到的数据的?

一个报文里面既有序号,又有确认序号,为什么是两个独立的字段呢?

为什么不使用一个序号,在发送数据时是发送序号,接收数据时是确认序号呢?


因为TCP是全双工协议,数据不仅仅可以从client->server,也可以server->client,而且有可能确认应答时不仅仅有确认信息,还有给对方发送的信息

所以

双方通信的时候,一个报文,既可以携带要发送的数据,也可能携带对历史报文的确认!

而仅仅只有一个序号是不能解决这种情况的。

2.3. 流量控制

TCP协议自带发送和接收缓冲区

所以TCP协议为什么要这样做呢?

  1. 提高应用层效率,将数据交给TCP后,自己就可以返回了,后面的数据发送应用层不关心。

  2. 只有TCP协议可以知道网络,和对方的状态信息;知道如何发,什么时候发,发多少,出错了怎么办,等细节问题。

  3. 因为缓冲区的存在,可以做到应用层和TCP解耦。

如果TCP在收发数据时,发送方一直发送大量的数据,而接收方来不及将TCP的接收缓冲区的数据及时取出,导致接收缓冲区空间被占满,那么剩余的数据接收方就只能丢弃(丢包)。

对于这种接收方来不及接收的情况,虽然TCP有超时重传机制,但是后续的处理还是会浪费一定的网络资源,所以在这里TCP的解决方案就是流量控制。

TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做

流量控制(Flow Control)

;


  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端(应答报文)通知发送端;


  • 窗口大小字段越大, 说明网络的吞吐量越高;


  • 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;


  • 发送端接受到这个窗口之后, 就会减慢自己的发送速度;


  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.


即16位窗口大小填入的值表示的是:自己接收缓冲区中剩余空间的大小

问题1:什么时候知道对方的接收能力?

取决于对方什么时候给我发送的第一个报文,所以是三次握手的时候协商窗口大小。根据对方的窗口大小来设置自己的滑动窗口的初始值。

问题2:如果窗口大小为0(接收缓冲区慢了)怎么办?

TCP中使用了两种方案:

  1. 发送端会停止发送数据,会轮询发送窗口探测报文(不携带数据)标志位PSH置为1。

  2. 一旦接收端接收缓冲区有剩余空间,会向发送端发送窗口更新报文。

2.4. 标志位

2.4.1. ACK、SYN

然而server端可能在任何一个时刻都有成百上千个报文在向server发送数据,那么server面对的大量TCP报文,是如何区分各个报文的类别的呢?


TCP通过标志位来进行区分不同种类的TCP报文,这就是这6个标志位的作用。

(每个标志位为1bit)


ACK:

前面的内容中我们已经使用过它,如果报文中存在ACK表示该报文为确认报文。

几乎所有的TCP通信中,ACK都会被设置。


SYN:

表示该报文为请求连接建立报文。

SYN通常配合TCP三次握手使用。

2.4.2. RST(reset)

三次握手不一定必须握手成功,是以较大概率建立连接的过程。第一次、第二次握手我们不担心会丢失,因为都有响应,担心的是第三次握手,它没有响应。

对于client而言,在第三次握手时,将ACK报文发送出去之后他就认为连接已经建立完成;对于server而言,在收到第三次握手的ACK报文时它就认为连接已经建立完成。所以双方的握手成功是有一个短暂的时间差的。

如果这个时候第三次握手的ACK报文丢失,server端认为连接未建立成功,此时客户端却认为已经建立了连接,所以开始向服务器发送消息,而这时服务器收到客户端访问自己的端口,服务器就会向客户端发送响应回的报文,将标志位RST置为1。当客户端收到该报文时就会意识到刚刚的连接并未建立成功,就会关闭掉连接。

所以

RST的作用:重置异常连接。

当然上面的例子只是一种情况,只要是双方连接出现异常,都可以进行reset,来进行连接重置。

2.4.3. PSH(push)

如果一方给另一方发送数据,而接收方的缓冲区已经快被占满了,需要接收方尽快将数据向上交付,这时发送方就可以发送报文,报文中的标志位PSH置为1.

所以

PST标志位的作用是:通知对方尽快将缓冲区中的数据向上交付。

2.4.4. URG

TCP是按序到达的,每个报文,什么时候被上层读到,基本是确定的。


如果想让某个数据尽快被上层读到,可以设置URG标志位,表示该本文中携带了紧急数据,需要被优先处理。

而紧急数据所在的位置是由

紧急指针

所指示的,即紧急指针所指向的位置是紧急数据在报文中所在的位置。

2.4.5. FIN

一般而言,建立连接时client发起建立连接请求,而断开连接是双方都可能发起的。

FIN标志位用于表示报文是断开连接的报文。

2.5. TCP三次握手

2.5.1. 握手过程

TCP三次握手是客户端和服务器建立连接的方式,本质其实是交换三次报文,目的是为了能够建立连接,便于后续的数据交互传输。

三次握手是双方OS中TCP自动完成的,用户层完全不参与。


第一次握手:客户端向服务器发送请求建立连接报文,其中标志位SYN置为1.


第二次握手:服务器收到客户端建立连接请求,向客户端发送确认建立连接报文,,其中标志位SYN、ACK置为1.


第三次握手:客户端向服务器发送确认建立连接报文,其中标志位ACK置为1.

对于一台服务器来说,同一时间可能有很多客户端向自己发送连接,那么server此时就存在大量的连接,那么server如何管理这些连接。

先描述,再组织。那么server端就一定存在某种结构体,其中存放的连接的各种属性,将他们管理起来。

所以建立连接的本质:三次握手成功,一定会在双方的OS内为维护该连接,创建对应的数据结构。

2.5.2. 常见面试题

为什么是三次握手,而不是两次握手,四次握手?

三次握手是为了确认:

1. 双方主机是否正常;2. 验证全双工

(即在同一时间既能接收消息,也能发送消息),且三次握手是能确认双方都有收发消息的最小次数。

第一次握手时,服务器收到客户端的报文(SYN)就表示自己有接收消息的能力;

第二次握手时,客户端如果收到服务器的报文(SYN+ACK)就表示自己有收发消息的能力,但是此时服务器还不能验证自己是否能发送消息;

第三次握手时,服务器收到客户端的报文(ACK)就表示自己的报文已经发送,有发送消息的能力。

如果是两次握手:无法验证全双工,即此时服务器无法验证自己是否有发送消息的能力。

如果是四次或更多次的握手:三次握手已经足以验证双方主机和网络的状态,过多次的握手会增加建立连接的成本。

少于三次的握手:服务器受到攻击的成本更低(SYN洪水攻击),一次握手客户端只需要发送连接请求,如果有大量连接的发送,服务器管理连接的成本就会很高;两次握手,客户端发送连接请求后就不再处理服务器发送的连接,如果有大量的恶意客户端,就会导致服务器管理了大量的正常连接,消耗了大量资源;而三次握手客户端和服务器消耗的成本是一样的,客户端攻击的成本更高。

2.6. TCP四次挥手

2.6.1. 挥手过程

断开连接的本质,双方达成都应断开的共识,就是一个通知对方的机制。

双方都要断开连接,客户端断开连接,服务器确认,服务器断开连接,客户端确认,所以是四次挥手。


为什么是四次挥手:四次挥手是协商断开连接的最小次数。

(一般不会在四次挥手时攻击)

  • 第一次挥手:客户端发送FIN包给服务器端,关闭数据传送,客户端进入FIN_WAIT_1状态;

  • 第二次挥手:服务器端接受到FIN包后,发送ACK包给客户端,并且确认序号+1,之后进入CLOSE_WAIT状态;

  • 第三次挥手:服务器端发送FIN包给客户端,关闭Server到Client的数据传送,并进入LAST_ACK状态,

  • 第四次挥手:客户端发送ACK包给服务器端,进入TIME_WAIT状态,,服务器接收到后也进入CLOSE状态

2.6.2. TIME_WAIT


TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.

主动断开连接的一方,最后要进入一个TIME_WAIT状态。此时它的四次挥手已经完成,但是还不能立即释放连接资源,因为无法保证最后的ACK被对方收到。

TIME_WAIT的时间是2MSL:


  • MSL是TCP报文的最大生存时间

    , 因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则

    服务器立刻重启, 可能会收到来自上一个进程的迟到的数据

    , 但是这种数据很可能是错误的);

  • 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);

所以这就是为什么,当我们结束掉一个tcp_server时,此时tcp_server是主动断开连接的一方,会进入TIME_WAIT状态,TCP连接还没有被释放,该端口还被占用着,立即以相同的端口重启时就会bind error。

如果出现的额绑定端口号失败时也是有解决办法的:

使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

使用方法:

int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)                                                
{                                                                  
         cerr << "socket error: " << errno << endl;                     
      	 return 2;                                                      
}                                                                  
// 在创建套接字之后使用                                                                       
 int opt = 1;                                                       
 setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));   

2.6.3. CLOSE_WAIT

当四次挥手时,首先收到FIN的一方会进入CLOSE_WAIT状态,如果此时自己没有close(fd),就会一直维持这个状态,不会释放连接。

在实际中维护连接的成本是比较高的,因为fd的数量是有限的,如果一直没有关闭,就会造成文件描述符泄露。

2.7.超时重传


主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;


如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;

但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了; 这时主机A就会重复发送未被应答的数据。

因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.

这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.

那么, 如果超时的时间如何确定 ?

  • 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.

  • 但是这个时间的长短, 随着网络环境的不同, 是有差异的.

  • 如果超时时间设的太长, 会影响整体的重传效率;

  • 如果超时时间设的太短, 有可能会频繁发送重复的包;

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

  • Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.

  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.

  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.

  • 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接

2.8. 滑动窗口

刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.

这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。

在实际发送时,一次可以允许发送多条数据:

那么到底一次能够发送多少数据呢?发送数据的大小不能由客户端决定,而是由服务器决定。

所以就有了滑动窗口,滑动窗口本质是发送缓冲区的一部分。(注意与TCP报头中的窗口大小区分,其指的是接收缓冲区的剩余空间大小)

滑动窗口的底层原理其实是两个数字或指针用来控制发送缓冲区中的一块空间。(win_start、win_end、win(窗口大小) win_start += ack – win_start win_end += win (当然这里也只是伪代码,形象演示而已,具体情况还是得看源代码))

滑动窗口中的数据可能是已经被发送的数据,且正在等待ACK确认;也可能是准备发送的数据,还没有被发送。

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值(

    与接收缓冲区的接收能力有关,其大小是不固定的

    ).

  • 发送前四个段的时候, 当前不需要等待任何ACK, 直接发送(后续还是需要ACK进行确认是否发送成功);

  • 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;

  • 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;

  • 窗口越大, 则网络的吞吐率就越高;

滑动窗口大小不固定指定的是:当对端接收缓冲区的数据没有被及时读走时,接收缓冲区的剩余空间变小,滑动窗口就只会左侧向左移动,而右侧不变;反之,当接收缓冲区的接收能力变强时,滑动窗口的大小又会变大。

滑动窗口只能向右滑动,不会向左滑动,左侧的数据是已经发送的数据。

如果滑动窗口中间的数据丢了,例如按顺序发送了1000-4000的数据,其中2001-3000的数据丢失了,服务器该如何发送ACK?

因为ACK指的是该确认序号之前的数据全部被收到了,所以这时只会发送1001,而4001不会被发送,因为2001-3000丢失了。

后续客户端发送数据时,服务器应答的确认序号一直是1001,当收到三个相同的确认应答时客户端会重发2001-3000数据。

当服务器收到重发得数据之后,会应答4001.

快重传相对于超时重传来说速度更快。

2.8. 拥塞控制


拥塞控制与流量控制的区别:

  • 流量控制是作用于

    接收者

    的,它是

    控制发送者的发送速度

    从而使接收者来得及接收,防止分组丢失的。

  • 拥塞控制是作用于

    网络

    的,它是

    防止过多的数据注入到网络中

    ,避免出现网络负载过大的情况。


为什么要有拥塞控制:

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.

因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.

TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;


拥塞窗口:

拥塞窗口是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。拥塞控制的本质就是使用算法控制拥塞窗口,从而避免过多的数据注入到网络。

但是其实真正决定传输速率的是发送方的滑动窗口大小,

发送窗口 = min(拥塞窗口,接收窗口)

发送开始的时候, 定义拥塞窗口大小为1;

每次收到一个ACK应答, 拥塞窗口加1;

像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快.为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.此处引入一个叫做慢启动的阈值

  • 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;

  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;


少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;


当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;


拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.

2.9. 延迟应答

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

  • 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;

  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;

  • 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;

  • 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;

一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

那么所有的包都可以延迟应答么?不是。

数量限制: 每隔N个包就应答一次; 时间限制: 超过最大延迟时间就应答一次;

具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;

2.10. 面向字节流

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

  • 调用write时, 数据会先写入发送缓冲区中;

  • 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;

  • 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;

  • 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;

  • 然后应用程序可以调用read从接收缓冲区拿数据;

  • 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 这就叫做字节流,例如:

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;

  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

2.11.粘包问题

  • 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.

  • 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.

  • 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.

  • 站在应用层的角度, 看到的只是一串连续的字节数据.

  • 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包 。

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.

  • 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;

  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;

  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔

  • 符不和正文冲突即可);

思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?

  • 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.

  • 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现”半个”的情况

2.12. 总结

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


可靠性:


  • 校验和


  • 序列号(按序到达)


  • 确认应答


  • 超时重发


  • 连接管理


  • 流量控制


  • 拥塞控制


提高性能:

  • 滑动窗口

  • 快速重传

  • 延迟应答

  • 捎带应答

其他: 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等



版权声明:本文为Edward_Asia原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。