UNP-UNIX網絡編程 第八章:基本udp套接字編程

一.

使用UDP的場合:DNS(域名系統)、NFS(網絡文件系統)、SNMP(簡單網絡管理協議)
UDP沒有像TCP那樣的連接,客戶端直接使用sendto函數向某服務器發送數據報,服務器端一直recvfrom函數阻塞,以接收任何客戶端發送的數據,把數據報再發送給客戶協議地址。
兩個新函數:sendto()和recvfrom();connect()在UDP套接字中的用法;異步錯誤。

int sendto(int sockfd, const void* buff, size_t nbytes, int flag, const struct sockaddr* to, socklen_taddrlen);  
int recvfrom(int sockfd, void* buff, size_t nbytes, int flag, struct sockaddr* from, socklen_t* addrlen);  
//成功返回字節數,失敗返回-1;

(1) flag後面說,這裏先置爲0
(2) sendto的地址結構指明發送目的地的套接字地址。addrlen指明地址長度,爲整數型。相當於TCP的connect中的套接字地址。
(3) recvfrom的地址結構指明發送此數據報的發送端的套接字地址。addrlen爲此套接字地址,爲整型地址。相當於TCP的accept中的套接字地址。
(4) 寫一個長度爲0的數據報是可行的,會形成一個只包含IP首部(20字節)和UDP首部(8字節)的IP數據報。所以recvfrom返回0,是可接受的。
而不是像TCP那樣read返回0表示關閉連接。
(5) recvfrom的套接字地址參數可以是NULL,表示不關心數據是誰發的。此時addrlen也必須是NULL。
對應TCP:

int connect (int sockfd , const struct sockaddr *servaddr , socklen_t addrlen);
int accept (int sockfd , struct sockaddr *cliaddr , socklen_t* addrlen);    
ssize_t read(int fd,void *buf,size_t nbytes);
ssize_t write(int fd,const void *buf,size_t nbytes)
#include    "unp.h"//服務器
int main(int argc, char **argv)
{/*首先正常情況下,函數永不會終止。它不像TCP連接那樣,還有終止連接的四次。*/
    int    sockfd;
    struct sockaddr_in  servaddr, cliaddr;

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);//SOCK_DGRAM爲數據報套接字

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//服務器IPv4地址被指定爲INADDR_ANY
    servaddr.sin_port        = htons(SERV_PORT);//指定端口

    Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));//bind()

    dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));//執行服務器處理工作
}

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{/*1.該函數永不終止;2.迭代服務器,非併發;3.協議無關,不查看協議相關結構(pcliaddr)的內容*/
    int         n;
    socklen_t   len;
    char        mesg[MAXLINE];

    for ( ; ; ) {
        len = clilen;
        n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

        Sendto(sockfd, mesg, n, 0, pcliaddr, len);
    }
}

大多數TCP服務器併發,UDP服務器迭代!!!
每個UDP套接字都會有一個接收緩衝區,類似於一個隊列(FIFO)。
多個數據報到達UDP服務器,則會排隊,調用recvfrom函數,從這個隊列頭取出數據報給進程。
而TCP是爲每個客戶一個連接fork一個子進程,並且每個連接一個套接字,每個套接字一個接收緩衝區,所以我們要併發監聽每個接收緩衝區。
而UDP是任何客戶發送的數據報放入一個接收緩衝區,所以根本無需什麼併發服務器,也不可能做成併發的。

#include    "unp.h"//客戶端
int main(int argc, char **argv)
{//協議相關
    int    sockfd;
    struct sockaddr_in  servaddr;

    if (argc != 2)
        err_quit("usage: udpcli <IPaddress>");
    //把服務器的IP地址和端口號填入一個IPv4的套接字地址結構
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = 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, (SA *) &servaddr, sizeof(servaddr));//某個協議類型的套接字地址結構
    exit(0);
}

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{//協議無關
    int     n;
    char    sendline[MAXLINE], recvline[MAXLINE + 1];
    socklen_t       len;//1
    struct sockaddr *preply_addr;//2
    preply_addr = Malloc(servlen);//3
    //此時,如果客戶的數據報丟失,客戶端就會永遠阻塞於Recvfrom(),應該設置一個超時(見14.2);
    //但是!!!!!!!!!!!!如果真的是服務器還沒有應答呢????見22.5
    while (Fgets(sendline, MAXLINE, fp) != NULL) //從標準輸入讀入一個文本行
    {   //Sendto將文本行發送給服務器,如果沒有指定端口,就臨時綁定一個
        Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
        //4------------------------------------------------------------------------------
        len = servlen;
        n = Recvfrom(sockfd, recvline, MAXLINE, 0, (SA *) replyaddr, &len);//讀服務器回射
        if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) 
        {
            printf("reply from %s (ignored)\n",Sock_ntop(preply_addr, len));
            continue;
        }
        //-------------------------------------------------------------------------------
        //n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
        //有危險,最後兩個NULL:任何主機都會向客戶發送數據報,並且被認爲了是服務器應答(改進)
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);//把文本行顯示到標準輸出
    }
}

