杰瑞科技汇

Linux网络编程教程该怎么学?

目录

  1. 基础准备:环境与工具
    • 开发环境
    • 必备工具
  2. 核心概念:OSI 七层模型与 TCP/IP 协议栈
    • 网络分层思想
    • TCP/IP 协议栈详解
    • Socket:应用程序与网络的接口
  3. 入门篇:使用 Socket API 进行 TCP 编程
    • TCP 特点回顾
    • 编写第一个 TCP 服务器
    • 编写第一个 TCP 客户端
    • 完整代码示例与分析
    • 编译与运行
  4. 进阶篇:使用 Socket API 进行 UDP 编程
    • UDP 特点回顾
    • UDP 服务器与客户端代码示例
    • TCP vs. UDP
  5. 深入篇:I/O 模型与并发编程
    • 阻塞 I/O (Blocking I/O)
    • 非阻塞 I/O (Non-blocking I/O)
    • I/O 多路复用
      • select
      • poll
      • epoll (Linux 高性能网络的核心)
    • 多线程/多进程模型
  6. 实战项目:简单聊天室
    • 需求分析
    • 设计思路
    • 代码实现
  7. 高级主题与最佳实践
    • 原子操作与锁
    • setsockoptgetsockopt
    • 优雅关闭
    • 调试技巧 (strace, netstat, tcpdump)
  8. 推荐资源

基础准备:环境与工具

开发环境

  • 操作系统: Linux (推荐 Ubuntu, CentOS, 或其他发行版),Windows 下的 WSL 2 也是一个不错的选择。
  • 编程语言: C 语言 (网络编程的“母语”,最底层,最核心)。
  • 编译器: GCC (GNU Compiler Collection)。

必备工具

  • GCC: 用于编译 C 代码。

    Linux网络编程教程该怎么学?-图1
    (图片来源网络,侵删)
    # Ubuntu/Debian
    sudo apt-get update
    sudo apt-get install build-essential
    # CentOS/RHEL
    sudo yum groupinstall "Development Tools"
  • 文本编辑器: Vim, VS Code, Sublime Text 等,VS Code 配合 C/C++ 插件是现代且高效的选择。

  • 调试工具: GDB (GNU Debugger)。

  • 网络工具: netstat, ss, tcpdump, nc (netcat)。


核心概念:OSI 七层模型与 TCP/IP 协议栈

在写代码之前,理解网络通信的基本原理至关重要。

Linux网络编程教程该怎么学?-图2
(图片来源网络,侵删)

网络分层思想

为了管理复杂的网络通信,人们设计了分层的模型,每一层都建立在下一层之上,专注于自己的任务,这样,每一层的改变都不会影响到其他层。

TCP/IP 协议栈

这是当今互联网事实上的标准模型,它将功能分为了四层:

TCP/IP 四层模型 对应 OSI 七层模型 主要协议/功能 说明
应用层 应用层 HTTP, FTP, SMTP, DNS 面向用户的应用程序,提供特定的网络服务。
传输层 传输层 TCP, UDP 提供端到端的数据传输服务,TCP 可靠,UDP 不可靠但快速。
网络层 网络层 IP, ICMP, ARP 负责数据包的路由和转发,确保数据从源主机到达目标主机。
网络接口层 数据链路层 & 物理层 Ethernet, Wi-Fi 负责在同一个局域网内设备间的数据传输。

Socket:应用程序与网络的接口

Socket (套接字) 是操作系统提供给应用程序进行网络通信的 API (Application Programming Interface),你可以把它想象成一个“插座”,应用程序通过这个插座,可以“插上”网络线,收发数据。

在 Linux 中,一切皆文件,Socket 在编程中被视为一个 文件描述符,我们可以使用标准的 read(), write() 函数来对它进行读写操作,这大大简化了编程模型。

Linux网络编程教程该怎么学?-图3
(图片来源网络,侵删)

入门篇:使用 Socket API 进行 TCP 编程

TCP (Transmission Control Protocol) 是一种面向连接的、可靠的、基于字节流的传输层协议,它的特点是“三次握手”建立连接,“四次挥手”断开连接,并保证数据无差错、不丢失、不重复且按序到达。

编写第一个 TCP 服务器

服务器的工作流程通常是:

  1. 创建套接字
  2. 绑定地址和端口 (Bind)
  3. 监听连接 (Listen)
  4. 接受连接 (Accept) - 阻塞等待客户端连接
  5. 与客户端通信 (Read/Write)
  6. 关闭连接 (Close)

代码示例:tcp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字 (AF_INET for IPv4, SOCK_STREAM for TCP, 0 for IP)
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    address.sin_port = htons(PORT);        // 将端口号从主机字节序转换为网络字节序
    // 2. 绑定地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 3. 开始监听,最大连接数为 3
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 接受一个新连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    // 5. 与客户端通信
    int valread = read(new_socket, buffer, BUFFER_SIZE);
    printf("Message from client: %s\n", buffer);
    send(new_socket, "Hello from server", strlen("Hello from server"), 0);
    printf("Hello message sent\n");
    // 6. 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

