当前位置:   article > 正文

UDP与TCP协议详解_介绍下tcp 和 udp

介绍下tcp 和 udp

前言

早socket部分,我们使用了udp与tcp协议进行client与server端的数据交互,但是当时只是片面的了解并使用,下面我们将详细介绍两个协议(尤其是TCP协议),他们是传输层重要协议,对我们网络知识的学习也至关重要;

再谈端口号

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

在这里插入图片描述

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

在这里插入图片描述

端口号划分规范

我们知道端口号是一个16位的短整型,因此其取值范围是0-65535.而端口号根据其取值可分为:

  • 0 - 1023:知名端口号。HTTP、FTP、SSH这些广为人知的应用层协议,它们的端口号是固定的
  • 1024 - 65535:操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。

每个进程都绑定一个或者多个端口号!

常见的知名端口号

  • ftp服务器:使用21号端口。
  • ssh服务器:使用22号端口。
  • telnet服务器:使用23号端口。
  • http服务器:使用80号端口。
  • https服务器:使用443号端口。

UDP协议

UDP – UserDatagramProtocol—用户数据报协议;

UDP协议段格式

在介绍UDP协议之前,我们需要先明确两点(网络基础层级通信讲过):

  • 1.任何协议都必须能够解决将自己的报头和有效载荷分离的问题。
  • 2**.任何协议都必须能够将自己的有效载荷交付给上层的哪一个进程**。
    UDP的协议格式如下:

在这里插入图片描述

UDP固定的8字节长度报头,就可以让自身的协议报头和数据(如果有)进行有效分离;

UDP报头内的16位目的端口号,则明确了交付给上层的哪一个协议;//我们之前写socket通信的端口号都是16位的与之呼应;

16位UDP长度,就是UDP报文的长度即UDP协议的首部加上数据的总长度(数据包长度)。

16位UDP检验和,是为了校验是否出错,比如出现了数据丢包,那么这个UDP报文就会直接被丢弃。

udp or tcp协议在系统内核中,系统是由C语言编写,且这里报头各个部分都是16位的,不难得出在系统内部使用位段进行有效管理与储存; 如下图:则填充报头就是给对应位段类型赋值!

在这里插入图片描述

那么IP呢??为啥只有源和目的端口号,怎么定位目标主机呢?;别忘了,IP是下一层网络层需要解决的问题,我们TCP/UDP是传输层;

UDP协议的特点

UDP传输的过程类似于寄信,发送方直接甩出去,不用管数据是否被接收方接收,因此有以下特点:

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

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

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

面向数据报:指的是不能够灵活控制读写数据的次数和数量,即应用层交给UDP多长的报文,UDP会原封不动的发送出去,既不会拆分,也不会合并
eg:用UDP传输50个字节的数据:如果发送端调用一次sendto, 发送50个字节, 那么接收端也必须调用对应的一次recvfrom, 接收50个字节; 而不能循环调用50次recvfrom, 每次接收1个字节。这点与TCP面向数据流不同;

UDP的缓冲区

之前socket编程的时候使用的sendto,recvfrom,read,write这类函数并不是我们直接将数据发到网络中直接发出去,而是在应用层的角度交给OS中的缓冲区(发送缓冲区/接收缓冲区);

而至于什么时候发送或是由谁发送,则是OS根据传输层协议决定的,这也正是传输层存在的意义

  • UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
  • UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃

UDP使用注意事项

eg:需要发送一个大于64k的数据,而用udp意味着udp的报头+数据大小一共16个位置储存,就是64k,显然小了,那么大于64k的数据发过去就需要在发送端手动分包,多次发送,在接收端手动组装,很麻烦;

基于UDP的应用层协议

  • NFS: 网络文件系统
  • TFTP: 简单文件传输协议
  • DHCP: 动态主机配置协议
  • BOOTP: 启动协议(用于无盘设备启动)
  • DNS: 域名解析协议

当然,我们自己写UDP程序自定义的应用层协议也包括在内(比如搞个双方英文字典查询功能协议等)。

TCP协议

TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名,在传输层进行传输工作且要对数据的传输进行一个详细的控制,更多的作用功能往往意味着更复杂的结构与实现,TCP是接下来的重头戏;

TCP协议段格式

在这里插入图片描述

任何协议要解决的两个问题:

1.4位首部长度(单位为x*4字节)解决了如何将报头与有效载荷分离;
2. 16位目的端口解决了交付给上层的哪个协议

