在未连接的Socket上发送UDP数据报的方法

来源:岁月联盟 编辑:zhuzhu 时间:2007-09-30
在未连接的Socket上发送UDP数据报的方法内容简介:

UDP是一个简单的面向数据报的传输层协议,我们先站在UDP客户端的角度来看看如何发送一个UDP数据报,以及协议栈为发送一个UDP数据报做了哪些事情。

UDP数据报可以在未连接的socket上发送(使用sendto系统调用,指定目的地址),也可以在已连接的socket上发送(使用send系统调用,不用指定目的地址),下面我们分两种情况讨论。

下面是一个在未连接的socket上发送UDP数据的用户态程序示例(注:该程序的格式和风格相当不好,只是为临时测试使用。),该程序目前还只管发送,不处理接收,关于接收,我们后面再作分析:

#include <sys/types.h>#include <sys/socket.h>#include <sys/ioctl.h>#include "my_inet.h"#include <stdio.h>#include <errno.h>#include <arpa/inet.h>#include <unistd.h>int main(){        int i;        struct sockaddr_in dest;        dest.sin_family = MY_PF_INET;        dest.sin_port = htons(16000);        dest.sin_addr.s_addr = 0x013010AC;  //目的地址是172.16.48.1(网络字节序)        //创建UDP数据报服务的socket。        int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );        if( fd < 0 ){            perror("socket: ");            return -1;        }        int bwrite = sendto( fd, "abcdefg", 7, 0, (struct sockaddr *)&dest, sizeof(dest) );        if( bwrite == -1 ){            perror("send: ");            close(fd);            return -1;        }        printf("sendto: %d/n", bwrite);        close( fd );        return 0;    }

创建socket的操作跟RAW协议的差不多,只有极少区别,内核中表示套接字的结构上的操作集,协议名略有不同而已。我们重点看sendto操作所引发的内核代码执行。sendto所到达的my_inet模块的第一站是myinet_sendmsg,一般来讲,该函数只要调用udp协议自己的udp_sendmsg即可,但在之前,它还有一样事情要完成,就是为这个socket执行绑定,这个绑定可能跟服务器端的bind系统调用有些区别。试想,如果我们用这个udp socket发送出去了一个数据报,但没有记录下这个udp socket,那等对端的回应数据报来的时候,我们就不知道哪个socket要接收这个数据报了。绑定就是记录这个udp socket。

myudp_hash是一个具有128项的哈希数组,每一项都是一个udp socket的链表,每个udp socket以自己的源地址端口号为哈希主键插入这个数组。源地址端口可以是用户自己指定的,也可以是由内核自动分配的。

内核自动分配的源端口号有一个范围,这个范围段似乎是由系统的内存大小决定的(具体有待进一步分析),如果内存大(似乎是有高端内存可用),范围段是32768-61000,否则就是1024-4999。udp_port_rover是一个全局变量,初始值为范围段的下限,每次新分配端口,记录下新分配的端口号,下一次再分配时,在前一次的基础上加1,然后查询对应的myudp_hash中的项,如果该项的链表不为空,则找下一项,直至遍历整个数组,如果为空,则分配成功。所以,当连续分配128个端口后(数组中的128项中,链表全不为空),这个查询必然失败,最后遍历数组完成时,得到的端口号必然是前一次分配的端口号加127,然后,端口号每次加128,再查询对应的数组项,看该端口号有没有被使用掉。

这个描述可能有点模糊,简单总结一下就是:每次分配一个端口号,先在前一次分配值的基础上以1为步进值递增,如果对应的哈希数组中的链表为空,则肯定没有被使用过,直接使用。如果遍历完整个哈希表都没有空的链表,则要查询链表中的每一项,以得到未使用的端口。

用户自己指定一个端口,则我们到对应的哈希数组中的链表查询,如果已被使用,并且不能重用,则分配端口号失败。对用户自己指定的端口,没有范围段的限制。这个一般用于服务端,而自动分配端口用于客户端。

绑定完成后,myinet_sendmsg会调用myudp_sendmsg,它与myraw_sendmsg所执行的操作相差并不多。先查询输出路由,然后添加协议首部,最后发送数据包。与raw相比,udp要在IP首部前添加一个UDP首部。以下是UDP首部的定义:

struct udphdr {            __u16   source;     //发送端端口号。            __u16   dest;       //目的端端口号。            __u16   len;        //UDP长度。            __u16   check;      //UDP检验和。        };

UDP是一个传输层协议,与下层的网络层协议相比,它不仅需要知道数据传输的两端的主机,还需要知道是主机上的哪个进程在进行数据传输,端口号其实就是用于标识发送进程和接收进程的。UDP长度是UDP头加上UDP数据的长度(不包括IP首部)。UDP检验和覆盖UDP首部和UDP数据。

由于UDP数据报在未连接的socket上进行发送,所以每次进入myraw_sendmsg,都要进行输出路由的查询,以确定源地址和目的地址。但我们知道,路由是有缓存的,所以,并没有太多的额外开销。认为在未连接的socket上发送UDP数据报开销要大的观点并不完全正确。

(责任编辑:云子)