第八章:Socket 网络编程
第八章:Socket 网络编程
掌握 POSIX Socket API:TCP/UDP 编程、Unix 域套接字、地址解析、并发服务器模型。
8.1 Socket API 概述
8.1.1 什么是 Socket
Socket(套接字) 是 POSIX 中网络通信的抽象接口。它扩展了"一切皆文件"的哲学,使得网络通信也使用 read()/write() 操作。
Socket 由以下要素唯一标识:
Socket = {协议族, 地址族, IP地址, 端口号}
或
Socket = {协议族, 文件路径} (Unix 域套接字)
8.1.2 Socket 类型
| 类型 | 宏 | 传输方式 | 可靠性 | 典型协议 |
|---|---|---|---|---|
| 流式套接字 | SOCK_STREAM | 面向连接,字节流 | ✅ 可靠 | TCP |
| 数据报套接字 | SOCK_DGRAM | 无连接,数据报 | ❌ 不保证 | UDP |
| 原始套接字 | SOCK_RAW | 原始数据包 | — | ICMP, 自定义协议 |
8.1.3 Socket 编程流程
TCP 服务器: TCP 客户端:
socket() socket()
bind()
listen()
accept() ←────── 三次握手 ──────→ connect()
read()/write() ←──── 数据 ────→ read()/write()
close() close()
8.2 TCP 服务器
/*
* tcp_server.c - 基本 TCP 回声服务器
* 编译: gcc -Wall -o tcp_server tcp_server.c
* 测试: echo "hello" | nc localhost 8080
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
#define PORT 8080
#define BUF_SIZE 4096
static volatile int g_running = 1;
static void handle_signal(int sig)
{
(void)sig;
g_running = 0;
}
static void handle_client(int client_fd, struct sockaddr_in *client_addr)
{
char buf[BUF_SIZE];
char addr_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr->sin_addr, addr_str, sizeof(addr_str));
printf("客户端连接: %s:%d\n", addr_str, ntohs(client_addr->sin_port));
ssize_t n;
while ((n = read(client_fd, buf, sizeof(buf))) > 0) {
printf("收到 %zd 字节: %.*s", n, (int)n, buf);
write(client_fd, buf, n); /* 回声 */
}
printf("客户端断开: %s:%d\n", addr_str, ntohs(client_addr->sin_port));
close(client_fd);
}
int main(void)
{
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
/* 创建 socket */
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { perror("socket"); return 1; }
/* 允许地址重用 */
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 绑定地址 */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY,
};
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind"); close(server_fd); return 1;
}
/* 监听 */
if (listen(server_fd, 128) == -1) {
perror("listen"); close(server_fd); return 1;
}
printf("TCP 服务器启动,监听端口 %d\n", PORT);
while (g_running) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr *)&client_addr,
&client_len);
if (client_fd == -1) {
if (errno == EINTR) continue;
perror("accept");
break;
}
handle_client(client_fd, &client_addr);
}
close(server_fd);
printf("\n服务器已关闭\n");
return 0;
}
8.3 TCP 客户端
/*
* tcp_client.c - TCP 客户端
* 编译: gcc -Wall -o tcp_client tcp_client.c
* 用法: ./tcp_client [host] [port]
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
const char *host = argc > 1 ? argv[1] : "127.0.0.1";
int port = argc > 2 ? atoi(argv[2]) : 8080;
/* 解析主机名 */
struct addrinfo hints = {
.ai_family = AF_INET,
.ai_socktype = SOCK_STREAM,
}, *result;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
int ret = getaddrinfo(host, port_str, &hints, &result);
if (ret != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
/* 创建 socket 并连接 */
int sockfd = socket(result->ai_family, result->ai_socktype,
result->ai_protocol);
if (sockfd == -1) { perror("socket"); freeaddrinfo(result); return 1; }
if (connect(sockfd, result->ai_addr, result->ai_addrlen) == -1) {
perror("connect"); close(sockfd); freeaddrinfo(result); return 1;
}
freeaddrinfo(result);
printf("已连接到 %s:%d\n", host, port);
printf("输入消息(Ctrl+D 退出):\n");
/* 读取用户输入并发送 */
char buf[1024];
while (fgets(buf, sizeof(buf), stdin)) {
size_t len = strlen(buf);
write(sockfd, buf, len);
ssize_t n = read(sockfd, buf, sizeof(buf));
if (n <= 0) break;
buf[n] = '\0';
printf("回声: %s", buf);
}
close(sockfd);
printf("连接已关闭\n");
return 0;
}
8.4 UDP 编程
8.4.1 UDP 特性
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠(重传、排序) | 不可靠(可能丢包、乱序) |
| 数据边界 | 字节流(无边界) | 数据报(有边界) |
| 速度 | 较慢 | 较快 |
| 典型用法 | Web、文件传输 | DNS、视频流、游戏 |
8.4.2 UDP 服务器与客户端
/*
* udp_echo.c - UDP 回声服务器/客户端
* 编译: gcc -Wall -o udp_echo udp_echo.c
* 服务器: ./udp_echo server 9090
* 客户端: ./udp_echo client 127.0.0.1 9090
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static int run_server(int port)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) { perror("socket"); return 1; }
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = INADDR_ANY,
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
printf("UDP 服务器启动,端口 %d\n", port);
char buf[1024];
struct sockaddr_in client_addr;
socklen_t client_len;
while (1) {
client_len = sizeof(client_addr);
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &client_len);
if (n <= 0) break;
buf[n] = '\0';
char addr_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, addr_str, sizeof(addr_str));
printf("来自 %s:%d: %s", addr_str, ntohs(client_addr.sin_port), buf);
sendto(fd, buf, n, 0,
(struct sockaddr *)&client_addr, client_len);
}
close(fd);
return 0;
}
static int run_client(const char *host, int port)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) { perror("socket"); return 1; }
struct sockaddr_in server_addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
};
inet_pton(AF_INET, host, &server_addr.sin_addr);
printf("UDP 客户端连接 %s:%d\n", host, port);
char buf[1024];
while (fgets(buf, sizeof(buf), stdin)) {
sendto(fd, buf, strlen(buf), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
(struct sockaddr *)&from_addr, &from_len);
if (n > 0) {
buf[n] = '\0';
printf("回声: %s", buf);
}
}
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 3) {
fprintf(stderr, "用法: %s server <port>\n"
" %s client <host> <port>\n", argv[0], argv[0]);
return 1;
}
if (strcmp(argv[1], "server") == 0)
return run_server(atoi(argv[2]));
if (strcmp(argv[1], "client") == 0 && argc >= 4)
return run_client(argv[2], atoi(argv[3]));
fprintf(stderr, "未知命令\n");
return 1;
}
8.5 Unix 域套接字 (Unix Domain Socket)
8.5.1 特性
Unix 域套接字用于同一台机器上的进程间通信,比 TCP 快得多(不经过网络协议栈):
| 对比项 | TCP Socket | Unix 域套接字 |
|---|---|---|
| 地址 | IP:端口 | 文件路径 |
| 网络协议 | 经过 TCP/IP 栈 | 内核内部直接传递 |
| 速度 | 较慢 | 较快(避免协议栈开销) |
| 跨机器 | ✅ | ❌ |
8.5.2 Unix 域套接字示例
/*
* unix_socket.c - Unix 域套接字回声服务器
* 编译: gcc -Wall -o unix_socket unix_socket.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCKET_PATH "/tmp/posix_unix_socket"
static int run_server(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) { perror("socket"); return 1; }
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
unlink(SOCKET_PATH); /* 移除旧的 socket 文件 */
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind"); close(fd); return 1;
}
listen(fd, 5);
printf("Unix 域套接字服务器启动: %s\n", SOCKET_PATH);
int client_fd = accept(fd, NULL, NULL);
if (client_fd == -1) { perror("accept"); close(fd); return 1; }
char buf[1024];
ssize_t n;
while ((n = read(client_fd, buf, sizeof(buf))) > 0) {
buf[n] = '\0';
printf("收到: %s", buf);
write(client_fd, buf, n);
}
close(client_fd);
close(fd);
unlink(SOCKET_PATH);
printf("服务器已关闭\n");
return 0;
}
static int run_client(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) { perror("socket"); return 1; }
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("connect"); close(fd); return 1;
}
printf("已连接到 Unix 域套接字\n");
const char *msg = "Hello via Unix domain socket!\n";
write(fd, msg, strlen(msg));
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
buf[n] = '\0';
printf("回声: %s", buf);
}
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "用法: %s server|client\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "server") == 0) return run_server();
if (strcmp(argv[1], "client") == 0) return run_client();
return 1;
}
8.6 地址解析
8.6.1 getaddrinfo()——推荐接口
/*
* addrinfo.c - 使用 getaddrinfo 解析主机名
* 编译: gcc -Wall -o addrinfo addrinfo.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
const char *host = argc > 1 ? argv[1] : "localhost";
struct addrinfo hints = {
.ai_family = AF_UNSPEC, /* IPv4 或 IPv6 */
.ai_socktype = SOCK_STREAM, /* TCP */
}, *result, *rp;
int ret = getaddrinfo(host, NULL, &hints, &result);
if (ret != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
printf("解析 '%s' 的结果:\n", host);
for (rp = result; rp; rp = rp->ai_next) {
char addr_str[INET6_ADDRSTRLEN];
void *addr;
if (rp->ai_family == AF_INET) {
addr = &((struct sockaddr_in *)rp->ai_addr)->sin_addr;
} else {
addr = &((struct sockaddr_in6 *)rp->ai_addr)->sin6_addr;
}
inet_ntop(rp->ai_family, addr, addr_str, sizeof(addr_str));
printf(" %s: %s (协议族: %s)\n",
rp->ai_family == AF_INET ? "IPv4" : "IPv6",
addr_str,
rp->ai_socktype == SOCK_STREAM ? "TCP" : "UDP");
}
freeaddrinfo(result);
return 0;
}
8.7 并发服务器模型
8.7.1 模型对比
| 模型 | 并发方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 串行处理 | 无并发 | 简单测试 | 简单但不实用 |
| 多进程 | fork() | 进程隔离 | 安全但开销大 |
| 多线程 | pthread_create() | 通用 | 开销小,共享地址空间 |
| I/O 多路复用 | select()/poll()/epoll | 高并发 | 单线程高并发,编程复杂 |
| 混合模型 | 多线程 + epoll | 生产环境 | 综合最优 |
8.7.2 多进程并发服务器
/*
* tcp_fork_server.c - 多进程并发 TCP 服务器
* 编译: gcc -Wall -o tcp_fork_server tcp_fork_server.c
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <signal.h>
#include <errno.h>
#define PORT 8081
static void sigchld_handler(int sig)
{
(void)sig;
/* 回收所有已终止子进程(WNOHANG 非阻塞) */
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(void)
{
/* 注册 SIGCHLD 处理器 */
struct sigaction sa = {
.sa_handler = sigchld_handler,
.sa_flags = SA_RESTART | SA_NOCLDSTOP,
};
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY,
};
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 128);
printf("多进程 TCP 服务器启动,端口 %d\n", PORT);
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr *)&client_addr, &len);
if (client_fd == -1) {
if (errno == EINTR) continue;
perror("accept");
break;
}
pid_t pid = fork();
if (pid == 0) {
/* 子进程处理客户端 */
close(server_fd);
char buf[1024];
ssize_t n;
while ((n = read(client_fd, buf, sizeof(buf))) > 0)
write(client_fd, buf, n);
close(client_fd);
_exit(0);
}
close(client_fd); /* 父进程关闭客户端 fd */
}
close(server_fd);
return 0;
}
8.8 注意事项
⚠️ 字节序转换:网络字节序为大端(Big-Endian),主机字节序可能是小端。始终使用
htons()/htonl()/ntohs()/ntohl()。
⚠️ SO_REUSEADDR:服务器重启时,端口可能处于
TIME_WAIT状态。使用SO_REUSEADDR允许重用。
⚠️ SIGPIPE:向已关闭的连接写入会产生
SIGPIPE信号,默认终止程序。忽略或处理它:signal(SIGPIPE, SIG_IGN)。
⚠️ 地址结构体初始化:使用前
memset()清零,特别是sockaddr_un的sun_path之后的填充字节。
⚠️ IPv6 兼容:使用
getaddrinfo()而非硬编码 IPv4,以同时支持 IPv4 和 IPv6。
8.9 扩展阅读
man 7 socket、man 7 tcp、man 7 udp、man 7 unix- 《Unix Network Programming》 — W. Richard Stevens 著
- 《TCP/IP Illustrated》 — W. Richard Stevens 著
- Beej’s Guide to Network Programming:https://beej.us/guide/bgnet/
- RFC 793 (TCP)、RFC 768 (UDP)
8.10 本章小结
| 要点 | 说明 |
|---|---|
| Socket API | socket(), bind(), listen(), accept(), connect() |
| TCP | 面向连接、可靠、字节流 |
| UDP | 无连接、不可靠、数据报 |
| Unix 域套接字 | 同机进程间通信,避免网络栈开销 |
| getaddrinfo() | 推荐的地址解析接口,支持 IPv4/IPv6 |
| 并发模型 | 多进程、多线程、I/O 多路复用 |