《Unix 網路編程》08:基本UDP套接字編程

基本UDP套接字編程

系列文章導航:《Unix 網路編程》筆記

UDP 概述

流程圖

recvfrom 和 sendto

#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(int sockfd, const void * buff, size_t nbytes, int flags,
               const struct sockaddr *to, socklen_t addrlen);

參數說明

  • 前三個參數等同於 read 和 write 的三個參數:描述符、指向讀或寫緩衝區的指針、讀寫位元組數
  • sendto 的 to 參數指向一個含有數據報接收者的協議地址的套接字,其長度由 addrlen 指定
  • recvfrom 的 from 參數指向一個將由該函數在返回時填寫數據報發送者的協議地址的套接字地址結構,注意長度是一個指針

注意

  • 長度為 0 的數據報是可以發送,也可以被接收到的
  • 如果 recvfrom 的 from 參數是一個空指針,那麼相應的長度參數也必須是空指針,表示我們不關心發送者的協議地址
  • recvfrom 和 sendto 也可以用於 TCP,儘管通常沒有理由這麼做

程式程式碼

graph LR;

A[標準輸入/輸出] –fgets–> B[UDP-Client] –sendto/recvfrom–> C[UDP-Server]
C –recvfrom/sendto–> B –fputs–> A

伺服器端

udpserv01.c

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    sockfd = 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(sockfd, (SA *)&servaddr, sizeof(servaddr));

    dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}

dg_echo.c

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen) {
  int n;
  socklen_t len;
  char mesg[MAXLINE];
  
  for (;;) {
    len = clilen;
    n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &clilen);
    Sendto(sockfd, mesg, n, 0, pcliaddr, len);
  }
}

服務模型迭代伺服器

graph LR;

subgraph 緩衝FIFO
數據報3
數據報2
數據報1
end

subgraph Client
CliSocket –發送一個數據報–> 數據報3
end

subgraph UDPServer
數據報1 –從緩衝中取出數據報–> ServSocket
end

因為沒有連接的概念,無需維持狀態,所以是來一個消費一個,有點像消費隊列的感覺。

客戶端

udpcli01.c

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        err_quit("usage: udpcli <IPaddress>");

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

dg_cli.c

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) {
  int n;
  char sendline[MAXLINE], recvline[MAXLINE + 1];
  while (Fgets(sendline, MAxLINE, fp) != NULL) {
    Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
    n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);
    recvline[n] = 0;
    Fputs(recvline, stdout);
  }
}

對於客戶端而言,什麼時候指派一個套接字:

  • TCP 下,connect 調用時進行綁定
  • UDP 下,首次調用 sendto 時綁定

當前的問題

數據報的丟失

如果客戶端發送的請求在去或回來的途中丟失了,那麼客戶端將會永遠阻塞於 Recvfrom

簡單的解決方法是為該方法設置一個超時,但是這種方法不能判斷是發送的時候丟失了還是返回的途中丟失了,因此不具備可靠性。

接收到非目標的響應

問題

前文的程式碼中,由於如下語句:

n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);

後兩個參數為 NULL,則任何知道客戶端臨時埠的進程都能向客戶發送數據報,與正常的伺服器應答混淆。

解決方法

創建一個變數,接收發送方的協議地址,然後和我們期望的進行對比

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
    int n;
    char sendline[MAXLINE], recvline[MAXLINE + 1];
    socklen_t len;
    struct sockaddr *preply_addr;

    preply_addr = Malloc(servlen);

    while (Fgets(sendline, MAXLINE, fp) != NULL)
    {

        Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

        len = servlen;
        n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
        if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0)
        {
            printf("reply from %s (ignored)\n",
                   Sock_ntop(preply_addr, len));
            continue;
        }

        recvline[n] = 0; /* null terminate */
        Fputs(recvline, stdout);
    }
}

仍然存在缺陷

在多宿伺服器上,即一台主機有多個介面,也即有多個 IP 地址,有可能接收到的消息是從另一個介面發出的,從而被我們意外地攔截。

強端系統模型、弱端系統模型

解決方法:

  • 客戶通過在 DNS 中查找伺服器主機的名字來驗證該主機的域名(而不是 IP 地址)
  • 給伺服器的所有 IP 地址創建一個套接字,用 select 監聽他們(後面有例子)

伺服器未運行

從客戶端的程式上來看,如果伺服器沒有運行,那麼程式就會阻塞在 recvfrom 方法上。