报头组成:

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

  • 32位序号/32位确认号: 与确认应答机制,控制数据的接收次序与紧急重传机制有关,下文详细讲;

  • 4位首部长度:表示该TCP报头有多少个4字节(32位bit);所以TCP报头最大长度:15 * 4字节 = 60字节(算上选项在内,因此选项的最大长度为60-120 = 40字节)

  • 6位标志位:

    1. URG: 紧急指针是否有效
    2. ACK: 确认号是否有效
    3. PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
    4. RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
    5. SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
    6. FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
  • 16位窗口大小:与流量控制有关,下文详细说;

  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.(与信息安全相关,我们不做深究)

  • 16位紧急指针: 标识哪部分数据是紧急数据(可打破顺序让接收端优先接收);

TCP协议的特点

  • 面向连接的
  • 可靠的
  • 面向字节流的

下面将报文首部的使用与TCP特性做详细解析:

确认应答(ACK)机制

对于TCP双方发送的每一个报文,作为接收的一方都需要发送确认应答信息(ACK)来表示自己已经接收到了对端发送的报文,这样的机制即为确认应答机制,而这种机制保证了对端接收到了信息,也即保证了发送数据的可靠性

TCP将每个字节的数据都进行了编号. 即为序列号;
在这里插入图片描述

那么主机A与B进行数据传输的时候如下图:

在这里插入图片描述

应答时ACK标志位置1,每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 从而让发送端下一次从相应位置开始发

再观察报头,可以看到报头中的32位序号和32位确认序号,就是填充数据确认序列号和序列号的位置;

序列号:填我将编好的第几个数据发给你;

确认序列号:填我收到了你的第x数据,返回给你一个x+1提醒你下次从x+1位置的数据开始发;

因为通信是双方的,则这个序列号/确认序列号成对存在;

同时,这个序列号也解决了数据到达先后顺序的问题,使接收方可以按顺序接收则不会出错,这也体现了TCP的可靠性;

超时重传机制

网络通信中可能存在这种情况:主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;

那么如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
在这里插入图片描述

情况二:数据到达了主机B,但是主机B的确认应答消息丢失了,那么A还是以为自己的数据没到,再次发送相同消息;这不就重复了吗?

在这里插入图片描述

没关系,既然B之前已经收到了A的1~1000消息,那么确认序号就是1001发回A,即便A没收到确认,但是之后主机B意识到消息重复了,然后舍弃;

以上TCP超时重传机制也保证了TCP的可靠性;

那么这个重传所需要等待的时间怎么规范呢?

时间太长,影响整体效率;时间太短,相同数据包可能重复发送;因此,TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间;

  • Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是- 500ms的整数倍.
  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
  • 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接,释放资源。

连接管理机制

**TCP是有连接的!**之前对于三次握手,四次挥手只是片面了解,接下来详细讲解:

在这里插入图片描述

三次握手

联系之前socket编程的接口,其实三次握手是由客户端connect()主动发起,之后由操作系统内部建立,accpet只是把建立连接的结果拿上到应用层,客户端把含有SYN(同步报文段)标志位的报文发给server请求建立连接;server系统内部发回给client带有SYN+ACK标志位的报文,表示已收到,确认允许连接请求;client收到确认信息后,发送ACK确认报文给server;

因为connect与accept都是应用层上拿到连接的接口,则建立连接的具体过程由双方操作系统内部建立起来连接并将连接返回给connect()与accept(),我们可以通过他们的返回值判断连接是否建立成功!(连接由connect发起,操作系统内部自动完成!accpet只是看结果用);

四次挥手

每次挥手由断开方调用close(连接sock)发起挥手给对方发送FIN(结束报文段),在对方收到断开消息后,回应ACK,并准备调用close(sock)与之也准备进行挥手断开(数据得处理完),同时等待ACK(last_ack);

三次握手和四次挥手充分体现了标志位对区分普通报文和建立连接时的请求\应答报文的重要性;

过程中的相关状态

三次握手:

服务端状态转化:

