BSD实现
在BSD中UDP头部数据结构如下:
/** Udp protocol header.* Per RFC 768, September, 1981.*/
struct udphdr {u_short uh_sport; /* source port */u_short uh_dport; /* destination port */short uh_ulen; /* udp length */u_short uh_sum; /* udp checksum */
};
IP首部实现可以参考lwip,不过笔者简单讲一讲一个相似的结构体struct ipovly。
struct ipovly是在BSD内部使用的一个覆盖IP首部之上的结构,其实并不是真正的IP首部,这是为了方便计算校验和。
/** Overlay for ip header used by other protocols (tcp, udp).*/
struct ipovly {caddr_t ih_next, ih_prev; /* for protocol sequence q's */u_char ih_x1; /* (unused) */u_char ih_pr; /* protocol */short ih_len; /* protocol length */struct in_addr ih_src; /* source internet address */struct in_addr ih_dst; /* destination internet address */
};
IP/UDP首部结构体:
/** UDP kernel structures and variables.*/
struct udpiphdr {struct ipovly ui_i; /* overlaid ip structure */struct udphdr ui_u; /* udp header */
};
#define ui_next ui_i.ih_next
#define ui_prev ui_i.ih_prev
#define ui_x1 ui_i.ih_x1
#define ui_pr ui_i.ih_pr
#define ui_len ui_i.ih_len
#define ui_src ui_i.ih_src
#define ui_dst ui_i.ih_dst
#define ui_sport ui_u.uh_sport
#define ui_dport ui_u.uh_dport
#define ui_ulen ui_u.uh_ulen
#define ui_sum ui_u.uh_sum
UDP发送
UDP发送实现简单介绍如下:
参数
inp:UDP 套接字在内核中的表示,包含了套接字的所有状态和配置信息,例如IP/UDP首部等信息
m:指向输出mbuf链
addr:一个可选指针,指向sockaddr_in结构中的目的地址
control:一个可选指针,指向sendmsg的控制信息
程序执行
1.如果我们没有使用sendmsg这些带控制信息的API,那么就丢弃control参数。
2.指定了目的地址:判断套接字传递过来的目的地址是不是任意地址(0.0.0.0),如果是,那么该地址无意义,返回错误。如果不是,需要通过in_pcbconnect函数临时连接到该目的地址,不过在连接之前需要暂时提升当前程序优先级并保存本地地址。因为临时连接会改变套接字中的外部地址、端口和本地地址, 此时的套接字信息是不完整的。如果此时有 UDP 数据报到达,协议栈在查找匹配的套接字时,可能因地址信息变化而将数据报交给错误的套接字,导致数据被错误的进程接收。
未指定目的地址:判断是不是已经连接上了(例如调用 connect()
函数实现连接),否则也报错。
connect()
函数的本质也是调用in_pcbconnect函数。
3.关于M_PREPEND和mtod可以看mbuf那一节。程序先申请内存,再设置IP/UDP首部,接下来就是计算校验和。
4.检验与计算校验和,在前文笔者简单介绍过校验和的计算,分为初始化、构建、计算、填写四个步骤,读者可以返回前文查看。
初始化与构建:将IP首部覆盖为ui_next,ui->ui_prev,并覆盖其他参数,这两个参数不会影响校验和结果,因为它们被设置为0。现在IP首部被覆盖为:ui_next,ui_prev,ui_x1,ui_pr,ui_len。其中IP首部中的ui_len、ui_src和ui_dst是真正影响校验和结果的参数。计算和填写就不多赘述了,是通过in_cksum实现的。
正如我们前面所说的,UDP的IPv4是允许不进行校验的,只要把校验和填0。而如果计算出来的结果是0,校验和会填写为0xffff。
5.udp_output会填充IP头部的三个字段:服务类型TOS、全长len、生存时间TTL,而ip_output则会填充其他字段。
6.使用ip_output发送数据报。
7.如果socket是临时连接上的,那么调用in_pcbdisconnect断连临时连接的socket,同时恢复本地地址并恢复程序优先级,之后程序结束。
其中in_pcbconnect将会耗费程序近三分之一的时间。
int udp_output(inp, m, addr, control)register struct inpcb *inp;register struct mbuf *m;struct mbuf *addr, *control;
{register struct udpiphdr *ui;register int len = m->m_pkthdr.len;struct in_addr laddr;int s, error = 0;if (control)m_freem(control); /* XXX */if (addr) {laddr = inp->inp_laddr;if (inp->inp_faddr.s_addr != INADDR_ANY) {error = EISCONN;goto release;}/** Must block input while temporarily connected.*/s = splnet();error = in_pcbconnect(inp, addr);if (error) {splx(s);goto release;}} else {if (inp->inp_faddr.s_addr == INADDR_ANY) {error = ENOTCONN;goto release;}}/** Calculate data length and get a mbuf* for UDP and IP headers.*/M_PREPEND(m, sizeof(struct udpiphdr), M_DONTWAIT);if (m == 0) {error = ENOBUFS;goto release;}/** Fill in mbuf with extended UDP header* and addresses and length put into network format.*/ui = mtod(m, struct udpiphdr *);ui->ui_next = ui->ui_prev = 0;ui->ui_x1 = 0;ui->ui_pr = IPPROTO_UDP;ui->ui_len = htons((u_short)len + sizeof (struct udphdr));ui->ui_src = inp->inp_laddr;ui->ui_dst = inp->inp_faddr;ui->ui_sport = inp->inp_lport;ui->ui_dport = inp->inp_fport;ui->ui_ulen = ui->ui_len;/** Stuff checksum and output datagram.*/ui->ui_sum = 0;if (udpcksum) {if ((ui->ui_sum = in_cksum(m, sizeof (struct udpiphdr) + len)) == 0)ui->ui_sum = 0xffff;}((struct ip *)ui)->ip_len = sizeof (struct udpiphdr) + len;((struct ip *)ui)->ip_ttl = inp->inp_ip.ip_ttl; /* XXX */((struct ip *)ui)->ip_tos = inp->inp_ip.ip_tos; /* XXX */udpstat.udps_opackets++;error = ip_output(m, inp->inp_options, &inp->inp_route,inp->inp_socket->so_options & (SO_DONTROUTE | SO_BROADCAST),inp->inp_moptions);if (addr) {in_pcbdisconnect(inp);inp->inp_laddr = laddr;splx(s);}return (error);release:m_freem(m);return (error);
}