目录
1. 传输层是什么呢?
2. 再谈端口号
2.1. 端口号是什么
2.2. 协议号是什么
2.3. 认识知名端口号
2.4. 端口号的相关问题
2.4.1. 一个进程可以绑定多个端口号吗?
2.4.2. 一个端口号可以被多个进程绑定吗?
2.4.3. 为什么不使用PID来标识一个进程呢
2.5. netstat 命令
2.6. pidof 命令
3. 前置性认识
4. UDP 协议
4.1. UDP的基础认识
4.2. 理解UDP报文本身
4.3. UDP字段
4.4. UDP相关特性
4.5. UDP的缓冲区
4.6. 全双工 vs 半双工
4.7. UDP 使用注意事项
4.8. 基于UDP的应用层协议
1. 传输层是什么呢?
传输层是计算机网络中的一种网络协议层,位于网络层和应用层之间。
它的主要作用是将应用层的数据从发送端传输给服务端,确保数据在源主机和目标主机之间可靠地传输。
2. 再谈端口号
2.1. 端口号是什么
端口号是一个16位整数 (0 --- 65535),用于标识网络通信中的特定进程或服务的。在TCP/IP协议中,每个端口号都与一个特定的网络服务或进程相关联。端口号的作用是在主机上区分不同的网络服务和进程,使得数据能够正确地路由到目标进程。
在 TCP/IP 协议中, 用 "源IP","源端口号","目的IP","目的端口号","协议号",这样的一个五元组来标识一个通信, 在 Linux 中可以用 netstat 命令查看这个五元组。
2.2. 协议号是什么
协议号,通常指在网络协议栈中用于标识不同网络层之间协议的数字。
在传输层,特别是在IP协议(网络层)的上下文中,协议号用来标识上一层(即传输层)的具体协议类型,以便正确地处理和转发数据包。
在IP头部中,存在一个字段专门用于指示传输层的协议类型,这就是所谓的 "协议号"。这个字段告诉IP层数据包应该如何进一步处理,即数据包应该交给哪种传输层协议(如TCP、UDP或其他协议)进行处理。例如:
- TCP 的协议号是 6。这意味着当IP层接收到一个数据包,如果看到协议号是6,它就知道这个数据包应该交给TCP协议处理。
- UDP 的协议号是 17。同样地,IP层收到协议号为17的数据包,就会将其转发给UDP协议处理。
总的来说,协议号是网络协议栈中用于标识和区分不同传输层协议的机制,即服务端和客户端采用什么传输层协议进行通信的,它是确保数据包能够被正确处理和传输的关键。
2.3. 认识知名端口号
由于端口号是一个16位的整数, 故它的范围是0 到 65535,通常情况下, 端口号可以分为两类:
系统端口:系统端口是预留给一些常用的网络服务的端口号,范围从0到1023。例如,HTTP服务默认使用端口号80,HTTPS服务默认使用端口号443,FTP服务默认使用端口号21,等等。这些端口号被广泛应用于各种网络服务,并且通常由操作系统或网络协议栈预先分配。
动态端口:动态端口是指那些未被预先分配给特定服务的端口号,范围从1024到65535。这些端口号通常由客户端在需要与服务器建立连接时动态地分配使用。例如,当你使用Web浏览器访问一个网站时,操作系统会随机选择一个未被占用的动态端口号,并将其用于与Web服务器建立连接,因此我们以前在写套接字时, 客户端不需要显式的bind端口,操作系统在特定时机会自动给客户端bind端口号。
在 Linux 操作系统中,通常有一个特定的文件用来记录特定网络服务所使用的默认端口号,这个文件通常位于 /etc/services 路径下。
cat /etc/services
用户在实现自己的网络服务时,尽量避免使用0到1023范围内的端口号,因为这些端口号通常被系统或常见的网络服务占用。如果你的应用程序尝试绑定这些端口号,可能会引发权限问题或与系统服务冲突。
一般都使用1024到65535的端口,不会与系统服务发生冲突。
2.4. 端口号的相关问题
2.4.1. 一个进程可以绑定多个端口号吗?
可以, 端口号是用于标识网络通信中的特定进程或服务的,一个端口号标识一个进程,但并不是说,一个进程只能绑定一个端口号。
例如:一个Web服务器进程可能会同时监听80端口和443端口,分别提供HTTP和HTTPS服务。
2.4.2. 一个端口号可以被多个进程绑定吗?
不可以, 由于端口号是用来标识网络通信中的一个进程, 如果一个端口号被多个进程绑定, 那么这个端口号就无法标识唯一的一个进程,从而引发冲突。因此,一个端口号不可以被多个进程绑定,否则会造成端口冲突,影响通信的正常进行。
2.4.3. 为什么不使用PID来标识一个进程呢
我们以前不是学习过PID吗,PID作为进程的标识符,也是标识一个唯一的进程啊, 为什么网络模块还要自己在搞一个端口号呢?
原因如下:
- 首先,并不是所有的进程都需要提供网络服务。因此,对于网络模块而言,如果用PID来标识网络服务进程,那么操作系统是不是还需要判断哪些进程是进行网络服务的, 这会增加系统的复杂性和开销,对于要增加成本的事情,操作系统为什么要做呢?
- 其次, PID 是属于进程管理模块的, 而端口号是属于网络通信的一部分。 如果在网络中用 PID 来标识进程,那么就代表着将网络模块和进程管理模块强耦合在一起,万一未来进程管理模块出现变更,那么网络模块是不是也要改,这样的设计不利于系统的灵活性和扩展性。
因此,出于系统复杂度和耦合度的角度, 网络模块需要有一个端口号来标识进程的唯一性,将网络功能和进程管理模块进行解耦,降低系统的复杂性和维护成本,同时也提高了系统的可扩展性和灵活性。
通过端口号,不同的网络服务进程可以并行地提供各自的网络服务,而无需过多考虑系统内部的进程管理机制。
2.5. netstat 命令
netstat 是一个用来查看网络状态的重要工具。
语法:netstat [选项]
常用选项:
n:拒绝显示别名, 用于显示数字格式的IP地址和端口号;
l:仅列出处于 Listen 状态的网络服务;
p:包含 PID/Program name 信息;
t:仅显示 TCP 的网络服务;
u:仅显示 UDP 的网络服务;
a:显式所有连接状态的服务,包括LISTEN、ESTABLISHED、TIME_WAIT等等。
对于有些网络服务,可能需要提升权限,才能看到更多的信息,如下:
2.6. pidof 命令
pidof:通过一个进程名,获得进程id;
语法:pidof [进程名]
会得到一个该进程的PID;
pidof myserver | xargs kill -9
pidof 命令用于查找指定服务进程的PID,然后通过管道将结果传递给kill命令。但由于,管道是将 pidof 进程的输出做为 kill 进程的输入,注意,而此时的 kill 命令需要的是命令行参数,而不是标准输入的内容。因此这时候就需要使用 xargs 命令,它可以将标准输入的内容转换成命令行参数,并传递给 kill 命令,从而向指定服务进程发送特定信号。
标准输入: 代表的是特定文件。
命令行参数: 是 main () 的参数。
3. 前置性认识
接下来,我们就要正式进入传输层了, 就要真正的揭示UDP/TCP的底层细节问题, UDP为次,TCP为主;
同时,我们也知道, 传输层是位于应用层和网络层之间的。
应用层的数据并不是直接发送给对端主机的应用层, 而是将应用层的数据拷贝给传输层协议的缓冲区中,然后根据协议栈向下封装,通过网络发送给对端主机;对端主机收到数据后,传输层会将数据从网络层接收并进行解包,然后再将数据向上交付给对应的应用层。
因此,我们对于 UDP/TCP 都要谈论两个问题:
- UDP/TCP 是如何区分报头和有效载荷的。 只要清楚了如何区分报头和有效载荷, 我就知道,如何对报头和有效载荷进行分离以及如何封装报头的。
- UDP/TCP 如何向上交付。
4. UDP 协议
4.1. UDP的基础认识
UDP 报文格式如下:
对于UDP而言, 它是如何区分报头和有效载荷的呢?
UDP采用的是定长包头, 通过固定长度的报头 (8字节),来区分一个UDP报文中的报头和有效载荷。因此, 对于一个UDP报文而言, 前八个字节就是报文的报头, 后面都是有效载荷。
那UDP是如何向上交付的呢?
UDP在向上交付数据时,会根据UDP数据报文头部中的目的端口号信息,将数据交付给目标进程。这是因为在网络通信中,进程通过绑定端口号来监听网络上的数据,并且这个端口号是唯一的标识符。因此,UDP在接收到数据后,根据目的端口号确定了数据应该交付给哪个目标进程。
可是,作为一个接收方, 怎么知道UDP报文中哪一个是源端口号、哪一个是目的端口呢?
因为这是协议,既是一种约定,UDP报头是八字节,客户端知道,服务端也知道,且前16个比特位就是源端口,接下来的16个比特位就是目的端口, 这是一种约定,即协议。
为什么我们在编写应用层, 定义端口号时候, 都用的是 uint16_t 呢?
因为UDP协议用的端口号是16位的,而UDP协议是基于操作系统的,而操作系统提供了一系列的系统调用接口(比如socket接口)来支持网络通信,应用层通过这些接口与网络进行交互,因此,为了与UDP协议和操作系统保持一致,应用层需要使用16位的数据类型来定义端口号。
UDP是如何正确的提取整个完整报文的呢?
UDP报文头部中的16位UDP长度字段指示了整个UDP数据报文的长度,包括报头和数据部分。因此,通过提取UDP长度字段的数值,并减去8字节(UDP报头的大小),就可以得到UDP数据报文的有效载荷大小,即有效载荷的长度。
因此接收方可以根据UDP报文头部中的长度字段来正确提取整个完整的UDP数据报文,即UDP本身就能保证接收方获取一个完整的UDP报文,故我们说UDP是面向数据报的。
4.2. 理解UDP报文本身
这里的UDP报头如下 (用以举例):
struct udp_hdr
{
uint32_t src_port : 16;
uint32_t dst_port : 16;
uint32_t udp_len : 16;
uint32_t udp_check : 16;
};
未来我们所见到的所有报头都是位段,只不过位段中的字段不一样罢了。
注意,上面只是实现的大致思路,但实际上我们知道,位段有一个缺陷:可移植性太差,但是内核依旧采用位段来定义报头,并且我们之前自己定义协议时,也说过,建议不要定义结构化的数据,因为结构化的数据会存在内存对齐和大小端等问题,但是内核就这样做了,它的解决方案很复杂 (包括各种条件编译等等),它就是为了最大限度地减少内存消耗,让操作系统在有限的资源下运行尽可能多的任务,因为操作系统是所有软件都要用的基础设施,所以设计者在设计时,用尽了心思,以达到更好的效果。
有了上面的理解,我们需要在理解一下协议是如何封装的:
当应用层将数据向下交给传输层时 (本质上是拷贝的过程),假如此时的传输层协议是UDP,那么操作系统就会开辟一段空间, 并在内核中实例化一个UDP报头,例如: struct udp_hdr hdr = {10000, 20000, 0, 0};并将应用层的数据与该报头一起封装成一个UDP数据报。这个过程涉及到内存的开辟和拷贝,以及报头中相关字段的更新,如UDP报文长度和校验和,而这就是一个UDP报文,即这就是一个封装的过程。
有了封装的理解, 我们也可以类比到解包的过程,
UDP数据报,在操作系统看来,本质上就是一个对象,因此,我们可以得到这个对象的起始地址,直接访问前八个字节,在前八个字节中找到UDP长度,用这个UDP长度减去八字节,得到有效载荷的长度,然后,可以使用相关的拷贝函数将有效载荷从UDP数据报中拷贝到应用层的缓冲区中,从而将报头和有效载荷分离开来。而这就是一个解包的大致过程,因为它将数据报分解成了其组成部分:报头和有效载荷。当然,操作系统在实际操作中肯定更为复杂,因为它要考虑更多的细节,但这是它的大致思路。
4.3. UDP字段
- 源端口:代表这个UDP数据报是哪个进程发送的;
- 目的端口:代表这个UDP数据报是发送给哪个进程的;
- UDP长度:代表UDP数据报的整体大小 (报头 + 有效载荷)。UDP的报头固定为8个字节,故UDP长度最小是8字节 (只包含报头,无有效载荷);
- UDP校验和: 这个字段用于检测UDP数据报在传输过程中是否发生了错误。如果接收方计算出的校验和与UDP数据报中的校验和不匹配,那么该数据报将被认为是损坏的并被丢弃;
4.4. UDP相关特性
- 无连接:UDP是一种无连接的协议,通信的双方在发送数据之前不需要建立连接。每个UDP数据报都是独立的,发送方发送数据时不需要等待接收方的确认,也不需要保留连接状态信息。
- 不可靠:UDP是一种不可靠的协议,它不提供数据可靠性的保证。UDP数据报在传输过程中可能会丢失、重复、失序或损坏,而且UDP本身不提供任何重传、确认、流量控制等机制。
- 面向数据报:面向数据报是指每个数据报都是独立的,UDP 不会对应用层交给它的报文进行拆分或合并,而是按照应用层传递给它的数据报原样发送。这意味着 UDP 不会对数据进行任何拆分或者组合操作,而是以数据报的形式将数据发送出去。简而言之,数据怎么给你的,你就怎么给我发出去。
无连接和面向数据报很好理解,但在这里要对不可靠强调一下。
- 不可靠并不是UDP的缺点,而是它的特点;
- 数据在进行长距离传输时, 如果要保证可靠性,那么就意味着需要做更多的工作 (比如重传、确认、流量控制等机制),这些机制会增加协议的复杂性,而一旦复杂,那么其使用成本和维护成本都会更高;
- 而不可靠,意味着它会做更少的工作,只提供基本的数据传输服务,而不保证数据的可靠性,也就不考虑其他的机制,因此它就更简单,使用和维护成本就更低。
4.5. UDP的缓冲区
如何理解我们之前所学的IO接口,诸如 sendto / recvfrom、read / write、 recv / send 等等这些IO接口呢?
- 不要认为这些函数是在网络中进行数据收发,例如,sendto 是将数据中直接发送给网络的,recvfrom 是从网络中读取数据到应用层的。
- 上面的认识是错误的。
- 上面的这些函数本质上都是拷贝函数。 例如, sendto 只是将应用层的数据拷贝给了内核层的缓冲区,如果传输层协议提供了缓冲区,那么就拷贝传输层协议的缓冲区;而 recvfrom 只是将传输层协议的缓冲区的数据拷贝到了应用层的缓冲区 (即用户自己定义的缓冲区)。
- 可是,将应用层数据拷贝到传输层的缓冲区就完了吗?
- 那么传输层的缓冲区的数据什么时候发送、发多少, 出错了怎么办?
- 这些问题谁关心呢? 操作系统帮你关心,即传输层的协议解决这些问题。
- 传输层的协议(如TCP或UDP)负责管理传输层缓冲区中的数据,包括何时发送数据、发送多少数据以及如何处理发送过程中的错误等问题。应用层只是将数据拷贝给传输层的缓冲区,至于后续的传输过程以及错误处理等任务,都由传输层协议来完成。
- 因此,传输层的协议承担了管理数据传输的责任,而应用层只需将数据传递给传输层,无需关心具体的传输细节和错误处理等问题。
- UDP 协议没有发送缓冲区。 调用 sendto 会直接将应用层数据交给内核,内核再将该数据传给网络层协议进行后续的传输动作;
- UDP 协议具有接收缓冲区。 但这个缓冲区无法保证收到的UDP数据报的顺序和发送UDP数据报的顺序一致,即存在乱序问题; 且如果接收缓冲区如果满了, 再来UDP数据报则会直接被丢弃。
4.6. 全双工 vs 半双工
全双工:既可以收数据,又可以发数据,收发可以同时进行;
半双工:如果在收,就不能发;如果再发,就不能收, 收发并不能并行。
UDP和TCP都是全双工的协议,这意味着它们都支持在任意时刻进行并发的数据收发。
在UDP和TCP协议中,发送和接收是独立进行的。发送端可以在任何时候发送数据,而接收端可以在任何时候接收数据,这使得它们都具有全双工通信的能力。
那么UDP和TCP是如何支持全双工的呢?
只要发送和接受操作是相互独立的 (发送缓冲区和接收缓冲区并不冲突),那么就可以支持全双工。具体来说:
- UDP:虽然UDP没有专门的发送缓冲区,但发送操作是立即执行的,它不会阻塞接收操作。这意味着在UDP通信中,发送数据时并不会影响接收缓冲区,因此UDP可以实现全双工通信。
- TCP:TCP有发送缓冲区和接收缓冲区,但发送和接收操作是独立进行的。发送数据时,TCP会将数据放入发送缓冲区并立即返回,不会阻塞接收操作。同样,接收数据时,TCP会从接收缓冲区读取数据并立即返回,也不会影响发送操作。这使得TCP也能够实现全双工通信。
总的来说,UDP和TCP之所以能够支持全双工通信,是因为它们在发送和接收操作上相互独立,不会相互阻塞。
4.7. UDP 使用注意事项
UDP协议的数据报文有一个最大长度限制 (因为它是16位的整数),即64K(包括UDP头部)。在当今的互联网环境下,这个限制可能会限制数据传输的效率和灵活性,特别是在需要传输大量数据的情况下。
- 如果需要传输的数据超过了UDP数据报的最大长度限制,就需要在应用层进行手动的分包和多次发送。
- 发送端将数据分割成适当大小的数据块,然后分别封装成UDP数据报进行发送。
- 在接收端,需要接收所有的UDP数据报,并且按照顺序将它们拼装成完整的数据。
- 这就需要在应用层处理数据的分段和组装,增加了复杂性和开销。
4.8. 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
当然,也包括用户在编写UDP时自定义的应用层协议。