Skip to main content

网络编程

📅 2026-03-25 ✏️ 2026-03-25 CS

1 · 网络编程

主要参考 Beej’s Guide to Network Programming


1.1 · 1. Socket 是什么#

Socket 本质上是一个文件描述符 (file descriptor)。Unix 哲学 “everything is a file”,网络通信也不例外——通过 socket() 系统调用获取一个 fd,然后用它来 send() / recv()

1.1.1 · Internet Socket 的两种类型#

类型常量协议特点
Stream SocketSOCK_STREAMTCP可靠、有序、面向连接
Datagram SocketSOCK_DGRAMUDP不可靠、无连接、速度快
  • TCP (Stream): 数据按序到达、无差错。适用于 HTTP、SSH、telnet
  • UDP (Datagram): fire-and-forget,可能丢包、乱序。适用于游戏、音视频流、DHCP
  • 如果需要在 UDP 上实现可靠传输,可以在应用层加 ACK 机制(如 TFTP)

1.1.2 · 数据封装与网络分层模型

数据在发送时逐层封装 header:

Application Data
  → + TFTP Header
  → + UDP Header
  → + IP Header
  → + Ethernet Header

Unix 网络模型(简化 OSI):

说明
Application Layertelnet, ftp, HTTP…
Transport Layer (Host-to-Host)TCP, UDP
Internet LayerIP, 路由
Network Access LayerEthernet, Wi-Fi

1.2 · 2. IP 地址、结构体与字节序#

1.2.1 · 2.1 IPv4 与 IPv6#

  • IPv4: 4 字节,点分十进制 192.0.2.111
  • IPv6: 16 字节,冒号分隔十六进制 2001:0db8:63b3:0001::3490

1.2.1.1 · 子网 (Subnet)#

  • IP = 网络部分 + 主机部分
  • 用 netmask 区分:192.0.2.12/24 表示前 24 位是网络
  • CIDR 记法允许任意位数的 netmask

1.2.1.2 · 端口号 (Port)#

  • 16-bit 数字 (0-65535)
  • IP 地址 = 酒店地址,端口 = 房间号
  • < 1024 的端口是特权端口

1.2.1.3 · 私有网络

  • 10.x.x.x, 192.168.x.x, 172.16-31.x.x (RFC 1918)

1.2.2 · 2.2 字节序 (Byte Order)#

  • Big-Endian = Network Byte Order(高位在前)
  • Little-Endian = 大多数 Intel CPU 的 Host Byte Order(低位在前)

转换函数:

函数含义
htons()Host to Network Short (16-bit)
htonl()Host to Network Long (32-bit)
ntohs()Network to Host Short
ntohl()Network to Host Long

规则:数据上线前 htonX(),下线后 ntohX()

1.2.3 · 2.3 关键结构体#

// 通用地址结构(旧式,14字节手动填)
struct sockaddr {
    unsigned short sa_family;   // AF_INET 或 AF_INET6
    char           sa_data[14]; // 地址 + 端口
};

// IPv4 专用(可与 sockaddr 互相强转)
struct sockaddr_in {
    short int          sin_family;  // AF_INET
    unsigned short int sin_port;    // 端口,必须 htons()
    struct in_addr     sin_addr;    // IP 地址
    unsigned char      sin_zero[8]; // 填充,memset 为 0
};

struct in_addr {
    uint32_t s_addr; // 32-bit IPv4 地址,Network Byte Order
};

// IPv6 专用
struct sockaddr_in6 {
    u_int16_t       sin6_family;   // AF_INET6
    u_int16_t       sin6_port;     // 端口,Network Byte Order
    u_int32_t       sin6_flowinfo; // IPv6 flow info
    struct in6_addr sin6_addr;     // IPv6 地址
    u_int32_t       sin6_scope_id; // Scope ID
};

struct in6_addr {
    unsigned char s6_addr[16]; // 128-bit IPv6 地址
};

// 通用存储:足够大以容纳 IPv4 和 IPv6
struct sockaddr_storage {
    sa_family_t ss_family; // 检查此字段决定强转类型
    // ... 内部填充
};

