【传输层】Udp协议和Tcp协议的原理(一)

文章目录

  • 前言
  • 一、Udp原理
  • 二、Tcp原理
  •         1.TCP协议段格式
  •         2.超时重传机制
  •         3.深刻理解三次握手
  •         4.深刻理解四次挥手
  • 总结


前言

传输层:TCP/UDP

传输层负责两台主机之间的数据传输,如传输控制协议(TCP),能够确保数据可靠的从源主机发送到目标主机。

前几篇文章都是讲解的应用层的内容,下面我们进入传输层的内容,传输层就是负责数据能够从发送端传输接收端。

 传输层最典型的协议有两个,一个是UDP协议,一个是TCP协议,在讲解UDP协议之前我们先重新认识一下端口号:

端口号标识了一个主机上进行通信的不同应用程序,在网络通信时,用IP值标识一台主机,用port值标识一台主机上不同的进程。

在TCP/IP协议中,用源IP,源端口号,目的IP,目的端口号,协议号这样一个五元组来标识一个通信,如下图:

 简单的理解就是:源IP就是自己主机的IP,目的IP就是你要连接的服务器的IP。源端口号代表自己主机打开的某个进程的端口号,这个一般都是操作系统分配的,目的端口号是要连接服务器的某个进程的端口号。上图中TCP下面的6代表协议号。

端口范围划分

0 - 1023::知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
下面我们认识一下知名端口号:
有些服务器是非常常用的 , 为了使用方便 , 人们约定一些常用的服务器 , 都是用以下这些固定的端口号 :
ssh 服务器 , 使用 22 端口
ftp 服务器 , 使用 21 端口
telnet 服务器 , 使用 23 端口
http 服务器 , 使用 80 端口
https 服务器 , 使用 443
当然我们也可以用cat /etc/services命令来查看知名端口号:

认识知名端口号后,我们自己写一个程序使用端口号的时候就需要避开使用这些知名端口号了。

下面有两个问题:

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

可以!我们一定要记住,要保证端口号到进程的唯一性,当有多个端口号都指向一个进程的时候,是不影响端口号到进程的唯一性的。

如上图,当一个进程绑定了多个端口号的时候,我们发现每个不同的端口号都会指向一个进程,所以不影响端口号到进程的唯一性。 

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

 不可以!当然还是有特例的,但是99%的情况都不可以。

 可以看到如果一个端口号被多个进程绑定,那么就乱套了根本不知道是哪个进程。

下面我们再学习两个指令,实际上这两个指令我们之前学套接字的时候是用过的:

netstat:

netstat 是一个用来查看网络状态的重要工具 .
语法 netstat [ 选项 ]
功能 :查看网络状态
常用选项
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服务状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
pidof:
在查看服务器的进程 id 时非常方便 .
语法 pidof [ 进程名 ]
功能 :通过进程名 , 查看进程 id


一、Udp协议

UDP协议端格式如下图:

 之前我们说过,从应用层向下传输数据的时候,需要添加报头,从数据链路层向应用层传输数据的时候需要去掉报头,去掉报头的过程被称为解包,那么UDP是如何解包的呢?实际上UDP解包的做法很简单,只需要固定报头长度,这样一来我们解包的时候只需要将固定的报头长度拿走不就剩下有效载荷了吗,那么解包完成是如何分用的呢?实际上也很简单,因为我们UDP协议的报头中是有16位目的端口号的,所以直接将数据交给上层特定的协议就完成了分用的过程。

实际上所谓的报头就是一个结构化的数据,比如UDP里面就存放源端口,目的端口,然后还有UDP长度和检验和,如下图:

 而我们添加报头的原理非常简单:

 当我们调用sendto发送数据时,首先在操作系统内部开一部分空间,然后计算出报头结构体大小,用一个指针从空间开始位置向后偏移一个刚刚报头结构体大小的空间,然后我们填充结构体的信息就放在了报头的位置,后面的位置直接将发送的数据拷贝过来即可。

