3UNIX网络编程(第3版)

套接字编程简介

Socket基础

socketbindlistenaccept

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章 套接字编程简介

Socket基础

一、导读

3.1 本章学习目标

第3章是从理论走向实践的关键转折点。前两章建立了网络协议的知识框架,本章则将这些知识转化为可运行的代码。你将系统学习套接字编程的核心API——这些API是所有网络程序的基础,无论你编写的是Web服务器、聊天应用还是分布式系统,都离不开它们。

本章的核心学习目标包括:

  • 掌握socket()函数的参数含义:地址族、套接字类型和协议如何选择?返回值是什么?
  • 理解bind()的作用和使用场景:什么时候需要bind?服务器和客户端的bind有何不同?
  • 深入理解listen()的机制:监听队列的工作原理?backlog参数的实际含义?
  • 掌握accept()的阻塞行为和返回值:accept返回的新描述符与监听描述符的区别?
  • 理解connect()的完整流程:TCP三次握手在connect调用中的具体过程?
  • 能够独立编写TCP客户/服务器的基本框架:将各个API串联成完整的通信流程。

3.2 为什么本章如此重要

套接字API是网络编程的"Hello World"——看似简单,实则蕴含着大量容易忽视的细节。Stevens在本书中花了大量篇幅讨论这些API的边界情况和错误处理,因为网络程序中80%的bug都源于对这些基础API的误用。

常见的错误包括:

  • 不理解 accept() 返回新描述符的意义,导致在子进程中忘记关闭监听描述符。
  • 不理解 listen() 的backlog含义,导致高并发时连接被拒绝。
  • 不理解 connect() 可能返回 EINTR(被信号中断),导致连接建立失败。
  • 不理解 bind() 中使用 INADDR_ANY 和特定IP地址的区别。

本章将逐一澄清这些误区,帮助你写出正确的网络代码。

3.3 核心问题引导

在阅读本章时,请带着以下问题思考:

socket() 返回的描述符和 open() 返回的描述符有什么共同点?它们可以互换使用吗?

为什么TCP服务器需要两个不同的描述符(listenfd和connfd),而客户端只需要一个?

listen() 的backlog参数是128,是否意味着最多只能有128个客户同时连接?

accept() 返回后,如果用新的描述符发送数据,监听描述符是否还能接受新的连接?

如果客户端 connect() 一个没有进程监听的端口,会发生什么?错误是如何传递的?

为什么 bind() 端口号小于1024需要root权限?


二、核心概念详解

3.1 socket()函数:创建通信端点

3.1.1 函数原型

c
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 返回值:成功返回非负描述符,出错返回-1

3.1.2 参数详解

domain(地址族):

常量值说明
AF_INET2IPv4 Internet协议
AF_INET610IPv6 Internet协议
AF_UNIX1UNIX内部协议(本机进程间通信)
AF_UNSPEC0未指定

type(套接字类型):

常量值说明
SOCK_STREAM1字节流套接字(TCP)
SOCK_DGRAM2数据报套接字(UDP)
SOCK_RAW3原始套接字
SOCK_SEQPACKET5有序分组套接字

可以在type上使用OR操作添加标志:

  • SOCK_CLOEXEC:设置close-on-exec标志,fork后子进程不会继承该描述符
  • SOCK_NONBLOCK:设置非阻塞标志

protocol(协议):

通常为0,表示让系统根据domain和type自动选择合适的协议。例如 AF_INET + SOCK_STREAM 自动选择TCP,AF_INET + SOCK_DGRAM 自动选择UDP。

3.1.3 返回值与错误处理

c
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket error");
    exit(1);
}

常见错误:

  • EAFNOSUPPORT:不支持指定的地址族
  • EINVAL:不支持指定的套接字类型
  • EMFILE:进程的文件描述符已达上限
  • ENFILE:系统的文件描述符已达上限

文件描述符的本质:socket() 返回的是一个文件描述符——一个非负整数,在进程的文件描述符表中索引一个内核对象。对于套接字描述符,内核创建的是一个 struct socket 对象,它包含了协议信息、缓冲区、状态等。

c
// 文件描述符的分配规则
// 进程的文件描述符表从0开始,分配最小的可用描述符
// 0 = stdin, 1 = stdout, 2 = stderr
// 所以socket()通常返回3或更大的值

// 验证:
int fd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket fd = %d\n", fd);  // 通常输出 3

// 如果先关闭stdin
close(0);
int fd2 = socket(AF_INET, SOCK_STREAM, 0);
printf("socket fd = %d\n", fd2);  // 输出 0!

