《网络应用程序设计》课件第3章 UDP套接字与原始套接字的编程_第1页
《网络应用程序设计》课件第3章 UDP套接字与原始套接字的编程_第2页
《网络应用程序设计》课件第3章 UDP套接字与原始套接字的编程_第3页
《网络应用程序设计》课件第3章 UDP套接字与原始套接字的编程_第4页
《网络应用程序设计》课件第3章 UDP套接字与原始套接字的编程_第5页
已阅读5页,还剩128页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第3章UDP套接字与原始套接字的编程3.1概述3.2UDP套接字编程3.3连接UDP套接字的功能3.4UDP编程中的错误检测及处理方法3.5UDP套接字在OICQ服务中的应用3.6原始套接字3.7服务器编程模型习题3.1概述 Internet协议簇支持一个面向无连接的传输协议

用户数据报协议(UDP,UserDatagramProtocol)。UDP协议向应用程序提供了一种发送经过封装的IP数据报的方法,而且不需要在发送方和接收方之间建立连接就可以进行数据报通信。

UDP协议与TCP协议提供的服务不同,所以基于UDP协议的应用程序同基于TCP的应用程序有不相同的地方,它们的编程模型也不相同。图3-1给出了典型的UDP客户机/服务器程序的函数调用模型。图3-1UDP客户机/服务器程序的编程模型

在前面的章节中,已经介绍了基于TCP套接字编程的函数调用模型,比较TCP和UDP编程模型可以看出,UDP协议不需要事先在客户机、服务器程序之间建立连接。服务器端在调用socket函数生成一个UDP套接字后,利用bind函数将套接字与本地IP、端口号绑定,然后,服务器端调用recvfrom函数等待接收由客户端发送来的数据。客户机首先调用socket函数创建一个UDP套接字,然后利用sendto函数将请求包发送至服务器端;服务器端收到请求包后,根据请求进行处理,调用sendto函数将处理结果作为应答数据发送给客户机。客户机调用recvfrom函数接收服务器端发送来的应答数据。当通信结束后,客户机调用close函数关闭UDP套接字,而服务器端可以保留已建立的UDP套接字继续与其他客户机进行数据通信。3.2UDP套接字编程 3.1节中已介绍了基于UDP协议网络编程的一般模型,其中涉及到了在套接字编程中曾简单介绍过的数据报发送和接收函数sendto和recvfrom,下面我们列出可用于数据报发送、接收的高级套接字函数。这些函数是:

#include<sys/types.h>

#include<sys/socket.h>

#include<sys/uio.h>

intsendto(intfd,char*buf,intlen,intflags,structsockaddr*toaddr,intaddrlen);

intrecvfrom(intfd,char*buf,intlen,intflags,structsockaddr*fromaddr,

intaddrlen);

intsendmsg(intfd,structmsghdr*msgp,intflags);

intrecvmsg(intfd,structmsghdr*msgp,intflags); UDP套接字在通信中发送和接收的数据是以数据报为单位的。当应用程序调用函数sendto发送数据时,首先应将数据封装生成一个UDP数据报,然后发送;当应用程序调用函数recvfrom接收数据时。UDP协议将返回一个完整的UDP数据报数据内容。在使用UDP套接字进行编程时,我们必须注意以下几个问题:

(1) UDP套接字在发送数据时不会因发送缓冲区而出现阻塞。UDP协议没有专门为UDP套接字设置发送缓冲区,当应用程序通过调用函数sendto来发送数据时,该函数将要发送的数据从用户缓冲区拷贝到系统缓冲区,然后返回。UDP协议进一步把数据封装成一个UDP数据报,然后将这个UDP数据报传送给低层的IP协议,从而完成UDP数据报的发送任务。UDP协议是不可靠的协议,它没有必要保留已经发送的UDP数据报内容。所以,UDP套接字只有一个发送缓冲区大小,而这个大小就是可以发送的UDP数据报的最大长度。如果应用程序发送的数据量大于这个限制值,函数sendto将以错误返回,错误类型是EMSGSIZE。UDP套接字的发送缓冲区大小是不会发生变化的,所以,只要应用程序保证调用函数sendto发送的数据量小于这个限制值,发送操作总能够成功。因此,应用程序使用UDP套接字发送数据时,不会因发送缓冲区而出现阻塞。图3-2UDP套接字接收缓冲区

UDP套接字的接收缓冲区的大小是有限制的,当接收到新的UDP数据报时,如果这个UDP套接字的接收缓冲区队列已经满了,那么UDP协议将丢弃这个数据报,并且不向发送方返回任何错误信息。这种操作也是由UDP协议不保证接收数据的可靠性的特点所决定的。

(3) UDP服务器采用循环服务器的工作方式,不会被某一个客户机独占,但客户机可能被阻塞。UDP通信模式中,服务器一般采用循环服务器工作模式。在服务器与客户机之间不需要建立连接,UDP服务器能够交替地处理来自多个客户机的请求,这就意味着服务器在前后两次循环处理的请求可以是不同客户机的请求,任何一个客户机都无法独占服务器。 (4)发送数据时需指定接收方的地址。UDP套接字是面向无连接的套接字的,所以在套接字数据结构中不会保存接收方的IP地址及其端口号。如果应用程序要发送数据,就需要在调用发送函数sendto的同时指定接收方的地址。当应用程序接收数据报时,如果需要知道发送者的地址,则可以在调用接收函数recvfrom中提供空间由内核来填充;如果不关心对方的地址,则可以将函数recvfrom的参数from设置为空指针NULL,同时也必须将参数addrlen设置为NULL。

