作者: andylin02
关键词: UNIX网络编程, 套接字, TCP/IP, 并发服务器, I/O复用, unpv13e


一、这本书讲了什么?

1.1 书籍概述

《UNIX网络编程 卷1:套接字联网API》(第3版)是由网络专家Bill Fenner和Andrew M. Rudoff对已故UNIX网络专家W. Richard Stevens博士的经典著作进行全面修订的权威之作。全书以套接字编程为核心,系统解析TCP、UDP、SCTP等协议实现原理,新增了IPv6与密钥管理套接字相关内容,并删除了X/Open传输接口的内容。

这本书的目标读者是那些希望自己编写的程序能使用称为套接字(socket)的API进行彼此通信的人。书中的所有示例都是在UNIX系统上测试通过的真实的、可运行的代码,继承了Stevens一直强调的理念:“学习网络编程的方法就是下载这些程序,对其进行修改和改进。只有这样实际编写代码才能深入理解有关概念和技术”。

1.2 全书内容结构

本书内容详尽且具权威性,共分为四个主要部分:

第一部分:简介和TCP/IP(第1-2章)
  • 第1章:网络编程的基本概念、OSI模型、TCP/IP协议栈概述,以及一个简单的时间获取客户程序示例
  • 第2章:传输层:TCP、UDP和SCTP协议详解,包括TCP连接的建立与终止、TIME_WAIT状态等
第二部分:基本套接口编程(第3-9章)
  • 第3章:套接字编程简介——套接字地址结构、值-结果参数、字节排序函数
  • 第4章:基本TCP套接字编程——socket、connect、bind、listen、accept、close函数
  • 第5章:TCP客户/服务器程序示例
  • 第6章:I/O复用——select和poll函数
  • 第7章:套接字选项
  • 第8章:基本UDP套接字编程
  • 第9章:SCTP套接字编程(第3版新增)
第三部分:高级套接字编程(第10-21章)
  • 第10-13章:IPv4与IPv6互操作性、守护进程、高级I/O函数
  • 第14章:非阻塞I/O
  • 第15章:信号驱动I/O
  • 第16章:线程(Threads)
  • 第17-21章:IP选项、原始套接口、数据链路访问、广播与多播
第四部分:客户/服务器程序设计(第22-31章)
  • 第22-26章:各种客户/服务器设计范式(迭代服务器、并发服务器、预先派生进程/线程服务器等)
  • 第27-30章:客户端设计、流设备驱动机制等

二、学完本书能掌握哪些关键知识?

2.1 核心技术点清单

技术领域 具体知识点 难度等级
网络基础 TCP/IP协议栈、OSI模型、端口、IP地址、字节序 ⭐⭐
套接字基础 socket/bind/listen/accept/connect/close ⭐⭐
TCP编程 三次握手、四次挥手、TIME_WAIT状态、流量控制 ⭐⭐⭐
UDP编程 无连接通信、数据报边界、异步错误 ⭐⭐
I/O复用 select/poll/epoll模型 ⭐⭐⭐⭐
非阻塞I/O fcntl设置非阻塞、轮询机制 ⭐⭐⭐
信号驱动I/O SIGIO信号、异步通知 ⭐⭐⭐
并发服务器 多进程、多线程、线程池 ⭐⭐⭐⭐
协议无关性 IPv4/IPv6兼容、getaddrinfo函数族 ⭐⭐⭐
广播与多播 组播加入/离开、多播路由 ⭐⭐⭐
原始套接口 ICMP协议实现、ping/traceroute实现 ⭐⭐⭐⭐
高级I/O readv/writev散读集写、sendfile零拷贝 ⭐⭐⭐⭐

2.2 各章节核心知识点详解

第2章:TCP连接的建立与终止

TCP通过三次握手建立连接:

三次握手:

客户端                          服务器
CLOSED                         LISTEN
   |                              |
   |------- SYN (seq=x) --------> |  (第一次握手)
SYN_SENT                       SYN_RCVD
   |                              |
   |<----- SYN (seq=y, ACK=x+1) --|  (第二次握手)
ESTABLISHED                    SYN_RCVD
   |                              |
   |------- ACK (ack=y+1) -------> (第三次握手)
ESTABLISHED                    ESTABLISHED

四次挥手:

主动关闭方                     被动关闭方
ESTABLISHED                    ESTABLISHED
   |                              |
   |------- FIN (seq=u) -------> |  (第一次挥手)
FIN_WAIT_1                     CLOSE_WAIT
   |                              |
   |<------- ACK (ack=u+1) -----|  (第二次挥手)
FIN_WAIT_2                     CLOSE_WAIT
   |                              |
   |<------- FIN (seq=v) -------|  (第三次挥手)