UDP 的特点
UDP 传输的过程类似于寄信 .
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接; 经过我们实现TCP服务器,我们都知道TCP通信前需要先建立连接,而UDP绑定后就可以通信了。
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量。
面向数据报的意思是:
应用层交给 UDP 多长的报文 , UDP 原样发送 , 既不会拆分 , 也不会合并 ;
比如用 UDP 传输 100 个字节的数据 : 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;

UDP的缓冲区

UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;
UDP socket 既能读 , 也能写 , 这个概念叫做 全双工。
如何理解全双工呢,如下图:

我们在网络中的收发数据,实际上并不是想的那样直接发送,之前我们实现UDP和TCP的时候发现,每次调用sendto接口需要一个缓冲区,这个是我们用户的缓冲区,首先操作系统将用户缓冲区的数据拷贝到自己的发送缓冲区中,然后经过网络发送到另一台主机的操作系统的接收缓冲区,我们当时调用接收函数的时候同样需要一个缓冲区,操作系统会将接收缓冲区的数据拷贝到我们用户的缓冲区,这样就实现了半双工,而我们不是只能发送,实际上我们发送的过程中也可以接收对方像我们发送的数据,也就是说我们的客户端向服务端发送数据的同时服务端也可以向客户端直接发送数据,所以是全双工的。

我们TCP和UDP最本质的区别在于上图中UDP发送数据只能收到多少就发送多少,而TCP是可以控制收多少或者发多少,所以TCP也叫传输控制协议。

UDP使用注意事项:

我们注意到 , UDP 协议首部中有一个 16 位的最大长度 . 也就是说一个 UDP 能传输的数据最大长度是 64K( 包含 UDP 首部).
然而 64K 在当今的互联网环境下 , 是一个非常小的数字 .
如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包 , 多次发送 , 并在接收端手动拼装 ;

基于 UDP 的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议 ( 用于无盘设备启动 )
DNS: 域名解析协议
当然 , 也包括你自己写 UDP 程序时自定义的应用层协议 ;

二、TCP协议

TCP 全称为 " 传输控制协议 (Transmission Control Protocol"). 就是 要对数据的传输进行一个详细的控制。
首先我们看一下TCP协议段格式:

 我们可以看到TCP相比较UDP来讲设计是非常复杂的,下面我们先讲解一下4位首部长度:

首先从图上可以看到TCP协议报头的长度是0~31也就是32位,那么TCP协议是如何解包的呢?(将报头和有效载荷进行分离)首先我们先不谈选项和数据这两行,一共5行每行都是32个比特位那么一共就是20个字节,因为tcp协议是有标准长度的,就是我们刚刚计算的20字节,所以要想解包我们先读取20字节,然后将报头转化为一个结构化的数据,立马提取标准报头中的4位首部长度(4位首部长度就是TCP报头的总长度,注意:标准tcp长度是20字节,但是还有可能存在选项。tcp报文的总长度 = 4位首部长度*4字节,所以4位首部长度的范围是0*4~15*4(比特位1111(4个比特位)),而又因为我们的标准长度是20字节,所以范围是[20,60]。),一旦有了4位首部长度,我们就可以算出剩下报文的长度,比如4位首部长度是x:x*4(总长度)-20==剩余报文长度,如果为0则说明不带选项,最后只需要把tcp报头处理完毕,后面就剩下有效载荷了,这样就完成了解包,如何分用和udp一样,因为报文是有目的端口号的,所以直接向上层交付即可。

理解了4位首部长度和TCP是如何解包分用的后,我们再理解一下TCP报头:

 实际和UDP报头一样,都是一个结构体或者位段,只不过TCP报头里面的东西更多了:

下面我们学习一下TCP的可靠性:

首先要明白可靠性,我们就先要知道为什么会存在不可靠问题?

网络中存在不可靠的问题最主要的原因就是主机之间的距离太远了,为什么同样CPU与磁盘或者其他硬件IO的时候不会谈不可靠呢?如果一台主机中的硬盘,内存,CPU都相隔很远并且用线连接,那么就和我们的网络一样了 ,所以最主要的问题在于距离的长短。

那么都有哪些不可靠性的场景呢?

有丢包,乱序,校验错误,重复等场景。

