TCP和UDP的本質區別就在於:UDP是無連接不可靠的數據報協議,TCP是面向連接的可靠字節流。因此使用TCP和UDP編寫的應用程序存在一些差異。使用UDP編寫的一些常見的應用程序有:DNS(域名解析系統)、NFS(網絡文件系統)和SNMP(簡單網絡管理協議)。
類似與標准的read和write函數:
#include
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。
最基本的UDP回射服務器程序。
#include
#include
#include
#include
#include
#include
#include
#include
#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,0))<0)
{
printf("socket error\r\n");
return -1;
}
//服務器套接字結構
memset(&servaddr,0x00,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 0;
}
void dg_echo(int sockfd ,struct sockaddr* pcliaddr,socklen_t clilen)
{
char buf[MAXLEN];
int n;
int len = clilen;
while(1)
{
if((n=recvfrom(sockfd,buf,MAXLEN,0,pcliaddr,&len))<=0)//阻塞
{
printf("recvfrom error\r\n");
return ;
}
sendto(sockfd,buf,n,0,pcliaddr,len);
}
}
最基本的UDP回射客戶端程序。
#include
#include
#include
#include
#include
#include
#include
#include
#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!=2)
{
printf("usage: udpcli \r\n");
return -1;
}
memset(&servaddr,0x00,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<0)
{
printf("inet_pton error\r\n");
return -1;
}
sockfd = socket(AF_INET,SOCK_DGRAM,0);
dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
return 0;
}
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+1];
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
//指定服務器套接字結構直接sendto
sendto(sockfd,sendbuff,strlen(sendbuff),0,pservaddr,servlen);
if((n=recvfrom(sockfd,recvbuff,MAXLEN,0,NULL,NULL))<=0)
{
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返回發送者的套接字結構與服務器對比。
上述提到的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與對端進程進行數據交換。
對於TCP套接字來說,connect只能調用一次,不過對於UDP套接字可以調用多次,一般處於兩個目的:
1.指定新的IP地址和端口號。
2.斷開套接字。
對於第二個目的來說,為了斷開一個UDP套接字連接,我們再次調用connect時把套接字地址結構的地址簇成員設置為AF_UNSPEC
。這麼做可能返回一個EAFNOSUPPORT錯誤,不過沒有關系。使套接字斷開連接的是在已連接UDP套接字上調用connect的進程。
那麼現在問題來了,調用 connect和不調用connect的UDP套接字到底哪個效率高呢?
答:當應用進程知道自己要給同一目的的地址發送多個數據報時,顯示連接套接字效率更高。臨時連接未連接的UDP套接字大約會耗費每個UDP傳輸三分之一的開銷。
這裡的調用跟TCP調用connect類似,客戶程序指定服務器套接字結構。
#include
#include
#include
#include
#include
#include
#include
#include
#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!=2)
{
printf("usage: udpcli \r\n");
return -1;
}
memset(&servaddr,0x00,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<0)
{
printf("inet_pton error\r\n");
return -1;
}
sockfd = socket(AF_INET,SOCK_DGRAM,0);
dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
return 0;
}
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+1];
if(connect(sockfd,(struct sockaddr*)pservaddr,servlen)<0)
{
printf("connect error\r\n");
return ;
}
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
write(sockfd,sendbuff,strlen(sendbuff));
if((n=read(sockfd,recvbuff,MAXLEN))==-1)
{
printf("read error!\r\n");
return ;
}
recvbuff[n]='\0';
fputs(recvbuff,stdout);
}
}
1.分別創建TCP監聽套接字和UDP套接字。
2.將監聽套接字和UDP套接字分別加入select的描述符集。
3.當UDP套接字可讀則FD_ISSET(udpfd,&rset)
返回,直接回射。
4.當TCP監聽套接字可讀則FD_ISSET(listenfd,&rset)
返回,創建子進程並對connfd已連接套接字進行讀寫。
5.除此之外,還需要注冊一個信號處理函數,以處理客戶進程中斷導致子進程返回的情況,防止產生僵屍進程。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 = 1;
struct sockaddr_in cliaddr, servaddr;
/* 4create listening TCP socket */
if((listenfd = socket(AF_INET, SOCK_STREAM, 0))<0)
{
printf("socket error\r\n");
return -1;
}
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))<0)
{
printf("bind error\r\n");
return -1;
}
if(listen(listenfd, 5)<0)
{
printf("listenfd error\r\n");
return -1;
}
/* 4create UDP socket */
if((udpfd = socket(AF_INET, SOCK_DGRAM, 0))<0)
{
printf("socket error\r\n");
return -1;
}
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))<0)
{
printf("bind error\r\n");
return -1;
}
signal(SIGCHLD, sig_chld); /* must call waitpid() */
FD_ZERO(&rset);
maxfdp1 = max(listenfd, udpfd) + 1;
for ( ; ; )
{
FD_SET(listenfd, &rset);
FD_SET(udpfd, &rset);
if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0)
{
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()) == 0)
{ /* child process */
close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
close(connfd); /* parent closes connected socket */
}
if (FD_ISSET(udpfd, &rset))
{
len = sizeof(cliaddr);
n = recvfrom(udpfd, mesg, MAXLINE, 0, (struct sockaddr *) &cliaddr, &len);
sendto(udpfd, mesg, n, 0, (struct sockaddr *) &cliaddr, len);
}
}
}
void str_echo(int connfd)
{
ssize_t nread;
char readbuff[MAXLINE];
memset(readbuff,0x00,sizeof(readbuff));
while((nread=read(connfd,readbuff,MAXLINE))>0)
{
write(connfd,readbuff,strlen(readbuff));
memset(readbuff,0x00,sizeof(readbuff));
}
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
#if 1
while((pid=waitpid(-1,&stat,WNOHANG))>0)
printf("waitpid:child terminated,pid=%d\r\n",pid);
#endif
return ;
}
由於有了TCP的基礎,這部分相對簡單,不過簡單的代價就是TCP提供的很多功能沒有了,例如:檢測丟失的分組並重傳,驗證相應是否來自正確的對端等等。
另外,UDP沒有流量控制,所以一般UDP不用與傳送大量數據;UDP套接字還可能產生ICMP異步錯誤,這可以通過tcpdump來查看這些錯誤,只有已連接的UDP套接字(connect)才能接收到這些錯誤。