编写第一个 TCP 客户端

客户端的工作流程相对简单:

  1. 创建套接字
  2. 连接服务器 (Connect)
  3. 与服务器通信 (Read/Write)
  4. 关闭连接 (Close)

代码示例:tcp_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IPv4 地址从文本转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 2. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    // 3. 发送数据
    send(sock, hello, strlen(hello), 0);
    printf("Hello message sent\n");
    // 4. 接收数据
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server message: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

编译与运行

  1. 保存文件: 将上面的代码分别保存为 tcp_server.ctcp_client.c
  2. 编译:
    gcc tcp_server.c -o tcp_server
    gcc tcp_client.c -o tcp_client
  3. 运行:
    • 首先在终端运行服务器:
      ./tcp_server

      你会看到 "Server listening on port 8080..."。

    • 然后在另一个新终端运行客户端:
      ./tcp_client
    • 观察输出:
      • 客户端终端会显示:
        Hello message sent
        Server message: Hello from server
      • 服务器终端会显示:
        Server listening on port 8080...
        Message from client: Hello from client
        Hello message sent

进阶篇:使用 Socket API 进行 UDP 编程

UDP (User Datagram Protocol) 是一种无连接的、不可靠的、数据报式的传输层协议,它不保证数据包的顺序或是否到达,但开销小,传输快。

UDP 服务器与客户端代码示例

UDP 的编程模型比 TCP 简单,没有 listen()accept() 的概念,客户端和服务器是对等的,都可以使用 sendto()recvfrom() 来收发数据。

