网络传输管理之UDP协议
1.1 传输层简介
1.端口号
TCP/IP协议栈为了区分一台计算机上运行的多个程序,引入了端口号的概念,由于端口号是用来识别同一台计算机中进行通信的不同应用程序,它也被称为程序地址。
数据链路层有MAC地址用来识别同一链路中不同的计算机,网络层有IP地址用来识别网络中互连的主机或路由器,这里引入的程序地址(即端口号Port)是在传输层用来识别本机中正在进行通信的应用程序,并准确的进行数据传输。例如提供www服务的HTTP程序端口号为80,提供文件传输服务的FTP程序端口号为21等,根据MAC地址+IP地址+HTTP端口号进行通信的图示如下:
由于网卡上MAC地址与IP地址是绑定关系,因此实际上两个应用进行通信时,只需要确定通信双方的IP地址与端口号就行了,但由于传输层协议有TCP/UDP两种,要想唯一识别一个通信还需要确定协议号。因此,TCP/IP或UDP/IP通信中通常采用5个信息来识别一个通信,它们是“源IP地址”、“目标IP地址”、“协议号”、“源端口号”、“目标端口号”,只要其中某一项不同就会被认为是不同的通信。
在实际通信中,要事先确定端口号,确定端口号的方法可分为两种:
(1)标准既定端口号:也称为静态端口号或熟知端口号,指每个应用程序都有特定的端口号,每个端口号都有其对应的使用目的。例如HTTP、TELNET、FTP等广为使用的应用协议中所使用的端口号就是固定的,这类知名端口号一般由0到1023的数字分配而成(1024到49151的数字也被正式注册为端口号了,这些端口号可以用于任何通信用途),如下表列举了部分常用的TCP/UDP端口号对应的应用协议;
(2)时序分配法:也称为动态分配法,动态分配的端口号可称为短暂端口号,此时服务端有必要确定监听端口号,但接受服务的客户端没必要确定端口号,而全权交给操作系统分配一个端口号。操作系统可以为每个应用程序分配互不冲突的端口号,这样操作系统就可以动态的管理端口号了。根据这种动态分配端口号的机制,即使是同一个客户端程序发起的多个TCP连接,识别这些通信连接的5部分数字也不会全部相同。动态分配的端口号的取值范围在49152到65535之间。
从上表可以看出,不同的通信协议可以使用相同的端口号,例如TCP与UDP使用同一个端口号,但使用目的各不相同。数据到达IP层后,会检查IP首部中的协议号,再传给相应的协议,即便是同一个端口号,由于传输协议是各自独立进行处理的,因此相互之间不会受到影响。
2.传输层的作用
IP首部中有一个协议字段用来标识网络层的上一层所采用的是哪一种传输层协议,根据这个字段的协议号就可以识别IP传输的数据部分究竟是哪一种传输层协议。
在TCP/IP协议栈中能够实现传输层功能的、具有代表性的协议是TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)。两者的区别主要如下:
(1)TCP协议:面向连接的、可靠的流(不间断的数据结构)协议。TCP为提供可靠性传输,实行“顺序控制”或“重发控制”机制,此外还具备“流量控制”、“拥塞控制”、提高网络利用率等众多功能;
(2)UDP协议:不具有可靠性的数据报协议,细微的处理会交给上层的应用去完成。UDP协议虽然可以确保发送消息的大小,却不能保证消息一定会到达,因此应用有时会根据自己的需要进行重发处理。
虽然TCP是可靠性的传输协议,能为应用提供可靠的传输服务,但并不一定就优于UDP协议。可靠传输的保障是以牺牲传输效率为代价的,UDP虽然不提供可靠的传输服务,但可实现较高的传输效率,主要用于那些对高速传输和实时性有较高要求的通信或广播通信中。比如使用TCP进行实时通话,可能会因数据包丢失重传而影响流畅交流,采用UDP则不会进行重传处理,即使数据包丢失也只会影响一小部分通话。此外,在多播与广播通信中也常使用UDP协议而非TCP协议,比如RIP(Routing Information Protocol)与DHCP(Dynamic Host Configuration Protocol)等基于广播的协议都依赖于UDP协议。因此,TCP与UDP协议应该根据应用的目的按需使用。
1.2 UDP协议原理与实现
UDP协议不提供复杂的控制机制,利用IP提供面向无连接的通信服务,并且它是将应用程序发来的数据在收到的那一刻立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况下,UDP也无法进行流量控制等避免网络拥塞的行为,传输途中即使出现丢包UDP也不负责重发。甚至当出现包的到达顺序乱掉时也没有纠正的功能。如果需要这些细节控制,那么不得不交由采用UDP的应用程序去处理。
由于UDP面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此常用于以下几个方面:
(1)包总量较少的通信(DNS、SNMP等);
(2)视频、音频等多媒体通信(即时通信);
(3)限定于LAN等特定网络中的应用通信;
(4)广播、多播通信(RIP、DHCP等)。
1.UDP报文格式
用户进程使用UDP来传送数据时,UDP协议会在数据前加上首部组成UDP报文,并交给IP协议来发送,而IP层将报文封装在IP数据报中并交给底层发送,在底层IP数据报会被封装在物理数据帧中。因此,一份用户数据在被发送时,经历了三次封装过程,如下图所示:
在接收端,物理网络先接收到数据帧,然后逐层将数据递交给上层协议,每一层都在向上层递交前去除掉一个首部。在UDP层,它将从IP层得到UDP报文,UDP协议会根据该报文首部中的目的端口字段将报文递交给用户进程,绑定到这个目的端口的进程将得到报文中的数据。
UDP报文称为用户数据报,同前面讲的其他协议相同,用户数据报的结构也可以分为两部分:UDP首部和UDP数据区,报文结构如下图所示:
UDP首部比较简单,它由四个16位字段组成,分别指出了该用户数据报从哪个端口来、要到哪个端口去、包含首部与数据区的总长度和校验和。源端口号与目的端口号都是16位的,这样端口号的取值范围在0到65535之间,源端口号是主机上发送该用户数据报的进程所绑定的本地端口号,而目的端口号是该报文要送达的目的主机上应用进程所绑定的端口号。在用户数据报的发起端通常会将目的端口号填写为服务器上某个熟知的端口(标准既定端口号),对源端口号字段的填写是可选的,如果客户端期望服务器为自己返回数据,则必须填写源端口号字段,服务器会在收到的报文中提取到这个源端口号,并在返回数据时使用到。客户端进程也可以不填写源端口号字段,此时该字段置0,但若选用,源端口号字段往往是一个随机分配的短暂端口号(时序分配法)。
UDP首部中的校验和字段是可选的,如果不使用校验和可以直接将该字段填入0,在某些高可靠性的局域网中使用UDP时减少校验和的计算可以增加UDP的处理速度。如果使用校验和,则校验和的计算超出了UDP报文本身,为了计算校验和,UDP引入了伪首部的概念,伪首部的组成结构如下图所示:
这里的伪首部完全是虚拟的,并不会和用户数据报一起被发送出去,只是在校验和的计算过程中被用到而已。因此,UDP校验和的计算覆盖了三部分:UDP伪首部、UDP首部和UDP数据区,算法同前面IP首部校验和相同,即16位的二进制反码求和。伪首部主要来自于运载UDP报文的IP数据报首部,将源IP地址和目的IP地址加入到校验和的计算中可以验证用户数据报是否已经到达正确的终点。前面介绍过,确定一个唯一的连接需要5个字段信息,所以伪首部中还应包含协议字段用于说明这个报文是属于UDP而不是TCP的。伪首部中最后一个总长度字段同UDP首部中的总长度字段值相同。
2.UDP数据报描述
UDP数据报首部的结构比较简单,在LWIP中用于描述UDP首部的数据结构如下:
// rt-thread\components\net\LWIP-1.4.1\src\include\LWIP\udp.h #define UDP_HLEN 8 /* Fields are (of course) in network byte order. */ PACK_STRUCT_BEGIN struct udp_hdr { PACK_STRUCT_FIELD(u16_t src); PACK_STRUCT_FIELD(u16_t dest); /* src/dest UDP ports */ PACK_STRUCT_FIELD(u16_t len); PACK_STRUCT_FIELD(u16_t chksum); } PACK_STRUCT_STRUCT; PACK_STRUCT_END |
这个结构很简洁,除了使用结构体封装宏定义每个字段外,还应该注意四个字段中保存的值都应该与网络字节序保持一致,即将某主机上的数据填写到这些字段时,要经过小端到大端的变换。
传输层的一个重要作用就是对通信双方的管理,与任务控制块管理任务运行状态、事件控制块管理任务间通信类似,传输层也引入了控制块用来管理通信双方的连接,实际上之前IP层也引入了一个raw_pcb原始协议控制块用来管理通信双方在IP层的连接。
在UDP协议中UDP控制块是整个UDP协议实现中最为核心的东西,LWIP使用UDP控制块来描述一个UDP连接的所有信息,包括源端口号、目的端口号、源IP地址、目的IP地址、协议类型等。用户使用UDP进行编程以及内核对UDP报文的处理,本质上都是对UDP控制块的操作,理解到这个本质很关键。在LWIP中用于描述UDP控制块的数据结构如下:
// rt-thread\components\net\LWIP-1.4.1\src\include\LWIP\udp.h struct udp_pcb { /* Common members of all PCB types */ IP_PCB; /* Protocol specific PCB members */ struct udp_pcb *next; u8_t flags; /** ports are in host byte order */ u16_t local_port, remote_port; #if LWIP_IGMP /** outgoing network interface for multicast packets */ ip_addr_t multicast_ip; #endif /* LWIP_IGMP */ #if LWIP_UDPLITE /** used for UDP_LITE only */ u16_t chksum_len_rx, chksum_len_tx; #endif /* LWIP_UDPLITE */ /** receive callback function */ udp_recv_fn recv; /** user-supplied argument for the recv callback */ void *recv_arg; }; /* This is the common part of all PCB types. It needs to be at the beginning of a PCB type definition. It is located here so that changes to this common part are made in one location instead of having to change all PCB structs. */ #define IP_PCB \ /* ip addresses in network byte order */ \ ip_addr_t local_ip; \ ip_addr_t remote_ip; \ /* Socket options */ \ u8_t so_options; \ /* Type Of Service */ \ u8_t tos; \ /* Time To Live */ \ u8_t ttl \ /* link layer address resolution hint */ \ IP_PCB_ADDRHINT struct ip_pcb { /* Common members of all PCB types */ IP_PCB; }; #define UDP_FLAGS_NOCHKSUM 0x01U #define UDP_FLAGS_UDPLITE 0x02U #define UDP_FLAGS_CONNECTED 0x04U #define UDP_FLAGS_MULTICAST_LOOP 0x08U /** Function prototype for udp pcb receive callback functions * addr and port are in same byte order as in the pcb * The callback is responsible for freeing the pbuf * if it's not used any more. * ATTENTION: Be aware that 'addr' points into the pbuf 'p' so freeing this pbuf * makes 'addr' invalid, too. * @param arg user supplied argument (udp_pcb.recv_arg) * @param pcb the udp_pcb which received data * @param p the packet buffer that was received * @param addr the remote IP address from which the packet was received * @param port the remote port from which the packet was received */ typedef void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p, ip_addr_t *addr, u16_t port); /* udp_pcbs export for exernal reference (e.g. SNMP agent) */ extern struct udp_pcb *udp_pcbs; |
系统为每一个连接分配一个UDP控制块,并把它们组织在一个全局链表udp_pcbs上,当UDP层收到IP层递交的报文时,会去遍历这个链表,找出与报文首部信息匹配的控制块,并调用控制块中注册的函数最终完成报文的处理。UDP控制块链表的组织形式如下图所示:
宏IP_PCB也可以称为IP控制块,在前面介绍raw_pcb时已经介绍过;本地端口号与远程端口号都是16位使用主机字节序;回调函数指针的定义也在上面给出了,回调函数及回调参数是用户程序与协议栈内核进行通信的纽带;flags控制块状态字段由四个宏定义标识该UDP控制块的四种状态,分别为是否进行校验和计算、是否使用UDP-Lite、是否处于连接状态、是否转发组播包等。
3.UDP数据报的操作
UDP报文的发送依靠与接收依靠IP层提供的服务,用户程序可以按照如下过程使用UDP收发数据:
UDP报文的发送过程:首先应该为数据开辟一个pbuf,将用户数据填写到pbuf的数据区域,最好pbuf数据区前面已经为UDP、IP和以太网首部预留了足够的空间;然后用户程序将该数据pbuf作为参数,调用UDP提供的数据发送函数udp_send或udp_sendto。当UDP层发送数据pbuf时,它会在该pbuf中填入UDP首部区域形成一个完整的UDP报文,最后调用IP层的函数来发送报文。上面udp_sendto调用ip_route是为了获得本地IP地址,便于计算UDP报文首部的校验和字段(需要伪首部参与计算,伪首部中需要本地IP字段)。
本质上,udp_send通过调用函数udp_sendto来实现其功能,udp_sendto通过调用函数udp_sendto_if来完成对报文的组装和发送,在调用udp_sendto_if之前,udp_sendto需要跟IP层进行交互操作,即调用IP层的函数ip_route为报文寻找一个网络接口结构,这个接口结构中记录了整个系统中的有效IP地址。这里的重点是函数udp_sendto_if,它的主要功能是完成UDP报文的组装,同时调用IP层的发送函数ip_output_if发送报文。
在IP层,当收到一个包含UDP报文的数据报时,函数udp_input会被调用,以处理报文。该函数的处理过程相对繁琐,首先是进行一些报文合法性的检验,然后根据报文中的端口信息查找匹配的UDP控制块,并把报文递交给控制块中注册的用户自定义函数处理。这个函数最大的难点在于根据报文中的源端口与目的端口寻找匹配的UDP控制块。链表上所有的控制块可以分为两大类,即已经绑定了远端IP地址和端口号的已连接控制块,以及未绑定远端信息的未连接控制块。与报文中目的端口号和源端口号都匹配且处于连接状态的控制块将被视作最佳匹配的控制块,若没有这样的控制块,则与目的端口号相匹配的未连接控制块将被视作查找结构。关于目的IP地址与本地IP地址的匹配,有三种情况可视为匹配:一是非广播包,且控制块绑定到本地任意IP地址上(IP为0);二是控制块绑定的IP地址与IP包中的目的IP地址完全相同;三是对于广播包,控制块绑定的IP地址与目的IP地址处于同一个子网内。
使用UDP传输、处理数据的关键在于用户自定义的数据处理函数,当UDP匹配到某个控制块时,将回调用户注册的处理函数,用户程序应该负责报文pbuf的删除工作。如果找不到匹配的端口号,而该数据报确实是发送给本地的,则一个端口不可达报文会被返回给源主机。
更多内容详见下一节:LWIP协议栈解析(八)——网络传输管理之TCP协议