(5)在需要多点传送数据时,使用UDP套接字。目前,TCP协议不支持多点传送数据,因为,如果要使用TCP协议进行多点传送的话,就必须要为每一个传送建立一个连接。所以,在需要多点传送数据时就往往采用UDP协议。 /**********************函数udps_respon负责处理数据通信***********************/ voidudps_respon(intsockfd) { intn; charmsg[1024]; intaddrlen; structsockaddr_inaddr; for(;;) { n=recvfrom(sockfd,msg,1024,0,(struct sockaddr*)&addr,&addrlen); /*

响应客户机请求

*/ sendto(sockfd,msg,n,0,addr,addrlen); } } /******************************以下为主程序部分*******************************/ intmain(intargc,char*argv[]) { intsockfd; structsockaddr_inaddr; /*

创建一个UDP数据报类型的套接字

*/ sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { fprintf(stderr,"Socketerror"); exit(1); } bzero(&addr,sizeof(addr)); addr.sin_family=AF_INET; addr.sin_addr.s_addr=htonl(INADDR_ANY); addr.sin_port=htons(SERVER_PORT);

/*

服务器为套接字绑定一个端口号

*/ if(bind(sockfd,(structsockaddr*)&addr,sizeof(addr))<0) { fprintf(stderr,"Binderror"); exit(1); } /*

调用通信函数与客户端进行通信

*/ udps_respon(sockfd); /*

关闭套接字

*/ close(sockfd); }

这是一个简单的UDP服务器,它不需要在通信前与客户机建立固定连接,直接使用UDP套接字来接收客户机发送的UDP数据报。但是,在接受客户机请求前,服务器必须设置自己的公认的地址和端口号。它一次只能处理一个客户机的请求,而不能并发的处理多个客户机的请求,所以它是一个典型的循环服务器。它不被一个客户端所独立占有,能够交替处理多个客户机的请求,当一个客户机出现错误时不会影响服务器对来自其他客户机请求的处理。 3.2.2UDP客户机编程示例 客户机:

#include<sys/type.h> #include<sys/socket.h> #include<netinet/in.h> #include<stdio.h> #defineSERVER_PORT8080 /**********************函数udps_requ负责处理数据通信*************************/ voidudpc_requ(intsockfd,conststructsockaddr_in*addr,intlen) { charbuf[1024]; intn; for(;fgets(buf,1024,stdin)!=NULL;) { /*

向服务器端发送数据

*/ sendto(sockfd,buf,strlen(buf),0,addr,len); /*

接收服务器端的回应

*/ n=recvfrom(sockfd,buf,1024,0,NULL,NULL); buf[n]='\0'; fputs(buf,stdout); } } /******************************主程序部分*******************************/ intmain(intargc,char*argv[]) { intsockfd; structsockaddr_inaddr; if(argc!=3) { fprintf(stderr,

"usage:clientipaddrport"); exit(1); } /*

创建一个UDP数据报类型的套接字

*/ sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { fprintf(stderr,"Socketerror"); exit(1); }

/*

调用通信函数进行数据通信

*/ udpc_requ(sockfd,&addr,sizeof(addr)); /*

关闭套接字

*/ close(sockfd); }

这是一个简单的UDP客户端程序。由于采用面向无连接的通信模式,因此它不需要跟服务器端建立连接,直接在函数sendto中指定服务器端的地址,调用sendto函数向服务器发送数据。同时,可以接收来自服务端的应答数据报。当数据传输发生错误时,服务端不会有阻塞的危险,但是客户端可能会因为数据报在传输过程中的丢失而在调用函数recvfrom处阻塞。因为UDP协议不能够保证数据可靠到达,所以,对于可能遇到的问题或错误,用户应在程序中加以处理。3.3连接UDP套接字的功能

1.连接UDP套接字的建立

UDP套接字也可以调用connect函数,调用的方法和流式套接字相同,但其调用的结果与流式套接字调用connect函数的结果不同。它没有三次握手过程,因为UDP协议中不需要在发送和接收方之间建立连接。UDP套接字只是记录了目的方的IP地址和端口号,这些信息被包含在调用connect函数的套接字中,并在调用后立即返回给进程。我们把调用connect函数后的UDP套接字称为连接UDP套接字,把未调用connet函数的UDP套接字称为未连接UDP套接字。 UDP套接字调用connect函数后,将检查每个到达的数据报,UDP协议将数据报中的目的地址与connect函数的套接字中保存的IP地址进行比较,二者一致时该套接字接收这个数据报,反之丢弃这个数据报。UDP连接套接字具有以下一些特点。

(1)由于连接套接字已经记录了该套接字相应的目的地址,因此发送数据时可以不用指定服务器的目的地址。UDP协议将自动根据保存的地址填充要发送的UDP数据报。

(2)对于连接套接字,UDP协议在内核中检查连接套接字收到的数据报,并使得连接套接字只接收那些来自目的地址的UDP数据报。 UDP协议检查每个到达的数据报,根据数据报的目的端口号选择接收套接字,UDP协议检查该套接字是否是连接套接字。如果这个套接字为UDP未连接套接字,则协议将数据报存放在该套接字接收缓冲区队列中。如果这个套接字为UDP连接套接字,则协议将数据报的目的IP地址与套接字保存的IP地址比较,只有在相同时,才将数据报存放在套接字的接受缓冲区队列中,否则,丢弃。因此,当UDP客户机只与一个服务器通信时,调用函数connect,将这个UDP套接字转化为UDP连接套接字,保证只接收这个服务器的信息。 2.数据报发送以及错误返回情况