代码示例:udp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8081
#define BUFFER_SIZE 1024
int main() {
    int sock;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    // 1. 创建套接字 (SOCK_DGRAM for UDP)
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);
    // 2. 绑定地址和端口
    if (bind(sock, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP Server listening on port %d...\n", PORT);
    int len, n;
    len = sizeof(cliaddr);
    // 3. 循环接收数据
    while (1) {
        n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
        buffer[n] = '\0';
        printf("Client : %s\n", buffer);
        sendto(sock, (const char *)"Hello from UDP server", strlen("Hello from UDP server"), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
        printf("Hello message sent.\n");
    }
    close(sock);
    return 0;
}

代码示例:udp_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8081
#define BUFFER_SIZE 1024
int main() {
    int sock;
    char *hello = "Hello from UDP client";
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr;
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0) {
        perror("inet_pton failed");
        exit(EXIT_FAILURE);
    }
    // 2. 发送数据
    sendto(sock, (const char *)hello, strlen(hello), MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("Hello message sent.\n");
    // 3. 接收数据
    int len;
    socklen_t servaddr_len = sizeof(servaddr);
    int n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&servaddr, &servaddr_len);
    buffer[n] = '\0';
    printf("Server : %s\n", buffer);
    close(sock);
    return 0;
}

TCP vs. UDP

特性 TCP (传输控制协议) UDP (用户数据报协议)
连接性 面向连接 无连接
可靠性 可靠 (保证数据顺序、无丢失) 不可靠 (不保证顺序和送达)
速度 较慢 较快
开销 较高 (头部20字节,有握手挥手) 较低 (头部8字节)
应用场景 Web浏览, 文件传输, 邮件 视频会议, 在线游戏, DNS查询

深入篇:I/O 模型与并发编程

一个简单的 TCP 服务器一次只能处理一个客户端连接,当它正在与客户端 A 通信时,客户端 B 的连接请求只能排队等待,为了同时处理多个客户端,我们需要引入并发编程。

阻塞 I/O (Blocking I/O)

这是我们前面例子中使用的默认模式。

  • accept(): 如果没有连接请求,进程会阻塞(暂停执行)。
  • read(): 如果没有数据可读,进程会阻塞
  • 缺点: 效率极低,一个进程在同一时间只能处理一个 I/O 操作。

非阻塞 I/O (Non-blocking I/O)

通过设置套接字为非阻塞模式,当 I/O 操作无法立即完成时,函数会立即返回一个错误码(如 EAGAINEWOULDBLOCK),而不是阻塞进程。

  • 缺点: 需要不断地轮询(循环调用 I/O 函数)来检查数据是否准备好,这会消耗大量 CPU 资源,效率依然不高。

I/O 多路复用

这是解决高并发的关键,它允许单个进程同时监视多个 I/O 流(多个套接字),并在其中任何一个“就绪”(可读、可写或出现异常)时通知进程。

select

  • 原理: 创建一个文件描述符集合(fd_set),通过 select() 函数将这个集合交给内核,内核会检查这些 fd 的状态,当有 fd 就绪时,select() 返回,并修改 fd_set 以指示哪些 fd 就绪了。
  • 缺点:
    1. 数量限制: fd_set 的大小是固定的(通常是 1024),限制了能监视的 fd 数量。
    2. 性能问题: 每次调用 select() 都需要将整个 fd_set 从用户空间拷贝到内核空间,并且内核需要线性扫描所有 fd,当 fd 数量很多时,性能会急剧下降。

poll

poll 改进了 select 的数量限制问题,它使用 pollfd 结构数组,理论上可以监视任意数量的 fd。

  • 缺点: 仍然存在性能问题,因为内核仍然需要线性扫描所有 pollfd 结构。

epoll (Linux 高性能网络的核心)

epoll 是 Linux 特有的,是 selectpoll 的增强版,是构建高性能网络服务器的首选。

  • 原理:
    1. epoll_create(): 在内核中创建一个 epoll 实例,返回一个 fd。
    2. epoll_ctl(): 向 epoll 实例中添加、修改或删除要监视的 fd。
    3. epoll_wait(): 阻塞等待,直到被监视的 fd 中有事件发生(如可读、可写),它只返回发生事件的 fd,无需扫描所有 fd。
  • 优点:
    1. 没有数量限制: 所能监视的 fd 数量只受系统内存限制。
    2. 效率高: epoll_wait() 只返回就绪的 fd,避免了 selectpoll 的线性扫描。
    3. 支持边缘触发 (Edge-Triggered, ET): 这是 epoll 最强大的特性,LT(水平触发)是默认的,只要 fd 处于就绪状态,每次 epoll_wait() 都会返回它,而 ET 模式下,只有 fd 从不可变为可(状态发生变化)时,epoll_wait() 才会通知一次,这要求我们必须一次性将数据读完,否则可能会错过事件,但能极大提高效率。

多线程/多进程模型

I/O 多路复用解决了“监视”多个连接的问题,但“处理”这些连接仍然需要并发。

  • 多进程模型 (fork):
    • accept() 一个连接后,fork() 一个子进程来处理该连接。
    • 优点:编程简单,进程间数据隔离,安全性高。
    • 缺点:创建进程的开销大,进程间通信复杂。
  • 多线程模型 (pthread):
    • accept() 一个连接后,创建一个新线程来处理该连接。
    • 优点:创建线程的开销比进程小,共享内存方便。
    • 缺点:线程间共享数据需要加锁,编程更复杂,有死锁风险。
  • 线程池模型 (I/O 多路复用 + 线程池):
    • 这是最常用的高性能模型,主线程负责 epoll_wait(),当有连接就绪时,将任务(fd)放入一个任务队列。
    • 一个预先创建好的线程池从队列中取出任务,并进行处理。
    • 优点:避免了频繁创建/销毁线程的开销,资源利用率高,是 C10k 问题的标准解决方案。

实战项目:简单聊天室

让我们用 epoll + 多线程来实现一个简单的、支持多客户端的聊天室。

需求分析

  1. 服务器启动,监听指定端口。
  2. 多个客户端可以连接到服务器。
  3. 任何一个客户端发送的消息,服务器会广播给所有其他在线客户端。
  4. 客户端断开连接时,服务器能感知并移除它。

设计思路

  1. 主线程: 负责 epoll 的创建、事件循环 (epoll_wait) 和新连接的接受 (accept),当有新连接时,将其 fd 添加到 epoll 实例中,并设置为 ET (Edge-Triggered) 模式。
  2. 工作线程池: 负责处理已连接的客户端的读写事件,从 epoll_wait 返回后,将 fd 放入一个任务队列。
  3. 任务队列: 线程安全的队列,用于在主线程和工作线程之间传递 fd
  4. 用户列表: 一个全局数据结构(如链表),存储所有在线客户端的 fd 和信息,当有客户端发来消息时,遍历此列表进行广播。

代码实现 (简化版)

这是一个非常复杂的工程,这里只展示核心逻辑。

chat_server.c (核心逻辑)

#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 <sys/epoll.h>
#include <fcntl.h>
#include <pthread.h>
#define MAX_EVENTS 1024
#define PORT 8888
#define BUFFER_SIZE 1024
// 设置 fd 为非阻塞
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL");
        return -1;
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL O_NONBLOCK");
        return -1;
    }
    return 0;
}
// 全局变量和函数声明 (线程安全、用户列表等)
// ...
int main() {
    int listen_fd, conn_fd;
    struct epoll_event ev, events[MAX_EVENTS];
    int epoll_fd;
    // 1. 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); // 直接创建为非阻塞
    if (listen_fd < 0) { /* ... error ... */ }
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_fd, SOMAXCONN);
    // 2. 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) { /* ... error ... */ }
    // 3. 添加监听 fd 到 epoll
    ev.events = EPOLLIN; // 监听可读事件
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) { /* ... error ... */ }
    printf("Chat server started on port %d\n", PORT);
    // 4. 事件循环
    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 阻塞等待
        if (nfds < 0) { /* ... error ... */ }
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listen_fd) {
                // 新连接
                struct sockaddr_in client_addr;
                socklen_t client_len = sizeof(client_addr);
                while ((conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len)) > 0) {
                    printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                    set_nonblocking(conn_fd); // 设置新连接为非阻塞
                    ev.events = EPOLLIN | EPOLLET; // ET 模式
                    ev.data.fd = conn_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev);
                }
            } else if (events[i].events & EPOLLIN) {
                // 已连接的 fd 有数据可读 (ET模式)
                int client_fd = events[i].data.fd;
                char buffer[BUFFER_SIZE];
                ssize_t count;
                // ET 模式必须循环读取,直到返回 EAGAIN
                while ((count = read(client_fd, buffer, BUFFER_SIZE - 1)) > 0) {
                    buffer[count] = '\0';
                    printf("Received from client %d: %s\n", client_fd, buffer);
                    // TODO: 将广播任务放入线程池队列
                }
                if (count == 0) {
                    // 客户端关闭连接
                    printf("Client %d disconnected\n", client_fd);
                    close(client_fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                } else if (count < 0 && errno != EAGAIN) {
                    // 发生错误
                    perror("read");
                    close(client_fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                }
            }
            // TODO: 处理 EPOLLOUT 事件 (发送数据)
        }
    }
    close(listen_fd);
    close(epoll_fd);
    return 0;
}