3.1.4 常见用法示例

c
// TCP套接字(IPv4)
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);

// UDP套接字(IPv4)
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);

// TCP套接字(IPv6)
int tcp6_sock = socket(AF_INET6, SOCK_STREAM, 0);

// 带CLOEXEC标志的TCP套接字(防止fork后子进程继承)
int safe_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);

// 原始套接字(需要root权限)
int raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

3.2 bind()函数:绑定本地地址

3.2.1 函数原型

c
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回值:成功返回0,出错返回-1

3.2.2 参数详解

bind() 将一个本地地址(IP地址+端口号)与套接字描述符关联。

  • sockfd:socket() 返回的描述符
  • addr:指向协议特定地址结构的指针。对于IPv4,是 struct sockaddr_in *
  • addrlen:地址结构的长度
c
// 绑定到特定端口(所有接口)
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0 - 所有接口
addr.sin_port = htons(8080);               // 端口8080

bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

3.2.3 INADDR_ANY vs 特定IP

c
// 方式1:绑定到INADDR_ANY(推荐用于服务器)
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 服务器将接收来自所有网络接口的连接
// 适用于多网卡或多IP的服务器

// 方式2:绑定到特定IP
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
// 服务器只接收来自192.168.1.100这个接口的连接
// 适用于需要限制访问来源的场景

INADDR_ANY的含义:当服务器绑定到 INADDR_ANY 时,它告诉内核"如果有数据报到达本机的任何IP地址且端口匹配,都交付给这个套接字"。当服务器回复时,内核自动选择数据报到达时的那个IP地址作为源地址。

3.2.4 端口号的分配

c
// 方式1:指定端口号
addr.sin_port = htons(8080);  // 绑定到8080端口

// 方式2:让系统自动分配(端口号设为0)
addr.sin_port = htons(0);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
// bind后可以用getsockname获取系统分配的端口号
struct sockaddr_in assigned_addr;
socklen_t len = sizeof(assigned_addr);
getsockname(sockfd, (struct sockaddr *)&assigned_addr, &len);
printf("Assigned port: %d\n", ntohs(assigned_addr.sin_port));

端口号小于1024的限制:在大多数UNIX系统中,绑定到0-1023范围的端口号需要超级用户权限。这些是"知名端口",保留给标准服务(如80=HTTP,22=SSH,443=HTTPS)。普通用户只能绑定1024-65535范围的端口。

3.2.5 常见错误

c
// 错误1:忘记设置sin_family
struct sockaddr_in addr;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "0.0.0.0", &addr.sin_addr);
// 忘记 addr.sin_family = AF_INET;
// 可能导致bind失败或行为异常

// 错误2:地址结构未清零
struct sockaddr_in addr;  // 未初始化,sin_zero包含垃圾数据
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// sin_zero中的垃圾数据可能导致bind失败
// 正确做法:memset(&addr, 0, sizeof(addr));

// 错误3:端口号未转换字节序
addr.sin_port = 8080;  // 错误!应该用htons(8080)
// 在小端序机器上,8080 (0x1F90) 被解释为 0x901F = 36895

3.3 listen()函数:开始监听

3.3.1 函数原型

c
#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 返回值:成功返回0,出错返回-1

3.3.2 两个关键动作

listen() 对套接字做了两件事:

将套接字从主动模式转为被动模式:默认情况下,socket() 创建的套接字是主动的(active),可以用来 connect()。调用 listen() 后,套接字变为被动的(passive),只能用来 accept() 传入的连接。

创建监听队列:内核为该套接字维护两个队列:

- 未完成连接队列(incomplete connection queue):已收到SYN,等待三次握手完成。状态为SYN_RCVD。

- 已完成连接队列(completed connection queue):三次握手已完成,等待 accept() 取出。状态为ESTABLISHED。

SYN到达 → [未完成队列] → 收到ACK → [已完成队列] → accept()取出

3.3.3 backlog参数的含义

backlog 参数提示系统已完成连接队列的最大长度。但实际行为因系统而异:

  • 传统实现:backlog 是已完成队列和未完成队列的总长度。
  • 现代Linux:backlog 是已完成队列的长度。未完成队列的长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 控制。
c
// 设置backlog
listen(sockfd, 128);  // 已完成连接队列最大128

// 当已完成队列满时:
// - TCP忽略新的SYN(不回复SYN+ACK)
// - 或者回复SYN+ACK后丢弃(取决于实现)
// - 客户端的connect可能超时

