版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)
文檔簡介
第3章UDP套接字與原始套接字的編程3.1概述3.2UDP套接字編程3.3連接UDP套接字的功能3.4UDP編程中的錯誤檢測及處理方法3.5UDP套接字在OICQ服務(wù)中的應(yīng)用3.6原始套接字3.7服務(wù)器編程模型習(xí)題3.1概述 Internet協(xié)議簇支持一個面向無連接的傳輸協(xié)議
用戶數(shù)據(jù)報協(xié)議(UDP,UserDatagramProtocol)。UDP協(xié)議向應(yīng)用程序提供了一種發(fā)送經(jīng)過封裝的IP數(shù)據(jù)報的方法,而且不需要在發(fā)送方和接收方之間建立連接就可以進行數(shù)據(jù)報通信。
UDP協(xié)議與TCP協(xié)議提供的服務(wù)不同,所以基于UDP協(xié)議的應(yīng)用程序同基于TCP的應(yīng)用程序有不相同的地方,它們的編程模型也不相同。圖3-1給出了典型的UDP客戶機/服務(wù)器程序的函數(shù)調(diào)用模型。圖3-1UDP客戶機/服務(wù)器程序的編程模型
在前面的章節(jié)中,已經(jīng)介紹了基于TCP套接字編程的函數(shù)調(diào)用模型,比較TCP和UDP編程模型可以看出,UDP協(xié)議不需要事先在客戶機、服務(wù)器程序之間建立連接。服務(wù)器端在調(diào)用socket函數(shù)生成一個UDP套接字后,利用bind函數(shù)將套接字與本地IP、端口號綁定,然后,服務(wù)器端調(diào)用recvfrom函數(shù)等待接收由客戶端發(fā)送來的數(shù)據(jù)??蛻魴C首先調(diào)用socket函數(shù)創(chuàng)建一個UDP套接字,然后利用sendto函數(shù)將請求包發(fā)送至服務(wù)器端;服務(wù)器端收到請求包后,根據(jù)請求進行處理,調(diào)用sendto函數(shù)將處理結(jié)果作為應(yīng)答數(shù)據(jù)發(fā)送給客戶機??蛻魴C調(diào)用recvfrom函數(shù)接收服務(wù)器端發(fā)送來的應(yīng)答數(shù)據(jù)。當(dāng)通信結(jié)束后,客戶機調(diào)用close函數(shù)關(guān)閉UDP套接字,而服務(wù)器端可以保留已建立的UDP套接字繼續(xù)與其他客戶機進行數(shù)據(jù)通信。3.2UDP套接字編程 3.1節(jié)中已介紹了基于UDP協(xié)議網(wǎng)絡(luò)編程的一般模型,其中涉及到了在套接字編程中曾簡單介紹過的數(shù)據(jù)報發(fā)送和接收函數(shù)sendto和recvfrom,下面我們列出可用于數(shù)據(jù)報發(fā)送、接收的高級套接字函數(shù)。這些函數(shù)是:
#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套接字在通信中發(fā)送和接收的數(shù)據(jù)是以數(shù)據(jù)報為單位的。當(dāng)應(yīng)用程序調(diào)用函數(shù)sendto發(fā)送數(shù)據(jù)時,首先應(yīng)將數(shù)據(jù)封裝生成一個UDP數(shù)據(jù)報,然后發(fā)送;當(dāng)應(yīng)用程序調(diào)用函數(shù)recvfrom接收數(shù)據(jù)時。UDP協(xié)議將返回一個完整的UDP數(shù)據(jù)報數(shù)據(jù)內(nèi)容。在使用UDP套接字進行編程時,我們必須注意以下幾個問題:
(1)?UDP套接字在發(fā)送數(shù)據(jù)時不會因發(fā)送緩沖區(qū)而出現(xiàn)阻塞。UDP協(xié)議沒有專門為UDP套接字設(shè)置發(fā)送緩沖區(qū),當(dāng)應(yīng)用程序通過調(diào)用函數(shù)sendto來發(fā)送數(shù)據(jù)時,該函數(shù)將要發(fā)送的數(shù)據(jù)從用戶緩沖區(qū)拷貝到系統(tǒng)緩沖區(qū),然后返回。UDP協(xié)議進一步把數(shù)據(jù)封裝成一個UDP數(shù)據(jù)報,然后將這個UDP數(shù)據(jù)報傳送給低層的IP協(xié)議,從而完成UDP數(shù)據(jù)報的發(fā)送任務(wù)。UDP協(xié)議是不可靠的協(xié)議,它沒有必要保留已經(jīng)發(fā)送的UDP數(shù)據(jù)報內(nèi)容。所以,UDP套接字只有一個發(fā)送緩沖區(qū)大小,而這個大小就是可以發(fā)送的UDP數(shù)據(jù)報的最大長度。如果應(yīng)用程序發(fā)送的數(shù)據(jù)量大于這個限制值,函數(shù)sendto將以錯誤返回,錯誤類型是EMSGSIZE。UDP套接字的發(fā)送緩沖區(qū)大小是不會發(fā)生變化的,所以,只要應(yīng)用程序保證調(diào)用函數(shù)sendto發(fā)送的數(shù)據(jù)量小于這個限制值,發(fā)送操作總能夠成功。因此,應(yīng)用程序使用UDP套接字發(fā)送數(shù)據(jù)時,不會因發(fā)送緩沖區(qū)而出現(xiàn)阻塞。圖3-2UDP套接字接收緩沖區(qū)
UDP套接字的接收緩沖區(qū)的大小是有限制的,當(dāng)接收到新的UDP數(shù)據(jù)報時,如果這個UDP套接字的接收緩沖區(qū)隊列已經(jīng)滿了,那么UDP協(xié)議將丟棄這個數(shù)據(jù)報,并且不向發(fā)送方返回任何錯誤信息。這種操作也是由UDP協(xié)議不保證接收數(shù)據(jù)的可靠性的特點所決定的。
(3)?UDP服務(wù)器采用循環(huán)服務(wù)器的工作方式,不會被某一個客戶機獨占,但客戶機可能被阻塞。UDP通信模式中,服務(wù)器一般采用循環(huán)服務(wù)器工作模式。在服務(wù)器與客戶機之間不需要建立連接,UDP服務(wù)器能夠交替地處理來自多個客戶機的請求,這就意味著服務(wù)器在前后兩次循環(huán)處理的請求可以是不同客戶機的請求,任何一個客戶機都無法獨占服務(wù)器。 (4)發(fā)送數(shù)據(jù)時需指定接收方的地址。UDP套接字是面向無連接的套接字的,所以在套接字?jǐn)?shù)據(jù)結(jié)構(gòu)中不會保存接收方的IP地址及其端口號。如果應(yīng)用程序要發(fā)送數(shù)據(jù),就需要在調(diào)用發(fā)送函數(shù)sendto的同時指定接收方的地址。當(dāng)應(yīng)用程序接收數(shù)據(jù)報時,如果需要知道發(fā)送者的地址,則可以在調(diào)用接收函數(shù)recvfrom中提供空間由內(nèi)核來填充;如果不關(guān)心對方的地址,則可以將函數(shù)recvfrom的參數(shù)from設(shè)置為空指針NULL,同時也必須將參數(shù)addrlen設(shè)置為NULL。
(5)在需要多點傳送數(shù)據(jù)時,使用UDP套接字。目前,TCP協(xié)議不支持多點傳送數(shù)據(jù),因為,如果要使用TCP協(xié)議進行多點傳送的話,就必須要為每一個傳送建立一個連接。所以,在需要多點傳送數(shù)據(jù)時就往往采用UDP協(xié)議。 /**********************函數(shù)udps_respon負(fù)責(zé)處理數(shù)據(jù)通信***********************/ voidudps_respon(intsockfd) { intn; charmsg[1024]; intaddrlen; structsockaddr_inaddr; for(;;) { n=recvfrom(sockfd,msg,1024,0,(struct sockaddr*)&addr,&addrlen); /*
響應(yīng)客戶機請求
*/ sendto(sockfd,msg,n,0,addr,addrlen); } } /******************************以下為主程序部分*******************************/ intmain(intargc,char*argv[]) { intsockfd; structsockaddr_inaddr; /*
創(chuàng)建一個UDP數(shù)據(jù)報類型的套接字
*/ 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);
/*
服務(wù)器為套接字綁定一個端口號
*/ if(bind(sockfd,(structsockaddr*)&addr,sizeof(addr))<0) { fprintf(stderr,"Binderror"); exit(1); } /*
調(diào)用通信函數(shù)與客戶端進行通信
*/ udps_respon(sockfd); /*
關(guān)閉套接字
*/ close(sockfd); }
這是一個簡單的UDP服務(wù)器,它不需要在通信前與客戶機建立固定連接,直接使用UDP套接字來接收客戶機發(fā)送的UDP數(shù)據(jù)報。但是,在接受客戶機請求前,服務(wù)器必須設(shè)置自己的公認(rèn)的地址和端口號。它一次只能處理一個客戶機的請求,而不能并發(fā)的處理多個客戶機的請求,所以它是一個典型的循環(huán)服務(wù)器。它不被一個客戶端所獨立占有,能夠交替處理多個客戶機的請求,當(dāng)一個客戶機出現(xiàn)錯誤時不會影響服務(wù)器對來自其他客戶機請求的處理。 3.2.2UDP客戶機編程示例 客戶機:
#include<sys/type.h> #include<sys/socket.h> #include<netinet/in.h> #include<stdio.h> #defineSERVER_PORT8080 /**********************函數(shù)udps_requ負(fù)責(zé)處理數(shù)據(jù)通信*************************/ voidudpc_requ(intsockfd,conststructsockaddr_in*addr,intlen) { charbuf[1024]; intn; for(;fgets(buf,1024,stdin)!=NULL;) { /*
向服務(wù)器端發(fā)送數(shù)據(jù)
*/ sendto(sockfd,buf,strlen(buf),0,addr,len); /*
接收服務(wù)器端的回應(yīng)
*/ 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); } /*
創(chuàng)建一個UDP數(shù)據(jù)報類型的套接字
*/ sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { fprintf(stderr,"Socketerror"); exit(1); }
/*
調(diào)用通信函數(shù)進行數(shù)據(jù)通信
*/ udpc_requ(sockfd,&addr,sizeof(addr)); /*
關(guān)閉套接字
*/ close(sockfd); }
這是一個簡單的UDP客戶端程序。由于采用面向無連接的通信模式,因此它不需要跟服務(wù)器端建立連接,直接在函數(shù)sendto中指定服務(wù)器端的地址,調(diào)用sendto函數(shù)向服務(wù)器發(fā)送數(shù)據(jù)。同時,可以接收來自服務(wù)端的應(yīng)答數(shù)據(jù)報。當(dāng)數(shù)據(jù)傳輸發(fā)生錯誤時,服務(wù)端不會有阻塞的危險,但是客戶端可能會因為數(shù)據(jù)報在傳輸過程中的丟失而在調(diào)用函數(shù)recvfrom處阻塞。因為UDP協(xié)議不能夠保證數(shù)據(jù)可靠到達,所以,對于可能遇到的問題或錯誤,用戶應(yīng)在程序中加以處理。3.3連接UDP套接字的功能
1.連接UDP套接字的建立
UDP套接字也可以調(diào)用connect函數(shù),調(diào)用的方法和流式套接字相同,但其調(diào)用的結(jié)果與流式套接字調(diào)用connect函數(shù)的結(jié)果不同。它沒有三次握手過程,因為UDP協(xié)議中不需要在發(fā)送和接收方之間建立連接。UDP套接字只是記錄了目的方的IP地址和端口號,這些信息被包含在調(diào)用connect函數(shù)的套接字中,并在調(diào)用后立即返回給進程。我們把調(diào)用connect函數(shù)后的UDP套接字稱為連接UDP套接字,把未調(diào)用connet函數(shù)的UDP套接字稱為未連接UDP套接字。 UDP套接字調(diào)用connect函數(shù)后,將檢查每個到達的數(shù)據(jù)報,UDP協(xié)議將數(shù)據(jù)報中的目的地址與connect函數(shù)的套接字中保存的IP地址進行比較,二者一致時該套接字接收這個數(shù)據(jù)報,反之丟棄這個數(shù)據(jù)報。UDP連接套接字具有以下一些特點。
(1)由于連接套接字已經(jīng)記錄了該套接字相應(yīng)的目的地址,因此發(fā)送數(shù)據(jù)時可以不用指定服務(wù)器的目的地址。UDP協(xié)議將自動根據(jù)保存的地址填充要發(fā)送的UDP數(shù)據(jù)報。
(2)對于連接套接字,UDP協(xié)議在內(nèi)核中檢查連接套接字收到的數(shù)據(jù)報,并使得連接套接字只接收那些來自目的地址的UDP數(shù)據(jù)報。 UDP協(xié)議檢查每個到達的數(shù)據(jù)報,根據(jù)數(shù)據(jù)報的目的端口號選擇接收套接字,UDP協(xié)議檢查該套接字是否是連接套接字。如果這個套接字為UDP未連接套接字,則協(xié)議將數(shù)據(jù)報存放在該套接字接收緩沖區(qū)隊列中。如果這個套接字為UDP連接套接字,則協(xié)議將數(shù)據(jù)報的目的IP地址與套接字保存的IP地址比較,只有在相同時,才將數(shù)據(jù)報存放在套接字的接受緩沖區(qū)隊列中,否則,丟棄。因此,當(dāng)UDP客戶機只與一個服務(wù)器通信時,調(diào)用函數(shù)connect,將這個UDP套接字轉(zhuǎn)化為UDP連接套接字,保證只接收這個服務(wù)器的信息。 2.?dāng)?shù)據(jù)報發(fā)送以及錯誤返回情況
UDP協(xié)議在進行數(shù)據(jù)報通信時,可能會有以下幾種情況發(fā)生:
(1)數(shù)據(jù)報成功到達服務(wù)器端,并且被服務(wù)器接收。
(2)數(shù)據(jù)報成功到達服務(wù)器端,但是服務(wù)器端的UDP套接字接收緩沖區(qū)已滿,此時服務(wù)器端將自動丟棄這個數(shù)據(jù)報,并且不向客戶機返回任何錯誤信息。 (3)數(shù)據(jù)報成功到達服務(wù)器端,但是數(shù)據(jù)報指定的目的端口上沒有接收此數(shù)據(jù)報的進程。此時服務(wù)器端上的UDP協(xié)議將丟棄這個數(shù)據(jù)報,并且向客戶機返回一個ICMP錯誤報文,通知客戶機在目的端口上沒有接收進程。當(dāng)這個ICMP消息到達客戶機UDP協(xié)議之后,UDP協(xié)議將向客戶機報告這個錯誤。我們調(diào)用函數(shù)sendto發(fā)送數(shù)據(jù),該函數(shù)只要將數(shù)據(jù)報發(fā)送至目的系統(tǒng)的緩沖區(qū)就完成調(diào)用返回,而ICMP錯誤報文是在sendto()函數(shù)調(diào)用完成之后返回的,錯誤的產(chǎn)生和發(fā)現(xiàn)時間是不一致的,所以該錯誤被稱為異步錯誤。對于UDP套接字,當(dāng)這個錯誤到達時,如果客戶機正在進行系統(tǒng)調(diào)用,則系統(tǒng)將返回一個ECONNRESET類型的錯誤。對于未連接UDP套接字,Linux系統(tǒng)也返回ECONNRESET。 (4)數(shù)據(jù)報未成功到達服務(wù)器。這種情況又分為兩種:①
如果數(shù)據(jù)報因為目的地址不可到達而在網(wǎng)絡(luò)中被丟棄,并且這個數(shù)據(jù)報的傳送經(jīng)過了路由器,那么傳送路徑上的某個路由器將向客戶機UDP協(xié)議返回ICMP錯誤消息,通知發(fā)送端目的地址不可到達??蛻魴CUDP協(xié)議接收到這個ICMP錯誤消息之后,如果客戶機正在進行系統(tǒng)調(diào)用,則這個系統(tǒng)調(diào)用將以ENETUNREACH或EHOSTUNREACH類型的錯誤返回。②
如果數(shù)據(jù)報在網(wǎng)絡(luò)中傳輸時,由于字節(jié)發(fā)生錯誤而被路由器丟棄,或者由于路由器的緩沖區(qū)滿,該數(shù)據(jù)報也將被丟棄,而且發(fā)送端也不會得到任何返回的錯誤信息。
在進行基于UDP套接字編程時,我們應(yīng)根據(jù)實際情況去選擇使用連接UDP套接字還是未連接UDP套接字。通??蛻魴C進程只與一個服務(wù)器進程通信時,使用連接套接字比較方便,這樣可以避免服務(wù)器端接收來自其他客戶端的UDP數(shù)據(jù)報。當(dāng)服務(wù)器進程需要同多個客戶端進程進行數(shù)據(jù)報通信,并且是循環(huán)服務(wù)的方式時,使用未連接套接字。這是一般采用的原則,最終采用什么方法還應(yīng)根據(jù)具體要求加以變通。 3.連接UDP套接字的取消 連接UDP套接字的取消與TCP套接字的關(guān)閉不同,它不像后者有專門的close套接字函數(shù)來關(guān)閉連接,連接UDP套接字只需再次調(diào)用connect函數(shù),使用一個非法的套接字地址對這個套接字調(diào)用connect函數(shù),執(zhí)行此調(diào)用,connect函數(shù)套接字將會丟棄原來保存的地址。設(shè)置套接字地址的地址簇為AF_UNSPEC,則connect函數(shù)調(diào)用以錯誤返回,其錯誤類型為EAFNOSUPPORT,表示UDP協(xié)議不支持AF_UNSPEC類型的套接字地址。這樣,這個UDP連接套接字的地址信息被清除,可以取消連接UDP套接字。
取消連接UDP套接字的操作如下:
structsockaddr_inaddr; intsockfd;
… addr.sin_family=AF_UNSPEC;
… connect(sockfd,(structsockaddr*)&addr,sizeof(addr));
對一個連接UDP套接字再次調(diào)用connect函數(shù),可以完成以下兩個任務(wù):
(1)斷開已連接套接字。
(2)指定新的IP地址和端口號,即創(chuàng)建一個新的連接。3.4UDP編程中的錯誤檢測及處理方法
1.UDP協(xié)議不保證數(shù)據(jù)報可靠到達 如果應(yīng)用程序要求實現(xiàn)傳送的UDP數(shù)據(jù)報可靠地到達接收方,我們必須在應(yīng)用程序中檢測并處理各種可能的錯誤。例如,采用數(shù)據(jù)重傳和超時重發(fā)來實現(xiàn):發(fā)送方保存需要發(fā)送的數(shù)據(jù)報,接收方應(yīng)用程序接收到數(shù)據(jù)報之后,向發(fā)送者返回一個確認(rèn)數(shù)據(jù)報,然后發(fā)送方才將這個數(shù)據(jù)報從緩沖區(qū)中釋放出去。因為發(fā)送的數(shù)據(jù)報和對方返回的確認(rèn)數(shù)據(jù)報都有可能丟失,所以,如果發(fā)送者在指定的時間內(nèi)沒有收到接收方的確認(rèn)信息,將重新發(fā)送這個數(shù)據(jù)報。
調(diào)用alarm()函數(shù)是最常用的超時控制方法,編程也比較簡單。因為信號可以中斷函數(shù)的阻塞,而alarm()函數(shù)可以在設(shè)定的時限到達時發(fā)出SIGALRM信號,所以我們可以在程序中捕獲SIGALRM信號,喚醒用戶進程作下一步的操作。在Linux內(nèi)核2.4~20,i386體系源程序代碼中,alarm()函數(shù)所對應(yīng)的系統(tǒng)調(diào)用函數(shù)名稱是sys_alarm(),在linux/kernel/timer.c中實現(xiàn),函數(shù)定義如下:
asmlinkageunsignedlongsys_alarm(unsignedintseconds)
注意,該函數(shù)的定時單位是秒,當(dāng)參數(shù)設(shè)為0時函數(shù)將取消定時操作。alarm()函數(shù)與進程相關(guān),而與文件描述符或者說是與輸入輸出通道無關(guān)。當(dāng)我們使用多個輸入輸出通道時,我們可能無法區(qū)分究竟在哪個通道上發(fā)生了阻塞。當(dāng)超時到達時,alarm()函數(shù)將發(fā)出信號SIGALRM,這個信號的默認(rèn)操作是終止進程,這就意味著如果我們不捕獲SIGALRM,我們的程序在阻塞一段時間后便會自動結(jié)束。當(dāng)我們捕獲了SIGALRM信號并處理后,一般情況下會給阻塞函數(shù)返回EINTR。 此外,還有一點需要引起注意:alarm()是一次性函數(shù),而不是周期性函數(shù)。
下面我們給出處理這種錯誤時的部分示例:
#include<signal.h> … voidsig_handler(); intmain() { inttimde_out,n;
structsigactionact; intsockfd; charmsg[100],buff[100]; structsockaddr_inaddr; /*
創(chuàng)建一個UDP數(shù)據(jù)報類型的套接字
*/ 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); /*
調(diào)用通信函數(shù)與客戶端進行通信
*/ strcpy(msg,"message"); sendto(sockfd,msg,sizeof(msg),0,addr,addrlen); for(;;) { timde_out=0; /*
設(shè)立信號機制,發(fā)送后等待20s后,若無返回,則認(rèn)為發(fā)送超時,重新發(fā)送
*/ alarm(20); /*
讀應(yīng)答
*/ n=recvfrom(sockfd,buf,size(buf),0,(structsockaddr*)&addr,&addrlen); if(n<0&&errno==EINTR) { if(timed_out) {printf("Servernotresponding,
retrying...\n"); /*重新發(fā)送數(shù)據(jù)
*/ proc_timeout(); }else continue;…}}/*for*/}/*main*/
voidhandler()/*信號處理函數(shù)*/{timed_out=1;};
proc_timeout(){/*
重新發(fā)送數(shù)據(jù)
*/};
由上面的程序可見,利用alarm()函數(shù)實現(xiàn)操作控制的方法比較簡單:首先設(shè)置信號SIGALRM的處理函數(shù),在調(diào)用讀函數(shù)之前,調(diào)用alarm()函數(shù)設(shè)置在超時到達時發(fā)送信號SIGALRM。如果讀函數(shù)被信號SIGALRM中斷,則表示超時到達。
2.UDP協(xié)議不保證數(shù)據(jù)報順序到達 因為UDP協(xié)議是面向無連接的,該協(xié)議不保證數(shù)據(jù)報能夠順序到達,這就意味著所有的UDP數(shù)據(jù)報并不能按照發(fā)送的先后順序到達接收方被處理。如果應(yīng)用程序要求數(shù)據(jù)報必須是按照順序到達并處理的,我們就要在設(shè)計UDP報文時對每個發(fā)送的數(shù)據(jù)報進行順序編號。接收方在接收到一個數(shù)據(jù)報之后,根據(jù)數(shù)據(jù)報中的數(shù)據(jù)序號將其放入緩沖區(qū)的合適的位置,符合時才處理這個數(shù)據(jù)報,否則等待接收順序靠前的數(shù)據(jù)報。
3.UDP協(xié)議沒有流量控制
UDP協(xié)議本身不提供流量控制功能,雖然UDP協(xié)議為每個套接字建立了一個接收緩沖區(qū)隊列,接收到的數(shù)據(jù)報被拷貝到該隊列中,但是如果在通信過程中數(shù)據(jù)報發(fā)送速度大于接收速度,當(dāng)套接字接收緩沖區(qū)滿后,UDP協(xié)議將丟棄之后到達的數(shù)據(jù)報,從而造成大量的數(shù)據(jù)報丟失。針對這種情況,我們可以在確保UDP數(shù)據(jù)報可靠到達的基礎(chǔ)上進行如下的流量控制: 發(fā)送方應(yīng)用程序創(chuàng)建一個發(fā)送緩沖區(qū)隊列,每發(fā)送一個數(shù)據(jù)報,首先將它拷貝到該隊列中,然后再發(fā)送給接收方。每個數(shù)據(jù)報在接收到接收方返回的確認(rèn)信息前將一直保存在這個緩沖區(qū)隊列中。如果發(fā)送緩沖區(qū)隊列已滿,則暫停發(fā)送新的數(shù)據(jù)報,直到緩沖區(qū)隊列再次出現(xiàn)空間為止。通過這種方法可以實現(xiàn)簡單的流量控制。3.5UDP套接字在OICQ服務(wù)中的應(yīng)用 UDP協(xié)議經(jīng)常使用于語音、圖像、文字等格式的數(shù)據(jù)傳輸中,很多即時通信程序都使用UDP套接字編程來實現(xiàn)數(shù)據(jù)通信,例如,目前使用的最廣泛的聊天程序OICQ。本節(jié)我們將給出兩個代碼片段,來分別說明OICQ服務(wù)器端和客戶機程序的基本實現(xiàn)原理。
先來看實現(xiàn)發(fā)送數(shù)據(jù)的客戶端程序:
#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;
/*
創(chuàng)建一個數(shù)據(jù)報類型的套接字
*/ 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; /*
發(fā)送數(shù)據(jù)
*/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); }
服務(wù)器端程序:
#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);}
/*
接收數(shù)據(jù)
*/ 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);}
上面兩個程序經(jīng)編譯調(diào)試通過后,在一臺主機分別運行接收程序和發(fā)送程序,就可以看到通過UDP協(xié)議的數(shù)據(jù)通信過程了。當(dāng)然,也可以在兩臺機器上啟動兩個不同的進程來運行,只要修改上述程序中的IP地址,就可以實現(xiàn)客戶機和服務(wù)器的通信模擬。3.6原始套接字 3.6.1
原始套接字定義 使用流式套接字或數(shù)據(jù)報套接字,應(yīng)用程序可以實現(xiàn)基于TCP協(xié)議或UDP協(xié)議的數(shù)據(jù)交互。這種交互屬于比較高層次的網(wǎng)絡(luò)通信方式,它向程序員屏蔽了TCP、UDP和IP數(shù)據(jù)包的具體格式,簡化了編程工作;但同時也限制了應(yīng)用程序?qū)νㄐ艆f(xié)議的支持范圍,降低了用戶對數(shù)據(jù)的操作能力,影響了編程的靈活性。原始套接字則支持我們直接對IP數(shù)據(jù)包進行操作,可以允許用戶訪問ICMP和IGMP等多種協(xié)議的數(shù)據(jù)包,允許用戶訪問內(nèi)核不處理的IP數(shù)據(jù)包,允許用戶讀寫包括首部在內(nèi)的IP數(shù)據(jù)包,允許用戶基于IP層開發(fā)新的高層通信協(xié)議。
原始套接字的使用分為三個步驟:原始套接字的創(chuàng)建、屬性的設(shè)置以及數(shù)據(jù)的發(fā)送和接收。
1.原始套接字的創(chuàng)建 將函數(shù)socket(intdomain,inttype,intprotocol)中的參數(shù)type設(shè)為SOCK_RAW,并將參數(shù)protocol設(shè)為某種指定類型(如IPPROTO_ICMP、IPPROTO_IGMP、IPPROTO_IP等),我們就可以創(chuàng)建一個原始套接字。
#include<linux/in.h> ... intsockfd sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); ....
標(biāo)準(zhǔn)的協(xié)議類型通常由系統(tǒng)頭文件<linux/in.h>定義,在Linux2.4.20內(nèi)核中,它們位于第23~47行。protocol等于IPPROTO_ICMP,表示這是一個使用ICMP協(xié)議工作的原始套接字;protocol等于IPPROTO_IGMP,表示這是一個使用IGMP協(xié)議工作的原始套接字;protocol等于IPPROTO_IP,表示這個原始套接字可以接收內(nèi)核送達的任何類型的IP數(shù)據(jù)包。 原始套接字直接發(fā)送和接收IP數(shù)據(jù)包,是一種面向無連接的套接字,而且它只能由超級用戶或系統(tǒng)管理員來創(chuàng)建。
無論是否設(shè)置了IP_HDRINCL屬性,原始套接字接收的都是整個IP數(shù)據(jù)包,即我們的接收緩存區(qū)中的數(shù)據(jù)包含IP數(shù)據(jù)包的首部。
3.?dāng)?shù)據(jù)發(fā)送和接收 端口是TCP、UDP等傳輸層協(xié)議中的概念,在原始套接字中不存在端口。原始套接字通過IP地址來識別主機??梢允褂胹endto()、sendmsg()、recvfrom()和recvmsg()函數(shù),來收發(fā)數(shù)據(jù)包;也可以先調(diào)用connect()、bind()函數(shù)綁定對方或本地地址,然后再使用write()、writev()、send()、read()、readv()和recv()等函數(shù)來收發(fā)數(shù)據(jù)。 2.原始套接字屬性的設(shè)置
IP數(shù)據(jù)包由首部和數(shù)據(jù)實體組成,如果沒有對原始套接字設(shè)置IP_HDRINCL屬性,則在發(fā)送數(shù)據(jù)時,數(shù)據(jù)緩存區(qū)中存放的是IP數(shù)據(jù)包的數(shù)據(jù)實體部分;如果設(shè)置了IP_HDRINCL屬性,則數(shù)據(jù)發(fā)送緩存區(qū)中存放的是整個IP數(shù)據(jù)包,包括IP數(shù)據(jù)包的首部。IP_HDRINCL是通過調(diào)用函數(shù)setsockopt()來進行設(shè)置的:
intoptval=1; if(setsockopt(sockfd,
IPPROTO_IP,
IP_HDRINCL,
&optval,
sizeof(optval))<0) exit(1); … 3.6.2ICMP協(xié)議中原始套接字的應(yīng)用 在第1章中,我們已經(jīng)知道ICMP(Internet消息控制協(xié)議)是TCP/IP協(xié)議簇的一個組成部分,主要作用是通過傳遞網(wǎng)絡(luò)故障、網(wǎng)絡(luò)擁塞、路由錯誤、中間主機崩潰及重啟等信息,來協(xié)調(diào)路由器、源主機和中間主機之間的工作。 使用ICMP協(xié)議通信時,一般不設(shè)置IP_HDRINCL選項,在數(shù)據(jù)發(fā)送緩存區(qū)僅僅填寫ICMP數(shù)據(jù)包,不需要考慮IP首部;但在接收數(shù)據(jù)時,接收緩存區(qū)內(nèi)存放的是IP首部加ICMP數(shù)據(jù)包,所以首先必須找到ICMP數(shù)據(jù)包的起始位置,然后才能取出ICMP數(shù)據(jù)包進行處理。通常我們采用結(jié)構(gòu)來讀寫ICMP消息的固定長度部分,包括描述IP數(shù)據(jù)包首部的結(jié)構(gòu)和描述ICMP數(shù)據(jù)包首部的結(jié)構(gòu)。我們可以自行定義這些結(jié)構(gòu),也可以采用系統(tǒng)頭文件中的結(jié)構(gòu)定義:iphdr和icmphdr。 iphdr描述了IP數(shù)據(jù)包的首部,在Linux2.4.20內(nèi)核中,它們位于<linux/in.h>文件的第116~136行,其代碼及注釋如下:
structiphdr{ #ifdefined(__LITTLE_ENDIAN_BITFIELD)__u8ihl:4,
/*
首部長度,以4字節(jié)為單 位進行計量
*/version:4; /*
版本
*/ #elifdefined(__BIG_ENDIAN_BITFIELD)__u8version:4,
ihl:4; #else #error"Pleasefix<asm/byteorder.h>" #endif__u8tos; /*
服務(wù)類型
*/__u16tot_len; /*
數(shù)據(jù)包總長
*/__u16id; /*
標(biāo)識
*/__u16frag_off; /*
標(biāo)識位和碎片偏移
*/__u8ttl; /*
生存時間(timetolive)*/ __u8protocol;/*
協(xié)議:TCP、UDP、ICMP等
*/__u16check; /*
首部校驗和
*/__u32saddr; /*
源IP地址
*/__u32daddr; /*
目的IP地址
*/ }; icmphdr描述了ICMP數(shù)據(jù)包的首部,在Linux2.4.20內(nèi)核中,它們位于<linux/icmp.h>文件的第66~81行,其代碼如下: structicmphdr{ __u8type; __u8code;__u16checksum;union{struct{__u16id;__u16sequence;}echo; __u32gateway;struct{__u16__unused;__u16mtu;}frag;}un; };
下面我們以一段Ping程序代碼片斷為例,來說明 如何使用原始套接字實現(xiàn)ICMP協(xié)議的數(shù)據(jù)交互。在 這個例子中,源主機向目的主機發(fā)出回顯請求(ICMP_ECHO,type=0,code=0),目的主機返回回顯響應(yīng)(ICMP_ECHOREPLY,type=8,code=0),相關(guān)的數(shù)據(jù)包格式如圖3-3所示。其中,標(biāo)識符是源主機的進程號,序列碼用來標(biāo)識發(fā)出回顯請求的次序,時間戳表示數(shù)據(jù)包發(fā)出的時刻,通過比較回顯響應(yīng)時刻和源主機當(dāng)前時刻的差值,可以測出ICMP數(shù)據(jù)包的往返時間。在這個例子中,用戶進程一共向目的主機發(fā)送三次回顯請求。圖3-3ICMP回顯請求和響應(yīng)的?數(shù)據(jù)包格式#include<linux/in.h>...intnTimeout=0; /*
超時標(biāo)志
*/voidrecv_process(char*buf);/*
處理接收到的數(shù)據(jù)
*/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數(shù)據(jù)包
*/{ 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; /*
設(shè)置超時信號捕獲函數(shù)
*/act.sa_mask=0;act.sa_flags=0;sigaction(SIGALRM,&act,NULL);sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); /*
創(chuàng)建原始套接字
*/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); /*
設(shè)置超時值為5s*/ gettimeofday(&tv,NULL); /*
獲得當(dāng)前時間戳
*/
*(buf+2)=htonl(tv.sec);
*(buf+3)=htonl(tv.usec); FillIcmpHdr((char*)buf,8); /*
填寫數(shù)據(jù)包,按雙字節(jié)計算數(shù)據(jù)長度
*/ 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); /*
處理接收到的數(shù)據(jù)
*/ alarm(0); nTimeout=0;n++;}close(sockfd);}
函數(shù)recv_process()用來處理對方主機返回的回顯響應(yīng)數(shù)據(jù)包,這個數(shù)據(jù)包內(nèi)含有IP數(shù)據(jù)首部,必須將其過濾掉。recv_process()的主要代碼如下:
voidrecv_process(char*buf) /*
填充ICMP數(shù)據(jù)包
*/ { iphdr*pIph; icmphdr*pIcmph; intiplen,icmplen; pIph=(iphdr*)buf; /*
確定IP數(shù)據(jù)包起始位置
*/ iplen=ip->ihl<<2; pIcmph=(icmphdr*)(buf+iplen); /*
確定ICMP數(shù)據(jù)包起始位置
*/ /*
確定收到的數(shù)據(jù)包是否是針對當(dāng)前進程的回顯響應(yīng)
*/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); /*
編制函數(shù),計算時間差值
*/ ... /*
顯示接收結(jié)果
*/} IGMP(Internet組管理協(xié)議)是另一種最常使用原始套接字進行操作的協(xié)議,這個協(xié)議用來協(xié)調(diào)多播路由器與主機之間的工作:多播路由器使用IGMP協(xié)議來查詢多播組內(nèi)有哪些主機;主機則在加入和退出多播組時使用IGMP協(xié)議向路由器發(fā)出通告,或者使用IGMP協(xié)議響應(yīng)多播路由器的查詢。 與ICMP協(xié)議類似,IGMP數(shù)據(jù)包也是嵌入在IP數(shù)據(jù)包內(nèi)進行傳輸?shù)模覀兛梢酝ㄟ^調(diào)用sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_IGMP)創(chuàng)建一個支持協(xié)議的原始套接字。IGMP協(xié)議中原始套接字的使用方法與ICMP協(xié)議中原始套接字使用方法基本一致,只是協(xié)議的具體格式有所差別,其編程方法可參閱前面有關(guān)套接字編程的例子。 3.6.3IP_HDRINCL選項 當(dāng)對原始套接字設(shè)置了IP_HDRINCL屬性后,我們就可以對IP數(shù)據(jù)包的首部進行操作,可以對封裝在IP數(shù)據(jù)包內(nèi)的任何協(xié)議(如TCP、UDP等)進行操作,也可以定制自己的協(xié)議首部。在創(chuàng)建原始套接字時,應(yīng)將協(xié)議類型設(shè)為IPPROTO_IP(值為0):
intsockfd sockfd=socket(AF_INET,SOCK_RAW,
IPPROTO_IP);
下面是一個構(gòu)造TCP數(shù)據(jù)包的函數(shù),其中用到的tcphdr結(jié)構(gòu)由linux2.4.20內(nèi)核頭文件<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數(shù)據(jù)首部
*/ pTcph->dest=port; pTcph->seq=random(); pTcph->doff=5; pTcph->syn=1; pIph->version=4; /*
填寫IP數(shù)據(jù)首部
*/ 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); /*
計算整個數(shù)據(jù)包的校驗和
*/ return*(pIph->tot_len); /*
返回需要發(fā)送的數(shù)據(jù)總長
*/ }
主程序可以不斷地調(diào)用這個函數(shù),然后向服務(wù)器發(fā)送。這將引發(fā)用戶進程與服務(wù)器的三次握手過程(syn=1)。由于用戶進程送出的源地址是一個隨機數(shù),幾乎全都是不可達地址,因此三次握手將無法完成。服務(wù)器由于偵聽套接字的連接隊列滿而被阻塞,從而引起服務(wù)失效。3.7服務(wù)器編程模型
3.7.1循環(huán)服務(wù)器
1.UDP循環(huán)服務(wù)器 基于UDP的編程常采用循環(huán)服務(wù)器的編程模式,循環(huán)服務(wù)器的實現(xiàn)較為簡單:UDP服務(wù)器每次從套接字上讀取一個客戶端的請求并處理請求,然后將結(jié)果返回給客戶機。循環(huán)服務(wù)器的處理過程如圖3-4所示。圖3-4循環(huán)服務(wù)器處理過程
循環(huán)服務(wù)器的程序處理方法為: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的通信是面向無連接的,因此沒有一個客戶機可以獨占服務(wù)器,只要處理過程不是死循環(huán),服務(wù)器對于每一個客戶機的請求總是能夠滿足的。 圖3-5為一個小型控制系統(tǒng)示例,所用計算機都工作在端口PORT1上,計算機1負(fù)責(zé)從工業(yè)現(xiàn)場采集數(shù)據(jù),對原始數(shù)據(jù)進行初步處理(濾波、去偽等)后原始發(fā)送給計算機2(服務(wù)器),計算機2對數(shù)據(jù)進行處理(濾波、修正、轉(zhuǎn)換等),然后將結(jié)果以廣播形式發(fā)送出去。收到結(jié)果數(shù)據(jù)后,計算機1和計算機2不作處理,計算機3判斷是否需要報警,計算機4將結(jié)果送去顯示,計算機5將數(shù)據(jù)存檔。如何區(qū)分?jǐn)?shù)據(jù)來源則由應(yīng)用層的數(shù)據(jù)協(xié)議來保證,例如,每個數(shù)據(jù)包中都標(biāo)明數(shù)據(jù)包類型是原始數(shù)據(jù)、結(jié)果數(shù)據(jù),還是其他數(shù)據(jù)。圖3-5一個小型控制系統(tǒng)示例
計算機2的程序代碼如下:#include<...>#definePORT18989#defineCOMPUTERX"55"...
intmain(){charbuf[256];intsockfd;structsockaddr_inaddr_2;/*
計算機2的地址結(jié)構(gòu)
*/structsockaddr_inaddr_x;/*
廣播地址結(jié)構(gòu)
*/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; /*
用來存放數(shù)據(jù)包類型代碼
*/ intn=recvfrom(sockfd,buf,256,0,(structsockaddr*)&addr_1,sizeof(addr_1)); ... /*
解析接收到的數(shù)據(jù)包,并將數(shù)據(jù)包類型代碼放入nDataComeFrom*/
if(nDataComeFrom==1){process(buf,&nlen...); /*
數(shù)據(jù)處理,結(jié)果放在buf中,數(shù)據(jù)長度放在nlen中
*//*
以廣播形式將處理結(jié)果數(shù)據(jù)發(fā)送出去
*/sendto(sockfd,buf,nlen,0,(structsockaddr*)&addr_x,sizeof(addr_x));}close(sockfd);} 2.TCP循環(huán)服務(wù)器 現(xiàn)在我們考慮基于TCP的服務(wù)器采用循環(huán)服務(wù)器工作模式的實現(xiàn)方法。TCP服務(wù)器接受一個客戶端的連接,然后進行處理,直到完成這個客戶機的所有請求后,斷開連接。TCP循環(huán)服務(wù)器一次只能處理一個客戶端的請求,只有在這個客戶的所有請求都滿足后,服務(wù)器才可以繼續(xù)后面的請求。這樣,如果有一個客戶端占住服務(wù)器不放時,其他的客戶機都不能工作了,因此,TCP服務(wù)器一般很少用循環(huán)服務(wù)器模型。只有當(dāng)數(shù)據(jù)處理工作所需時間很短(如時鐘服務(wù))或者服務(wù)器只能為單一用戶提供服務(wù)時才被使用,而且這時應(yīng)該設(shè)置超時控制。
圖3-6是一個使用TCP循環(huán)服務(wù)的例子,這是一個通過網(wǎng)絡(luò)控制的可移動機械手,服務(wù)器程序運行在機械手的移動平臺上,用戶可以通過網(wǎng)絡(luò)遠程操控機械手完成搬運物體的工作,這個機械手不允許多人同時操作。若采用UDP協(xié)議來實現(xiàn)這個系統(tǒng),由于UDP協(xié)議的不可靠性,很可能出現(xiàn)后發(fā)出的指令被首先執(zhí)行,先發(fā)出的指令被延后執(zhí)行的狀況,用戶將感到機械手不可控制。當(dāng)然,我們也可以在應(yīng)用層的協(xié)議里加上時序控制,但那樣將加大編程的難度。而采用TCP的循環(huán)模式,并對阻塞函數(shù)添加超時控制,情況將會比較理想。有關(guān)TCP循環(huán)服務(wù)的程序代碼,可參見前面套接字的內(nèi)容,這里不再贅述。圖3-6一個使用TCP循環(huán)服務(wù)的例子
3.7.2并發(fā)服務(wù)器 針對上述TCP循環(huán)服務(wù)器的缺陷,人們提出了并發(fā)服務(wù)器的編程模型。并發(fā)服務(wù)器的思想是:每一個客戶機的請求并不由服務(wù)器偵聽進程直接處理,而是由服務(wù)器偵聽進程創(chuàng)建一個自己的子進程負(fù)責(zé)處理服務(wù)請求,父進程仍負(fù)責(zé)偵聽客戶機的請求。并發(fā)服務(wù)器的處理流程如圖3-7所示。圖3-7并發(fā)服務(wù)器的處理流程
1.TCP并發(fā)服務(wù)器 基本的TCP并發(fā)服務(wù)器采用這樣的流程:先創(chuàng)建一個偵聽套接字,等待客戶機的請求,每當(dāng)接受一個客戶機請求時,就創(chuàng)建一個子進程,并在子進程中進行數(shù)據(jù)處理,父進程則繼續(xù)等待新的客戶機請求,直到服務(wù)程序滿足退出條件。TCP并發(fā)服務(wù)器的程序處理方法為:
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并發(fā)服務(wù)器
UDP協(xié)議雖然在套接字函數(shù)處不大容易阻塞,但如果對某一個客戶機進行數(shù)據(jù)處理的時間過長,則服務(wù)器在這段時間內(nèi)將不能接收其他客戶機的請求,同時UDP協(xié)議又是不可靠的通信協(xié)議,不保證數(shù)據(jù)是否能夠到達目的地址,所以就會造成數(shù)據(jù)包的丟失。這時可以采用并發(fā)的UDP服務(wù)結(jié)構(gòu):服務(wù)程序每收到一項請求,就單獨為這個客戶機創(chuàng)建一個進程,完成相應(yīng)的數(shù)據(jù)處理任務(wù),然后關(guān)閉套接字描述符。UDP并發(fā)服務(wù)器處理方法如下: #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) /*
子進程,處理數(shù)據(jù)并返回結(jié)果
*/{sendto(sockfd,buf,256,0,(structsockaddr*)&addr,sizeof(addr));close(sockfd);exit(0);}elseif(pid<0){fprintf(stderr,"forkerror");exit(1);}/*
父進程,繼續(xù)循環(huán)等待客戶請求
*/}}
創(chuàng)建子進程是一項開銷很大的工作,如果客戶非常多,而大多數(shù)的數(shù)據(jù)處理的時間又很短,那么服務(wù)器的大量時間和資源都消耗在創(chuàng)建和銷毀子進程上,系統(tǒng)效率會很低,也難以及時響應(yīng)客戶的請求。我們可以將循環(huán)服務(wù)和并發(fā)服務(wù)進行適當(dāng)?shù)鼐C合,采用延遲創(chuàng)建子進程的方法,即平時服務(wù)器工作在循環(huán)狀態(tài),當(dāng)預(yù)測某一次服務(wù)耗時較長就為它創(chuàng)建一個子進程,而主進程繼續(xù)工作在循環(huán)模式中。
在網(wǎng)絡(luò)上,常常會有這樣的情況:很多非法用戶或沒有操作權(quán)限的用戶與服務(wù)器建立了連接,并試圖進行操作,這時服務(wù)器應(yīng)該先檢查收到的數(shù)據(jù)包是否為合法請求(這種檢查通常耗費時間極短)。如果是非法請求,服務(wù)器就拒絕服務(wù),則繼續(xù)在循環(huán)方式下工作;如果是合法請求,則服務(wù)器創(chuàng)建一個子進程進行數(shù)據(jù)處理,主進程依然在循環(huán)方式下工作。
我們還可以設(shè)置一個預(yù)測器,估計一項服務(wù)是否可以在很短時間內(nèi)完成。因為實際上服務(wù)器的服務(wù)范圍是有限的,可以根據(jù)理論知識和經(jīng)驗數(shù)據(jù)(即服務(wù)器在歷史上處理各類數(shù)據(jù)所消耗的時間)來進行推理,考慮是否創(chuàng)建子進程,如運算服務(wù),如果發(fā)現(xiàn)是解二元一次方程、求幾個數(shù)的平均值等,則所需的服務(wù)時間極短,在循環(huán)服務(wù)中即可迅速完成;如果發(fā)現(xiàn)是求大型方陣的特征值、較大圖像的卷積運算等,就創(chuàng)建子進程;如果是一種以前未處理過的運算類型,那么我
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025深圳市勞動合同(空白)
- 中國光電煙霧報警器行業(yè)市場前瞻與投資戰(zhàn)略規(guī)劃分析報告
- 2024-2030年中國民辦高校行業(yè)發(fā)展運行現(xiàn)狀及投資潛力預(yù)測報告
- 2023-2028年中國外賣行業(yè)發(fā)展監(jiān)測及市場發(fā)展?jié)摿︻A(yù)測報告
- 屠宰及肉類初加工設(shè)備項目可行性研究報告建議書
- 中國加長貨叉托盤車項目投資可行性研究報告
- 2024-2030年中國中藥材馬錢草行業(yè)發(fā)展?jié)摿︻A(yù)測及投資戰(zhàn)略研究報告
- 2024年智能儀器儀表市場分析報告
- 2025用房屋作抵押借款合同
- 2024-2030年中國青海省旅游行業(yè)市場調(diào)查研究及投資前景展望報告
- 《鴻蒙智能互聯(lián)設(shè)備開發(fā)(微課版)》全套教學(xué)課件
- 山西省晉中市2023-2024學(xué)年高一上學(xué)期期末考試 物理 含解析
- 裝卸工安全培訓(xùn)課件
- 中成藥學(xué)完整版本
- 安全與急救學(xué)習(xí)通超星期末考試答案章節(jié)答案2024年
- 2024-2025學(xué)年度廣東省春季高考英語模擬試卷(解析版) - 副本
- 2024電力安全工器具及小型施工機具預(yù)防性試驗規(guī)程
- 基于單片機的2.4G無線通信系統(tǒng)
- 《建筑力學(xué)》期末機考資料
- 廣東省廣州市2023-2024學(xué)年三年級上學(xué)期英語期中試卷(含答案)
- DB11T 1282-2022 數(shù)據(jù)中心節(jié)能設(shè)計規(guī)范
評論
0/150
提交評論