Fork me on GitHub

从1写TCPIP协议栈7:UDP协议与输入输出

前言

  链路层的ETH包结构、网络层中的ARP\ICMP。本篇我们正式进入传输层协议TCP/UDP的章节,柿子先挑软的捏~我们先搞UDP协议。

UDP协议简介

字段含义

  UDP是一种基于IP协议的单纯简单的用户数据报协议,它不提供差错纠正、队列管理等错误处理机制,但是它提供Checksum等一定程度的错误检查机制。目前RFC0768是UDP协议的正式规范:

Source/Destination port:源端口与目的端口,端口的含义一会儿专门说。

Lenght:UDP数据报长度(包含UDP头部)

Checksum:校验值,端对端校验。Checksum采用16位的Intel校验和,但UDP数据可以是奇数甚至为0,所以当长度不足偶数时,会自动填充一些0上去以满足校验计算。

UDP伪头部

RFC0768说明Checksum 采用端到端校验,也就是需要计算IP头部,来看看所谓的UDP伪头部:

  可以看出就是把IP头的后面给算到了UDP的Checksum校验范围中,同时要求UDP 伪头部的长度和UDP头部中的长度字段一致,重要的是UDP的伪头部不会作为UDP数据被发送到Port对应的进程中!观察伪头部的字段可以看出,每次经过NAT网关时,也就是每个IP跳都需要重新计算UDP-Checksum。虽然RFC1122要求UDP的校验是必须的,但是这种要求其实违背了分层的理念,之所如此是因为相对于UDP协议本身来说这种违背微乎其微;也有少数取消了校验和的场景,因为二层中一些校验机制已经足够间接检查UDP了。

Port端口

  端口在网络协议中有两种意思,一种是交换机、集线器上RJ45等物理端口,一种是对应应用层进程的逻辑端口,因此一般传输层协议TCP\UDP中都必须携带端口号来表明包的对象是上交给那个进程。本章节的UDP协议与TCP协议中的端口号指代后者。目前的情况是,物理端口和逻辑端口数量非常众多,为了区分会进行标号。因此如果按照端口号了来划分的话:

公共端口

0~1023,它们紧密绑定于一些服务,通常这些端口的通讯明确表明了某种服务的协议,如:80端口对应与HTTP通信,21端口绑定与FTP服务,25端口绑定于SMTP服务,135端口绑定与RPC(远程过程调用)服务。

注册端口

1024~49151,它们松散的绑定于一些服务,也就是说有许多服务绑定于这些端口,这些端口同样用于其他许多目的。

动态端口

9152~65535,理论上,不应为服务分配这些端口,通常机器从1024开始分配动态端口。

UDP的输入处理

首先我们必须定义好UDP的包结构,考虑到ip可以从IP协议拿取:

#pragma pack(1)
typedef struct _udp_header
{
	uint16_t srcport;
	uint16_t destport;
	uint16_t total_len;
	uint16_t checksum;
}_udp_header;
#pragma pack()

UDP的输出处理其实比较简单,就是把接收到的包进行分解,做一些基本的检查后根据UDP伪头部把有用的信息保存下来:


void _udp_input(_udp_packet* udp, _ip_addr* srcip, _eth_packet* packet)
{
	_udp_header* udp_hdr = (_udp_header*)packet->dataptr;
	uint16_t pre_checksum;
	uint16_t src_port;

	//输入包:长度检查
	if (packet->size < sizeof(_udp_header) || packet->size < swap_order16(udp_hdr->total_len))
	{
		printf("_udp_input error!: this size error!!!");
		return;
	}

	pre_checksum = udp_hdr->checksum;
	udp_hdr->checksum = 0;

	//输入包:checksum检查
	if (pre_checksum == 0)
	{
		printf("_udp_input error!: this checksum error!!!");
		return;
	}

	//输入包:Checlsum校验检查
	uint16_t checksum = checksum_peso(srcip, &_ip_cfg, UDP, (uint16_t*)udp_hdr, swap_order16(udp_hdr->total_len));
	checksum = (checksum == 0) ? 0xFFFF : checksum;
	if (checksum != pre_checksum) {
		return;
	}

	src_port = swap_order16(udp_hdr->srcport);
	_eth_packet_del_header(packet,sizeof(_udp_header));

	//保存四元组
	if (udp->handle)
	{
		udp->handle(udp,srcip,src_port,packet);
	}

}

UDP的输出处理

在拿到接收包中的源端口和源IP后,我们重新组包进行校验后,开始发送:

_type_drv_err _udp_output(_udp_packet* udp, _ip_addr* destip, uint16_t destport, _eth_packet* packet)
{
	_udp_header* udphdr;
	uint16_t checksum;

	_eth_packet_add_header(packet,sizeof(_udp_header));
	udphdr = (_udp_header*)packet->dataptr;
	udphdr->srcport = swap_order16(udp->local_port);
	udphdr->destport = swap_order16(destport);
	udphdr->total_len = swap_order16(packet->size);
	udphdr->checksum = 0;
	checksum = checksum_peso(&_ip_cfg, destip,UDP,(uint16_t*)udphdr,packet->size);
	udphdr->checksum = (checksum == 0) ? 0xFFFF : checksum;
	return _IP_output(UDP,destip,packet);
}

可能大家发现,关于端口和ip的绑定与存取本白这里并没有说明,是因为这部分是基于大佬的Time查询接口做的,需要传递的是一个函数指针,对于协议学习来说用处不大,我们也可以用包含四元的结构体数组去做。大家只要知道本质上我们需要进行目的端口、目的ip的二元组绑定就好了。

//参考
typedef _type_drv_err(*udp_handle_t)(_udp_packet* udp, _ip_addr* srcip, uint16_t srcport, _eth_packet* packet);
struct  _udp_packet
{
	enum {XUDP_STATE_FREE,XUDP_STATE_USED} state;
	uint16_t local_port;
	udp_handle_t handle;
};

作为服务器,我们需要绑定的为客户端的源IP、源Port。因为源IP与源Prot就是我们的发送方UDP-Socket。

调试

打开调试工具,随机发送一个UDP包:

binggo!!!!!

UDP小结

UDP协议为了提高传输效率,只需要保证目的IP和目的MAC得准确性即可。从这点看它得校验机制好像与协议本身背道而驰,这种违背分层得校验机制对数据传输造成了一定得拖累(虽然这种拖累微乎其微)。近年来大家有时也会直接取消校验来进行松懈使用。继续站在大多数应用场景来说,当采用校验机制时,我们已经违背了老祖宗得意愿,但是迫于现实又没办法,那就只能继续这样做喽~这种迫于无奈尤其体现在UDP_IPV4数据包穿过一个NAT网关得时候,因为此时端口号和IP地址可能会发生变化。除此之外本白再结合自己的工作领域谈一谈为什莫车载网络中要基于UDP协议K跨网段路由而不是TCP,在车载网关中,路由得目的是将一个网段得数据或信号做一定处理后转发到另一个网关,这最关注数据的实时性和准确性。考虑到车载以太网本身就是一个局域网,因此基于UDP路由是可以同时兼顾两者的,并且UDP可以减少开销。

posted @ 2023-03-08 21:44  张一默  阅读(89)  评论(0编辑  收藏  举报