backlog的选择:

  • 太小:高并发时连接被拒绝
  • 太大:浪费内核内存,且可能被SYN flood攻击利用
  • 推荐值:128或256,根据实际负载调整
c
// 查看系统对backlog的实际限制
// cat /proc/sys/net/core/somaxconn
// 默认值通常是128或4096
// listen()的backlog不能超过somaxconn

// 调整somaxconn(需要root)
// sysctl -w net.core.somaxconn=4096

3.3.4 listen只适用于TCP

listen() 只对面向连接的协议(如TCP)有意义。UDP是无连接的,不存在"监听连接"的概念。对UDP套接字调用 listen() 会返回 EOPNOTSUPP 错误。

3.4 accept()函数:接受连接

3.4.1 函数原型

c
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 返回值:成功返回非负已连接描述符,出错返回-1

3.4.2 关键行为

accept() 从已完成连接队列中取出第一个连接,返回一个新的文件描述符,代表与该客户的连接。

返回值的重要性:

  • listenfd(监听描述符):保持不变,继续用于监听新的连接。
  • connfd(已连接描述符):新返回的描述符,代表与特定客户的连接。用于 read/write 数据传输。
c
// accept的基本用法
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);

int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd < 0) {
    perror("accept error");
    // 处理错误...
}

// 获取客户信息
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, ip, sizeof(ip));
printf("Client connected: %s:%d\n", ip, ntohs(cliaddr.sin_port));

// 使用connfd进行数据传输
char buf[1024];
ssize_t n = read(connfd, buf, sizeof(buf));
write(connfd, buf, n);

// 处理完毕后关闭connfd
close(connfd);
// listenfd仍然有效,可以继续accept

3.4.3 阻塞行为

默认情况下,accept() 是阻塞的——如果没有已完成的连接在队列中等待,accept() 会一直等待,直到有连接到达。

c
// accept被信号中断的情况
// 如果进程在accept阻塞期间收到信号,accept返回-1,errno设为EINTR
int connfd;
while (1) {
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
    if (connfd < 0) {
        if (errno == EINTR) {
            // 被信号中断,重新accept
            continue;
        }
        perror("accept error");
        break;
    }
    // 处理连接...
}

3.4.4 addr参数的使用

c
// 方式1:获取客户地址
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
// cliaddr现在包含客户的IP和端口

// 方式2:不关心客户地址
int connfd = accept(listenfd, NULL, NULL);
// 不需要客户信息时,可以传NULL

// 方式3:addr和addrlen的初始化
// addrlen是值-结果参数:
// - 调用前:告诉accept缓冲区大小
// - 返回后:告诉accept实际写入的地址长度
socklen_t clilen = sizeof(cliaddr);  // 必须初始化!
accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
// clilen现在可能是 sizeof(struct sockaddr_in) 或更小

常见错误:忘记初始化 addrlen。如果 clilen 未初始化(包含随机值),accept() 可能写入超出缓冲区的数据,导致栈溢出。

3.5 connect()函数:发起连接

3.5.1 函数原型

c
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回值:成功返回0,出错返回-1

3.5.2 TCP三次握手过程

connect() 触发TCP三次握手。具体过程:

客户端发送SYN(connect() 返回前)

服务器回复SYN+ACK

客户端收到SYN+ACK后发送ACK(connect() 返回)

connect() 在三次握手完成之前不会返回(阻塞模式)。

c
// 客户端基本流程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);

if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
    perror("connect error");
    exit(1);
}
// 连接已建立,可以开始数据传输

3.5.3 自动绑定

如果客户端在 connect() 之前没有调用 bind(),内核会自动为套接字选择一个临时端口(ephemeral port)和源IP地址。

c
// 客户端通常不需要bind
// 内核在connect时自动选择:
// - 源IP:根据路由表选择到达目的地的最佳接口
// - 源端口:从临时端口范围中选择一个未使用的端口

// Linux临时端口范围:
// cat /proc/sys/net/ipv4/ip_local_port_range
// 32768    60999

// 如果需要知道自动分配的地址:
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
struct sockaddr_in localaddr;
socklen_t len = sizeof(localaddr);
getsockname(sockfd, (struct sockaddr *)&localaddr, &len);
printf("Local: %s:%d\n",
       inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));

3.5.4 connect的错误处理

c
// 常见connect错误