// DNS 查询结果,链表
struct addrinfo {
    int              ai_flags;     // AI_PASSIVE, AI_CANONNAME
    int              ai_family;    // AF_INET, AF_INET6, AF_UNSPEC
    int              ai_socktype;  // SOCK_STREAM, SOCK_DGRAM
    int              ai_protocol;  // 0 = any
    size_t           ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next;      // 链表下一项
};

1.2.4 · 2.4 IP 地址转换函数#

// 字符串 → 二进制(pton = presentation to network)
inet_pton(AF_INET,  "10.12.110.57", &(sa.sin_addr));
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr));
// 返回值: 1 成功, 0 地址格式错误, -1 出错

// 二进制 → 字符串(ntop = network to presentation)
char ip4[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);

inet_addr() / inet_aton() / inet_ntoa() 已弃用,不支持 IPv6


1.3 · 3. 核心系统调用#

1.3.1 · 调用流程概览

TCP Server:                    TCP Client:
getaddrinfo()                  getaddrinfo()
socket()                       socket()
setsockopt(SO_REUSEADDR)       connect()
bind()                         send() / recv()
listen()                       close()
accept()
  → fork() 子进程处理
    send() / recv()
    close()

UDP:
getaddrinfo()
socket()
bind()            (接收端)
sendto() / recvfrom()
close()

1.3.2 · 3.1 getaddrinfo() — 准备地址信息#

int getaddrinfo(const char *node,      // 主机名或 IP
                const char *service,    // 端口号或服务名 "http"
                const struct addrinfo *hints,
                struct addrinfo **res); // 结果链表
  • hints.ai_flags = AI_PASSIVE + node = NULL → 绑定本机所有接口
  • hints.ai_family = AF_UNSPEC → IPv4/IPv6 皆可
  • 用完后 freeaddrinfo(res)

1.3.3 · 3.2 socket() — 创建套接字#

int socket(int domain, int type, int protocol);
// 通常直接用 getaddrinfo 返回的值:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

1.3.4 · 3.3 bind() — 绑定端口#

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
  • 服务器端必须 bind,客户端通常不需要(内核自动分配)
  • “Address already in use” 错误 → 设置 SO_REUSEADDR:
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

1.3.5 · 3.4 connect() — 连接远端#

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
  • 客户端调用,无需先 bind()
  • 返回 -1 表示出错

1.3.6 · 3.5 listen() — 开始监听#

int listen(int sockfd, int backlog);
  • backlog: 等待队列大小(通常设 5~20)

1.3.7 · 3.6 accept() — 接受连接#

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 返回新的 socket fd 用于通信,原 sockfd 继续 listen
  • addr 填入对端地址信息

1.3.8 · 3.7 send() / recv() — TCP 数据传输#

int send(int sockfd, const void *msg, int len, int flags);
int recv(int sockfd, void *buf, int len, int flags);
  • send() 返回实际发送字节数,可能小于 len(需处理 partial send)
  • recv() 返回 0 表示对端关闭连接
  • 两者都是阻塞调用

1.3.9 · 3.8 sendto() / recvfrom() — UDP 数据传输#

int sendto(int sockfd, const void *msg, int len, unsigned int flags,
           const struct sockaddr *to, socklen_t tolen);

int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
             struct sockaddr *from, int *fromlen);
  • UDP 无连接,需要指定目标/来源地址
  • 如果对 UDP socket 调用了 connect(),也可以用 send()/recv()

1.3.10 · 3.9 close() / shutdown() — 关闭连接#

close(sockfd);  // 关闭 fd,不再读写