TIME_WAIT                      LAST_ACK
   |                              |
   |------- ACK (ack=v+1) -----> |  (第四次挥手)
TIME_WAIT (2MSL)               CLOSED
   |
CLOSED
第3章:套接字地址结构
#include <netinet/in.h>

struct in_addr {
    in_addr_t s_addr;           // 32位的IPv4地址,网络字节序
};

struct sockaddr_in {
    uint8_t        sin_len;     // 结构长度(16字节)
    sa_family_t    sin_family;  // 地址族:AF_INET
    in_port_t      sin_port;    // 16位TCP/UDP端口号,网络字节序
    struct in_addr sin_addr;    // 32位IPv4地址,网络字节序
    char           sin_zero[8]; // 未使用,用于填充
};

💡 关键点:sin_family、sin_port和sin_addr是POSIX规范必需的三个字段。当向套接字函数传递地址结构指针时,必须强制转换成 (struct sockaddr*) 类型,这是因为套接字函数早于ANSI C标准。

第6章:I/O复用

I/O复用是实现高并发服务器的关键技术,select/poll/epoll是对比:

特性 select poll epoll
最大连接数 通常1024(FD_SETSIZE) 无限制 无限制
效率 O(n) O(n) O(1)
触发方式 水平触发(LT) 水平触发(LT) 边缘触发(ET)/水平触发(LT)
内存拷贝 每次调用都拷贝 每次调用都拷贝 调用epoll_wait时拷贝
第22-30章:客户/服务器设计范式

本书第22-30章全面探讨了8种客户/服务器设计范式:

范式 描述 优点 缺点
TCP迭代服务器 一次只服务一个客户端 简单 有阻塞,不能同时处理多个客户
每个客户fork一个子进程 新客户到来时创建新进程 稳定,隔离性好 进程创建开销大
预先派生子进程 启动时创建进程池 减少fork开销 存在惊群问题
每个客户创建一个线程 使用pthread_create 轻量级 需要同步
预先创建线程池 启动时创建线程池 高性能,减少创建开销 实现复杂
I/O复用(select/poll) 单进程多路复用 无并发同步问题 大规模连接效率低
I/O复用(epoll) 事件驱动机制 高效处理大量连接 Linux特有
混合模型 复用+多线程池 最高性能 实现最复杂

三、需要具备什么基础能力?

3.1 硬性要求(必须掌握)

能力项 具体要求 检查标准
C语言 指针、结构体、动态内存分配、函数指针 能独立编写100行以上的C程序
UNIX/Linux基础 基本命令、文件权限、I/O重定向、管道 能熟练使用ls/cd/grep/find等命令
编译工具 gcc基本用法、Makefile 能手动编译多文件C程序
调试能力 gdb基本使用 能设置断点、查看变量、单步执行

3.2 前置知识要求

建议了解以下基本网络概念:OSI模型、TCP/IP各层功能和IP寻址。能够用C语言编写、编译和调试程序,包括使用指针、结构体和系统调用。熟悉UNIX系统编程概念,如进程创建(fork、exec)和基本输入/输出处理。

3.3 推荐的阅读路线

如果基础不够扎实,建议按以下顺序建立知识体系:

  1. 先读《The C Programming Language》(K&R) —— 打好C语言基础
  2. 再读《UNIX环境高级编程》(APUE) —— 学习UNIX系统编程基础
  3. 最后读《UNIX网络编程》(UNP) —— 进阶网络编程

⚠️ 注意:本书不适合完全的编程初学者。如果你刚开始学编程,建议先完成前两步。

四、需要准备什么工作?

4.1 软件环境搭建

第一步:安装编译工具链
# Ubuntu/Debian
sudo apt update
sudo apt install build-essential gcc gdb make

# 验证安装
gcc --version
make --version
第二步:下载并编译本书源代码

本书源代码可从 http://www.unpbook.com/ 下载得到unpv13e.tar.gz:

# 1. 下载源码并解压
tar zxvf unpv13e.tar.gz
cd unpv13e

# 2. 添加执行权限并配置
chmod u+x configure
./configure

# 3. 编译lib文件
cd lib
make          # 出现大量warning可忽略
cd ../libfree
make          # 可能遇到错误(见下方解决方案)
cd ../libgai
make

# 4. 回到主目录
cd ..

⚠️ 常见编译错误:在libfree目录下编译inet_ntop.c时可能遇到size_t类型不匹配错误:

// 将inet_ntop.c第60行的
size_t size;
// 修改为
socklen_t size;
第三步:设置头文件和库路径
# 1. 修改unp.h,将#include "../config.h"改为#include "config.h"
vim lib/unp.h