UDP协议在进行数据报通信时,可能会有以下几种情况发生:

(1)数据报成功到达服务器端,并且被服务器接收。

(2)数据报成功到达服务器端,但是服务器端的UDP套接字接收缓冲区已满,此时服务器端将自动丢弃这个数据报,并且不向客户机返回任何错误信息。 (3)数据报成功到达服务器端,但是数据报指定的目的端口上没有接收此数据报的进程。此时服务器端上的UDP协议将丢弃这个数据报,并且向客户机返回一个ICMP错误报文,通知客户机在目的端口上没有接收进程。当这个ICMP消息到达客户机UDP协议之后,UDP协议将向客户机报告这个错误。我们调用函数sendto发送数据,该函数只要将数据报发送至目的系统的缓冲区就完成调用返回,而ICMP错误报文是在sendto()函数调用完成之后返回的,错误的产生和发现时间是不一致的,所以该错误被称为异步错误。对于UDP套接字,当这个错误到达时,如果客户机正在进行系统调用,则系统将返回一个ECONNRESET类型的错误。对于未连接UDP套接字,Linux系统也返回ECONNRESET。 (4)数据报未成功到达服务器。这种情况又分为两种:①

如果数据报因为目的地址不可到达而在网络中被丢弃,并且这个数据报的传送经过了路由器,那么传送路径上的某个路由器将向客户机UDP协议返回ICMP错误消息,通知发送端目的地址不可到达。客户机UDP协议接收到这个ICMP错误消息之后,如果客户机正在进行系统调用,则这个系统调用将以ENETUNREACH或EHOSTUNREACH类型的错误返回。②

如果数据报在网络中传输时,由于字节发生错误而被路由器丢弃,或者由于路由器的缓冲区满,该数据报也将被丢弃,而且发送端也不会得到任何返回的错误信息。

在进行基于UDP套接字编程时,我们应根据实际情况去选择使用连接UDP套接字还是未连接UDP套接字。通常客户机进程只与一个服务器进程通信时,使用连接套接字比较方便,这样可以避免服务器端接收来自其他客户端的UDP数据报。当服务器进程需要同多个客户端进程进行数据报通信,并且是循环服务的方式时,使用未连接套接字。这是一般采用的原则,最终采用什么方法还应根据具体要求加以变通。 3.连接UDP套接字的取消 连接UDP套接字的取消与TCP套接字的关闭不同,它不像后者有专门的close套接字函数来关闭连接,连接UDP套接字只需再次调用connect函数,使用一个非法的套接字地址对这个套接字调用connect函数,执行此调用,connect函数套接字将会丢弃原来保存的地址。设置套接字地址的地址簇为AF_UNSPEC,则connect函数调用以错误返回,其错误类型为EAFNOSUPPORT,表示UDP协议不支持AF_UNSPEC类型的套接字地址。这样,这个UDP连接套接字的地址信息被清除,可以取消连接UDP套接字。

取消连接UDP套接字的操作如下:

structsockaddr_inaddr; intsockfd;

… addr.sin_family=AF_UNSPEC;

… connect(sockfd,(structsockaddr*)&addr,sizeof(addr));

对一个连接UDP套接字再次调用connect函数,可以完成以下两个任务:

(1)断开已连接套接字。

(2)指定新的IP地址和端口号,即创建一个新的连接。3.4UDP编程中的错误检测及处理方法

1.UDP协议不保证数据报可靠到达 如果应用程序要求实现传送的UDP数据报可靠地到达接收方,我们必须在应用程序中检测并处理各种可能的错误。例如,采用数据重传和超时重发来实现:发送方保存需要发送的数据报,接收方应用程序接收到数据报之后,向发送者返回一个确认数据报,然后发送方才将这个数据报从缓冲区中释放出去。因为发送的数据报和对方返回的确认数据报都有可能丢失,所以,如果发送者在指定的时间内没有收到接收方的确认信息,将重新发送这个数据报。

调用alarm()函数是最常用的超时控制方法,编程也比较简单。因为信号可以中断函数的阻塞,而alarm()函数可以在设定的时限到达时发出SIGALRM信号,所以我们可以在程序中捕获SIGALRM信号,唤醒用户进程作下一步的操作。在Linux内核2.4~20,i386体系源程序代码中,alarm()函数所对应的系统调用函数名称是sys_alarm(),在linux/kernel/timer.c中实现,函数定义如下:

asmlinkageunsignedlongsys_alarm(unsignedintseconds)

注意,该函数的定时单位是秒,当参数设为0时函数将取消定时操作。alarm()函数与进程相关,而与文件描述符或者说是与输入输出通道无关。当我们使用多个输入输出通道时,我们可能无法区分究竟在哪个通道上发生了阻塞。当超时到达时,alarm()函数将发出信号SIGALRM,这个信号的默认操作是终止进程,这就意味着如果我们不捕获SIGALRM,我们的程序在阻塞一段时间后便会自动结束。当我们捕获了SIGALRM信号并处理后,一般情况下会给阻塞函数返回EINTR。 此外,还有一点需要引起注意:alarm()是一次性函数,而不是周期性函数。

下面我们给出处理这种错误时的部分示例:

#include<signal.h> … voidsig_handler(); intmain() { inttimde_out,n;