int shutdown(int sockfd, int how);
// how: 0 = 禁止接收, 1 = 禁止发送, 2 = 两者都禁止
  • shutdown() 只改变可用性,不释放 fd(仍需 close()

1.3.11 · 3.10 辅助调用#

// 获取对端地址
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

// 获取本机主机名
int gethostname(char *hostname, size_t size);

1.4 · 4. 客户端-服务器模型#

1.4.1 · TCP Server 基本模式#

// 1. 准备地址
getaddrinfo(NULL, PORT, &hints, &servinfo);

// 2. 遍历结果,创建 socket 并 bind
for (p = servinfo; p != NULL; p = p->ai_next) {
    sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
    bind(sockfd, p->ai_addr, p->ai_addrlen);
}

// 3. 监听
listen(sockfd, BACKLOG);

// 4. accept 循环
while (1) {
    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
    if (!fork()) {          // 子进程
        close(sockfd);      // 子进程不需要 listener
        send(new_fd, "Hello, world!", 13, 0);
        close(new_fd);
        exit(0);
    }
    close(new_fd);          // 父进程不需要这个连接
}

1.4.2 · TCP Client 基本模式#

getaddrinfo(hostname, PORT, &hints, &servinfo);
for (p = servinfo; p != NULL; p = p->ai_next) {
    sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    connect(sockfd, p->ai_addr, p->ai_addrlen);
}
freeaddrinfo(servinfo);
recv(sockfd, buf, MAXDATASIZE-1, 0);
close(sockfd);

1.4.3 · UDP Server/Client#

  • Server: socket()bind()recvfrom()close()
  • Client: socket()sendto()close()
  • 无需 listen() / accept() / connect()

1.5 · 5. 进阶技术#

1.5.1 · 5.1 阻塞与非阻塞#

默认 socket 是阻塞的(accept()recv() 等会 sleep 等待数据)

设置非阻塞:

#include <fcntl.h>
fcntl(sockfd, F_SETFL, O_NONBLOCK);
  • 非阻塞下 recv() 无数据返回 -1,errno 设为 EAGAINEWOULDBLOCK
  • 忙轮询浪费 CPU,应使用 I/O 多路复用

1.5.2 · 5.2 poll() — I/O 多路复用#

#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

struct pollfd {
    int   fd;       // socket fd
    short events;   // 监听事件 (POLLIN, POLLOUT)
    short revents;  // 返回的就绪事件
};
事件含义
POLLIN有数据可读 / 有新连接可 accept
POLLOUT可写不阻塞
POLLHUP对端关闭
  • timeout 毫秒,-1 表示永久等待,0 表示立即返回

1.5.3 · 5.3 select() — 经典 I/O 多路复用#

int select(int numfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

// fd_set 操作宏
FD_SET(fd, &set);    // 添加 fd 到集合
FD_CLR(fd, &set);    // 从集合移除 fd
FD_ISSET(fd, &set);  // 检查 fd 是否就绪
FD_ZERO(&set);       // 清空集合
  • select()修改传入的 fd_set,需要每次调用前重新拷贝
  • listener socket 就绪 = 有新连接待 accept
  • 已连接 socket 就绪且 recv() 返回 0 = 对端关闭

大量连接时 poll()/select() 性能差,考虑 epoll (Linux) / kqueue (BSD) 或 libevent

1.5.4 · 5.4 处理 Partial Send#

send() 可能不会一次发完所有数据,需要循环发送:

int sendall(int s, char *buf, int *len) {
    int total = 0;
    int bytesleft = *len;
    int n;

    while (total < *len) {
        n = send(s, buf + total, bytesleft, 0);
        if (n == -1) break;
        total += n;
        bytesleft -= n;
    }
    *len = total;
    return n == -1 ? -1 : 0;
}

1.5.5 · 5.5 数据封装与协议设计#

TCP 是字节流,没有消息边界。接收端必须自己划分消息,常见方法:

  1. 固定长度消息:简单但浪费带宽
  2. 长度前缀:header 中包含 payload 长度(推荐)
  3. 分隔符:如 \r\n(文本协议常用)

长度前缀示例:

[1 byte len][8 bytes username][N bytes message]
    18        B e n j a m i n   H e y ...
  • 长度字段使用 Network Byte Order
  • 发送用 sendall()
  • 接收需循环 recv() 直到收满完整包(可能收到 partial packet)

1.5.6 · 5.6 序列化#

  • 文本编码: 人可读,但慢且占空间
  • 原始二进制: 快,但不可移植(字节序、对齐、浮点格式差异)
  • 推荐使用标准序列化方案(如 Protocol Buffers, MessagePack)

1.5.7 · 5.7 广播 (Broadcast)#

  • 仅限 UDP,使用 setsockopt() 设置 SO_BROADCAST
  • 发送到广播地址(如 192.168.1.255),同一子网所有 listener 都能收到