[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;

[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN的确认报文SYN+ACK.

[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文ACK,则认为连接建立成功,就进入ESTABLISHED状态, 可以进行读写数据了.
客户端状态转化:

[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段SYN;

[SYN_SENT -> ESTABLISHED]收到服务端的ack+syn,则认为connect调用成功,则进入ESTABLISHED状态, 开始读写数据;

四次挥手(假设客户端先close):

服务端状态变化:

[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器 返回确认报文段并进入CLOSE_WAIT;

[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了服务端的FIN)

[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.

客户端状态变化:

[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入
FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2*MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.

为什么是3次连接

linux下一切皆文件,连接可能很多,本质上的创建和管理也是需要struct,队列等成本;

我们知道报文可能丢失,那么前几次报文丢失都无所谓,收不到他的确认报文ACK我们会超时重传,但是如果最后一个报文ACK丢失了呢?(我们永远不能保证最后一个报文ACK的可靠性!因为他是保证倒数第二条可靠性的ACK报文)

下面放三次握手与四次握手进行对比:

在这里插入图片描述

上图其实也是RST(复位报文段)标志位的使用场景之一;

在这里插入图片描述

在奇数次挥手下,最后的ACK由那么我们的client发出,他会认为连接已成功,则进程建立连接,消耗一定的成本,创建链接内存并管理,但是server没有收到最后的ack,则不会认为连接正常创建,也就不会进行;

那么在这种特殊情况下,这一部分无用的成本嫁接到了client上无疑是最好的选择,因为client:server是n:1的,如果全让server来承担,那不早崩了;

连接不是100%连接成功,因此为了最小成本,我们选择三次握手;

挥手中TIME_WAIT状态导致bind error的原因

在之前socket编程中,总是发现用过端口8080启动server以后,直接将server终止掉,并重启会发现bind error;

netstat命令发现存在TIME_WAIT状态的server服务,并没有被彻底关闭,而是TIME_WAIT状态,端口号被占用,所以新服务bind失败;
在这里插入图片描述

为什么存在TIME_WAIT?这是TCCP协议规定先close挥手的一方需要等2*MSL(Maximum Segment Lifetime)的时间才能回到CLOSED状态。Centos7上默认配置的值是60s;

为什么要TIME_WAIT等2*MSL?

  1. 两倍的报文最大传输时间;保证两个传输方向上迟到的报文到达另一方并收到回应,防止出现迟到数据影响之后的进程;

  2. 理论上保证最后一个报文可靠到达对方,假设client先close 那我client的TCP协议等2*MSL,万一server也想close发过来FIN 我的回应last_ACK丢了,超时可以再重你发个last_ACK;

怎么解决?

在server的TCP连接没有完全断开之前不允许重新监听,这在某些情况下是不合理的:

服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了,就会出现问题。

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

int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  • 1
  • 2
CLOSE_WAIT状态

一旦服务器忘记与客户端挥手关闭close(sock)的时候,那么netstat就会观察到:
在这里插入图片描述

我们的server处于close_wait状态,那么可能持续很长一段时间之后,积攒大量的client挥手server没挥手而留下的垃圾,这时候检查服务器bug…看看是不是忘记close()挥手了;

滑动窗口

顾名思义,这个机制类似与算法里的滑动窗口;

之前我们提到过TCP的确认应答策略,即对于连接双方发送的每一个数据端,都需要进行ACK确认应答,收到ACK后再发送下一个数据段,但这样做有一个明显的缺陷,串行就是性能较差,尤其是数据往返时间较长的时候:

在这里插入图片描述

引入滑动窗口机制;我们将发送缓冲区分为三个部分:

在这里插入图片描述

其中红色标注部分就是滑动窗口,也就是说滑动窗口本质是发送缓冲区的一部分,其因为应发送的数据不断变化,而区域也不断变化,就像在滑动一样,故称为滑动窗口。

TCP协议报头里的16位窗口大小就是这个滑动窗口的大小,他的大小是受接收方接收缓冲区可用大小制约的,这个大小通过之前通信时接收方发来报头里的确认序号传递;

串行效率差我们来并行

在这里插入图片描述

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个段)

  • 发送前四个段的时候, 不需要等待任何 比特科技 ACK, 直接发送

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

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

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

窗口扩大或者缩小的原理就和算法的滑动窗口类似,由left和start两个位置,移动这两个边界卡出窗口;

其次,缓冲区大小是有限的,那么滑动窗口一直向右移动,是否会移出滑动缓冲区呢?
TCP的发送缓冲区可以看作一个数组char send_buffer[SIZE]数组;而滑动窗口本质上就是数组的一部分,我们可以将这个数组看成一个环形的结构,那么滑动窗口一直在数组中移动,同过两个边界卡窗口,则实现内存利用,并不会移出发送缓冲区。
在这里插入图片描述

滑动窗口可以让发送方一次发送大量数据,从而保证效率。

滑动窗口可以配合对端的接收能力,来实现流量控制。

快重传

在并发传送数据过程中发生了丢包,除了超时重传机制应该怎么解决,这里分两种情况:

情况1:

数据包已经抵达, ACK被丢了

在这里插入图片描述

这种情况很容易解决,因为假设A的11000数据发过去了,但是B发回来的ack没收到,那么如果B发回来的2001或者更大位置的ack被A收到了(B一定是按顺序收的,如果收到大的index数据,那么证明之前的小序列号的数据已经收了),则证明A的11001其实已经被B收到了,只是ack丢了,不用重新发1~1000了;

情况2:

发送的数据发生了丢包。

在这里插入图片描述

假如A的11000发过去了,那么此时B只收到了11000,提醒A下一个是1001以后,A发给B的10012000数据丢了!那么B将收不到1001,即便是之后的20013000,3001~4000都过来了,B也不能收,只能先存缓冲区,并且返回接下来应该收到的序列1001;

如果A连续收到3次同样的ACK以后,就会意识到自己这个数据包丢了,然后重新发送给B,此时B终于拿到1001,紧接着把之前存到缓冲区的2000 ~3000 ~4000的数据拼起来,最后直接返回个4001ACK给A,A之后就能从4001继续传输数据了;

这种对付丢包问题的机制就被称为“高速重发机制”(也被叫做“快重传”);

这种快重传(效率)可以理解为并发大量数据包时候的一种丢包处理方法,与超时重传(可靠性)不矛盾;

流量控制

其实,在引入滑动窗口这个机制后,流量控制自然显现出来了;流量控制是TCP协议中保证效率的一个重要措施

接收端处理数据的速度是有限的,如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起超时重传等等一系列连锁反应。

因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
    窗口大小字段越大, 说明网络的吞吐量越高;

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

  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端同时防止接收端的窗口更新通知丢失,也要定期发送窗口探测数据包。

在这里插入图片描述

至于接收端如何把窗口大小告诉发送端呢?

之前我们介绍的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;建立连接的时候就能给到发送端;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位。

拥塞控制

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

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

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

  • 此处引入一个概念程为拥塞窗口,发送开始的时候, 定义拥塞窗口大小为1,每次收到一个ACK应答, 拥塞窗口加1(相当于每次启动拥塞窗口大小是上次的一倍);

  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;

像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快

  • 了不增长的那么快, 因此不能使拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值

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

  • 在这里插入图片描述

  • 当TCP通信开始后, 网络吞吐量会逐渐上升;

  • 如之后如果网络发生拥塞,拥塞控制生效,吞吐量会立刻下降;

  • 这里的拥塞判断有不同策略:1.发生快重传(连续收到重复确认的次数维度) 2. 发生超时重传(等待确认的时间维度)

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

延迟应答

关于确认应答机制,如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小;

  • 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就减少500K;
  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了
  • 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些(把这500k加上), 也能处理过来
  • 因此如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M,显然吞吐量加大了,这就是延迟应答;

在这里插入图片描述

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

延迟应答的具体方式有两种:

  • 数量限制: 每隔N个包就应答一次(如上图),一般N = 2;
  • 时间限制: 超过最大延迟时间就应答一次,一般T= 200ms,注意,这个延迟时间应远小于超时重传时间,不然就帮倒忙了;

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一个发送数据,一个确认收到” 的. 意味着接收方需要发送确认收到的报文给发送方;

可是往往接收方也需要回复数据给确认方,那么就可以回复ACK顺便捎带上数据了;

在这里插入图片描述

通过抓包工具,可以发现四次挥手怎么只有三次?原因是确认方收到断开信息以后,自己也要断开,就把2,3次挥手用捎带应答优化了!

在这里插入图片描述

TCP异常处理

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

此外,TCP在面对异常情况可能会发送RST包(重新建立连接请求)。发送RST包的情况通常有:1、端口未打开;2、请求超时;3、提前关闭;4、在一个已关闭的socket上收到数据

TCP小节

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

可靠性保证:

  1. 校验和
  2. 序列号
  3. 确认应答
  4. 超时重传
  5. 连接管理
  6. 流量控制
  7. 拥塞控制

提高效率的措施:

  1. 滑动窗口
  2. 快速重传
  3. 延迟应答
  4. 捎带应答

面向字节流特性

由于读/写缓冲区的存在,TCP程序的读和写操作不需要一一匹配:

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

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

而UDP只有接收缓冲区,是面向数据包的,固定的发送与读取!

粘包问题

粘包问题中的 “包” , 是指的应用层的数据包,不是分离报头和有效载荷的报;

站在传输层的角度, TCP是一个一个报文过来的. 按照报头里的序号排好序放在缓冲区中,而站在应用层的角度, 看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包

在这里插入图片描述

解决方法:明确两个包之间的边界

  • 对于定长的包,保证每次都按固定大小读取即可;eg:没内容的HTTP请求报文Request,就直接按sizeof(Request)单位大小读取;

  • 对于变长的包,发送方可以在包头的位置, 添加约定一个包总长度的字段length,从而就知道了包的结束位置;接收方就有能力根据单位length分开不同的包了;//TCP

  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔 符不和正文冲突即可) eg:HTTP的报头与body正文的分隔符空行/n这种规定的方式,在包与包之间加特殊分隔符;

UDP不需要考虑粘包:

  • UDP报头本身就存在数据包长度,按照那个大小就能有效读取;

  • 面向数据报,要么收到完整的UDP报文, 要么不收;不会出现"不完整的半个"的情况.

TCP与UDP对比

我们既然说了TCP是可靠连接,那么是不是TCP就一定优于UDP呢?这个问题已经老生常谈了。
TCP和UDP的优缺点不能简单、绝对的进行比较。

  • TCP用于可靠传输的情况,应用于文件传输重要状态更新等场景,其也有一系列提高效率的机制保证效率。
  • UDP用于对高速传输实时性要求较高的通信领域,比如早期的qq,视频传输等。另外UDP可以用于广播。

UDP实现可靠传输;(经典面试题)//回答tcp可靠性的的核心方法

  • 引入确认应答, 确保对端收到了数据;

  • 引入超时重传, 如果隔一段时间没有应答, 就重发数据;

  • 引入序列号, 保证数据顺序;

关于listen第二个参数backlog

Linux内核协议栈为一个TCP连接管理使用2个队列

1.半连接队列(用来保存处于SYN_RECV状态的链接请求)
2.全连接队列(accepted队列)(用来保存双方处于ESTABLISHED状态的链接,但server应用层没有调用accept取走的请求)//也叫等待队列

客户端:

	当connect到来的时候无论**等待队列**有没有空余地方在客户端眼里都是连接成功的,因为一开始调用connect函数SYN请求,服务端立刻给予客户端SYN+ACK确认,客户端连接状态从SYN_SENT转换到ESTABLISHED之后再向服务端发ACK确认。也就是所谓的**三次握手过程**,所以client一直都是ESTABLISHED状态;
  • 1

服务端:

	当客户端connect来到时,**服务端进入SYN_RCVD状态并给予SYN+ACK响应**。当下次客户端完成三次握手,收到客户端的ACK时:**如果队列中有空间,则服务端的连接也建立成功,否则服务端眼里没有成功**。每**建立成功一次,要往队列中放入刚才建立好的连接,也就是队列空间-1,服务端状态从SYN_RCVD变为ESTABLISHED**,如果**不成功**还是原来的**SYN_RCVD**状态。当服务端调用accept成功时,就从队列中拿走一个连接,则队列空间+1可以再放进来一个新连接了。
  • 1

实验:我们在server不调用accept的情况下,设置listen(listen_sock,backlog = 2)后,启动多个client端;

经过实验,启动前三个客户端与server进行连接没问题,双方状态都是ESTABLISHED;

红框圈出来的是client端连接,紫框圈出来的是服务端连接,青色框圈出来的是连接数够了之后,出现状况的连接(半连接);
在这里插入图片描述

而第四个client运行以后,服务端变成SYN_RCVD状态了,这是因为成功三次握手3个连接以后等待队列满了,第四个开始server就会看到没空间了,则server眼里连接就是不成功的(半连接),放入半连接队列;
在这里插入图片描述

backlog + 1=全连接队列 (等待队列)所能存入连接的个数的个数;

因此,backlog的数量不能多也不能少;(有点像线程池那种操作)

不能少:它是底层通过全连接队列维护是**提高效率(等待队列)**的一种机制,因此必须有backlog来指明全连接队列的大小

不能多:越靠后的连接等待的时间越长,那么client就会失去被服务的耐性,并且,长的服务队列也需要消耗资源,而与其把资源消耗在长的服务队列中,不如利用到提高服务器的吞吐量上;

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/494240
推荐阅读
相关标签
  

闽ICP备14008679号