UDP 套接字程式設計入門
概述
在使用TCP編寫的應用程式和使用UDP編寫的應用程式之間存在一些本質差異,其原因在於這兩個傳輸層之間的差別:UDP是無連線不可靠的資料報協議,不同於TCP提供的面向連線的可靠位元組流。從資源的角度來看,相對來說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);