注意: 这是一个简化版本,省略了线程池、任务队列、用户列表、广播逻辑和线程同步(互斥锁)等关键部分,实现一个完整的聊天室是本教程的最佳实践项目。


高级主题与最佳实践

原子操作与锁

在多线程/多进程服务器中,多个线程可能同时访问共享资源(如用户列表、全局计数器),这会导致 竞态条件,必须使用 (如 pthread_mutex_t)来保护这些共享资源,确保同一时间只有一个线程能访问。

setsockoptgetsockopt

这两个函数用于设置和获取套接字的选项。

  • 常用选项:
    • SO_REUSEADDR: 防止 "Address already in use" 错误,允许快速重启服务器。
    • SO_KEEPALIVE: 启动 TCP 的保活机制,检测死连接。
    • TCP_NODELAY: 禁用 Nagle 算法,减少小数据包的延迟,适合实时性要求高的场景。

优雅关闭

当服务器需要关闭时,不能简单地 exit(),应该:

  1. 停止接受新连接 (shutdown(listen_fd, SHUT_RD))。
  2. 遍历所有已连接的客户端,向它们发送一个 "Server is shutting down" 的消息。
  3. 等待客户端处理完数据并主动断开连接。
  4. 超时后,强制关闭所有连接。
  5. 释放所有资源(内存、套接字等)。

调试技巧

  • strace: 跟踪程序调用的系统调用和接收到的信号。
    strace -e trace=network ./your_server
  • netstat / ss: 查看网络连接状态。
    ss -tulpn | grep :8080
  • tcpdump: 抓取和分析网络数据包。
    tcpdump -i lo -nn port 8080
  • GDB: 使用 GDB 的 attach 命令可以附加到正在运行的进程进行调试。

推荐资源

书籍

  • 《UNIX 网络编程 卷1:套接字联网API (第3版)》 - by W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. (圣经级著作,必读)
  • 《Linux多线程服务端编程》 - by 陈硕. (国内经典,讲解 C++ 实现,但设计思想和并发模型对 C 语言同样适用)
  • 《TCP/IP详解 卷1:协议》 - by W. Richard Stevens. (深入理解 TCP/IP 协议栈)

在线资源

  • Beej's Guide to Network Programming: 一份非常友好、实用的英文网络编程入门指南,有中文翻译版。
  • man 手册: Linux 下最权威的文档,使用 man 7 socket, man 2 accept, man 2 epoll 等命令查阅系统调用和库函数的详细说明。
  • GitHub: 搜索一些简单的网络库或项目,如 libevent, muduo (陈硕的 C++ 网络库),阅读其源码是提升水平的绝佳方式。

Linux 网络编程是一个广阔而深入的领域,本教程为你铺平了从基础到进阶的道路,请务必:

  1. 动手实践: 把每个例子都敲一遍,修改它,破坏它,理解它。
  2. 深入阅读: 阅读《UNIX 网络编程》,理解其背后的设计哲学。
  3. 挑战项目: 尝试实现一个简单的 HTTP 服务器、一个 RPC 框架,或者一个聊天室。

祝你学习愉快!

分享:
扫描分享到社交APP
上一篇
下一篇