structsigactionact; intsockfd; charmsg[100],buff[100]; structsockaddr_inaddr; /*

创建一个UDP数据报类型的套接字

*/ sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { fprintf(stderr,"Socketerror"); exit(1); } bzero(&addr,sizeof(addr)); addr.sin_family=AF_INET; addr.sin_addr.s_addr=htonl(INADDR_ANY); addr.sin_port=htons(SERVER_PORT); /*

套接字绑定一个端口号

*/ if(bind(sockfd,(structsockaddr*)&addr,sizeof(addr))<0) { fprintf(stderr,"Binderror"); exit(1); } act.sa_handler=sig_handler; sigemptyset(&act.sa_mask); act.sa_flags=0; sigaction(SIGALRM,&act,NULL); /*

调用通信函数与客户端进行通信

*/ strcpy(msg,"message"); sendto(sockfd,msg,sizeof(msg),0,addr,addrlen); for(;;) { timde_out=0; /*

设立信号机制,发送后等待20s后,若无返回,则认为发送超时,重新发送

*/ alarm(20); /*

读应答

*/ n=recvfrom(sockfd,buf,size(buf),0,(structsockaddr*)&addr,&addrlen); if(n<0&&errno==EINTR) { if(timed_out) {printf("Servernotresponding,

retrying...\n"); /*重新发送数据

*/ proc_timeout(); }else continue;…}}/*for*/}/*main*/

voidhandler()/*信号处理函数*/{timed_out=1;};

proc_timeout(){/*

重新发送数据

*/};

由上面的程序可见,利用alarm()函数实现操作控制的方法比较简单:首先设置信号SIGALRM的处理函数,在调用读函数之前,调用alarm()函数设置在超时到达时发送信号SIGALRM。如果读函数被信号SIGALRM中断,则表示超时到达。

2.UDP协议不保证数据报顺序到达 因为UDP协议是面向无连接的,该协议不保证数据报能够顺序到达,这就意味着所有的UDP数据报并不能按照发送的先后顺序到达接收方被处理。如果应用程序要求数据报必须是按照顺序到达并处理的,我们就要在设计UDP报文时对每个发送的数据报进行顺序编号。接收方在接收到一个数据报之后,根据数据报中的数据序号将其放入缓冲区的合适的位置,符合时才处理这个数据报,否则等待接收顺序靠前的数据报。

3.UDP协议没有流量控制

UDP协议本身不提供流量控制功能,虽然UDP协议为每个套接字建立了一个接收缓冲区队列,接收到的数据报被拷贝到该队列中,但是如果在通信过程中数据报发送速度大于接收速度,当套接字接收缓冲区满后,UDP协议将丢弃之后到达的数据报,从而造成大量的数据报丢失。针对这种情况,我们可以在确保UDP数据报可靠到达的基础上进行如下的流量控制: 发送方应用程序创建一个发送缓冲区队列,每发送一个数据报,首先将它拷贝到该队列中,然后再发送给接收方。每个数据报在接收到接收方返回的确认信息前将一直保存在这个缓冲区队列中。如果发送缓冲区队列已满,则暂停发送新的数据报,直到缓冲区队列再次出现空间为止。通过这种方法可以实现简单的流量控制。3.5UDP套接字在OICQ服务中的应用 UDP协议经常使用于语音、图像、文字等格式的数据传输中,很多即时通信程序都使用UDP套接字编程来实现数据通信,例如,目前使用的最广泛的聊天程序OICQ。本节我们将给出两个代码片段,来分别说明OICQ服务器端和客户机程序的基本实现原理。

先来看实现发送数据的客户端程序:

#include<stdio.h> #include<stdlib.h> #include<ermo.h> #include<string.h> #include<sys/types.h> #include<netinet/in.h> #include<sys/socket.h> #defineSERVER_PORT8003 #defineMSG_BUF_SIZE512 intport=SERVER_PORT; voidmain() { intsockfd; intcount=0; intflag; charbuf[MSG_BUF_SIZE]; structsockaddr_inaddress;

/*

创建一个数据报类型的套接字

*/ if((sockfd=socket(AF_INET,SOCK_DGRAM,0))==-1) { fprintf(stderr,"socketerror"); exit(1); } memset(&address,0,sizeof(address)); address.sin_family=AF_INET; address.sin_addr.s_addr=inet_addr(""); address.sin_port=htons(port); flag=1; /*

发送数据

*/do{sprintf(buf,"packet%d\n",count);if(count>30){sprintf(buf,"over");flag=0;}

sendto(sockfd,buf,sizeof(buf),0,(structsockaddr*)&address,sizeof(address));count++;}while(flag); }

服务器端程序:

#include<stdio.h> #include<stdlib.h> #include<ermo.h> #include<string.h> #include<sys/types.h> #include<netinet/in.h> #include<sys/socket.h> #defineSERVER_PORT8003 #defineMSG_BUF_SIZE512

intport=SERVER_PORT; /*ip地址表示本机

*/ char*hostname=""; voidmain() {

intsinlen;intport=SERVER_PORT;charmessage[MSG_BUF_SIZE];intsockfd;structsockaddr_insin;structhostent*server_host_name;if((sockfd=socket(PF_INET,SOCK_DGRAM,0))==-1){fprintf(stderr,"socketerror");exit(1);} server_host_name=gethostbyname(hostname);bzero(&sin,sizeof(sin));sin.sin_family=AF_INET;sin.sin_addr.s_addr=htonl(INADDR_ANY);sin.sin_port=htons(port); if((bind(sockfd,(structsockaddr*)&sin,sizeof(sin)))==-1){fprintf(stderr,"binderror");exit(1);}

/*

接收数据

*/ while(1) { sinlen=sizeof(sin);recvfrom(sockfd,message,256,0,(structsockaddr*)&sin,&sinlen);printf("nDatacomefromserver:%s\n",message);if(strncmp(message,"over",4)==0)break;}close(sockfd);}

