一、再谈端口号
1.1 端口号标识网络进程
如何通过端口号找到主机上的网络进程?
- 在socket编程中bind绑定是最为重要的一步:
- 他将套接字与指定的本地 IP 地址和端口号关联起来,这意味着指定的套接字可以接收来自指定 IP 地址和端口号的数据包。
- 同时系统也会为端口号与进程pcb指针建立映射关系,这意味着通过端口号就可以找到与之关联的进程。
1.2 使用“五元组”标识一个通信
端口号(Port)标识了一个主机上进行通信的不同的应用程序
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看);
netstat 查看网络状态的重要工具
1.3 端口号的范围划分
- 0 - 1023:这个范围内的端口号被保留用于一些特定的服务和应用程序,称为“系统端口”或“熟知端口”Well-Know Port Number)。例如:HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。系统一般不允许用户自己绑定这个范围内的端口号。
- 1024 - 65535:这个范围内的端口号大多数是可以自由绑定的,但也有个别例外,比如mysql绑定3306号端口。除去这些例外,其他的端口号可以由用户显式bind绑定,也可以由操作系统动态分配(客户端端口号绑定)。
常用的熟知端口
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
- ssh服务器, 使用22端口
- ftp服务器, 使用21端口
- telnet服务器, 使用23端口
- http服务器, 使用80端口
- https服务器, 使用443端口
提示:在
/etc/services
文件中可以查看到系统中所有的知名端口号。我们自己编写的程序使用端口号时, 要避开这些知名端口号.
两个问题
- 一个进程是否可以bind多个端口号?当然可以,一个进程可以从多个端口收发数据。
- 一个端口号是否可以被多个进程bind?不可以,端口号指向系统中唯一的一个进程。
介绍两个小工具:
pidof <进程名>
:通过进程命查看进程ID,在查看服务器的进程ID时非常方便。... | xargs proc ...
:xargs可以将前面命令行中管道文件的输出转换为之后程序的命令行参数。
打一套组合拳:
二、UDP协议
2.1 UDP数据报格式
UDP(User Datagram Protocol,用户数据报协议)是工作在OSI(开放系统互连,Open Systems Interconnection)模型中传输层的一种协议,使用IP作为底层协议。UDP协议的格式相对简单,主要由以下几个部分组成:
UDP数据报分为首部和用户数据部分,整个UDP数据报作为IP数据报的数据部分封装在IP数据报中。UDP数据报文结构包括:
- 源端口号(Source Port):占用2个字节(16位),表示发送方端口号。在要求对方回信时选用,不要求时可使用全0。
- 目的端口号(Destination Port):占用2个字节(16位),表示接收方端口号。在终点交付报文时必须使用。
- 长度(Length):占用2个字节(16位),表示UDP数据报的长度,包括首部和数据部分。其标定的长度最小值是8B(只有首部),最大值是2^16 = 64KB。
- 校验和(Checksum):占用2个字节(16位),用于检测UDP数据报在传输过程中是否有错误。如果检测到校验和出错,就会直接丢弃该报文。
- 数据(Data):占用0个或多个字节,是实际传输的数据部分。
UDP如何将报头与有效载荷进行分离?
- UDP采用定长报头,UDP在读取报文时读取完前8个字节后剩下的就都是有效载荷
UDP如何决定将有效载荷交付给上层的哪一个协议?
- UDP是通过报头当中的目的端口号来找到对应的应用层进程的,即由目的端口号决定。
- 当UDP数据报到达时,操作系统会根据目的端口号找到相应的应用程序或服务,并将数据报交付给它进行处理
在Linux内核中数据报是怎样被封装和管理的?
-
首先Linux内核是使用C语言编写的,那么UDP作为传输层协议(内核中)自然也是一样。
-
所以UDP报头结构在内核中其实就是一个结构体类型:
-
所以当应用程调用sendto接口发送数据时,系统就会创建并填充UDP报头。
-
sk_buff(socket buffer)结构是linux网络代码中重要的数据结构,它负责管理和控制接收或发送数据包的信息
-
sk_buff结构其实就是对数据报的封装,将UDP协议的报头和数据内容封装成一份完整的数据报。
-
sk_buff结构中包含prev和next指针,操作系统将所有进程发出的sk_buff通过链表组织到一起,按序发送到网络。
-
对端主机将接收到的数据存入新创建的sk_buff结构当中,并依据端口号将其分派给对应进程的UDP接收缓冲区。
-
客户端调用recvfrom接口从UDP缓冲区读取数据,得到源端口号和报文内容。
注意:sk_buff结构较为复杂,上面的内容只是我片面的理解,甚至可能存在错误。
详情请阅读:Linux内核:sk_buff解析 - 唐稚骅 - 博客园 (cnblogs.com)
2.2 UDP协议的特点
- 无连接:UDP在发送数据前不进行连接,发送结束时也没有连接可以释放,减少了开销和发送数据之前的时延。
- 不可靠:没有确认机制,没有重传机制,如果因为网络故障使该段无法发送到对方,UDP协议层也不会给应用层返回任何错误信息。也正是因为UDP不保证可靠交付,使得主机不必维持复杂的连接状态。
- 面向报文:发送方的UDP对应用程序交下来的报文,再添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。发送方发送了多少次数据报,接收方就需要接收多少次,不能一次性合并接收,也不存在接收到不完整的数据报。
- 无拥塞控制:很多实时应用(如IP电话、实时视频会议等)要求源主机以恒定的速率发送数据,并且允许网络发生拥塞时丢失一些数据,却不允许数据有太大的时延,UDP正好适合这种要求。
- 首部开销小:UDP只有8个字节的首部,适用于对时间敏感、但对可靠性要求不高的应用场景。
- 全双工通信:虽然UDP没有发送缓冲区,但是UDP的socket既能读,也能写,所以也是全双工通信协议。
综上所述,UDP协议的格式简单,开销小,适用于对实时性要求较高但对可靠性要求不高的应用场景。
面向数据报
-
应用层交付给UDP多长的报文,UDP就原样发送,既不会拆分,也不会合并,这就叫做面向数据报。
-
可以想像成快递:你朋友给你发了1个快递,你就只能收1个快递,你不能只收0.5个快递,也不能收2个快递,你只能收1个,即发多少就收多少。
-
UDP发送的数据不能太大,UDP的报文最大长度是64KB(UDP首部+UDP数据)
-
如果需要传输的数据超过64K,就需要在应用层进行手动分包,多次发送,并在接收端进行手动拼装。
2.3 UDP的缓冲区
- UDP没有真正意义上的发送缓冲区。调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
- UDP具有接收缓冲区。但是这个接收缓冲区不能保证UDP数据报的接收顺序和发送顺序一致;如果缓冲区满了,再到达的UDP数据报就会被丢弃。
为什么UDP没有发送缓冲区
- 因为不需要,UDP协议的设计目标是提供一种简单的、无连接的通信方式。
- UDP协议的设计初衷是为了实现高效的数据传输,尽量减少协议本身的开销。不提供可靠性和流量控制的机制,因此不需要发送缓冲区
- 调用sendto会把数据直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
为什么UDP要有接收缓冲区?
虽然 UDP 是一种无连接的、不可靠的传输协议,但仍然需要接收缓冲区来暂时存储接收到的 UDP 数据包。以下是几个理由:
-
数据处理速度不匹配:发送端的数据处理速度可能快于接收端的数据处理速度。如果没有接收缓冲区,接收端处理数据的速度跟不上发送端发送数据的速度,可能导致数据包在传输过程中被丢弃或溢出。通过接收缓冲区,接收端可以临时存储尚未处理的数据包,以便之后逐个处理。
-
数据包乱序、丢失处理:由于 UDP 协议本身不提供拥塞控制和重传机制,可能会导致数据包乱序到达或部分数据包丢失。接收缓冲区可以暂时保存乱序到达的数据包,并允许应用程序按顺序处理数据。此外,接收缓冲区还可以让应用程序检查是否丢失了某些数据包,并采取适当的措施。
-
简化应用程序设计:即使 UDP 是一种简单的协议,但应用程序仍需要一定的缓冲区来存储接收的数据。接收缓冲区的存在可以简化应用程序设计,让应用程序不需要关心接收时的数据处理细节,只需从缓冲区中读取数据即可。
总之,虽然 UDP 协议不提供可靠性保证,但接收缓冲区仍然是必要的,用于在一定程度上处理乱序到达、丢包等情况,并为应用程序提供一个暂存数据的地方,以便后续处理。
三、基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
提示:当然, 也包括你自己写UDP程序时自定义的应用层协议。