# 2. 拷贝头文件到系统目录
sudo cp config.h /usr/include
sudo cp lib/unp.h /usr/include

# 3. 拷贝静态库
sudo cp libunp.a /usr/lib
sudo cp libunp.a /usr/lib64
第四步:开启daytime服务并测试
# 安装xinetd
sudo apt-get install xinetd

# 编辑daytime配置
sudo vim /etc/xinetd.d/daytime
# 将两个disable后面的yes改成no

# 重启xinetd服务
sudo service xinetd restart

# 测试示例代码
cd intro
gcc daytimetcpcli.c -o daytimetcpcli -lunp
./daytimetcpcli 127.0.0.1

如果看到类似 01 JUL 2016 15:41:15 CST 的输出,说明环境配置成功。

五、学习注意事项与常见坑

5.1 常见的UNIX网络编程问题

问题类别 具体问题 解决方案
连接问题 如何建立和维护连接,如何处理中断的连接 使用心跳机制、设置超时重连
并发问题 如何处理多个客户端同时连接服务器 使用多进程、多线程或I/O复用
数据传输问题 分包、粘包问题 使用固定长度消息或消息分隔符
性能问题 如何优化网络传输速度,减少延迟 使用非阻塞I/O、零拷贝技术
安全性问题 如何保护网络通信的安全性 加密、身份验证、防攻击策略
错误处理 网络连接失败、数据传输异常 检查所有系统调用返回值
端口重用 bind报"address already in use" 设置SO_REUSEADDR套接字选项
跨平台问题 如何处理不同操作系统的网络特性 使用协议无关编程方法

5.2 理解TIME_WAIT的重要性

TIME_WAIT是TCP连接关闭时主动关闭方进入的状态,持续时间为2MSL(约1-4分钟)。TIME_WAIT状态有两个存在的理由:

  1. 可靠地实现TCP全双工连接的终止
  2. 允许老的重复分节在网络中消逝

当某端口处于TIME_WAIT状态时,是无法被绑定的(bind)。如果在项目中关闭服务器后希望立即重启,而程序却要等1-4分钟,SO_REUSEADDR套接字选项就可以用来解决这个问题,实现瞬间重启服务器。

5.3 错误处理最佳实践

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

void err_sys(const char* msg) {
    perror(msg);
    exit(1);
}

int Socket(int family, int type, int protocol) {
    int n;
    if ((n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return n;
}

int Accept(int fd, struct sockaddr* addr, socklen_t* len) {
    int n;
    // 处理EINTR:被信号中断时重试
    while ((n = accept(fd, addr, len)) < 0) {
        if (errno != EINTR)
            err_sys("accept error");
    }
    return n;
}

5.4 常见错误码速查

错误码 含义 典型场景
EINTR 系统调用被信号中断 accept、read、write被信号打断
EAGAIN/EWOULDBLOCK 资源暂时不可用 非阻塞socket读无数据、写缓冲区满
EADDRINUSE 地址已在使用 bind时端口已被占用
ECONNREFUSED 连接被拒绝 connect时目标端口无服务
ETIMEDOUT 连接超时 connect时SYN无响应
EPIPE 管道破裂 向已关闭的连接写数据

六、学习文档与读书笔记总结

6.1 核心概念辨析

字节序问题

网络字节序采用大端字节序,而系统使用的主机字节序可能是大端也可能是小端。需要使用转换函数:

函数 功能
htons() 主机字节序 → 网络字节序(16位)
htonl() 主机字节序 → 网络字节序(32位)
ntohs() 网络字节序 → 主机字节序(16位)
ntohl() 网络字节序 → 主机字节序(32位)
值-结果参数

套接字函数中经常出现“值-结果参数”,即参数既是输入值(告诉内核结构体的大小),又是输出结果(内核告诉应用程序实际存储了多少数据)。例如accept函数的第三个参数。

6.2 TCP协议栈架构图

┌─────────────────────────────────────────────────┐
│                   应用层                         │
│        HTTP │ FTP │ SMTP │ Telnet │ ...         │
├─────────────────────────────────────────────────┤
│                   传输层                         │
│              TCP │ UDP │ SCTP                   │
├─────────────────────────────────────────────────┤
│                   网络层                         │
│                    IP                           │
├─────────────────────────────────────────────────┤
│                   链路层                         │
│          Ethernet │ Wi-Fi │ PPP                 │
└─────────────────────────────────────────────────┘

6.3 完整的TCP回射服务器(多进程并发版)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BACKLOG 5
#define BUFFER_SIZE 1024

void sig_chld(int signo) {
    pid_t pid;
    int stat;
    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
        printf("child %d terminated\n", pid);
    return;
}

void do_service(int conn_fd) {
    char buffer[BUFFER_SIZE];
    ssize_t n;
    
    while ((n = read(conn_fd, buffer, sizeof(buffer) - 1)) > 0) {
        buffer[n] = '\0';
        printf("Received: %s", buffer);
        write(conn_fd, buffer, n);
    }
    close(conn_fd);
}

int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        exit(1);
    }
    
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(1);
    }
    
    if (listen(listen_fd, BACKLOG) < 0) {
        perror("listen");
        exit(1);
    }
    
    signal(SIGCHLD, sig_chld);
    printf("Server listening on port %d\n", PORT);
    
    while (1) {
        conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (conn_fd < 0) {
            if (errno == EINTR)
                continue;
            perror("accept");
            continue;
        }
        
        printf("Client connected: %s:%d\n", 
               inet_ntoa(client_addr.sin_addr), 
               ntohs(client_addr.sin_port));
        
        if (fork() == 0) {
            close(listen_fd);
            do_service(conn_fd);
            close(conn_fd);
            exit(0);
        }
        close(conn_fd);
    }
    
    close(listen_fd);
    return 0;
}

