天天看點

UNIX網絡程式設計筆記(6)—UDP網絡程式設計基本UDP套接字程式設計

基本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)才能接收到這些錯誤。