// 1. ECONNREFUSED - 服务器未运行或端口未监听
// 服务器主机存在,但没有进程监听指定端口
// TCP收到ICMP Port Unreachable后返回此错误
if (connect(sockfd, ...) < 0) {
    if (errno == ECONNREFUSED) {
        fprintf(stderr, "Connection refused - server not running?\n");
    }
}

// 2. ETIMEDOUT - 连接超时
// 服务器不可达(防火墙丢弃SYN,无响应)
// 默认超时时间很长(75秒到几分钟)
if (errno == ETIMEDOUT) {
    fprintf(stderr, "Connection timed out\n");
}

// 3. ENETUNREACH / EHOSTUNREACH - 网络/主机不可达
// 路由表中没有到达目的网络的路由
if (errno == ENETUNREACH || errno == EHOSTUNREACH) {
    fprintf(stderr, "No route to host\n");
}

// 4. EINTR - 被信号中断
// connect被信号中断,可以重试
if (errno == EINTR) {
    // 重试connect(注意:可能需要重新创建套接字)
}

3.5.5 connect与非阻塞

c
// 设置非阻塞connect
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

int ret = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret < 0) {
    if (errno == EINPROGRESS) {
        // 连接正在进行中(三次握手尚未完成)
        // 使用select/poll/epoll等待连接完成
        fd_set wfds;
        FD_ZERO(&wfds);
        FD_SET(sockfd, &wfds);
        struct timeval tv = {5, 0};  // 5秒超时

        if (select(sockfd + 1, NULL, &wfds, NULL, &tv) > 0) {
            int error;
            socklen_t len = sizeof(error);
            getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
            if (error == 0) {
                printf("Connected!\n");
            } else {
                printf("Connect failed: %s\n", strerror(error));
            }
        } else {
            printf("Connect timeout\n");
            close(sockfd);
        }
    } else {
        perror("connect error");
    }
}

3.6 TCP客户/服务器的完整流程

3.6.1 TCP服务器流程

socket() → bind() → listen() → accept() → read/write → close()
                         ↑                       │
                         └────── 循环 ←──────────┘
c
// 完整的TCP服务器框架
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define MAXLINE 4096
#define LISTENQ 128

int main(int argc, char **argv) {
    int listenfd, connfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    char buf[MAXLINE];
    ssize_t n;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    // 1. 创建TCP套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket error");
        exit(1);
    }

    // 可选:设置SO_REUSEADDR,允许快速重启
    int optval = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    // 2. 绑定地址和端口
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(atoi(argv[1]));

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind error");
        exit(1);
    }

    // 3. 开始监听
    if (listen(listenfd, LISTENQ) < 0) {
        perror("listen error");
        exit(1);
    }

    printf("Server listening on port %s\n", argv[1]);

    // 4. 循环接受连接
    for (;;) {
        clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
        if (connfd < 0) {
            if (errno == EINTR)
                continue;  // 被信号中断,重试
            perror("accept error");
            continue;
        }

        // 打印客户信息
        char cli_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
        printf("Connection from %s:%d\n", cli_ip, ntohs(cliaddr.sin_port));

        // 5. 处理客户请求(回显)
        while ((n = read(connfd, buf, MAXLINE)) > 0) {
            write(connfd, buf, n);
        }

        if (n < 0) {
            perror("read error");
        }

        // 6. 关闭连接
        close(connfd);
        printf("Connection closed\n");
    }

    close(listenfd);
    return 0;
}

3.6.2 TCP客户端流程

socket() → connect() → read/write → close()
c
// 完整的TCP客户端框架
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define MAXLINE 4096

int main(int argc, char **argv) {
    int sockfd, n;
    struct sockaddr_in servaddr;
    char sendline[MAXLINE], recvline[MAXLINE + 1];

    if (argc != 3) {
        fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
        exit(1);
    }

    // 1. 创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        exit(1);
    }

    // 2. 设置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
        fprintf(stderr, "inet_pton error for %s\n", argv[1]);
        exit(1);
    }

    // 3. 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect error");
        exit(1);
    }

    printf("Connected to %s:%s\n", argv[1], argv[2]);

    // 4. 交互循环
    while (fgets(sendline, MAXLINE, stdin) != NULL) {
        n = strlen(sendline);
        if (write(sockfd, sendline, n) != n) {
            perror("write error");
            exit(1);
        }

        n = read(sockfd, recvline, MAXLINE);
        if (n < 0) {
            perror("read error");
            exit(1);
        } else if (n == 0) {
            printf("Server closed connection\n");
            break;
        }

        recvline[n] = '\0';
        fputs(recvline, stdout);
    }

    // 5. 关闭连接
    close(sockfd);
    return 0;
}