那么tcp可靠性是怎么保证的呢?

首先是不存在绝对的可靠性的,其次我们认为只有收到了应答,历史消息我才能100%确认收到,只有确认应答了才算可靠。为什么说不存在绝对的可靠性呢,我们双方通信一定存在最新的数据,而最新的数据是有可能没有应答的。

下面我们认识一下报头中序号和确认序号这两个概念:

 如上图,在真实的客户端和服务器通信的时候大多数客户端都会一次性给服务端发送很多数据段,而服务器也需要对客户端发来的数据段发出回应,在实际的网络通信中,我们不能保证数据到达对面的顺序一定是和发送的顺序是一样的,这就像我们买快递,我们按顺序买的商品但是快递到我们手上的时候是不一定按照购买时的顺序到达的。而且我们的客户端要收到服务端的确认,客户端怎么会知道当时给服务器发送的请求和现在服务器发来的回应的对应关系是什么呢?通俗的说就是客户端不知道服务器确认的是哪一条消息。由上面两个问题就注定了tcp数据段需要有方式标识数据段本身,所以我们的TCP报头中才有32位序号的存在。

 如上图,我们看待每个数据段的时候都应该把数据段看成报头+有效载荷,不能单纯的理解为请求与响应。报头中的确认序号就是来告诉对方已经收到了对方的内容。我们的确认序号一定是对方发送序号+1,因为这样可以保证我们收到了发送序号+1前面的所有报文,并且下一次让对方可以从+1序号开始发送,比如上图中10号发送数据段,我们的回应就应该是11号确认序号,下一次客户端发送就会从11号开始发送。在上图一旦我们通信过程中发生丢包问题,比如12号发送数据段丢失,但是13号数据段服务端已经收到了,这个时候服务端的确认序号只能填12,因为我们虽然收到了13号发送序号,但是由于13号之前的数据段不连续,所以客户端必须重新从12号数据段给我们发送。有聪明的小伙伴可能就想到了,我们的报头用一个序号不就可以了吗,当客户端发送的时候是发送序号,当服务端回应的时候是确认序号。思路没有但是,但是忽略了TCP是全双工的特性,因为我们的TCP是全双工的,所以很有可能同时发送和接收,所以我们必须有两个序号。

下面我们认识一下报头中的16位窗口大小:

我们都知道不管是客户端还是服务端都有自己的接收缓冲区和发送缓冲区,但是我们怎么知道对方的接收缓冲区是否是满的呢,如果是满的我们发过去的数据不就被丢弃了吗,所以我们必须要得到对方的接收缓冲区的剩余大小。而又因为TCP是全双工的,所以我们给对方发送消息的时候对方也有可能给我们发送消息,所以对方也要知道我们的接收缓冲区大小,所以就有了16位窗口大小,16位窗口大小就是接收缓冲区的剩余空间大小,注意:我们的报文中一定填的是自己的16位窗口大小,我们把自己的放在报文中对方收到了就知道我们还有多少剩余空间了。

我们后面学到TCP拥塞控制的时候,会发现当出现网络拥塞时,这个时候窗口大小取拥塞窗口大小和对方接受缓冲区窗口大小的较小值。

下面我们认识一下数据段类型:

SYN同步标志位(请求建立连接; 我们把携带SYN标识的称为同步报文段):在tcp三次握手的时候需要将SYN标记位设置为1,SYN只有在建立连接时才会被设置。

FIN断开链接标志位(通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段):当tcp通信中客户端由于一些原因退出,这个时候服务端就会将FIN标志位置为1,表示断开连接,当然FIN一旦为1那么SYN肯定被置为0了。

ACK确认报文标志位(确认号是否有效):当某一端确认收到另一端的报文时就将ACK标志位置为1表示收到了对方的数据段。

PSH标志位(:提示接收端应用程序立刻从TCP缓冲区把数据读走):psh实际上就是push的简写,当发送方将接收方的缓冲区传满后,接收方迟迟不将接收缓冲区的数据拿走,这个时候发送方就将PSH标志位置为1去催促接收方更新接收缓冲区大小。

