一、传输层简介
前面介绍了通过IP地址实现网际寻址,通过MAC地址实现链路内寻址,两个地址一起使用可实现两台主机间的寻址与通信。但我们在一台电脑上常用的网络服务不止一个,怎么区分某主机上究竟是哪个进程在请求服务或提供服务呢?
1.1 端口号
TCP/IP协议栈为了区分一台计算机上运行的多个程序,引入了端口号的概念,由于端口号是用来识别同一台计算机中进行通信的不同应用程序,它也被称为程序地址。
再回想下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地址”、“协议号”、“源端口号”、“目标端口号”,只要其中某一项不同就会被认为是不同的通信。
在实际通信中,要事先确定端口号,确定端口号的方法可分为两种:
- 标准既定端口号:也称为静态端口号或熟知端口号,指每个应用程序都有特定的端口号,每个端口号都有其对应的使用目的。例如HTTP、TELNET、FTP等广为使用的应用协议中所使用的端口号就是固定的,这类知名端口号一般由0到1023的数字分配而成(1024到49151的数字也被正式注册为端口号了,这些端口号可以用于任何通信用途),如下表列举了部分常用的TCP/UDP端口号对应的应用协议;
- 时序分配法:也称为动态分配法,动态分配的端口号可称为短暂端口号,此时服务端有必要确定监听端口号,但接受服务的客户端没必要确定端口号,而全权交给操作系统分配一个端口号。操作系统可以为每个应用程序分配互不冲突的端口号,这样操作系统就可以动态的管理端口号了。根据这种动态分配端口号的机制,即使是同一个客户端程序发起的多个TCP连接,识别这些通信连接的5部分数字也不会全部相同。动态分配的端口号的取值范围在49152到65535之间。
从上表可以看出,不同的通信协议可以使用相同的端口号,例如TCP与UDP使用同一个端口号,但使用目的各不相同。数据到达IP层后,会检查IP首部中的协议号,再传给相应的协议,即便是同一个端口号,由于传输协议是各自独立进行处理的,因此相互之间不会受到影响。
1.2 传输层的作用
前篇提到,IP首部中有一个协议字段用来标识网络层的上一层所采用的是哪一种传输层协议,根据这个字段的协议号就可以识别IP传输的数据部分究竟是哪一种传输层协议。
在TCP/IP协议栈中能够实现传输层功能的、具有代表性的协议是TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)。两者的区别主要如下:
- TCP协议:面向连接的、可靠的流(不间断的数据结构)协议。TCP为提供可靠性传输,实行“顺序控制”或“重发控制”机制,此外还具备“流量控制”、“拥塞控制”、提高网络利用率等众多功能;
- UDP协议:不具有可靠性的数据报协议,细微的处理会交给上层的应用去完成。UDP协议虽然可以确保发送消息的大小,却不能保证消息一定会到达,因此应用有时会根据自己的需要进行重发处理。
虽然TCP是可靠性的传输协议,能为应用提供可靠的传输服务,但并不一定就优于UDP协议。可靠传输的保障是以牺牲传输效率为代价的,UDP虽然不提供可靠的传输服务,但可实现较高的传输效率,主要用于那些对高速传输和实时性有较高要求的通信或广播通信中。比如使用TCP进行实时通话,可能会因数据包丢失重传而影响流畅交流,采用UDP则不会进行重传处理,即使数据包丢失也只会影响一小部分通话。此外,在多播与广播通信中也常使用UDP协议而非TCP协议,比如RIP(Routing Information Protocol)与DHCP(Dynamic Host Configuration Protocol)等基于广播的协议都依赖于UDP协议。因此,TCP与UDP协议应该根据应用的目的按需使用。
二、UDP协议原理与实现
UDP协议不提供复杂的控制机制,利用IP提供面向无连接的通信服务,并且它是将应用程序发来的数据在收到的那一刻立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况下,UDP也无法进行流量控制等避免网络拥塞的行为,传输途中即使出现丢包UDP也不负责重发。甚至当出现包的到达顺序乱掉时也没有纠正的功能。如果需要这些细节控制,那么不得不交由采用UDP的应用程序去处理。
由于UDP面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此常用于以下几个方面:
- 包总量较少的通信(DNS、SNMP等)
- 视频、音频等多媒体通信(即时通信)
- 限定于LAN等特定网络中的应用通信
- 广播、多播通信(RIP、DHCP等)
2.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首部中的总长度字段值相同。
在UDP协议基础上发展出了一个名为UDP-Lite的协议,该协议在IP数据报中的标识为136,它同UDP用户数据报很相似,但可以采用更为灵活的校验方式。传统UDP协议对整个报文进行完成的校验,若某一位发生变化,需要丢掉整个报文的代价比较大。在UDP-Lite协议中,一个报文到底需不需要使用校验和,或者校验和的覆盖范围都是用户可控的,同UDP采用相似的数据报组织结构,但它用总长度字段指出校验和的覆盖长度,这使得它更适应于网络传输差错率小,对轻微差错不敏感的应用中,比如实时视频播放、实时通话等。
2.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;
};
#