上面两个程序经编译调试通过后,在一台主机分别运行接收程序和发送程序,就可以看到通过UDP协议的数据通信过程了。当然,也可以在两台机器上启动两个不同的进程来运行,只要修改上述程序中的IP地址,就可以实现客户机和服务器的通信模拟。3.6原始套接字 3.6.1

原始套接字定义 使用流式套接字或数据报套接字,应用程序可以实现基于TCP协议或UDP协议的数据交互。这种交互属于比较高层次的网络通信方式,它向程序员屏蔽了TCP、UDP和IP数据包的具体格式,简化了编程工作;但同时也限制了应用程序对通信协议的支持范围,降低了用户对数据的操作能力,影响了编程的灵活性。原始套接字则支持我们直接对IP数据包进行操作,可以允许用户访问ICMP和IGMP等多种协议的数据包,允许用户访问内核不处理的IP数据包,允许用户读写包括首部在内的IP数据包,允许用户基于IP层开发新的高层通信协议。

原始套接字的使用分为三个步骤:原始套接字的创建、属性的设置以及数据的发送和接收。

1.原始套接字的创建 将函数socket(intdomain,inttype,intprotocol)中的参数type设为SOCK_RAW,并将参数protocol设为某种指定类型(如IPPROTO_ICMP、IPPROTO_IGMP、IPPROTO_IP等),我们就可以创建一个原始套接字。

#include<linux/in.h> ... intsockfd sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); ....

标准的协议类型通常由系统头文件<linux/in.h>定义,在Linux2.4.20内核中,它们位于第23~47行。protocol等于IPPROTO_ICMP,表示这是一个使用ICMP协议工作的原始套接字;protocol等于IPPROTO_IGMP,表示这是一个使用IGMP协议工作的原始套接字;protocol等于IPPROTO_IP,表示这个原始套接字可以接收内核送达的任何类型的IP数据包。 原始套接字直接发送和接收IP数据包,是一种面向无连接的套接字,而且它只能由超级用户或系统管理员来创建。

无论是否设置了IP_HDRINCL属性,原始套接字接收的都是整个IP数据包,即我们的接收缓存区中的数据包含IP数据包的首部。

3.数据发送和接收 端口是TCP、UDP等传输层协议中的概念,在原始套接字中不存在端口。原始套接字通过IP地址来识别主机。可以使用sendto()、sendmsg()、recvfrom()和recvmsg()函数,来收发数据包;也可以先调用connect()、bind()函数绑定对方或本地地址,然后再使用write()、writev()、send()、read()、readv()和recv()等函数来收发数据。 2.原始套接字属性的设置

IP数据包由首部和数据实体组成,如果没有对原始套接字设置IP_HDRINCL属性,则在发送数据时,数据缓存区中存放的是IP数据包的数据实体部分;如果设置了IP_HDRINCL属性,则数据发送缓存区中存放的是整个IP数据包,包括IP数据包的首部。IP_HDRINCL是通过调用函数setsockopt()来进行设置的:

intoptval=1; if(setsockopt(sockfd,

IPPROTO_IP,

IP_HDRINCL,

&optval,

sizeof(optval))<0) exit(1); … 3.6.2ICMP协议中原始套接字的应用 在第1章中,我们已经知道ICMP(Internet消息控制协议)是TCP/IP协议簇的一个组成部分,主要作用是通过传递网络故障、网络拥塞、路由错误、中间主机崩溃及重启等信息,来协调路由器、源主机和中间主机之间的工作。 使用ICMP协议通信时,一般不设置IP_HDRINCL选项,在数据发送缓存区仅仅填写ICMP数据包,不需要考虑IP首部;但在接收数据时,接收缓存区内存放的是IP首部加ICMP数据包,所以首先必须找到ICMP数据包的起始位置,然后才能取出ICMP数据包进行处理。通常我们采用结构来读写ICMP消息的固定长度部分,包括描述IP数据包首部的结构和描述ICMP数据包首部的结构。我们可以自行定义这些结构,也可以采用系统头文件中的结构定义:iphdr和icmphdr。 iphdr描述了IP数据包的首部,在Linux2.4.20内核中,它们位于<linux/in.h>文件的第116~136行,其代码及注释如下:

structiphdr{ #ifdefined(__LITTLE_ENDIAN_BITFIELD)__u8ihl:4,

/*

首部长度,以4字节为单 位进行计量

*/version:4; /*

版本

*/ #elifdefined(__BIG_ENDIAN_BITFIELD)__u8version:4,

ihl:4; #else #error"Pleasefix<asm/byteorder.h>" #endif__u8tos; /*

服务类型

*/__u16tot_len; /*

数据包总长

*/__u16id; /*

标识

*/__u16frag_off; /*

标识位和碎片偏移

*/__u8ttl; /*

生存时间(timetolive)*/ __u8protocol;/*

协议:TCP、UDP、ICMP等

*/__u16check; /*

首部校验和

*/__u32saddr; /*

源IP地址

*/__u32daddr; /*

目的IP地址

*/ }; icmphdr描述了ICMP数据包的首部,在Linux2.4.20内核中,它们位于<linux/icmp.h>文件的第66~81行,其代码如下: structicmphdr{ __u8type; __u8code;__u16checksum;union{struct{__u16id;__u16sequence;}echo; __u32gateway;struct{__u16__unused;__u16mtu;}frag;}un; };

