文章目录
前言
我记得其实之前讲过 TCP 和 UDP ,但是那是从怎么用的角度来讲解的,本质上我们还是把 Socket API 当作黑盒,所以现在我就开始就着这两个传输层协议开始讲解
在学习 HTTP 等应用层协议时,为了便于理解,可以简单的认为 HTTP 协议是将请求和响应直接发送到了网络当中,但实际应用层需要先将数据交给传输层,由传输层对数据做进一步处理后再将数据继续向下进行交付,该过程贯穿整个网络协议栈,最终才能将数据发送到网络当中
一样的,传输层下面其实还有网络协议栈,但是呢,怎么说呢,我们还是可以认为传输层是直接将数据传到网络当中
所以就抱着这样的前置知识开启我们的本篇吧!
一、再谈端口号
端口号(Port)标识一个主机上进行网络通信的不同的应用程序。当主机从网络中获取到数据后,需要自底向上进行数据的交付,而这个数据最终应该交给上层的哪个应用处理程序,就是由该数据当中的目的端口号来决定的
从网络中获取的数据在进行向上交付时,在传输层就会提取出该数据对应的目的端口号,进而确定该数据应该交付给当前主机上的哪一个服务进程
也就是说,端口号作用于传输层和应用层之间

所以也大概能看出,要唯一表述一个通信,其实可以通过这样的五元组来表示
"源IP地址","源端口号","目的IP地址","目的端口号","协议号"

所以对于服务器来说,一次通信流程大概是这样的
- 先提取出数据当中的目的 IP 地址和目的端口号,确定该数据是发送给当前服务进程的。
- 然后提取出数据当中的协议号,为该数据提供对应类型的服务。
- 最后提取出数据当中的源 IP 地址和源端口号,将其作为响应数据的目的 IP 地址和目的端口号,将响应结果发送给对应的客户端进程。
可以通过 netstat -n 来查看这样所有的五元组的信息

Local Address表示的就是 源IP地址 和 源端口号 ,Foreign Address 表示的就是 目的IP地址 和目的端口号 ,而 Proto 表示的就是协议类型
协议号是存在于 IP 报头当中的,其长度是 8 位。协议号指明了数据报所携带的数据是使用的何种协议,以便让目的主机的 IP 层知道应该将该数据交付给传输层的哪个协议进行处理
所以说,协议号是作用于网络层和传输层
端口号范围划分
端口号的长度是 16 位,因此端口号的范围是 0 ~ 65535 ,2^16 = 65536 嘛
0 ~ 1023:知名端口号。比如HTTP,FTP,SSH等这些广为使用的应用层协议,它们的端口号都是固定的
1024 ~ 65535:操作系统动态分配的端口号。客户端程序的端口号就是由操作系统从这个范围分配的
可以通过查看 /etc/services 文件来看看 0~1023 端口号都对应了哪些著名的协议

问你两个问题,一个端口号能不能被多个进程绑定?反之呢?
答案是:
- 一个端口号绝对不能被多个进程绑定,因为端口号的作用就是唯一标识一个进程,如果绑定一个已经被绑定的端口号,就会出现绑定失败的问题
- 一个进程是可以绑定多个端口号的,这与"端口号必须唯一标识一个进程"是不冲突的,只不过现在这多个端口唯一标识的是同一个进程罢了
还有两个个常用的查看 TCP、UDP 连接的指令,分别是
- netstat -nltp
- netstat -nlup
二、UDP协议
UDP协议格式
网络套接字编程时用到的各种接口,是位于应用层和传输层之间的一层系统调用接口,这些接口是系统提供的,我们可以通过这些接口搭建上层应用,比如 HTTP 。我们经常说 HTTP 是基于 TCP 的,实际就是因为 HTTP 在 TCP 套接字编程上搭建的
而 socket 接口往下的传输层实际就是由操作系统管理的,因此 UDP 是属于内核当中的,是操作系统本身协议栈自带的,其代码不是由上层用户编写的, UDP 的所有功能都是由操作系统完成,因此网络也是操作系统的一部分

- 16位源端口号:表示数据从哪里来
- 16位目的端口号:表示数据要到哪里去
- 16位UDP长度:表示整个数据报(UDP首部+UDP数据)的长度
- 16位UDP检验和:如果UDP报文的检验和出错,就会直接将报文丢弃
现在有两个问题:
UDP如何将报头与有效载荷进行分离?
UDP 的报头当中只包含四个字段,每个字段的长度都是 16 位,总共 8 字节。因此 UDP 采用的实际上是一种定长报头, UDP 在读取报文时读取完前8个字节后剩下的就都是有效载荷了
UDP如何决定将有效载荷交付给上层的哪一个协议?
UDP 上层也有很多应用层协议,因此 UDP 必须想办法将有效载荷交给对应的上层协议,也就是交给应用层对应的进程
应用层的每一个网络进程都会绑定一个端口号,服务端进程必须显示绑定一个端口号,客户端进程则是由系统动态绑定的一个端口号。 UDP 就是通过报头当中的目的端口号来找到对应的应用层进程的
说明一下: 内核中用哈希的方式维护了端口号与进程 ID 之间的映射关系,因此传输层可以通过端口号得到对应的进程 ID ,进而找到对应的应用层进程