改進1.試着獲取recvfrom的套接字地址和sendto發送的套接字地址是否一致,來決定此消息是否是來自對端服務器。就是比較servlen的大小和套接字地址結構本身。
如果服務器是多宿主機,即兩個IP地址,如Ip1,Ip2。由於我們在寫服務器端程序時,bind函數的參數是通配IP,所以當我們sendto時,是使用Ip1,而服務器回射時,內核自動選擇了Ip2,則這會讓我們客戶端誤判該回射消息不是來自服務器端。
兩個解決辦法:
1>recvfrom得到IP後,查詢DNS獲得主機的域名,以判斷消息是否來自該主機。
2>服務器端爲每個IP創建一個套接字,使用bind得到每個IP地址。然後使用select監聽這些套接字,等待其中一個變爲可讀,
說明客戶端使用的是這個IP,則服務器使用這個IP套接字回射就可以了。22.6

二. 服務器未運行

當我們先啓動客戶端,不啓動服務器端進程時,發生了什麼:
我們從控制檯輸入一行數據回車,然後客戶端將永遠阻塞在recvfrom函數上。

1. 底層機制:

(1)數據發送到服務器主機上,發送主機的目的端口並沒有開啓,所以返回一個端口不可達的ICMP(異步錯誤)消息,
這個消息是不會返回客戶端進程的(爲何?)。所以客戶端用於阻塞在recvfrom上。
異步錯誤:本例中錯誤由sendto函數引起,但是sendto成功返回(只要在接口輸出隊列中有存放ip數據報的空間,就返回成功),
ICMP到後來才返回錯誤(幾秒後),這就是異步錯誤。
(2)一個基本規則:對於一個UDP套接字,由它引發的異步錯誤不返回給它,除非它已經連接。(注意UDP也有connect函數)。
爲何ICMP消息不會返回客戶端進程?Unix這樣設計的道理是什麼?
假設我們使用客戶端連續發送3個消息,2個消息的目的服務器正常,最後一個服務器未啓動,則會有一個ICMP消息,
假設這個消息被recvfrom獲取,recvfrom返回一個負值表示錯誤,rrno爲錯誤類型,但是他不反悔錯誤的IP和端口號
此時客戶端並不知道哪個目的套接字出錯,內核無法告知進程,因爲recvfrom此時返回的信息只是errno值!!

2. 上面提到未連接UDP套接字發生的異步錯誤,不會返回給進程,這裏我們可以使用connect對一個UDP套接字進行連接。

Connect函數的調用和TCP一樣,參數指定目的服務器的套接字地址。注意沒有三次握手,只是檢查對端是否存在立即可知的錯誤。
已連接UDP套接字和未連接UDP套接字的不同:
(1) 已連接套接字,直接使用send,發送數據報給connect的目的服務器套接字。而不使用sendto。
(2) 已連接套接字,直接使用write,只接收來自connect目的服務器套接字的數據報,
也就意味着,已連接套接字只能和一個對端進行通信,而未連接套接字顯然可以和任何多個對端通信。
(3)已連接套接字發生異步消息會返回給進程,因爲此時已經知道目的套接字。而未連接套接字不會返回給進程。

注意1:一般對客戶端的UDP套接字進行connect,而服務器端還是sendto和recvfrom,connect只會影響本地套接字。

注意2:我們可以對一個已連接的UDP套接字指向多次connect,而TCP不可以。以下情況多次connect
(1) 指定新的IP地址和端口號
(2) 斷開套接字。此時把套接字地址結構的地址族(IPv4的sin_family)設爲AF_UNSPEC。

3. 當我們對一個未連接的UDP套接字連續sendto兩次,看看具體步驟:

連接套接字、發送第一個數據報、斷開套接字、連接套接字、發送第二個數據報、斷開套接字。
如果兩個數據報是同一個目的套接字,則我們應該使用顯然connect,之後會提高效率。因爲這樣只需要一個連接和斷開。
注:Unix中一個連接需要耗費一次UDP傳輸的三分之一的開銷。

4. 重新對dg_cli改進

(上面那個改進是面向無連接的,這個是有鏈接,所以不知道誰發來的這種錯誤就不存在了,這裏是改進接受到錯誤進程)

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{//協議無關
    int     n;
    char    sendline[MAXLINE], recvline[MAXLINE + 1];
    Connect(socket,(SA*)pservaddr,servlen);//1.聯結
    //此時,如果客戶的數據報丟失,客戶端就會永遠阻塞於Recvfrom(),應該設置一個超時(見14.2);
    //但是!!!!!!!!!!!!如果真的是服務器還沒有應答呢????見22.5
    while (Fgets(sendline, MAXLINE, fp) != NULL) //從標準輸入讀入一個文本行
    {   //Sendto將文本行發送給服務器,如果沒有指定端口,就臨時綁定一個
        Write(sockfd, sendline, strlen(sendline));//2改爲write()
        n = Read(sockfd, recvline, MAXLINE);//3.讀服務器回射改爲read()
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);//把文本行顯示到標準輸出
    }
}