URG标志位(紧急指针是否有效):如果我们的报文中有需要被尽快读取的数据,那么URG标志位就会被置为1.(紧急指针可以找到哪一段数据需要被尽快读取)

RST复位标志位(对方要求重新建立连接; 我们把携带RST标识的称为复位报文段):reset的缩写,有一种比较常见的场景就是,当我们服务端和客户端正常通信时,服务端突然断开连接,这个时候客户端还以为自己和服务端是连接状态,而服务端认为连接断开,然后客户端直接给服务端发送了数据(正常情况先建立链接,也就是三次握手后才能发送数据),这个时候服务端就会说你还没有和我建立连接怎么就直接发数据了,然后给客户端发送让客户端重新连接的响应,这个时候服务端给客户端发送的响应报文中就将RST标志位置为1,表示让客户端关闭连接重新和服务器建立连接。如下图:

 下面我们认识一下报头中的16位紧急指针:

当一个报文中有需要被尽快读取的数据的时候,我们就可以通过紧急指针找到这段数据,紧急指针找到这段数据的原理也很简单,就是通过有效载荷的偏移量实现的。

2.超时重传机制:

当主机A给主机B发送数据后,迟迟没有收到主机B的应答,这个时候主机A就会重发数据给主机B,但是,主机A也有可能是因为主机B发的响应中ACK丢失了导致误认没有回应,这个时候主机B就会收到很多重复数据,那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.

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

3.重新认识TCP三次握手:

 当我们客户端第一次挥手的时候,会将发送的报头中SYN标志为1表示自己想要建立连接,并且自身处于SYN_SEND阶段,服务器收到客户端第一次发送的报头就会给客户端做出回应,发送的报头中会将SYN标志位和ACK确认应答标志位置为1,表示收到了客户端的请求并且同意建立连接,并且自身处于SYN_RCVD(received)状态,客户端收到服务端发送的确认信息后客户端就建立连接成功,然后给服务端发送确认收到了服务端请求的报头,服务端收到客户端的确认信息后就建立连接成功。注意:三次握手不代表就一定建立连接成功,三次握手只是双方建立连接的前提。

我们可以发现,三次握手的过程中前两次握手都会受到对方的应答,所以前两次不管是失败还是成功都能得知对方的状况,第三次由客户端发起的信息服务端一旦因为某种原因没有收到ACK确认,就会导致客户端已经认为建立成功,而服务端却没有建立成功,这种情况下我们上面讲的超时重传就解决了这个问题,客户端没有收到服务端建立成功的应答,就会重新发送给服务端进行重新连接。所以:三次握手不一定成功,最担心的是最后一个ACK丢失,但是有配套的解决方案。而且连接是需要被管理起来的,由操作系统管理,先描述再组织,维护一个连接是有时空成本的。

那么为什么一定要三次握手,一次握手行不行呢?

这是绝对不行的。假设一个客户端是多线程的,多次向服务端发起连接请求,只要客户端向服务端发一个SYN报文服务器就将连接建立好了,一旦客户端这一台机器频繁的向服务端发起SYN报文,那么服务器因为维护已经建立好的连接就会消耗大量的资源,从而遭受SYN洪水。

那么两次握手行不行呢?

如果是两次握手,当客户端向服务器发送SYN报文,服务器收到后自己先建立连接,这不就和一次握手是一样的吗,客户端发送一次SYN报文服务器收到后就建立连接,然后向客户端发送SYN+ACK报文,这个时候不仅有客户端不愿意建立连接了或者有其他情况不连接的情况,服务端还是很容易遭受SYN洪水。客户端能否在第一次的时候自己先建立连接呢?不可以!连接是双方的事情,当发送SYN报文告诉对方自己要建立连接,对方需要同意并且回复你才可以建立连接,否则就是消耗资源。

三次握手能行的最重要的原因是用最少的成本验证全双工通信信道是通畅的,并且三次握手可以保证在我服务器建立连接前你客户端先把连接建立好了,这样我们就可以有效的防止单机对服务器进行攻击。这就像当某个朋友给你说有个不辣的辣椒让你吃,你会先让他自己吃验证一下自己才会吃。