通過 tcpdump 可以看到如下內容:

  • 確實有 ICMP 的錯誤資訊表示該協議地址 UNREACHABLE,但是這個錯誤不會返回給客戶端

  • 我們稱這個 ICMP 錯誤為 非同步錯誤 ,該錯誤由 sendto 引起,但是 sendto 本身卻成功返回

  • 一個基本規則是,除非它已連接,否則其引發的非同步錯誤並不返回給它

  • 後文將給出一個使用自己的守護進程獲取未連接套接字上這些錯誤的簡便方法

connect

簡介

和 TCP 的 connect 相比:

UDP 調用 connect 的話:

  • 仍然不會有三次我偶在的過程
  • 內核只是檢查是否存在立即可知的錯誤
  • 以及記錄對端的 IP 地址和埠號

和沒有 connect 的 UDP 相比:

  • 不能給輸出操作指定目的 IP 和埠號了,即不使用 sendto 而是使用 write/send,寫入內容自動發送到指定的協議地址

    其實可以用 sendto,但是不能指定目的地址,必須為空指針,第六個參數應該為 0(第五個參數為 NULL 時,第六個參數不再考慮)

  • 不必使用 recvfrom 獲悉數據報的發送者,而改用 readrecvrecvmsg 。內核會過濾掉其他來源的數據報。

  • 由已連接 UDP 套接字引發的非同步錯誤會返回給他們所在的進程

多次調用 connect

兩個作用

  • 指定新的協議地址:和 TCP 的 connect 只能調用一次不同,UDP 可以多次調用以切換不同的協議地址
  • 斷開套接字:將地址簇設為 AF_UNSPEC

性能

當要給同一目的地址發送多個數據報時,顯式連接套接字效率更高

graph LR;

subgraph 建立連接
A(連接套接字) –> A1(發送1) –> A2(發送2) –> A3(斷開連接)
end

subgraph 沒有建立連接
B(連接套接字) –> B1(發送) –> B2(斷開套接字) -.- B3(連接套接字) –> B4(發送) –> B5(斷開套接字)
end

改寫 dg_cli

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
    int n;
    char sendline[MAXLINE], recvline[MAXLINE + 1];

    Connect(sockfd, (SA *)pservaddr, servlen);

    while (Fgets(sendline, MAXLINE, fp) != NULL)
    {

        Write(sockfd, sendline, strlen(sendline));

        n = Read(sockfd, recvline, MAXLINE);

        recvline[n] = 0; /* null terminate */
        Fputs(recvline, stdout);
    }
}

此時當我們建立連接時並不會報錯,在我們用 Write 發送數據時會發現網路不可達的錯誤

流量控制

如果發送方的能力很強,且不斷發數據報,而接收方處理的速度比較慢,則有可能將接收方的緩衝區衝垮

可以通過如下命令查看 UDP 數據的統計資訊

netstat -s -p -u

可以通過如下方式增加緩衝區的大小:

n = 220 * 1024;
Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

UDP 外出埠的確定

通過 connect 應用到 UDP 的一個副作用來獲取:

int main(int argc, char **argv)
{
    int sockfd;
    socklen_t len;
    struct sockaddr_in cliaddr, servaddr;

    if (argc != 2)
        err_quit("usage: udpcli <IPaddress>");

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));

    len = sizeof(cliaddr);
    Getsockname(sockfd, (SA *)&cliaddr, &len);
    printf("local address %s\n", Sock_ntop((SA *)&cliaddr, len));

    exit(0);
}

select 結合 TCP 和 UDP

我們想做一個既能接收 TCP 請求,又能接收 UDP 請求的伺服器。

思路是:把之前的並發 TCP 伺服器和本章中的迭代 UDP 伺服器結合成使用 select 來複用 TCP 和 UDP 套接字的程式。

具體來說,就是:

  • UDP 的描述符綁定 TCP 的埠
  • select 監聽之前 TCP 提到的描述符,以及 UDP 的描述符
  • 在事件到達後判斷是哪一種,然後觸發相關的處理流程
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 */
    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 */
    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); /* must call waitpid() */

    FD_ZERO(&rset);
    maxfdp1 = max(listenfd, udpfd) + 1;
    for (;;)
    {
        printf("Hello UDP!\n");
        FD_SET(listenfd, &rset);
        FD_SET(udpfd, &rset);
        if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0)
        {
            printf("NREADY, restart!\n");
            if (errno == EINTR)
                continue; /* back to for() */
            else
                err_sys("select error\n");
        }

        if (FD_ISSET(listenfd, &rset))
        {
            printf("listenfd selected!\n");
            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 */
        }

        if (FD_ISSET(udpfd, &rset))
        {
            printf("udpfd selected!\n");
            len = sizeof(cliaddr);
            n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len);

            Sendto(udpfd, mesg, n, 0, (SA *)&cliaddr, len);
        }
    }
}
/* end udpservselect02 */