6.4 epoll I/O复用服务器框架

#include <sys/epoll.h>

#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024

int epoll_fd, listen_fd, conn_fd;
struct epoll_event ev, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];

// 1. 创建epoll实例
epoll_fd = epoll_create(1024);

// 2. 添加监听socket到epoll
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);

while (1) {
    // 3. 等待事件发生
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listen_fd) {
            conn_fd = accept(listen_fd, NULL, NULL);
            ev.events = EPOLLIN | EPOLLET;  // 边缘触发模式
            ev.data.fd = conn_fd;
            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev);
        } else {
            int fd = events[i].data.fd;
            int n = read(fd, buffer, BUFFER_SIZE);
            if (n == 0) {
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
            } else if (n > 0) {
                write(fd, buffer, n);
            }
        }
    }
}

6.5 学习资源推荐

  • 《UNIX网络编程 卷1:套接字联网API(第3版)》——导读(阿里云开发者社区):完整的目录导读和各章节链接
  • Unix网络编程 3rd vol1 读书笔记(阿里云开发者社区):记录重要内容和经过深入理解的知识点
  • 2021版!万字UNIX网络编程学习笔记(套接字篇)(腾讯云):涵盖环境搭建、套接字基础等
  • UNIX网络编程之旅-配置unp.h头文件环境(阿里云开发者社区):详细的环境搭建指导

七、常见问题快速诊断

7.1 运行时常见错误及解决方案

错误信息 原因 解决方案
connect error: Connection refused daytime服务未开启 开启xinetd中的daytime服务
address already in use 端口处于TIME_WAIT状态 设置SO_REUSEADDR套接字选项
fatal error: unp.h: No such file 未正确配置头文件 将unp.h和config.h复制到/usr/include
undefined reference to 'err_sys' 未链接libunp.a 编译时加上 -lunp 参数
inet_ntop.c: argument 'size' doesn't match prototype 类型不匹配 将size_t改为socklen_t

7.2 调试工具推荐

工具 用途
tcpdump / Wireshark 抓包分析网络协议
netstat -ni / netstat -nr 查看网络接口信息和路由表
strace 跟踪系统调用
gdb 调试程序
valgrind 检查内存泄漏

八、第一章预告

📌 下一篇:《UNIX网络编程》读书笔记(一):第一章 简介

下一篇将详细讲解:

  1. 客户/服务器模型:深入理解网络编程的基本架构,Web浏览器与服务器之间的通信是如何组织的
  2. OSI模型与TCP/IP协议栈:理解各层职责,搞清楚网络数据包在每一层是如何封装的
  3. 协议无关性:如何编写同时支持IPv4和IPv6的网络程序,getaddrinfo函数族的妙用
  4. 包裹函数设计模式:学习Steven大神如何优雅地处理错误,以及err_sys系列函数的实现原理
  5. 时间获取客户/服务器程序详解:逐行解读daytimetcpcli.cdaytimetcpsrv.c,分析每个系统调用的作用和潜在问题
  6. 测试环境配置:如何搭建和测试第一章的示例程序,验证TCP通信的基本流程
  7. 课后习题解析:第一章习题的解题思路和答案参考

学习目标:学完第一章后,你将能够:

  • 理解网络编程的基本架构——客户/服务器模型
  • 掌握包裹函数的错误处理设计模式
  • 成功编译并运行第一个网络程序
  • 为后续章节的深入学习打下坚实基础

敬请期待!


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