目录
传输层
再谈端口号
端口号范围划分
认识知名端口号(Well-Know Port Number)
两个问题
UDP 协议
UDP 协议端格式
UDP 的特点
面向数据报
UDP 的缓冲区
UDP 使用注意事项
基于 UDP 的应用层协议
进一步理解UDP协议
传输层
负责数据能够从发送端传输接收端.
再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在 TCP/IP 协议中, 用 "源 IP", "源端口号", "目的 IP", "目的端口号", "协议号" 这样一个
五元组来标识一个通信(可以通过 netstat -n 查看);
端口号范围划分
• 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的
端口号都是固定的.
• 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作
系统从这个范围分配的
认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些
固定的端口号:
• ssh 服务器, 使用 22 端口
• ftp 服务器, 使用 21 端口
• telnet 服务器, 使用 23 端口
• http 服务器, 使用 80 端口
• https 服务器, 使用 443
执行下面的命令, 可以看到知名端口号cat /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号
两个问题
1. 一个进程是否可以 bind 多个端口号?一个进程可以绑定多个端口号。
在网络编程中,一个进程可能需要监听多个端口以接收来自不同客户端或服务的连接请求。例如,一个Web服务器可能同时监听80端口(用于HTTP)和443端口(用于HTTPS)。这通常通过创建多个套接字(socket)并分别将它们绑定到不同的端口号来实现。
2. 一个端口号是否可以被多个进程 bind?但一个端口号只能被一个进程bind,因为端口号是为了确定网络中进程的唯一性的,如果多个进程尝试绑定同一个端口号,操作系统通常会返回错误,因为这会导致数据接收的混乱。
如何理解?
可以把端口号和进程理解成哈希关系,端口号是key值,一个key对应的value是唯一的,但一个value可以被多个key值所访问。
UDP 协议
UDP 协议端格式
• 16 位 UDP 长度, 表示整个数据报(UDP 首部+UDP 数据)的最大长度;
• 如果校验和出错, 就会直接丢弃;•udp的报头是一个结构体,源代码:
- UDP报头结构:
- 源端口(16位)
- 目的端口(16位)
- 长度(16位):UDP报头和数据的长度(以字节为单位)
- 校验和(16位):用于错误检测的可选字段
- 封装过程:
- 创建一个UDP报头,并填充源端口、目的端口、长度和校验和。
- 将有效载荷数据附加到UDP报头之后。
- 计算整个UDP数据报的校验和(如果需要)。
- 将完整的UDP数据报传递给IP层进行进一步封装和发送。
分用指的是从接收到的UDP数据报中提取出有效载荷数据的过程。这通常发生在接收数据时。
- 分用过程:
- 从接收到的IP数据报中提取出UDP数据报。
- 验证UDP报头的校验和(如果需要)。
- 使用UDP报头中的长度字段来确定有效载荷数据的长度。
- 提取出有效载荷数据,并将其传递给上层应用程序进行处理。
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 协议首部中有一个 16 位的最大长度. 也就是说一个 UDP 能传输的数
据最大长度是 64K(包含 UDP 首部).
然而 64K 在当今的互联网环境下, 是一个非常小的数字.
如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包, 多次发送, 并在接收端
手动拼装;
基于 UDP 的应用层协议
• NFS: 网络文件系统
• TFTP: 简单文件传输协议
• DHCP: 动态主机配置协议
• BOOTP: 启动协议(用于无盘设备启动)
• DNS: 域名解析协议
进一步理解UDP协议
1.udp报头
报头就是一个结构体变量:struct udphdr hadr={...}.,将报头构建好了发出去,对方也一定有一快内存空间来接受报头,物理内存会拿到报头的二进制数据,那么接收方想拿到udp中的报文,那么就直接定义指针指向它就行了:struct udphdr *h,就可以任意取其中的字段了。
所以双方就行udp协议的交换时,就是直接用结构体对二进制流的序列化和反序列化。
2.对报文的理解
udp存在缓冲区,所以决定了上层我们正在处理数据的时候(1.正在处理的报文),底层也一定2.有报文没有被处理,被缓冲区缓存了,3.此时OS也不断地从硬件中读取报文,当然还有4.准备放到缓冲区的报文。
当然操作系统内可能存在读取的多份报文,但还没有及时向上交付。所以在系统中,存在大量的报文,正在等待被向上交付,也存在正在被向下交付的报文。那这些报文哪些是向下交付的呢?哪些又是向上交付的呢?哪些是要等在被创建的?哪些又是等待被删除的?操作系统一定是要对报文进行管理的(先描述,再组织),OS存在管理报文的结构体:
struct sk_buff
下面是
struct sk_buff
中一些主要成员的简要说明:
- 数据相关:
char *head, *data, *tail, *end
:这些指针用于描述数据缓冲区。head
指向缓冲区的起始位置,data
指向实际数据的开始位置,tail
指向实际数据的结束位置,end
指向缓冲区的末尾。- 元数据:
unsigned int len, data_len
:len
表示从data
到tail
的长度,即实际数据的长度;data_len
表示从data
到end
的长度,即整个缓冲区中可用于数据的长度。unsigned int truesize
:表示sk_buff
结构体及其数据缓冲区的总大小。- 控制信息:
__u32 priority
:报文的优先级。__be16 protocol
:报文使用的协议类型。__u32 mark
:用于标记报文,以便在路由或过滤时使用。- 链表相关:
struct sk_buff *next, *prev
:用于将多个sk_buff
结构体链接成一个链表,表示一个完整的报文(如果报文太大,无法在一个sk_buff
中表示)。- 时间戳:
ktime_t tstamp
:报文的时间戳。- 其他:
void (*destructor)(struct sk_buff *skb)
:一个函数指针,用于在sk_buff
被释放时执行清理工作。在传输层中会给每个报文形成一个缓冲区,
struct sk_buff会管理这段缓冲区 ,
在添加数据之前,可能需要预留一定的空间用于存放报头或对齐数据。所以head和data一开始不是指向头的,一开始指向的是从应用层拷贝数据在缓冲区预留的空间的头,也就是有效载荷的头,接着添加报头的时候head指针会向前移动一个sizeof(struct udphdr)的大小,接着会填充报头的数据。这样就构建好了一个报文。
struct sk_buff中存在
struct sk_buff *next, *prev,
将多个sk_buff
结构体链接成一个链表,只要找到链表的头就获取到了整个报文,对报文进行链式处理。再将报文交给下层只需要将struct sk_buff结构交给下一层,继续以相同的方法添加报文。