此時我們connect時,並沒有發生錯誤,直到我們發送一個消息時才返回錯誤。而如果此時TCP的話,在connect時就會發生錯誤。
因爲UDP的connect不會觸發三次握手,而TCP的connect會觸發三次握手,發現目的端口不可達,則服務器會返回RST分組。

5. UDP是沒有流量控制的

假設一個客戶端連續發送大量的數據,則服務器端使用套接字接收緩衝區排隊接收這些數據,
但當發送來的數據超出套接字接收緩衝區時,服務器端就會自動丟棄到來的數據報,而此時客戶端和服務器端不會有任何的錯誤。

6. UDP中的IP地址和端口號

(1)未連接的UDP套接字,
如果我們沒有bind,則當sendto時,內核選擇一個本地IP地址和端口號,所以同一主機上兩次連續的sendto,
客戶端:兩個消息的源IP地址和端口號可能都不一樣。而且,服務器端:接收recvfrom後,回射消息,sendto時,
可能造成回射消息的源IP地址和端口號和recvfrom消息的目的IP地址和端口號不一樣(收發可能不是一個)。
(2)已連接UDP套接字,
客戶端:如果沒有bind,多次connect同一個目的IP端口號時,已連接套接字的本地IP和端口號可能都是不一樣的。
我們可以使用getsockname來獲取已連接UDP套接字的本地IP和端口號;recvmsg來獲取未連接的UDP套接字的本地IP和端口號。

7. 使用select的TCP和UDP來重寫回射服務器

/* include udpservselect01 */
#include    "unp.h"

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;
    void                sig_chld(int);

/* -----------------------4create listening TCP socket --------------------*/
//1.創建監聽TCP套接字
//創建一個監聽TCP套接字並捆綁服務器的衆所周知端口,設置SO_REUSEADDR套接字選項以防該端口上已有連接存在。
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    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));
    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);
/*-------------------------------------------------------------------------*/
/* --------------------------4create UDP socket ---------------------------*/
//2.創建一個UDP套接字並捆綁與TCP套接字相同的端口。這裏無需在調用bind之前設置SO_REUSEADDR套接字選項,
//因爲TCP端口是獨立於UDP端口的
    udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);

    Bind(udpfd, (SA *) &servaddr, sizeof(servaddr));
/*-----------------------end udpservselect01-------------------------------*/

/* --------------------include udpservselect02---------------------------- */
    Signal(SIGCHLD, sig_chld);//3.給SIGCHLD建立信號處理程序,因爲TCP連接將
    //由某個子進程處理。我們已在圖5-11中給出了這個信號處理函數。
        /* must call waitpid() */
//4.準備select,我們給select初始化一個描述符集,並計算出我們等待的兩個描述符的較大者。
    FD_ZERO(&rset);
    maxfdp1 = max(listenfd, udpfd) + 1;

    for ( ; ; ) 
    {
/*5.調用select只是爲了等待監聽TCP套接字的可讀條件或UDP套接字的可讀條件。
既然我們的sig_chld信號處理函數可能中斷我們對select的調用,我們於是需要處理EINTR錯誤。 */
        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
                err_sys("select error");
        }
/*------------------6.處理新的客戶連接//這與第5章中採取的步驟相同。------------------*/
//當監聽TCP套接字可讀時,我們accept一個新的客戶連接,fork一個子進程,並在子進程中調用str_echo函數。

        if (FD_ISSET(listenfd, &rset)) 
        {
            len = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *) &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 */
        }
/*------------------6.處理數據報的到達----------------------------------------------*/
//如果UDP套接字可讀,那麼已有一個數據報到達。我們使用recvfrom讀入它,再使用sendto把它發回給客戶。
        if (FD_ISSET(udpfd, &rset)) 
        {
            len = sizeof(cliaddr);
            n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);
            Sendto(udpfd, mesg, n, 0, (SA *) &cliaddr, len);
        }
    }
}
/* end udpservselect02 */

注:
這裏涉及select的I/O複用,信號處理,fork子進程,TCP服務器,UDP服務器,套接字選項。
這裏的str_echo函數和第五章的相同,信號處理函數並沒有實現,注意函數裏要調用waitpid。
注意這裏有意思的地方:就是我們只是使用select監聽TCP的監聽套接字和UDP套接字。對於已連接的TCP套接字使用子進程來處理。
即TCP的並行服務器和UDP的迭代服務器。
前面我們曾使用select來監聽TCP監聽套接字和已連接套接字,使得程序完全的單進程。這裏並沒有這麼做,
因爲那樣太麻煩,這裏只是展示了使用select同時監聽TCP連接和UDP連接。很簡潔,很有意思,這段代碼仔細看。很多有意思的地方。
注意這裏:同一臺服務器,TCP套接字和UDP套接字使用同一個端口,這是可以的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章