什么是报头?
其实报头就是结构体,操作系统是用 C语言 写的,其实就是结构体里面搭配上位段

UDP数据封装
当应用层将数据交给传输层后,在传输层就会创建一个 UDP 报头类型的变量,然后填充报头当中的各个字段,此时就得到了一个 UDP 报头
此时操作系统再在内核当中开辟一块空间,将 UDP 报头和有效载荷拷贝到一起,此时就形成了 UDP 报文
(内核缓冲区里面有个结构体好像是叫做 sk_buff,里面有个指针 head 和 tail ,应用层传下来有效载荷, head 指向数据的头部,然后到了传输层, head -= sizeof(udp的报头) ,依次往下就形成了一个数据包,这些包还可以通过双向链表串联起来)
UDP数据分用
当传输层从下层获取到一个报文后,就会读取该报文的前8个字节,提取出对应的目的端口号,通过目的端口号找到对应的上层应用层进程,然后将剩下的有效载荷向上交付给该应用层进程
cpp
// Linux内核中sk_buff的简化结构
struct sk_buff {
/* 数据缓冲区指针 */
unsigned char *head; // 分配的内存块起始地址
unsigned char *data; // 当前协议层有效数据的起始地址
unsigned char *tail; // 当前协议层有效数据的结束地址
unsigned char *end; // 分配的内存块结束地址
/* 协议头指针 */
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw;
} h;
/* 网络层头指针 */
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh;
/* 链路层头指针 */
union {
struct ethhdr *ethernet;
unsigned char *raw;
} mac;
/* 链表管理 */
struct sk_buff *next;
struct sk_buff *prev;
/* 协议状态信息 */
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
/* 数据长度信息 */
__u32 len; // 数据总长度
__u32 data_len; // 分片数据长度
__u16 mac_len; // MAC头长度
/* 所有权和引用计数 */
struct sock *sk; // 所属socket
refcount_t users; // 引用计数
};
UDP协议特点
UDP传输的过程就类似于寄信,其特点如下:
- 无连接:知道对端的IP和端口号就直接进行数据传输,不需要建立连接
- 不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息
- 面向数据报:不能够灵活的控制读写数据的次数和数量
注意: 报文在网络中进行路由转发时,并不是每一个报文选择的路由路径都是一样的,因此报文发送的顺序和接收的顺序可能是不同的
面向数据报
应用层交给 UDP 多长的报文,UDP 就原样发送,既不会拆分,也不会合并,这就叫做面向数据报。
比如用 UDP 传输 100 个字节的数据,如果发送端调用一次 sendto ,发送 100 字节,那么接收端也必须调用对应的一次 recvfrom ,接收 100 个字节;而不能循环调用 10 次 recvfrom ,每次接收 10 个字节
UDP的缓冲区
UDP 没有真正意义上的发送缓冲区。调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
UDP 具有接收缓冲区。但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致;如果缓冲区满了,再到达的 UDP 数据就会被丢弃
UDP的 socket 既能读,也能写,因此 UDP 是全双工的
为什么UDP会有接收缓冲区呢?
如果 UDP 没有接收缓冲区,那么就要求上层及时将 UDP 获取到的报文读取上去,如果一个报文在 UDP 没有被读取,那么此时 UDP 从底层获取上来的报文数据就会被迫丢弃
一个报文从一台主机传输到另一台主机,在传输过程中会消耗主机资源和网络资源。如果UDP收到一个报文后仅仅因为上次收到的报文没有被上层读取,而被迫丢弃一个可能并没有错误的报文,这就是在浪费主机资源和网络资源
因此 UDP 本身是会维护一个接收缓冲区的,当有新的 UDP 报文到来时就会把这个报文放到接收缓冲区当中,此时上层在读数据的时就直接从这个接收缓冲区当中进行读取就行了,而如果 UDP 接收缓冲区当中没有数据那上层在读取时就会被阻塞。因此 UDP 的接收缓冲区的作用就是,将接收到的报文暂时的保存起来,供上层读取

一个现实的比喻是:
text
邮递员送信场景:
- 邮递员:有你的信!📨
- 你:我在洗澡,等会儿...
- 邮递员:不行,现在必须签收!❌
- 结果:信被撕毁丢弃 😱
- 数据包到达UDP层时,90%的网络资源已经消耗,此时丢弃是极大的浪费!
当然你也发现了 UDP 的报文长度使用 16 个 bit 位来表示,也就是说最多 64K ,其实是个很小的数字,如果需要传输的数据超过 64K ,就需要在应用层进行手动分包,多次发送,并在接收端进行手动拼装


总结
以下都是一些基于 UDP 的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议