下面我们以一段Ping程序代码片断为例,来说明 如何使用原始套接字实现ICMP协议的数据交互。在 这个例子中,源主机向目的主机发出回显请求(ICMP_ECHO,type=0,code=0),目的主机返回回显响应(ICMP_ECHOREPLY,type=8,code=0),相关的数据包格式如图3-3所示。其中,标识符是源主机的进程号,序列码用来标识发出回显请求的次序,时间戳表示数据包发出的时刻,通过比较回显响应时刻和源主机当前时刻的差值,可以测出ICMP数据包的往返时间。在这个例子中,用户进程一共向目的主机发送三次回显请求。图3-3ICMP回显请求和响应的 数据包格式#include<linux/in.h>...intnTimeout=0; /*

超时标志

*/voidrecv_process(char*buf);/*

处理接收到的数据

*/shortCheckSum(short*buf,intnSize)/*

计算校验和

*/{ unsignedlongchksum=0; short*buf2=buf; chksum=*buf2;buf2++;buf2++; for(inti=2;i<nSize-1;i++) {chksum+=*buf;buf++;}chksum=(chksum>>16)+(chksum&0xffff); chksum+=(chksum>>16); short*ps=(short*)&chksum; return~(*ps);}voidFillIcmpHdr(char*pIcmpHdr,intnDataSize) /*

填充ICMP数据包

*/{ icmphdr*pIcmph; staticintnSeq=0; pIcmph=(icmphdr*)pIcmpHdr; pIcmph->type=ICMP_ECHO; pIcmph->code=0; pIcmph->un.echo.id=htons(getpid()); pIcmph->un.echo.sequence=htons(nSeq);nSeq++; pIcmph->checksum=htons(CheckSum((short*)pIcmpPack,nDataSize));}voidsigalrm_handler(intsig){nTimeout=1;}

intmain(intargc,char*argv[]) /*

主程序入口

*/{unsignedlongbuf[64];intsockfd;structsockaddr_inaddr1;structsigactionact;timevaltv;act.sa_handler=sigalrm_handler; /*

设置超时信号捕获函数

*/act.sa_mask=0;act.sa_flags=0;sigaction(SIGALRM,&act,NULL);sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); /*

创建原始套接字

*/if(sockfd<0) exit(1);bzero(&addr1,sizeof(addr1));if(inet_aton(argv[1],&addr1.sin_addr)==0) /*

绑定对方主机IP地址

*/exit(2);intn=0,nlen;while(n<3){ alarm(5); /*

设置超时值为5s*/ gettimeofday(&tv,NULL); /*

获得当前时间戳

*/

*(buf+2)=htonl(tv.sec);

*(buf+3)=htonl(tv.usec); FillIcmpHdr((char*)buf,8); /*

填写数据包,按双字节计算数据长度

*/ nlen=sizeof(addr1); sendto(sockfd,buf,128,0,(structsockaddr*)&addr1,sizeof(addr1)); n=recvfrom(sockfd,buf,128,0,(structsockaddr*)&addr1,&nlen); if(n>0) recv_process(buf); /*

处理接收到的数据

*/ alarm(0); nTimeout=0;n++;}close(sockfd);}

函数recv_process()用来处理对方主机返回的回显响应数据包,这个数据包内含有IP数据首部,必须将其过滤掉。recv_process()的主要代码如下:

voidrecv_process(char*buf) /*

填充ICMP数据包

*/ { iphdr*pIph; icmphdr*pIcmph; intiplen,icmplen; pIph=(iphdr*)buf; /*

确定IP数据包起始位置

*/ iplen=ip->ihl<<2; pIcmph=(icmphdr*)(buf+iplen); /*

确定ICMP数据包起始位置

*/ /*

确定收到的数据包是否是针对当前进程的回显响应

*/if(pIcmph->type!=ICMP_ECHOREPLY||pIcmph->un.echo.id!=htons(getpid())) exit(3); pl=(unsignedlong*)pIcmph; ptv1=(timeval*)(pl+2); gettimeofday(tv2,NULL); tv2=GetInterval(*ptv1,tv2); /*

编制函数,计算时间差值

*/ ... /*

显示接收结果

*/} IGMP(Internet组管理协议)是另一种最常使用原始套接字进行操作的协议,这个协议用来协调多播路由器与主机之间的工作:多播路由器使用IGMP协议来查询多播组内有哪些主机;主机则在加入和退出多播组时使用IGMP协议向路由器发出通告,或者使用IGMP协议响应多播路由器的查询。 与ICMP协议类似,IGMP数据包也是嵌入在IP数据包内进行传输的,我们可以通过调用sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_IGMP)创建一个支持协议的原始套接字。IGMP协议中原始套接字的使用方法与ICMP协议中原始套接字使用方法基本一致,只是协议的具体格式有所差别,其编程方法可参阅前面有关套接字编程的例子。 3.6.3IP_HDRINCL选项 当对原始套接字设置了IP_HDRINCL属性后,我们就可以对IP数据包的首部进行操作,可以对封装在IP数据包内的任何协议(如TCP、UDP等)进行操作,也可以定制自己的协议首部。在创建原始套接字时,应将协议类型设为IPPROTO_IP(值为0):

intsockfd sockfd=socket(AF_INET,SOCK_RAW,

IPPROTO_IP);

下面是一个构造TCP数据包的函数,其中用到的tcphdr结构由linux2.4.20内核头文件<linux/tcp.h>的第23~56行定义。