4.重新认识TCP四次挥手:

 首先当客户端不想连接了,就给服务端发送FIN报文,此时自身处于第一次FIN_WAIT1(等待取消连接)阶段,然后服务器收到客户端的消息后给客户端回应ACK(确认收到)的报文,此时服务端自身处于CLOSE_WAIT(等待关闭连接)状态,客户端处于FIN_WAIT2(第二次等待取消连接)阶段(这里不直接取消的原因是客户端还没有服务端也同样取消的报文)。然后服务端给客户端主动发送FIN报文,这个时候服务端自身处于LAST_ACK(最后的确认)阶段,客户端处于TIME_WAIT()的阶段,客户端收到服务端也要取消连接的请求后向服务端发送ACK(确认收到)的报文,服务器一旦收到客户端确认收到的报文那么服务器就直接关闭文件描述符了。注意:在断开连接的时候任意一方都有可能断开连接,谁先发送FIN报文谁的状态就是FIN_WAIT。也就是说主动断开连接的一方,4次挥手状态已经完成后的状态为TIME_WAIT状态,并且会维持一段时间的TIME_WAIT状态。被动断开连接的一方,两次挥手完成进入CLOSE_WAIT状态。

注意:如果我们的服务器出现了大量的close_wait,第一说明服务器有bug,没有做关闭文件描述符的动作。第二服务器有压力,可能一直推送消息给client,导致来不及close。

为什么四次挥手中主动断开连接的一方最终要维持一段时间的TIME_WAIT状态呢?那么要维持多久呢?

因为最后一次ACK确认消息是主动断开连接的一方发出的,如果没有TIME_WAIT时间,一旦最后一次ACK确认丢包了或者因为某些原因对方没收到,而这个时候发出最后一次ACK的一方已经将文件描述符关闭的话,那么接收ACK的一方由于没收到ACK需要给另一方重发FIN段,但是对方已经关闭了重发都没有意义了,所以这就是一个bug了,而TIME_WAIT的状态就是为了解除这个bug,即使一方已经发出了最后一次ACK确认,但还是会等待一段时间才close,这段时间如果另一方有问题或者没接收到最后一次ACK,那么直接给对方重发FIN段即可。这个维持的时间一般是2*MSL,MSL(最大传送单元)代表从左到右或者从右到左的时间,如下图:

 上图中两个方框每一个都是一次MSL。除了上面的第一点理由还有第二个理由就是双方在断开连接的时候,网络中还有滞留的报文,保证滞留报文进行消散。

所以:1.保证最后一个ACK尽可能的被对方收到。

2.双方在断开连接的时候,网络中还有滞留的报文,保证滞留报文进行消散。

下面我们验证一下四次挥手的状态:

首先将上次http的代码修改一下,不用的功能去掉,在hander方法中写一个死循环,并且不要主动关闭文件描述符:

 然后我们运行起来:用telnet链接服务器,可以看到telnet的端口号为33366,pid为32759的是我们服务器创建的一个进程(因为我们的服务器是多进程版):

 当我们用ctrl+]输入quit指令将telnet断开连接后,因为是telnet主动断开连接的,我们发现telnet处于FIN_WAIT2阶段(FIN_WAIT1阶段由于时间太快没有截到图,FIN_WAIT2已经可以证明先退出的处于FIN_WAIT阶段),并且服务端32759这个进程处于CLOSE_WAIT阶段。

 由于我们服务器关慢了导致没有看到先断开连接的一方的TIME_WAIT阶段,下面我们重新来一次:

 上图为建立连接阶段

 上图为telnet断开连接阶段,这个时候服务器没有断开连接

 上图可以看到,当我们快速将服务器关闭,telnet的客户端立马处于TIME_WAIT阶段,并且这个时候服务端已经全部退出了。经过验证,我们发现四次挥手中需要记住的是主动断开连接的一方四次挥手后进入TIME_WAIT阶段,被动断开连接的一方两次挥手后处于CLOSE_WAIT阶段。


总结

TCP服务器的原理很多,下一篇继续是TCP服务器的内容。

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/131549387