网络程序设计第4章.docx
《网络程序设计第4章.docx》由会员分享,可在线阅读,更多相关《网络程序设计第4章.docx(29页珍藏版)》请在冰豆网上搜索。
网络程序设计第4章
第4章UDP插口编程
在上一章中我们已经介绍了TCP插口编程,本章我们介绍另一种重要的基于插口API的编程,即UDP插口编程。
我们将着重介绍UDP协议机制、UDP插口编程模式。
并用具体的例子来说明UDP插口编程中的方法和各种异常情况的处理。
同时我们还将介绍两种特殊的UDP插口编程:
广播和多播。
4-1概述
4-1-1UDP协议概述
TCP/IP协议栈中的用户数据报协议UDP提供简单的、不可靠的、无连接的数据传输服务。
与TCP相比,它比较简单但不可靠。
但是,对某些应用程序而言使用UDP比使用TCP更合适,如Internet中的域名系统DNS,网络文件系统NFS,简单网络管理协议SNMP。
特别是在今天的局域网环境中,如果不考虑发送方与接收方的处理速度的差异,UDP的可靠性几乎与TCP没有什么区别。
由于UDP与TCP的差异,因此利用UDP服务的插口编程与利用TCP服务的插口编程有很大不同。
因为不需要建立连接,所以每次发送和接收的数据报都包含了发送方和接收方的地址信息。
4-1-2UDP的数据输出过程
本节介绍UDP的数据输出过程。
因为UDP提供的是不可靠的数据传输服务,因此,UDP插口并没有发送缓存,尽管可以用SO_SNDBUF插口选项来修改插口发送缓存的大小,但对UDP插口而言,缓存大小仅仅表示写到插口的UDP数据报的大小上限。
应用进程的数据在沿协议栈向下传递时,以某种形式复制到内核的缓存中,当数据链路层把数据传出去后这个副本就被丢弃。
当应用进程调用write时,内核中的UDP从应用进程的缓存中复制所有数据到内存缓存中。
UDP简单地加上它的8字节首部以构成PDU并把PDU传递给IP。
IP再往下传递。
如果UDP应用进程发送一个大的数据报,它比TCP应用更有可能在底层被分片,因为TCP会把应用进程数据划分成MSS大小的块,但UDP却没有这种机制。
从写UDP插口的write调用成功地返回表示数据报或所有报文段已被加入到链路层的输出队列。
如果输出队列中没有足够的空间来存放数据报或它的某些报文段,UDP将返回错误ENOBUFS给应用进程。
需要说明的是,有些UDP的实现并不返回这种错误,在这种实现中,甚至数据报还没有发送就被丢弃的情况应用进程也不知道。
因此,使用UDP通信时,应用进程发送的数据不能太长。
4-2基本UDP插口编程
4-2-1UDP编程模式
图4-1给出了典型的UDP客户-服务器程序所使用的插口函数。
对UDP应用而言,如果从通信的角度来看,客户服务器的概念是比较模糊的:
任何时候,发送数据的一方可以认为是客户,接收数据的一方是服务器,而不像TCP应用,发起连接的一方为客户,而接收连接的一方称为服务器,自始至终都保持这种关系。
当然,可以从应用功能的角度,将提供服务的一方称为服务器,而将申请服务的一方称为客户。
图4-1UDP客户-服务器程序的一般通信过程
基于UDP的客户与服务器在通信之前不需要建立连接。
在数据传送完成之后,需要关闭插口,释放网络资源。
在大多数情况下,UDP客户和服务器方调用的插口函数是一样的,按时间顺序如下:
(1)打开一个插口(socket)。
(2)将插口绑定到指定地址和端口上(bind)。
(3)进行数据交换(sendto,recvfrom)。
(4)数据交换完成,关闭连接(close)。
在某些情况下,UDP通信两方中一方可以不绑定指定地址和端口,而是由内核自动分配,但至少有一方是要显示绑定地址和端口的。
而且,不绑定地址的一方必须首先向绑定地址的一方(服务器)发送数据,绑定地址和端口的一方从接收到的数据报中获取发送方的地址和端口,用于向没有显式绑定地址和端口的一方发送数据。
但大多数情况下,UDP应用都显式地将插口绑定到指定地址和端口上。
通常情况下,UDP客户-服务器间数据交换使用的系统调用为sendto和recvfrom。
这两个调用均可指定或返回对方的地址。
如果recvfrom的参数源地址参数from是空指针,则相应的源地址长度参数addrlen也必须是空指针,这表示我们并不关心发送数据方的协议地址。
这两个函数也可用于TCP,但一般不需要这么使用。
我们在第2章中已详细讨论了这两个函数。
另外,尽管UDP是面向无连接的通信,但是UDP应用也可以调用connect()函数。
但是,此处的connect()的功能并不是向对方发起连接请求(启动三次握手过程),内核只是记录connect()调用中指定的对方的IP地址和端口号,并立即返回给调用进程。
在这种情况下,UDP应用使用的数据交换函数就不再是sendto和recvfrom,而是使用TCP应用常用的write(或send)和read(或recv)。
我们将在4-2-4节中详细讨论这种情况。
图4-1只显示了一个简单的UDP客户-服务器的处理过程。
实际的网络应用程序可能要比此过程复杂得多。
如,需要改变插口行为时需要调用setsockopt(),需要获取插口信息时需要调用getsockopt()等等,最多时可能要用到第2章中介绍的所有与插口编程有关的函数。
下面我们就从这个最简单的客户-服务器模式开始介绍如何实现利用UDP的通信。
4-2-2实例
本节用一个具体的例子来介绍UDP插口编程。
我们重写第3章中的测试TCP性能的客户-服务器程序,改为测试UDP性能的客户-服务器程序。
我们仍使用第3章中的头文件common.h,但要增加一个宏定义:
#defineCLIENT_PORT3000/*客户进程端口号*/
下面给出客户程序。
程序4-1阻塞方式下的UDP客户程序udp_client.c。
1#include"common.h"
2
3charbuff[MAXBUFSIZE];/*发送和接收缓存*/
4intsend_size[5]={10,100,1000,10000,100000};/*发送数据大小*/
5
6main(argc,argv)
7intargc;
8char*argv[];
9{
10intsockfd=0;/*插口描述符*/
11structsockaddr_insrcaddr,destaddr;/*源地址,目的地址*/
12intalen;/*地址长度*/
13intslen,rlen,len;/*发送和接收长度变量*/
14intrdt;/*记录RoundTrip时间*/
15inti,j;
16structtimevaltv1,tv2;
17
18if(argc<2)
19{
20printf("使用方法:
udp_client\n");
21exit(0);
22}
23
24/*产生UDP插口*/
25if((sockfd=socket(AF_INET,SOCK_DGRAM,0))<0)
26{
27printf("产生插口失败,退出\n");
28exit
(1);
29}
30alen=sizeof(structsockaddr_in);/*地址结构长度*/
31/*由系统自动绑定一个IP地址*/
32bzero((char*)&srcaddr,alen);
33srcaddr.sin_family=AF_INET;
34srcaddr.sin_addr.s_addr=htonl(INADDR_ANY);
35srcaddr.sin_port=htons(CLIENT_PORT);
36/*绑定插口到本地的指定地址*/
37if(bind(sockfd,(structsockaddr*)&srcaddr,alen)<0)
38{
39printf("绑定插口失败,退出\n");
40exit
(2);
41}
42
43bzero((char*)&destaddr,alen);
44destaddr.sin_family=AF_INET;
45destaddr.sin_port=htons(SERVER_PORT);
46destaddr.sin_addr.s_addr=inet_addr(argv[1]);
47
48for(i=0;i<5;i++)/*测试不同大小的报文的RoundTrip时间*/
49{
50slen=send_size[i];/*本次测试数据大小*/
51gettimeofday(&tv1,(structtimezone*)0);
52
53for(j=0;j<50;j++)/*每一种大小测试50次*/
54{
55if((rlen=sendto(sockfd,buff,slen,0,
56(structsockaddr*)&destaddr,sizeof(destaddr)))<0)
57{
58printf("发送失败,程序退出,j=%derrno=%d\n",j,errno);
59close(sockfd);
60exit
(1);
61}
62bzero((char*)&srcaddr,sizeof(srcaddr));
63if((rlen=recvfrom(sockfd,buff,slen,0,
64(structsockaddr*)&srcaddr,&alen))<0)
65{
66printf("接收失败,程序退出,errno=%d\n",errno);
67close(sockfd);
68exit
(2);
69}
70}
71gettimeofday(&tv2,(structtimezone*)0);
72rdt=((tv2.tv_sec-tv1.tv_sec)*1000000+
73tv2.tv_usec-tv1.tv_usec)/50;
74printf("报文大小=%d,RoundTripTime=%d微秒\n",slen,rdt);
75}
76close(sockfd);
77}
第25行打开一个UDP插口,插口类型为SOCK_DGRAM。
第32—41行将插口绑定到端口CLIENT_PORT上。
如上一节所述,客户进程也可以不显式绑定到指定地址和端口,但要求通信由客户进程先发起。
第43—46行填写目的地址结构,用于sendto函数。
第46行中使用了地址转换函数inet_addr(),而没有使用inet_aton()。
因为在Solaris2.5中,不支持函数inet_aton()。
第55—61行发送指定长度的数据到目的地址。
如果发送失败,则进程退出。
如果是非阻塞方式,则需要改写错误处理部分。
如果在sendto调用中不指定目的地址(没有对UDP插口调用connect()的情况下),则返回的错误代码取决于实现:
在4.4BSD中,将返回EDESTADDRREQ,而Posix.1g中则返回ENOTCONN。
第62—69行接收数据报。
第62行初始化数据源地址结构变量,如果要从指定源地址接收数据报,则需要进一步设置该变量中的相关成员(地址和端口号)。
此处的设置表明可以接收从任何主机上的任意UDP端口上发送来的数据报。
下面给出UDP服务器程序。
程序