UDP(用戶數據報協議)無連接、不可靠、無序,但開銷小、速度快、支援廣播/多播。本文從基礎 API 入手,逐步實現簡易 UDP 回顯服務器,再擴展為多進程多人聊天室。
| 特性 | TCP | UDP |
|---|---|---|
| 連接建立 | 三次握手 | 無連接 |
| 可靠性 | 可靠、有序 | 不可靠、無序 |
| API | socket/listen/accept/read/write | socket/bind/recvfrom/sendto |
| 適用場景 | 文件傳輸、Web、郵件 | 視頻流、遊戲、DNS、廣播 |
#include <sys/socket.h>
int socket(int domain, int type, int protocol); // type=SOCK_DGRAM
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
recvfrom:接收數據並獲取發送方地址(輸出參數)。sendto:發送數據到指定地址。若預先 connect(),可用 send/recv。udpsrv.c#include <stdio.h>, <stdlib.h>, <string.h>, <unistd.h>, <arpa/inet.h>, <sys/socket.h>
#define PORT 8088
#define BUFSZ 1024
int main() {
int sd = socket(PF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(PORT), .sin_addr.s_addr = INADDR_ANY };
bind(sd, (struct sockaddr*)&addr, sizeof(addr));
printf("UDP server listening on port %d\n", PORT);
char msg[BUFSZ];
socklen_t alen = sizeof(addr);
while (1) {
int n = recvfrom(sd, msg, BUFSZ-1, 0, (struct sockaddr*)&addr, &alen);
if (n <= 0) continue;
msg[n] = '\0';
printf("Received from %s:%d: %s\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), msg);
sendto(sd, msg, n, 0, (struct sockaddr*)&addr, alen); // 原樣回發
}
close(sd);
return 0;
}
udpclt.c#include ... // 同上頭文件
#define PORT 8088
#define BUFSZ 1024
int main() {
int sd = socket(PF_INET, SOCK_DGRAM, 0);
struct sockaddr_in srv = { .sin_family = AF_INET, .sin_port = htons(PORT) };
inet_aton("127.0.0.1", &srv.sin_addr);
char msg[BUFSZ];
socklen_t alen = sizeof(srv);
while (fgets(msg, BUFSZ, stdin)) {
sendto(sd, msg, strlen(msg), 0, (struct sockaddr*)&srv, alen);
int n = recvfrom(sd, msg, BUFSZ-1, 0, (struct sockaddr*)&srv, &alen);
if (n > 0) { msg[n] = '\0'; printf("Echo: %s", msg); }
}
close(sd);
return 0;
}
設計:
struct sockaddr_in + next)。type=1 登錄、2 說話、3 退出。消息結構體 umsg 含類型、用戶名、文本。recvfrom,根據類型廣播/登錄/退出。fork():子進程讀 stdin 發送,父進程 recvfrom 顯示。udpmulsrv.ctypedef struct ucnode { struct sockaddr_in addr; struct ucnode *next; } *ucnode_t;
void broadcast(ucnode_t head, int sd, struct sockaddr_in *src, struct umsg *msg) {
for (ucnode_t p = head->next; p; p = p->next) {
if (memcmp(&p->addr, src, sizeof(*src)) != 0) // 不發給自己
sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&p->addr, sizeof(p->addr));
}
}
int main(int argc, char *argv[]) {
// ... 初始化 socket, bind ...
ucnode_t head = calloc(1, sizeof(*head)); // 頭節點
while (1) {
struct umsg msg; struct sockaddr_in cli; socklen_t alen = sizeof(cli);
recvfrom(sd, &msg, sizeof(msg), 0, (struct sockaddr*)&cli, &alen);
switch (msg.type) {
case '1': // 登錄
insert_node(head, &cli);
broadcast(head, sd, &cli, &msg); // 通知舊用戶有新用戶
break;
case '2': broadcast(head, sd, &cli, &msg); break; // 轉發消息
case '3': // 退出
broadcast(head, sd, &cli, &msg);
delete_node(head, &cli);
break;
}
}
}
udpmulclt.cpid_t pid = fork();
if (pid == 0) { // 子進程:發送
signal(SIGCHLD, SIG_IGN);
while (fgets(msg.text, sizeof(msg.text), stdin)) {
if (strcasecmp(msg.text, "quit\n") == 0) { msg.type = '3'; sendto(...); break; }
msg.type = '2'; sendto(sd, &msg, sizeof(msg), 0, (struct sockaddr*)&srv, alen);
}
close(sd); kill(getppid(), SIGKILL); exit(0);
}
// 父進程:接收顯示
while (1) {
recvfrom(sd, &msg, sizeof(msg), 0, NULL, NULL);
switch (msg.type) {
case '1': printf("%s joined!\n", msg.name); break;
case '2': printf("%s: %s", msg.name, msg.text); break;
case '3': printf("%s left!\n", msg.name); break;
}
}
kill(pid, SIGKILL); waitpid(pid, NULL, 0);
gcc -g -Wall -o udpmulsrv.out udpmulsrv.c
gcc -g -Wall -o udpmulclt.out udpmulclt.c
# 終端1:服務器
./udpmulsrv.out 0.0.0.0 8088
# 終端2/3/...:客戶端
./udpmulclt.out 127.0.0.1 8088 Alice
./udpmulclt.out 127.0.0.1 8088 Bob
struct sockaddr_in addr = { AF_INET } 等價於 memset(&addr,0,sizeof(addr)); addr.sin_family=AF_INET;。sendto 發送整個結構體,接收端按結構體解析,字段末尾手動加 \0 保險。bzero 非標準(BSD),跨平台用 memset(&v,0,sizeof(v)) 封裝宏 BZERO(v)。calloc 零初始化,刪除時 free 防洩漏。listen/accept,天然支援多客戶端。