1. 程式人生 > >UDP 套接字程式設計入門

UDP 套接字程式設計入門

概述

在使用TCP編寫的應用程式和使用UDP編寫的應用程式之間存在一些本質差異,其原因在於這兩個傳輸層之間的差別:UDP是無連線不可靠的資料報協議,不同於TCP提供的面向連線的可靠位元組流。從資源的角度來看,相對來說UDP套接字開銷較小,因為不需要維持網路連線,而且因為無需花費時間來連線連線,所以UDP套接字的速度也較快。
因為UDP提供的是不可靠服務,所以資料可能會丟失。如果資料對於我們來說非常重要,就需要小心編寫UDP客戶程式,以檢查錯誤並在必要時重傳。實際上,UDP套接字在區域網中是非常可靠的。

UDP 客戶 / 伺服器程式使用的套接字函式
圖:UDP 客戶 / 伺服器程式使用的套接字函式

上圖展示了客戶與伺服器使用UDP套接字進行通訊的過程。在UDP套接字程式中,客戶不需要與伺服器建立連線,而只管直接使用sendto

函式給伺服器傳送資料報。同樣的,伺服器不需要接受來自客戶的連線,而只管呼叫recvfrom函式,等待來自某個客戶的資料到達。

如何編寫UDP套接字程式

編寫UDP套接字應用程式,涉及一定的步驟:

  • 建立套接字
  • 命名套接字
  • 在伺服器端,等待客戶的訊息
  • 在客戶端,傳送客戶訊息
  • 關閉套接字

步驟1:建立套接字

可以使用系統呼叫socket來建立一個套接字並返回該套接字的檔案描述符。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

建立的套接字是一條通訊線路的一個端點。

domain
domain引數指定哪種協議族,常見的協議族包括 AF_UNIX 和 AF_INET。AF_UNIX 用於通過檔案系統實現的本地套接字,AF_INET 用於網路套接字。

type
type引數指定這個套接字的通訊型別,取值包括 SOCK_STREAM 和 SOCK_DGRAM。
SOCK_STREAM 即流套接字,基於 TCP,提供可靠,有序的服務。
SOCK_DGRAM 即資料報套接字,基於 UDP,提供不可靠,無序的服務。SOCK_DGRAM 型別的套接字為本文講述的重點。

protocol
protocol允許為套接字指定一種協議。對於 AF_UNIX 和 AF_INET,我們使用預設值即可。

以下程式碼建立一個 UDP socket,domain使用 AF_INET,type 使用 SOCK_DGRAM,protocol 協議使用預設的 0 值。

#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("cannot create socket");
        return 0;
    }

    printf("created socket, fd: %d\n", fd);
    exit(0);
}

步驟2:命名套接字

要想讓建立的套接字可以被其他程序使用,那必須給該套接字命名。對套接字命名的意思是指將該套接字關聯一個IP地址和埠號,可以使用系統呼叫bind來實現。

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, size_t address_len);

bind系統呼叫把引數address中的地址分配給與檔案描述符socket關聯的套接字,地址結構的長度由引數address_len傳遞。

每種套接字域都有其自己的格式,對於 AF_INET 域來說,套接字地址由結構 socket_in來指定,它至少包含以下幾個成員:

struct sockaddr_in {
    short int sin_family;  // AF_INET
    unsigned short int sin_port; // 埠號
    struct in_addr  sin_addr;  // IP地址
};

成員sin_port表示套接字的埠號。對於客戶套接字,我們一般不需要指定套接字的埠號,而對於伺服器套接字,我們需要指定套接字的埠號以便讓客戶正確向伺服器傳送資料。如果不需要指定埠號,可以將sin_port的值賦為0。

成員sin_addr表示套接字的地址,即機器的IP地址。如果我們沒特別為套接字繫結IP地址,可以讓作業系統選擇一個,即sin_addr使用地址0.0.0.0,使用INADDR_ANY來表示這個地址常量。

對一個套接字進行命名的程式碼如下:

#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char **argv)
{
    // 建立套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("cannot create socket");
        return 0;
    }

    printf("created socket, fd: %d\n", fd);

    // 命名套接字
    struct sockaddr_in myaddr;
    memset((void *)&myaddr, 0, sizeof(myaddr));
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    myaddr.sin_port = htons(6240);
    if (bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) {
        perror("bind failed");
        return 0;
    }
    printf("bind complete, port number: %d\n", ntohs(myaddr.sin_port));

    exit(0);
}

htonl(host to network, long,長整數從主機位元組序到網路位元組序的轉換)htons(host to network, short,短整數從主機位元組序到網路位元組序的轉換)這兩個函式用於字機位元組序和網路位元組序的轉換。

步驟三a:伺服器接收客戶訊息

不同於TCP提供的面向連線的可靠位元組流協議,UDP 是無連線不可靠的資料報協議。伺服器不接收來自客戶的連線,而只管呼叫recvfrom系統呼叫,等待客戶的資料到達。recvfrom的宣告如下:

