基本UDP套接字程式設計
1. 概述
TCP和UDP的本質差別就在于:UDP是無連接配接不可靠的資料報協定,TCP是面向連接配接的可靠位元組流。是以使用TCP和UDP編寫的應用程式存在一些差異。使用UDP編寫的一些常見的應用程式有:DNS(域名解析系統)、NFS(網絡檔案系統)和SNMP(簡單網絡管理協定)。
2. sendto和recvfrom函數
類似與标準的read和write函數:
#include <sys/socket.h>
ssize_t recvfrom (int sockfd,void *buff,size_t nbytes,int flags,
struct sockaddr *from,socklen_t *addrlen);
ssize_t sendto (inat sockfd,const void * buff,size_t nbytes,int flags,
const struct sockaddr*to,socklen_t addrlen);
參數說明:
回憶read和write函數,前三個參數分别是:fd,buf,nbytes分别表示:描述符,指向讀入或寫出緩沖區的指針和讀寫的位元組數,跟我們上述的recvfrom和sendto就是對應的。
對于
sendto
來說,顧名思義,我們需要一個參數包含資料報接收者的協定位址(IP和端口号),上述
const struct sockaddr * to
就是這樣一個參數,它指向了接收者的協定位址,另外我們需要一個addrlen,防止核心讀取指針位址越界,這個套路跟以前見過TCP套接字函數中的用法一樣。
對于
recvfrom
來說,
struct sockaddr * from
和
socklen_t *addrlen
是值-結果參數,傳回發送資料者的協定位址結構,如果部關系發送者的協定位址,那麼我們可以完全把這兩個參數設定為NULL。
3. UDP回射伺服器程式
最基本的UDP回射伺服器程式。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define SERV_PORT 1024
#define MAXLEN 1024
void dg_echo(int sockfd,struct sockaddr*pcliaddr,socklen_t clilen);
int main()
{
int sockfd;
struct sockaddr_in servaddr,cliaddr;
if((sockfd=socket(AF_INET,SOCK_DGRAM,))<)
{
printf("socket error\r\n");
return -;
}
//伺服器套接字結構
memset(&servaddr,,sizeof(servaddr));
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_family=AF_INET;
bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
dg_echo(sockfd,(struct sockaddr *)&cliaddr,sizeof(cliaddr));
return ;
}
void dg_echo(int sockfd ,struct sockaddr* pcliaddr,socklen_t clilen)
{
char buf[MAXLEN];
int n;
int len = clilen;
while()
{
if((n=recvfrom(sockfd,buf,MAXLEN,,pcliaddr,&len))<=)//阻塞
{
printf("recvfrom error\r\n");
return ;
}
sendto(sockfd,buf,n,,pcliaddr,len);
}
}
4. UDP回射用戶端程式
最基本的UDP回射用戶端程式。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define SERV_PORT 1024
#define MAXLEN 1024
void dg_cli(FILE*,int ,const struct sockaddr*,socklen_t);
int main(int argc, char ** argv)
{
int sockfd;
struct sockaddr_in servaddr;
if(argc!=)
{
printf("usage: udpcli <IPaddress>\r\n");
return -;
}
memset(&servaddr,,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
if(inet_pton(AF_INET,argv[],&servaddr.sin_addr)<)
{
printf("inet_pton error\r\n");
return -;
}
sockfd = socket(AF_INET,SOCK_DGRAM,);
dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
return ;
}
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+];
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
//指定伺服器套接字結構直接sendto
sendto(sockfd,sendbuff,strlen(sendbuff),,pservaddr,servlen);
if((n=recvfrom(sockfd,recvbuff,MAXLEN,,NULL,NULL))<=)
{
printf("recvfrom error\r\n");
return ;
}
recvbuff[n]='\0';//防止越界
fputs(recvbuff,stdout);//輸出回射資料
}
}
小結
對于上述程式有幾個問題需要注意:
1.最簡單的UDP回射服務與用戶端程式,在正常情況下,運作的很好。不過我們不知道資料報是否會在以下兩種情況下丢失:
1.客戶資料->伺服器方向
2.伺服器應答->用戶端
,請求丢失和應答丢失都有可能造成用戶端程式在recvfrom函數的阻塞。
2.如果不啟動伺服器程式,直接運作用戶端,當我們輸入資料之後(sendto正常傳回),然而沒有相應的伺服器進行回射,用戶端會阻塞在recvfrom函數,經過tcpdump工具分析,伺服器主機響應一個
port unreachable
的ICMP消息。不過這個ICMP消息不傳回給客戶程序,稱之為ICMP異步錯誤。
3.如果某個程序直到用戶端程序的臨時端口号,該程序也可以向用戶端程序發送資料報,這些資料報就會跟伺服器應答混淆,解決的辦法就是用戶端程式通過recvfrom傳回發送者的套接字結構與伺服器對比。
5. UDP調用connect
上述提到的ICMP異步錯誤不會傳回到UDP套接字,通過connect函數可以解決。這個connect與TCP的connect還是有差別的,因為畢竟UDP,至少時不需要經過三路握手的過程,不過可以檢測出是否存在立即可知的錯誤,例如一個顯然不可打的目的地,記錄對端的IP位址和端口号,立即傳回到用戶端程序。
因為調用connect,UDP程式也發生了細微的變化:
1.UDP套接字分為已連接配接套接字(調用connect成功後),和未連接配接套接字(預設)。
2.不能使用sendto來指定輸出操作的ip位址和端口号了,需要改用send或write,這些資料報将發送到由connect指定的協定位址上。
3.不使用recvfrom來獲得資料報的發送者,改用read或recv,在已連接配接的UDP套接字上,輸入操作傳回的資料報來自connect指定的協定位址。
4.異步錯誤會傳回給已連接配接UDP套接字所在程序,未連接配接UDP套接字不會收到。
一句話總結就是,應用程序調用connect指定對端的IP位址和端口号,然後使用read和write與對端程序進行資料交換。
5.1 UDP套接字多次調用connect
對于TCP套接字來說,connect隻能調用一次,不過對于UDP套接字可以調用多次,一般處于兩個目的:
1.指定新的IP位址和端口号。
2.斷開套接字。
對于第二個目的來說,為了斷開一個UDP套接字連接配接,我們再次調用connect時把套接字位址結構的位址簇成員設定為
AF_UNSPEC
。這麼做可能傳回一個EAFNOSUPPORT錯誤,不過沒有關系。使套接字斷開連接配接的是在已連接配接UDP套接字上調用connect的程序。
5.2 性能
那麼現在問題來了,調用 connect和不調用connect的UDP套接字到底哪個效率高呢?
答:當應用程序知道自己要給同一目的的位址發送多個資料報時,顯示連接配接套接字效率更高。臨時連接配接未連接配接的UDP套接字大約會耗費每個UDP傳輸三分之一的開銷。
5.3 使用connect的UDP客戶程式
這裡的調用跟TCP調用connect類似,客戶程式指定伺服器套接字結構。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define SERV_PORT 1024
#define MAXLEN 1024
//udp socket with connect
void dg_cli(FILE*,int ,const struct sockaddr*,socklen_t);
int main(int argc, char ** argv)
{
int sockfd;
struct sockaddr_in servaddr;
if(argc!=)
{
printf("usage: udpcli <IPaddress>\r\n");
return -;
}
memset(&servaddr,,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
if(inet_pton(AF_INET,argv[],&servaddr.sin_addr)<)
{
printf("inet_pton error\r\n");
return -;
}
sockfd = socket(AF_INET,SOCK_DGRAM,);
dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
return ;
}
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+];
if(connect(sockfd,(struct sockaddr*)pservaddr,servlen)<)
{
printf("connect error\r\n");
return ;
}
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
write(sockfd,sendbuff,strlen(sendbuff));
if((n=read(sockfd,recvbuff,MAXLEN))==-)
{
printf("read error!\r\n");
return ;
}
recvbuff[n]='\0';
fputs(recvbuff,stdout);
}
}
6. 使用select的TCP+UDP回射伺服器函數
1.分别建立TCP監聽套接字和UDP套接字。
2.将監聽套接字和UDP套接字分别加入select的描述符集。
3.當UDP套接字可讀則
FD_ISSET(udpfd,&rset)
傳回,直接回射。
4.當TCP監聽套接字可讀則
FD_ISSET(listenfd,&rset)
傳回,建立子程序并對connfd已連接配接套接字進行讀寫。
5.除此之外,還需要注冊一個信号處理函數,以處理客戶程序中斷導緻子程序傳回的情況,防止産生僵屍程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
#define SERV_PORT 1024
#define MAXLINE 1024
void sig_chld(int);
void str_echo(int);
int max(int a,int b)
{
return a>b?a:b;
}
int main(int argc, char **argv)
{
int listenfd, connfd, udpfd, nready, maxfdp1;
char mesg[MAXLINE];
pid_t childpid;
fd_set rset;
ssize_t n;
socklen_t len;
const int on = ;
struct sockaddr_in cliaddr, servaddr;
/* 4create listening TCP socket */
if((listenfd = socket(AF_INET, SOCK_STREAM, ))<)
{
printf("socket error\r\n");
return -;
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<)
{
printf("bind error\r\n");
return -;
}
if(listen(listenfd, )<)
{
printf("listenfd error\r\n");
return -;
}
/* 4create UDP socket */
if((udpfd = socket(AF_INET, SOCK_DGRAM, ))<)
{
printf("socket error\r\n");
return -;
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if(bind(udpfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<)
{
printf("bind error\r\n");
return -;
}
signal(SIGCHLD, sig_chld); /* must call waitpid() */
FD_ZERO(&rset);
maxfdp1 = max(listenfd, udpfd) + ;
for ( ; ; )
{
FD_SET(listenfd, &rset);
FD_SET(udpfd, &rset);
if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < )
{
if (errno == EINTR)
continue; /* back to for() */
else
printf("select error\r\n");
}
if (FD_ISSET(listenfd, &rset))
{
len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &len);
if ( (childpid = fork()) == )
{ /* child process */
close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit();
}
close(connfd); /* parent closes connected socket */
}
if (FD_ISSET(udpfd, &rset))
{
len = sizeof(cliaddr);
n = recvfrom(udpfd, mesg, MAXLINE, , (struct sockaddr *) &cliaddr, &len);
sendto(udpfd, mesg, n, , (struct sockaddr *) &cliaddr, len);
}
}
}
void str_echo(int connfd)
{
ssize_t nread;
char readbuff[MAXLINE];
memset(readbuff,,sizeof(readbuff));
while((nread=read(connfd,readbuff,MAXLINE))>)
{
write(connfd,readbuff,strlen(readbuff));
memset(readbuff,,sizeof(readbuff));
}
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
#if 1
while((pid=waitpid(-,&stat,WNOHANG))>)
printf("waitpid:child terminated,pid=%d\r\n",pid);
#endif
return ;
}
7. UDP總結
由于有了TCP的基礎,這部分相對簡單,不過簡單的代價就是TCP提供的很多功能沒有了,例如:檢測丢失的分組并重傳,驗證相應是否來自正确的對端等等。
另外,UDP沒有流量控制,是以一般UDP不用與傳送大量資料;UDP套接字還可能産生ICMP異步錯誤,這可以通過tcpdump來檢視這些錯誤,隻有已連接配接的UDP套接字(connect)才能接收到這些錯誤。