intsend_syn(char*buf,intnSize,unsignedshortport,structsockaddraddr) { iphdr*pIph; tcphdr*pTcph; bzero(buf,nSize); pIph=(iphdr*)buf; pTcph=(tcphdr*)(buf+sizeof(iphdr)); pTcph->source=htons(9090); /*

填写TCP数据首部

*/ pTcph->dest=port; pTcph->seq=random(); pTcph->doff=5; pTcph->syn=1; pIph->version=4; /*

填写IP数据首部

*/ pIph->ihl=sizeof(iphdr)>>2; pIph->tot_len=sizeof(iphdr)+sizeof(tcphdr); pIph->ttl=128; pIph->protocol=IPPROTO_TCP; pIph->saddr=random(); pIph->daddr=addr->sin_addr; pTcph->check=ChkSum(buf); /*

计算整个数据包的校验和

*/ return*(pIph->tot_len); /*

返回需要发送的数据总长

*/ }

主程序可以不断地调用这个函数,然后向服务器发送。这将引发用户进程与服务器的三次握手过程(syn=1)。由于用户进程送出的源地址是一个随机数,几乎全都是不可达地址,因此三次握手将无法完成。服务器由于侦听套接字的连接队列满而被阻塞,从而引起服务失效。3.7服务器编程模型

3.7.1循环服务器

1.UDP循环服务器 基于UDP的编程常采用循环服务器的编程模式,循环服务器的实现较为简单:UDP服务器每次从套接字上读取一个客户端的请求并处理请求,然后将结果返回给客户机。循环服务器的处理过程如图3-4所示。图3-4循环服务器处理过程

循环服务器的程序处理方法为:intsockfd;structsockaddr_inaddr,clientaddr;intn,

addrlen;charbuf[512];if((sockfd=socket(AF_INETSOCK_DGRAM0))<0){printf("socketerror.\n");exit(1);}bzero(&addr,sizeof(servaddr));addr.sin_family=AF_INET;addr.sin_port=htons(serverport); /*

端口号

*/addr.sin_addr.s_addr=htonl(INADDR_ANY);

if(bind(sockfd,…)<0){printf("binderror.\n");exit(1);}for(;;){addrlen=sizeof(clientaddr);n=recvfrom(sockfdbufsizeof(buf)0(structsockaddr*)&clientaddr&addrlen);if(n<0&&errno==EINTR)continue;

elseif(n<0){printf("recvfromerror:%s\n"strerror(errno));continue;}doit(sockfd); */处理请求

*/sendto(sockfdbufsizeof(buf)0(structsockaddr*)&clientaddraddrlen);}

由于基于UDP的通信是面向无连接的,因此没有一个客户机可以独占服务器,只要处理过程不是死循环,服务器对于每一个客户机的请求总是能够满足的。 图3-5为一个小型控制系统示例,所用计算机都工作在端口PORT1上,计算机1负责从工业现场采集数据,对原始数据进行初步处理(滤波、去伪等)后原始发送给计算机2(服务器),计算机2对数据进行处理(滤波、修正、转换等),然后将结果以广播形式发送出去。收到结果数据后,计算机1和计算机2不作处理,计算机3判断是否需要报警,计算机4将结果送去显示,计算机5将数据存档。如何区分数据来源则由应用层的数据协议来保证,例如,每个数据包中都标明数据包类型是原始数据、结果数据,还是其他数据。图3-5一个小型控制系统示例

计算机2的程序代码如下:#include<...>#definePORT18989#defineCOMPUTERX"55"...

intmain(){charbuf[256];intsockfd;structsockaddr_inaddr_2;/*

计算机2的地址结构

*/structsockaddr_inaddr_x;/*

广播地址结构

*/sockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){ fprintf(stderr,"Socketerror"); exit(1);} bzero(&addr_2,sizeof(addr_2));addr_2.sin_family=AF_INET;addr_2.sin_addr.s_addr=htonl(INADDR_ANY);addr_2.sin_port=htons(PORT1); if(bind(sockfd,(structsockaddr*)&addr_2,sizeof(addr_2))<0){ fprintf(stderr,"Inet_atonerror"); exit(1);} bzero(&servaddr,sizeof(addr_x));addr_x.sin_family=AF_INET;addr_x.sin_port=htons(PORT1);if(inet_aton(COMPUTERX,&addr_x.sin_addr)==0) exit(1); intnDataComeFrom=0; /*

用来存放数据包类型代码

*/ intn=recvfrom(sockfd,buf,256,0,(structsockaddr*)&addr_1,sizeof(addr_1)); ... /*

解析接收到的数据包,并将数据包类型代码放入nDataComeFrom*/

if(nDataComeFrom==1){process(buf,&nlen...); /*

数据处理,结果放在buf中,数据长度放在nlen中

*//*

以广播形式将处理结果数据发送出去

*/sendto(sockfd,buf,nlen,0,(structsockaddr*)&addr_x,sizeof(addr_x));}close(sockfd);} 2.TCP循环服务器 现在我们考虑基于TCP的服务器采用循环服务器工作模式的实现方法。TCP服务器接受一个客户端的连接,然后进行处理,直到完成这个客户机的所有请求后,断开连接。TCP循环服务器一次只能处理一个客户端的请求,只有在这个客户的所有请求都满足后,服务器才可以继续后面的请求。这样,如果有一个客户端占住服务器不放时,其他的客户机都不能工作了,因此,TCP服务器一般很少用循环服务器模型。只有当数据处理工作所需时间很短(如时钟服务)或者服务器只能为单一用户提供服务时才被使用,而且这时应该设置超时控制。

