目录
一、铺垫知识
1、传输层
通过HTTP/HTTPS的学习,对应用层有了一定的认识,接下来就是下一层协议:传输层协议。
两台计算机通过TCP/IP协议通讯的大致过程如下:
中间的传输层和网络层是在内核实现的,而应用层是在用户层实现的,这也就注定了在进行学习传输层的时候必然就是要学习在Linux内核中的关于网络的部分,实际上传输层在操作系统的内部是提供了一套对应的系统调用,然后进行正常的数据读取,那对于这部分系统调用也有很多的接口,比如在创建套接字的时候有listen,bind,receive这样的接口,所以下一步要学习的内容就在传输层当中。
2、端口号
在学传输层前,先谈谈端口号,对于端口号的话题在之前已经学习过了,这里只是想说对于端口号的认识,作为一台主机来说,服务器可能会有很多的应用服务,每一个应用服务都要绑定一个明确的端口号,那这个端口号的意义就是能够保证让数据传输给上面的那一个应用程序,因为对于进程的区分就可以借助这个端口号来进行区分,借助ip地址和端口号就可以做到在全网找到唯一的一台主机,而从这个主机上找到唯一的一个进程,这样就能精确的找到对应的网络服务了。
2.1、五元组表示 一个进程通信
在 TCP/IP 协议中,用 “源IP”,“源端口号”,“目的 IP”,“目的端口号”,“协议号” 这样一个五元组来标识一个通信。 举例:打开浏览器后,添加多个同账户页面访问 CSDN,虽然源 IP 地址是一样的,但是源端口号不同,就表示两个不同的通信。通过这个五元组,服务器能够准确的区分请求是从哪里来的。
所以现在我们可以大致的清楚通信的流程:
- 先提取出数据当中的目的 IP 地址和目的端口号,确定该数据是发送给当前服务进程的。
- 然后提取出数据当中的协议号,为该数据提供对应类型的服务。
- 最后提取出数据当中的源 IP 地址和源端口号,将其作为响应数据的目的 IP 地址和目的端口号,将响应结果发送给对应的客户端进程。
2.2、端口号范围划分
端口号是一个 16 位的整数,它的取值范围是 0~65535。
- 0~1023:知名端口号。比如 HTTP,FTP,SSH 等这些广为使用的应用层协议,它们的端口号都是固定的。(类比 120 或 110)
- 1024~65535:操作系统动态分配的端口号。客户端程序的端口号就是由操作系统从这个范围分配的,允许用户手动绑定。
2.3、知名端口
有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号:
- ssh 服务器使用 22 端口
- ftp 服务器使用 21 端口
- telnet 服务器使用 23 端口
- http 服务器使用 80 端口
- https 服务器使用 443
2.4、查看端口号
可以通过命令 vim /etc/services 查看文件内容,该文件是记录网络服务名和它们对应使用的端口号及协议。当自己写一个程序使用端口号时,要避开这些知名端口号。
2.5、问题
一个进程可不可以 bind 多个端口号?
可以。假设绑定了两个端口号 A 和 B,这两个端口号标识的是同一个进程 ,这与端口号用来标识进程的唯一性并不冲突。
一个端口号是否可以被多个进程 bind?
不行,因为端口号的作用就是标识唯一的一个进程。
如果绑定了多个进程,如何找到对应的进程呢?所以如果绑定一个已经被绑定的端口号就会出现绑定失败的问题。
3、pidof & netstat 命令
①netsate 命令
netstat 是一个用来查看网络状态的重要工具。
sudo netstat -ntlp 查看TCP相关网络信息
sudo netstat -nulp 查看UDP相关网络信息
- n:拒绝显示别名,能显示数字的全部转化成数字。
- l:仅列出有在 Listen(监听)的服务状态。
- p:显示建立相关链接的程序名(进程:process)。
- t(tcp):仅显示 tcp 相关选项。
- u(udp):仅显示 udp 相关选项。
- a(all):显示所有选项,默认不显示 LISTEN 相关。
②pidof命令
通过进程名来查看服务器进程的 pid。
ps axj | grep UdpServer
pidof UdpServer
二、UDP协议
学习传输层的协议,要从UDP协议入手,其中一个原因是UDP协议还是相对简单一些。
任何协议进行封装后都要进行解包,那如何辨别有效载荷和报头呢?
对于这个问题在自定义协议当中,我们采取的策略是使用了一个\n来进行标记,在前面也有对应的字符串长度来进行解析,而在http协议中采用的方案是用了空行来进行对应的分割工作,那在UDP当中呢?如下:
1、UDP协议格式
- 16 位源端口号:表示数据从哪里来。
- 16 位目的端口号:表示数据要到哪里去。
- 16 位 UDP 长度:表示整个数据报(UDP 首部 + UDP 数据)的最大长度。
- 16 位 UDP 检验和:如果UDP报文的检验和出错,就会直接将报文丢弃。
UDP的定义十分简单粗暴,就是一个定长报头,规定前面的这些字段就是定长的,规定前8个字节就是报头,剩下的部分就是有效载荷。
为什么我们前面在应用层编写代码的时候,每一次写端口号都喜欢用 uint16_t 呢?
其根本原因就是因为传输层协议中的端口号就是 16 位的。
send 数据并不是直接发送到网络里,而是发给了传输层。然后传输层再通过网络协议栈继续发送。那接收端如何将报头和有效载荷进行分离?
UDP 采用的是固定报头,UDP 的报头中只包含四个字段,每个字段的长度都是 16 位,总共 8 字节,所以直接提取前 8 个字节就是报头,其他的就是有效载荷。UDP 具有将报文一个个正确接收的能力,UDP 是面向数据报的。
UDP 如何交付?(有效载荷交给上层的哪一个协议?)
应用层每个进程都绑定有端口号,UDP 就是通过报头当中的目的端口号来找到对应的应用层进程的,把有效载荷交出去。
2、UDP报文
这里的报头其实就是一种结构化数据对象(位段):
struct udp_hdr
{
uint16_t src_port; // 源端口
uint16_t dsc_port; // 目的端口
uint16_t udp_len; // UDP长度
uint16_t udp_check; // 校验和
};
这个结构体中的内容其实都与上面的图中所示的报头字段一一对应,而实际上对于UDP的报头理解确实可以理解为是对于UDP的字段填充的过程。
1.1、UDP数据封装的过程
那么如果要是使用UDP协议向网络中发送Hello World这样的字段,该如何理解这个过程呢?
首先要知道应用层 sendto 数据是发给传输层的。
创建一块内存,计算出有效载荷的起始地址,拷贝有效载荷,强转填写报头部分,最后形成 UDP 报文。
操作系统会提供一个类似于缓冲区的这样的一段区域,那在这个缓冲区中就会构建对应的UDP请求,未来都是会在这个地方通过操作系统向对方发送UDP对应的报文,那这段UDP的报文会在内核中进行流动,它未来是要向下交互到底层硬件进行发送的,这就必然意味着在操作系统的内部会存在很多的UDP的报文
站在接收方的角度来讲,未来它也会受到大量的UDP的报文,所以操作系统必然要对于这么多的UDP报文进行管理,因此在操作系统内部会存在这个叫做sk_buff的结构体
在这个结构体中会有对应的start,end,pos指针,那当未来要构建一个UDP的报文,在内核的层面上就会定义一段缓冲区,之后把前面定义的报头部分拷贝进来,然后再把用户空间的Hello World拷贝进来,然后把对应的start,end,pos这样的指针进行一个描述,这样对于当前UDP的报文就管理起来了,之后在内核中可以使用链表,来把这一个一个的UDP报文进行链接管理起来,这样就能实现对于内核中UDP报文的管理工作
至此,UDP的传输原理其实就是这样,虽然在内核中的实际实现并不是这样,但是基本思想确实如此,未来有机会再对于源码中的UDP部分进行分析详解。
1.2、UDP数据交付的过程
因为是定长报头,直接取出目的端口号,把有效载荷向上交付给指定协议(进程)。
3、UDP特点
UDP 传输的过程类似于寄信。
- 无连接:知道对端的 IP 和端口号就直接进行传输,不需要建立连接。
- 不可靠:没有确认机制,没有重传机制。如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息。
- 面向数据报:不能够灵活的控制读写数据的次数和数量。要读取就必须读取一个完整的报文。
3.1、面向数据报
举例理解:别人发了三个快递,那么我们就一定要收到三个,不会出现只收到一个,一个半这样的情况。如果只有一个包裹,那我们也不能只拿走一半。结论:发送了一个报文,要么不读,要么 recvfrom 等到读取完一整个报文再返回。
应用层交给 UDP 多长的报文,UDP 原样发送,既不会拆分,也不会合并。
用 UDP 传输 100 个字节的数据:如果发送端调用一次 sendto,发送 100 字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节;而不能循环调用10次recvfrom,每次接收10个字节。
4、UDP缓冲区
严格意义上来说:
- UDP 没有真正意义上的发送缓冲区。因为它没有可靠机制,不需要把数据暂存起来。它直接调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
- UDP 是只有接收缓冲区。这个缓冲区是用来保存用户暂时来不及处理的报文,但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致,如果缓冲区满了,再到达的 UDP 数据就会被丢弃。
UDP 的 socket 既能读,也能写,所以是全双工的。
为什么 UDP 要有缓冲区?
如果 UDP 没有接收缓冲区,那么就要求上层及时将 UDP 获取到的报文读取上去,如果一个报文在 UDP 没有被读取,那么此时 UDP 从底层获取上来的报文数据就会被迫丢弃。
5、UDP传输最大长度
UDP 协议首部中有一个 16 位的最大长度,因此一个 UDP 能传输的数据最大长度是 64K(包含 UDP 报头的大小)。
然而 64K 在当今的互联网环境下是一个非常小的数字,如果我们要传输的数据大于 64K,就需要在应用层进行手动分包,多次发送并在接收端进行手动拼装。
6、基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
当然也包括我们自己写 UDP 程序时自定义的应用层协议。