#include <sys/socket.h>
int recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *src_addr, socklen_t *src_len);

recvfrom的引數說明如下。
- socket:建立的套接字描述符
- buffer:指向輸入緩衝區的指標
- length:緩衝區大小
- flags:在本文中,可以將 flags 置為0即可
- src_addr:指向客戶套接字地址的指標
- src_len:地址長度
recvfrom的返回值為讀入資料的長度。

我們來看下伺服器是如何使用recvfrom來接收客戶的資料。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>

#define BUFSIZE 2048
#define SERVICE_PORT 6240

int main(int argc, char **argv)
{
    struct sockaddr_in myaddr;  // 伺服器地址
    struct sockaddr_in remaddr; // 客戶地址
    socklen_t addrlen = sizeof(remaddr);
    int recvlen;
    int fd;
    unsigned char buf[BUFSIZE];

    // 建立套接字
    if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("cannot create socket\n");
        return 0;
    }

    // 命名套接字
    memset((char *)&myaddr, 0, sizeof(myaddr));
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    myaddr.sin_port = htons(SERVICE_PORT);
    if (bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) {
        perror("bind failed");
        return 0;
    }

    // 伺服器接收客戶訊息
    while (1) {
        printf("waiting on port %d\n", SERVICE_PORT);
        recvlen = recvfrom(fd, buf, BUFSIZE, 0, (struct sockaddr *)&remaddr, &addrlen);
        printf("received %d bytes\n", recvlen);
        if (recvlen > 0) {
            buf[recvlen] = 0;
            printf("received message: \"%s\"\n", buf);
        }
    }
}

伺服器在迴圈裡面不斷呼叫recvfrom函式,接收客戶的資料,並輸出接收到的客戶資料的長度和具體內容。

步驟三b:客戶向伺服器發達訊息

UDP是無連線的,故客戶可以直接向伺服器傳送訊息而不需要建立連線。客戶使用sendto系統呼叫向伺服器傳送訊息:

#include <sys/socket.h>
int sendto(int socket, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);

sendto函式的引數說明如下:

  • socket:建立的套接字描述符
  • buffer:輸出緩衝區的指標
  • length:緩衝區大小
  • flags:正常應用中,flags一般設定為0
  • dest_addr:指向伺服器套接字地址的指標
  • dest_len:地址長度

以下程式碼演示客戶如何使用UDP套接字向伺服器傳送訊息:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <sys/socket.h>

#define BUFLEN 2048
#define MSGS 5
#define SERVICE_PORT 6240

int main(void)
{
    struct sockaddr_in myaddr, remaddr;
    int fd, i, slen = sizeof(remaddr);
    const char *server = "127.0.0.1";  // 伺服器IP
    char buf[BUFLEN];

    // 建立套接字
    if ((fd=socket(AF_INET, SOCK_DGRAM, 0))==-1)
        printf("socket created\n");

    // 命名套接字
    memset((char *)&myaddr, 0, sizeof(myaddr));
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    myaddr.sin_port = htons(0);  // 無須指定特定的埠
    if (bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) {
        perror("bind failed");
        return 0;
    }

    // 構造伺服器的地址
    memset((char *) &remaddr, 0, sizeof(remaddr));
    remaddr.sin_family = AF_INET;
    remaddr.sin_port = htons(SERVICE_PORT);
    if (inet_aton(server, &remaddr.sin_addr)==0) {
        fprintf(stderr, "inet_aton() failed\n");
        exit(1);
    }

    // 客戶向伺服器傳送訊息
    for (i=0; i < MSGS; i++) {
        printf("Sending packet %d to %s port %d\n", i, server, SERVICE_PORT);
        sprintf(buf, "This is packet %d", i);
        if (sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&remaddr, slen)==-1)
            perror("sendto");
    }

    // 關閉套接字
    close(fd);
    return 0;
}

上面程式碼中,為方便起見,我們使用了inet_aton來構造伺服器的套接字地址,inet_aton的作用是將一個字串IP地址轉換為一個32位的網路位元組序的IP地址。

執行客戶程式,可以看到以下輸出:

$ ./send
Sending packet 0 to 127.0.0.1 port 6240
Sending packet 1 to 127.0.0.1 port 6240
Sending packet 2 to 127.0.0.1 port 6240
Sending packet 3 to 127.0.0.1 port 6240
Sending packet 4 to 127.0.0.1 port 6240

伺服器程式則看到以下輸出:

$ ./recv
waiting on port 6240
received 16 bytes
received message: “This is packet 0”
waiting on port 6240
received 16 bytes
received message: “This is packet 1”
waiting on port 6240
received 16 bytes
received message: “This is packet 2”
waiting on port 6240
received 16 bytes
received message: “This is packet 3”
waiting on port 6240
received 16 bytes
received message: “This is packet 4”
waiting on port 6240

步驟四:關閉套接字

作業系統為每個套接字分配了一個檔案描述符,為了讓作業系統回收該檔案描述符,可以使用 close 系統呼叫:

close(fd);

參考資料