强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第八章: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 特性

特性TCPUDP
连接面向连接无连接
可靠性可靠(重传、排序)不可靠(可能丢包、乱序)
数据边界字节流(无边界)数据报(有边界)
速度较慢较快
典型用法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 SocketUnix 域套接字
地址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_unsun_path 之后的填充字节。

⚠️ IPv6 兼容:使用 getaddrinfo() 而非硬编码 IPv4,以同时支持 IPv4 和 IPv6。


8.9 扩展阅读

  1. man 7 socketman 7 tcpman 7 udpman 7 unix
  2. 《Unix Network Programming》 — W. Richard Stevens 著
  3. 《TCP/IP Illustrated》 — W. Richard Stevens 著
  4. Beej’s Guide to Network Programminghttps://beej.us/guide/bgnet/
  5. RFC 793 (TCP)RFC 768 (UDP)

8.10 本章小结

要点说明
Socket APIsocket(), bind(), listen(), accept(), connect()
TCP面向连接、可靠、字节流
UDP无连接、不可靠、数据报
Unix 域套接字同机进程间通信,避免网络栈开销
getaddrinfo()推荐的地址解析接口,支持 IPv4/IPv6
并发模型多进程、多线程、I/O 多路复用