3.6.3 描述符的生命周期

理解描述符的生命周期对于编写正确的网络程序至关重要:

服务器端:
socket() → listenfd (生命周期:整个服务器运行期间)
accept() → connfd  (生命周期:单个客户连接期间)
close(connfd) → 关闭单个连接
close(listenfd) → 关闭服务器(通常在服务器退出时)

客户端:
socket() → sockfd (生命周期:整个客户端运行期间)
close(sockfd) → 关闭连接并退出

关键区别:

  • listenfd 是"工厂"——不断生产连接
  • connfd 是"产品"——每个连接一个,用完即弃
  • 多个 connfd 可以同时存在(并发服务器)
  • listenfd 在整个服务器生命周期内只有一个

三、实战案例

3.1 时间获取服务器

c
// 一个简单的 daytime 服务器
// 客户端连接后获取当前时间,然后服务器关闭连接

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

int main(int argc, char **argv) {
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buf[64];
    time_t ticks;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(1300);  // 使用非标准端口

    int optval = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(listenfd, 128);

    printf("Daytime server listening on port 1300\n");

    for (;;) {
        struct sockaddr_in cliaddr;
        socklen_t clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);

        char cli_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
        printf("Connection from %s:%d\n", cli_ip, ntohs(cliaddr.sin_port));

        ticks = time(NULL);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        write(connfd, buf, strlen(buf));

        close(connfd);
    }

    return 0;
}

测试:

bash
# 终端1:启动服务器
./daytime_server

# 终端2:使用nc连接
nc localhost 1300
# 输出: Tue Oct  6 15:30:45 2026

3.2 获取套接字信息的工具函数

c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>

void print_sock_info(int sockfd, const char *label) {
    struct sockaddr_in local, remote;
    socklen_t local_len = sizeof(local);
    socklen_t remote_len = sizeof(remote);

    printf("=== %s (fd=%d) ===\n", label, sockfd);

    if (getsockname(sockfd, (struct sockaddr *)&local, &local_len) == 0) {
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &local.sin_addr, ip, sizeof(ip));
        printf("  Local:  %s:%d\n", ip, ntohs(local.sin_port));
    }

    if (getpeername(sockfd, (struct sockaddr *)&remote, &remote_len) == 0) {
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &remote.sin_addr, ip, sizeof(ip));
        printf("  Remote: %s:%d\n", ip, ntohs(remote.sin_port));
    } else {
        printf("  Remote: (not connected)\n");
    }
}

四、本章小结

本章系统介绍了套接字编程的核心API,核心要点回顾:

socket():创建通信端点,返回文件描述符。参数决定地址族(IPv4/IPv6)、套接字类型(TCP/UDP)和协议。

bind():将套接字绑定到本地地址。服务器必须bind,客户端通常不需要。INADDR_ANY 绑定所有接口。端口号需要 htons() 转换。

listen():将套接字转为被动模式,创建监听队列。backlog控制已完成连接队列的大小。仅适用于TCP。

accept():从监听队列取出连接,返回新的已连接描述符。listenfd保持不变继续监听。阻塞等待直到有连接到达。

connect():触发TCP三次握手。自动绑定源地址(如果未bind)。常见错误包括ECONNREFUSED、ETIMEDOUT、EINTR。

完整流程:服务器 socket→bind→listen→accept→read/write→close,客户端 socket→connect→read/write→close。


五、思考与练习

思考题

如果服务器在 accept() 返回后、处理请求前崩溃了,客户端会收到什么错误?为什么?

为什么 accept() 要返回新的描述符而不是复用 listenfd?如果复用会有什么问题?

一个服务器可以 bind() 到同一个端口但不同的IP地址吗?如果可以,数据如何路由?

listen() 的backlog设为128,但 somaxconn 系统参数是64,实际生效的值是多少?

如果客户端 connect() 一个防火墙后面的服务器,防火墙丢弃所有SYN包,客户端多久会收到超时错误?

编程练习

多接口服务器:编写一个服务器,分别绑定到 127.0.0.1 和 0.0.0.0 的同一端口,观察行为。解释为什么一个成功另一个失败(或都成功/都失败)。

连接信息显示器:修改echo服务器,在每个客户连接时打印详细的连接信息(本地地址/端口、远程地址/端口、使用的文件描述符编号)。

带超时的connect:实现一个非阻塞connect,设置5秒超时。如果超时则打印错误并退出,而不是等待系统默认的长时间超时。

3/10套接字编
0%