图3-6是一个使用TCP循环服务的例子,这是一个通过网络控制的可移动机械手,服务器程序运行在机械手的移动平台上,用户可以通过网络远程操控机械手完成搬运物体的工作,这个机械手不允许多人同时操作。若采用UDP协议来实现这个系统,由于UDP协议的不可靠性,很可能出现后发出的指令被首先执行,先发出的指令被延后执行的状况,用户将感到机械手不可控制。当然,我们也可以在应用层的协议里加上时序控制,但那样将加大编程的难度。而采用TCP的循环模式,并对阻塞函数添加超时控制,情况将会比较理想。有关TCP循环服务的程序代码,可参见前面套接字的内容,这里不再赘述。图3-6一个使用TCP循环服务的例子

3.7.2并发服务器 针对上述TCP循环服务器的缺陷,人们提出了并发服务器的编程模型。并发服务器的思想是:每一个客户机的请求并不由服务器侦听进程直接处理,而是由服务器侦听进程创建一个自己的子进程负责处理服务请求,父进程仍负责侦听客户机的请求。并发服务器的处理流程如图3-7所示。图3-7并发服务器的处理流程

1.TCP并发服务器 基本的TCP并发服务器采用这样的流程:先创建一个侦听套接字,等待客户机的请求,每当接受一个客户机请求时,就创建一个子进程,并在子进程中进行数据处理,父进程则继续等待新的客户机请求,直到服务程序满足退出条件。TCP并发服务器的程序处理方法为:

intsockfd,newsockfd; structsockaddr_inaddr;if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0){printf("socketerror.\n");exit(1);}bzero(&addr,sizeof(servaddr));addr.sin_family=AF_INET;addr.sin_port=htons(serverport);/*

端口号

*/addr.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(sockfd,…)<0){printf("binderror.\n");exit(1);}if((listen(sockfd,5)<0){printf("listenerror.\n");exit(1);}for(;;){newsockfd=accept(sockfd,…,…);if(newsockfd<0&&errno==EINTR)continue;elseif(newsockfd<0){printf("accepterror.\n");exit(1);}if(fork()==0){close(sockfd);doit(newsockfd); /*

处理请求

*/…exit(0);}close(newsockfd);} 2.UDP并发服务器

UDP协议虽然在套接字函数处不大容易阻塞,但如果对某一个客户机进行数据处理的时间过长,则服务器在这段时间内将不能接收其他客户机的请求,同时UDP协议又是不可靠的通信协议,不保证数据是否能够到达目的地址,所以就会造成数据包的丢失。这时可以采用并发的UDP服务结构:服务程序每收到一项请求,就单独为这个客户机创建一个进程,完成相应的数据处理任务,然后关闭套接字描述符。UDP并发服务器处理方法如下: #include<...> #definePORT18989 voidsigchld_handler(intsig) { while(waitpid(-1,NULL,WNOHANG)>0){} return; }intmain(){charbuf[256];intsockfd;structsockaddr_inaddr; intpid;structsigactionsigact; sigact.sa_handler=sigchld_handler; sigact.sa_mask=0; sigact.sa_flags=0; sigsigaction(SIGCHLD,&sigact,NULL); sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { fprintf(stderr,"Socketerror"); exit(1); } bzero(&addr,sizeof(addr));addr.sin_family=AF_INET;addr.sin_addr.s_addr=htonl(INADDR_ANY);addr.sin_port=htons(PORT1);if(bind(sockfd,(structsockaddr*)&addr,sizeof(addr))<0){ fprintf(stderr,"Inet_atonerror"); exit(1);}

while(1){intn=recvfrom(sockfd,buf,256,0,(structsockaddr*)&addr_1,sizeof(addr_1));if(n>0){if((pid=fork())==0) /*

子进程,处理数据并返回结果

*/{sendto(sockfd,buf,256,0,(structsockaddr*)&addr,sizeof(addr));close(sockfd);exit(0);}elseif(pid<0){fprintf(stderr,"forkerror");exit(1);}/*

父进程,继续循环等待客户请求

*/}}

创建子进程是一项开销很大的工作,如果客户非常多,而大多数的数据处理的时间又很短,那么服务器的大量时间和资源都消耗在创建和销毁子进程上,系统效率会很低,也难以及时响应客户的请求。我们可以将循环服务和并发服务进行适当地综合,采用延迟创建子进程的方法,即平时服务器工作在循环状态,当预测某一次服务耗时较长就为它创建一个子进程,而主进程继续工作在循环模式中。

在网络上,常常会有这样的情况:很多非法用户或没有操作权限的用户与服务器建立了连接,并试图进行操作,这时服务器应该先检查收到的数据包是否为合法请求(这种检查通常耗费时间极短)。如果是非法请求,服务器就拒绝服务,则继续在循环方式下工作;如果是合法请求,则服务器创建一个子进程进行数据处理,主进程依然在循环方式下工作。

我们还可以设置一个预测器,估计一项服务是否可以在很短时间内完成。因为实际上服务器的服务范围是有限的,可以根据理论知识和经验数据(即服务器在历史上处理各类数据所消耗的时间)来进行推理,考虑是否创建子进程,如运算服务,如果发现是解二元一次方程、求几个数的平均值等,则所需的服务时间极短,在循环服务中即可迅速完成;如果发现是求大型方阵的特征值、较大图像的卷积运算等,就创建子进程;如果是一种以前未处理过的运算类型,那么我

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论