歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> UNIX網絡編程筆記(6)—UDP網絡編程

UNIX網絡編程筆記(6)—UDP網絡編程

日期:2017/3/1 11:46:09   编辑:關於Linux

基本UDP套接字編程

1. 概述

TCP和UDP的本質區別就在於:UDP是無連接不可靠的數據報協議,TCP是面向連接的可靠字節流。因此使用TCP和UDP編寫的應用程序存在一些差異。使用UDP編寫的一些常見的應用程序有:DNS(域名解析系統)、NFS(網絡文件系統)和SNMP(簡單網絡管理協議)。


2. sendto和recvfrom函數

類似與標准的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 * fromsocklen_t *addrlen是值-結果參數,返回發送數據者的協議地址結構,如果部關系發送者的協議地址,那麼我們可以完全把這兩個參數設定為NULL。


3. UDP回射服務器程序

最基本的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);
    }
}

4. UDP回射客戶端程序

最基本的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返回發送者的套接字結構與服務器對比。


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   
#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);
    }
}

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   
#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 ;
}

7. UDP總結

由於有了TCP的基礎,這部分相對簡單,不過簡單的代價就是TCP提供的很多功能沒有了,例如:檢測丟失的分組並重傳,驗證相應是否來自正確的對端等等。
另外,UDP沒有流量控制,所以一般UDP不用與傳送大量數據;UDP套接字還可能產生ICMP異步錯誤,這可以通過tcpdump來查看這些錯誤,只有已連接的UDP套接字(connect)才能接收到這些錯誤。

Copyright © Linux教程網 All Rights Reserved