TCP編寫應用程序和使用UDP編寫應用程序之間存在一些本質差異,其原因在于這兩個傳輸層之間的差異:UDP是無連接不可靠的數據報協議,非常不同于TCP提供的面向連接的可靠字節流。使用UDP編寫的一些常見應用程序由:DNS(域名系統)、NFS(網絡文件系統)和SNMP(簡單網絡管理協議)。
圖1-1給出了典型的UDP客戶/服務器程序函數調用。客戶不與服務器建立連接,而是只管使用sendto函數給服務器發送數據報,其中必須指定目的地(即服務器)的地址作為參數。類似地,服務器不接受來自客戶的連接,而是只管調用recvfrom函數,等待來自某個客戶的數據到達。recfrom將與所接收的數據報一道返回客戶的協議地址,因此服務器可以把響應發送給正確的客戶。
UDP客戶/服務器程序所用的套接字函數
2 recvfrom和sendto函數
這兩個函數類似于標準的read和write函數,不過需要三個額外的參數。
#include /*ReadNbytesintoBUFthroughsocketFD. IfADDRisnotNULL,fillin*ADDR_LENbytesofitwiththaaddressof thesender,andstoretheactualsizeoftheaddressin*ADDR_LEN. Returnsthenumberofbytesreador-1forerrors. Thisfunctionisacancellationpointandthereforenotmarkedwith __THROW.*/ externssize_trecvfrom(int__fd,void*__restrict__buf,size_t__n, int__flags,__SOCKADDR_ARG__addr, socklen_t*__restrict__addr_len); /*SendNbytesofBUFonsocketFDtopeerataddressADDR(whichis ADDR_LENbyteslong).Returnsthenumbersent,or-1forerrors. Thisfunctionisacancellationpointandthereforenotmarkedwith __THROW.*/ externssize_tsendto(int__fd,constvoid*__buf,size_t__n, int__flags,__CONST_SOCKADDR_ARG__addr, socklen_t__addr_len);
前三個參數__fd、__buf和__n等同于read和write函數的三個參數:描述符、指向讀入或寫出緩沖區的指針和讀寫字節數。
sendto的__addr參數指向一個含有數據接收者的協議地址(如IP地址及端口號)的套接字地址結構,其大小由__addr_len參數指定。recvfrom的__addr參數指向一個由該函數在返回時填寫數據報發送者的協議地址的套接字地址結構,而在該套接字地址結構中填寫的字節數則放在__addr_len參數所指的整數中返回給調用者。注意,sendto的最后一個參數是一個整數值,而recvfrom的最后一個參數是一個指向整數值的指針(即值-結果參數)。
recvfrom的最后兩個參數類似于accept的最后兩個參數:返回時其中套接字地址結構的內容告訴我們是誰發送了數據報(UDP情況下)或是誰發起了連接(TCP情況下)。sendto的最后兩個參數類似于connect的最后兩個參數:調用時其中套接字地址結構被我們填入數據報將發往(UDP情況下)或與之建立連接(TCP情況下)的協議地址。
這兩個函數都把所讀寫數據的長度作為函數返回值。在recvfrom使用數據報協議的典型用途中,返回值就是所接收數據報中的用戶數據量。
寫一個長度為0的數據報是可行的。在UDP情況下,這會形成一個只包含一個IP首部(對于IPv4通常是20字節,對于IPv6通常是40字節)和一個8字節UDP首部而沒有數據的IP數據報。這也意味著對于數據報協議,recvfrom返回0值是可接受的:它并不像TCP套接字上read返回0值那樣表示對端已關閉連接。既然UDP是無連接的,因此也就沒有諸如關閉一個UDP連接之類事情。
如果recvfrom的__addr參數是一個空指針,那么相應的長度參數(addrlen)也必須是一個空指針,表示我們并不關心數據發送者的協議地址。
recvfrom和sendto都可用于TCP,盡管通常沒有理由這樣做。
3 UDP回射服務器程序:main函數
我們的UDP客戶程序和服務器程序依循圖1-1中所示的函數調用流程。圖1-2描述了它們使用的函數。
使用UDP的簡單回射客戶/服務器
下面是main函數。
#defineSERV_PORT9877 intmain(intargc,char**argv) { intsockfd; structsockaddr_inservaddr,cliaddr; //我們通過將socket函數的第二個參數指定為SOCK_DGRAM(IPv4協議中的數據報套接字)創建一個UDP套接字。正如 //TCP服務器程序的例子,用于bind的服務器IPv4地址被指定為INADDR_ANY,而服務器的眾所周知端口是9877 sockfd=socket(AF_INET,SCOK_DGRAM,0); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_addr.s_addr=htonl(INADDR_ANY); seraddr.sin_port=htons(SERV_PORT); if((bind(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr)))<0) { printf("binderror!n"); return-1; } //接著調用函數dg_echo來執行服務器的處理工作 dg_echo(sockfd,(structsockaddr*)&servaddr,sizeof(cliaddr)); } 4 UDP回射服務器程序:dg_echo函數
下面給出dg_echo函數。
voiddg_echo(intsockfd,structsockaddr*pcliaddr,socklen_tclilen) { intn; socklen_tlen; charmesg[MAXLINE]; //該函數是一個簡單的循環,它使用recvfrom讀入下一個到達服務器端口的數據報,再使用sendto把它發送回發送者。 for(;;) { len=clilen; n=recvfrom(sockfd,mesg,MAXLINE,0,pcliaddr,&len); sendto(sockfd,mesg,n,0,pcliaddr,len); } }
首先,該函數永不終止,因為UDP是一個無連接的協議,它沒有像TCP中EOF之類的東西。
其次,該函數提供的是一個迭代服務器(iterative server),而不是像TCP服務器那樣可以提供一個并發服務器。其中沒有對fork的調用,因此單個服務器進程就得處理所有客戶。一般來說,大多數TCP服務器是并發的,而大多數UDP服務器是迭代的。
對于本套接字,UDP層隱含有排隊發生。事實上每個UDP套接字都有一個接收緩沖區,到達該套接字的每個數據報都進入這個套接字接收緩沖區。當進程調用recvfrom時,緩沖區中的下一個數據報以FIFO(先入先出)順序返回給進程。這樣,在進程能夠讀該套接字中任何已排好的數據報之前,如果有多個數據報到達該套接字,那么相繼到達的數據報僅僅加到該套接字的接收緩沖區。然而這個緩沖區的大小是有限的。可以用SO_RECVNUF套接字選項來改變大小。
總結了TCP客戶/服務器在兩個客戶與服務器建立連接時的情形。
兩個客戶的TCP客戶/服務器小結
服務器主機上有兩個已連接套接字,其中每一個都由各自的套接字接收緩沖區。
展示了兩個客戶發送數據報到UDP服務器的情形。
兩個客戶的UDP客戶/服務器小結
其中只有一個服務器進程,它僅有的單個套接字用于接收所有到達的數據報并發回所有的響應。該套接字有一個接收緩沖區用來存放所到達的數據報。
上面的main函數是協議相關的(它創建一個AF_INET協議的套接字,分配并初始化一個IPv4套接字的地址結構),而dg_echo函數是協議無關的。dg_echo協議無關理由如下:調用者必須分配一個正確大小的套接字地址結構,且指向該結構的指針和該結構的大小都必須作為參數傳遞給dg_echo。dg_echo絕不查看這個協議相關結構的內容,而是簡單地把一個指向該結構的指針傳遞給recvfrom和senfto。recvfrom返回時把客戶的IP地址和端口號填入該結構,而隨后作為目的地址傳遞給sendto的又是同一個指針(pcliaddr),這樣所接收的任何數據報就被回射給發送該數據報的客戶。
5 UDP回射客戶程序:main函數
UDP客戶程序的main函數。
intmain(intargc,char**argv) { intsockfd; structsockaddr_inservaddr; if(argc!=2) { printf("usage:udpclin"); exit(1); } //把服務器的IP地址和端口號填入一個IPv4的套接字地址結構。該結構將傳遞給dg_cli函數,以指明數據報將發往何處。 bzero(&servaddr,sizeof(servaddr)); servaddr.sin_famliy=AF_INET; servaddr.sin_port=htons(SERV_PORT); inet_pton(AF_INET,argv[1],&servaddr.sin_addr); sockfd=socket(AF_INET,SOCK_DGRAM,0); dg_cli(stdin,sockfd,(structsockaddr*)&servaddr,sizeof(servaddr)); exit(0); } 6 UDP回射客戶程序:dg_cli函數
下面是dg_cli函數,它執行客戶的大部分工作。
voiddg_cli(FILE*fp,intsockfd,conststructsockaddr*pservaddr,socklen_tservlen) { intn; charsendline[MAXLINE],recvline[MAXLINE 1]; //客戶處理循環有四個步驟:使用fgets從標準輸入讀入一個文本行,使用sendto將該文本行發送給服務器,使用 //recvfrom讀回服務器的回射,使用fputs把回射的文本行顯示到標準輸出。 while(fgets(sendline,MANLINE,fd)!=NULL) { sendto(sockfd,sendline,strlen(sendline),0,pservaddr,servlen); n=recvfrom(sockfd,recvline,MAXLINE,0,NULL,NULL); recvline[n]=0;//nullterminate fputs(recvline,stdout); } }
7 數據報的丟失
這個UDP客戶/服務器例子是不可靠的。如果一個客戶數據報丟失(譬如說,被客戶主機與服務器主機之間的某個路由器丟棄),客戶將永遠阻塞在dg_cli函數中的recvfrom調用,等待一個永遠不會到達的服務器應答。防止這樣永久阻塞的一般方法是給客戶的recvfrom調用設置一個超時。
然而僅僅設置超時并不是完整的解決辦法,因為我們不能知道超時的原因。
8 驗證接收到的響應
客戶臨時端口號的任何進程都可往客戶發送數據報,而且這些數據報會與正常的服務器應答混雜。解決辦法是把recvfrom調用以返回數據報發送者的IP地址和端口號,保留來自數據報所發往服務器的應答,而忽略任何其他數據報。
首先把main函數改為標準回射服務器。
servaddr.sin_port=htons(7);
我們接著重寫dg_cli函數以分配另一個套接字地址結構用于存放由recvfrom返回的結構。
voiddg_cli(FILE*fp,intsockfd,conststructsockaddr*pservaddr,socklen_tservlen) { intn; charsendline[MAXLINE],recvline[MAXLINE 1]; socklen_tlen; structsockaddr*preply_addr; preply_addr=malloc(servlen); while(fgets(sendline,MAXLINE,fp)!=NULL) { sendto(sockfd,sendline,strlen(sendline),0,pservaddr,servlen); n=recvfrom(sockfd,recvline,MAXLINE,0,preply_addr,&len); if(len!=servlen||memcmp(pservaddr,preply_addr,len)!=0) { printf("replyfrom%s(ignored)n",sock_ntop(preply_addr,len)); continue; } recvline[n]=0;//nullterminate fputs(recvline,stdout); } }
更多關于云服務器,域名注冊,虛擬主機的問題,請訪問三五互聯官網:m.shinetop.cn