learn.ochresign
CODE

示例代码

各篇教程里用到的全部代码,都在 Linux 下编译运行过,教程里贴出的输出就是实际结果。按教程分组,点文件名展开查看,也可以直接下载。

任务 3:现代 C++ 五个小例子(RAII、智能指针、移动、lambda)

5 个文件 · 出自 这篇教程

raii.cpp 49 行

下载 raii.cpp

#include <fcntl.h>
#include <unistd.h>

#include <iostream>
#include <stdexcept>

// 不用 RAII:每条退出路径都要记得 close
void withoutRaii(bool fail) {
    int fd = ::open("/etc/hostname", O_RDONLY);
    std::cout << "opened fd " << fd << std::endl;
    if (fail) {
        throw std::runtime_error("something went wrong");  // 忘了 close,fd 泄漏
    }
    ::close(fd);
    std::cout << "closed fd " << fd << std::endl;
}

// 用 RAII:构造时拿到 fd,析构时一定 close
class File {
public:
    explicit File(const char* path) : fd_(::open(path, O_RDONLY)) {
        std::cout << "File opened fd " << fd_ << std::endl;
    }
    ~File() {
        if (fd_ >= 0) ::close(fd_);
        std::cout << "File closed fd " << fd_ << std::endl;
    }
    File(const File&) = delete;             // 禁止拷贝:否则两个对象会 close 同一个 fd
    File& operator=(const File&) = delete;
    int fd() const { return fd_; }

private:
    int fd_;
};

void withRaii(bool fail) {
    File f("/etc/hostname");
    if (fail) {
        throw std::runtime_error("something went wrong");  // f 的析构函数照样会执行
    }
    std::cout << "using fd " << f.fd() << std::endl;
}  // 离开作用域,f 析构

int main() {
    try { withoutRaii(true); } catch (const std::exception& e) { std::cout << "caught: " << e.what() << std::endl; }
    try { withRaii(true); } catch (const std::exception& e) { std::cout << "caught: " << e.what() << std::endl; }
    withRaii(false);
    return 0;
}
unique.cpp 28 行

下载 unique.cpp

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Connection {
    explicit Connection(int id) : id(id) { std::cout << "Connection " << id << " created" << std::endl; }
    ~Connection() { std::cout << "Connection " << id << " destroyed" << std::endl; }
    int id;
};

int main() {
    {
        std::unique_ptr<Connection> a = std::make_unique<Connection>(1);
        std::cout << "a->id = " << a->id << std::endl;
        // std::unique_ptr<Connection> b = a;          // 编译错误:unique_ptr 不能拷贝
        std::unique_ptr<Connection> b = std::move(a);   // 可以移动:所有权从 a 转给 b
        std::cout << "a is " << (a ? "not null" : "null") << ", b->id = " << b->id << std::endl;
    }  // b 离开作用域,Connection 1 被自动 delete
    std::cout << "--- vector of unique_ptr ---" << std::endl;
    std::vector<std::unique_ptr<Connection>> conns;
    conns.push_back(std::make_unique<Connection>(2));
    conns.push_back(std::make_unique<Connection>(3));
    conns.erase(conns.begin());  // 从容器里删掉,就自动销毁
    std::cout << "conns.size() = " << conns.size() << std::endl;
    std::cout << "--- end of main ---" << std::endl;
    return 0;
}
shared.cpp 43 行

下载 shared.cpp

#include <iostream>
#include <memory>

struct Node {
    explicit Node(const char* name) : name(name) { std::cout << name << " created" << std::endl; }
    ~Node() { std::cout << name << " destroyed" << std::endl; }
    const char* name;
    std::shared_ptr<Node> next;  // 强引用
    std::weak_ptr<Node> prev;    // 弱引用:不增加计数
};

int main() {
    std::cout << "--- shared_ptr 计数 ---" << std::endl;
    {
        auto a = std::make_shared<Node>("A");
        std::cout << "use_count = " << a.use_count() << std::endl;
        {
            auto b = a;  // 拷贝:计数 +1
            std::cout << "use_count = " << a.use_count() << std::endl;
        }  // b 析构:计数 -1
        std::cout << "use_count = " << a.use_count() << std::endl;
    }  // a 析构:计数变成 0,对象被销毁

    std::cout << "--- 循环引用:两个 shared_ptr 互相指着 ---" << std::endl;
    {
        auto x = std::make_shared<Node>("X");
        auto y = std::make_shared<Node>("Y");
        x->next = y;
        y->next = x;  // 循环了
    }  // 看输出:X 和 Y 都没有被销毁,内存泄漏
    std::cout << "--- 用 weak_ptr 打破循环 ---" << std::endl;
    {
        auto p = std::make_shared<Node>("P");
        auto q = std::make_shared<Node>("Q");
        p->next = q;
        q->prev = p;  // 反向用 weak_ptr
        if (auto locked = q->prev.lock()) {  // 使用前先 lock,拿到一个临时的 shared_ptr
            std::cout << "Q's prev is " << locked->name << std::endl;
        }
    }
    std::cout << "--- end of main ---" << std::endl;
    return 0;
}
move.cpp 53 行

下载 move.cpp

#include <cstring>
#include <iostream>
#include <utility>
#include <vector>

// 一个自己管理内存的缓冲区,演示拷贝和移动的区别
class Buffer {
public:
    explicit Buffer(size_t size) : data_(new char[size]), size_(size) {
        std::cout << "construct " << size_ << " bytes" << std::endl;
    }
    ~Buffer() { delete[] data_; }

    // 拷贝构造:新开一块内存,把内容复制过来(深拷贝,慢)
    Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
        std::memcpy(data_, other.data_, size_);
        std::cout << "copy " << size_ << " bytes" << std::endl;
    }

    // 移动构造:直接把对方的指针拿过来,再把对方置空(快,不复制数据)
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
        std::cout << "move " << size_ << " bytes" << std::endl;
    }

    Buffer& operator=(const Buffer&) = delete;
    Buffer& operator=(Buffer&&) = delete;
    size_t size() const { return size_; }

private:
    char* data_;
    size_t size_;
};

Buffer makeBuffer() {
    Buffer b(1024);
    return b;  // 编译器会直接在调用方构造(返回值优化),连移动都省了
}

int main() {
    Buffer a(1024 * 1024);
    Buffer b = a;             // a 还要用,只能拷贝
    Buffer c = std::move(a);  // a 不再用了,把它的内存“搬”给 c
    std::cout << "a.size() = " << a.size() << ", c.size() = " << c.size() << std::endl;
    Buffer d = makeBuffer();
    std::cout << "--- vector ---" << std::endl;
    std::vector<Buffer> v;
    v.reserve(2);
    v.push_back(Buffer(16));   // 临时对象是右值,自动走移动
    v.emplace_back(32);        // 直接在 vector 里构造,连移动都没有
    return 0;
}
lambda.cpp 64 行

下载 lambda.cpp

#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>

// 一个“按钮”:被点击时调用事先设置好的回调。网络库里的 Channel 就是这个思路
class Button {
public:
    using Callback = std::function<void(int)>;
    void setOnClick(Callback cb) { onClick_ = std::move(cb); }
    void click(int times) {
        if (onClick_) onClick_(times);
    }

private:
    Callback onClick_;
};

int add(int a, int b) { return a + b; }

int main() {
    // 1. 最简单的 lambda:[捕获](参数) { 函数体 }
    auto square = [](int x) { return x * x; };
    std::cout << "square(5) = " << square(5) << std::endl;

    // 2. 按值捕获 [=] 和按引用捕获 [&]
    int count = 0;
    auto byValue = [count]() { return count + 1; };  // 拷了一份 count,之后外面怎么改都不影响
    auto byRef = [&count]() { ++count; };            // 引用外面的 count,能修改它
    byRef();
    byRef();
    std::cout << "count = " << count << ", byValue() = " << byValue() << std::endl;

    // 3. std::function 能装下任何“可调用的东西”:普通函数、lambda
    std::function<int(int, int)> op = add;
    std::cout << "op(2, 3) = " << op(2, 3) << std::endl;
    op = [](int a, int b) { return a * b; };
    std::cout << "op(2, 3) = " << op(2, 3) << std::endl;

    // 4. 回调:把“发生某事时做什么”交给别人保存,事件发生时由别人来调用
    Button button;
    int total = 0;
    button.setOnClick([&total](int times) {
        total += times;
        std::cout << "clicked " << times << " times, total " << total << std::endl;
    });
    button.click(1);
    button.click(3);

    // 5. 用 map 存“命令名 -> 处理函数”,这就是阶段 2 命令分发的雏形
    std::map<std::string, std::function<std::string(const std::string&)>> commands;
    commands["PING"] = [](const std::string&) { return std::string("PONG"); };
    commands["ECHO"] = [](const std::string& arg) { return arg; };
    for (std::string name : {"PING", "ECHO", "GET"}) {
        auto it = commands.find(name);
        if (it == commands.end()) {
            std::cout << name << " -> ERR unknown command" << std::endl;
        } else {
            std::cout << name << " -> " << it->second("hello") << std::endl;
        }
    }
    return 0;
}

任务 4:亲眼看到三次握手、四次挥手和端口

2 个文件 · 出自 这篇教程

lazy_server.py 12 行

下载 lazy_server.py

# 一个“偷懒”的服务器:接受连接后既不读也不关,用来观察 CLOSE_WAIT
import socket
import time

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9003))
s.listen()
print("listening on 9003")
conn, addr = s.accept()
print("accepted", addr, "- now sleeping 60s without closing")
time.sleep(60)
quick_client.py 9 行

下载 quick_client.py

# 连上 9003,发 3 个字节,马上关闭
import socket
import time

c = socket.create_connection(("127.0.0.1", 9003))
c.sendall(b"bye")
c.close()
print("client closed")
time.sleep(30)  # 先别退出,方便用 ss 观察

任务 5:跑通阻塞版 echo server,亲眼看到它的问题

1 个文件 · 出自 这篇教程

blocking_echo.cpp 41 行

下载 blocking_echo.cpp

// 第一课:最简单的阻塞 echo server,一次只能服务一个客户端
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>

int main() {
    // 1. socket:向系统要一个“插座”(文件描述符 fd)
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 2. bind:把插座绑定到 本机任意IP:8888
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8888);
    if (bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }

    // 3. listen:开始营业,等客人上门
    listen(listen_fd, 128);
    printf("listening on 8888\n");

    while (true) {
        // 4. accept:接待一个客人,拿到和他专属的 conn_fd(没人来就一直卡在这)
        int conn_fd = accept(listen_fd, nullptr, nullptr);
        printf("new client fd=%d\n", conn_fd);

        char buf[1024];
        while (true) {
            // 5. read:等客人说话(不说话就一直卡在这)
            ssize_t n = read(conn_fd, buf, sizeof(buf));
            if (n <= 0) break;            // 0 = 客人挂断,<0 = 出错
            write(conn_fd, buf, n);       // 6. write:原样说回去
        }
        close(conn_fd);
        printf("client fd=%d closed\n", conn_fd);
    }
}

任务 6:跑通 epoll 版 echo server,500 个连接全部回显

3 个文件 · 出自 这篇教程

nonblock_demo.cpp 33 行

下载 nonblock_demo.cpp

// 演示非阻塞 read:没数据时立刻返回 -1,errno 是 EAGAIN;有数据时正常读到。
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cerrno>
#include <cstdio>
#include <cstring>

int main() {
    // socketpair 创建一对互相连通的 socket:往 sv[0] 写的,能从 sv[1] 读到
    int sv[2];
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    // 把 sv[1] 设成非阻塞
    int flags = fcntl(sv[1], F_GETFL);
    fcntl(sv[1], F_SETFL, flags | O_NONBLOCK);

    char buf[100];
    ssize_t n = read(sv[1], buf, sizeof(buf));  // 还没人写,阻塞的话会永远卡在这里
    printf("1st read returned %zd, errno = %d (%s)\n", n, errno, strerror(errno));
    printf("EAGAIN = %d, so this means: no data right now, try again later\n", EAGAIN);

    write(sv[0], "hello", 5);
    n = read(sv[1], buf, sizeof(buf));
    printf("2nd read returned %zd: %.*s\n", n, static_cast<int>(n), buf);

    close(sv[0]);  // 关闭另一端
    n = read(sv[1], buf, sizeof(buf));
    printf("3rd read returned %zd (peer closed)\n", n);
    close(sv[1]);
    return 0;
}
epoll_echo.cpp 70 行

下载 epoll_echo.cpp

// 第二课:epoll 版 echo server,单线程同时服务成千上万个客户端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>

// 把 fd 设成非阻塞:read/accept 没数据时立刻返回,而不是卡住
static void set_nonblock(int fd) {
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8888);
    if (bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
    listen(listen_fd, 1024);
    set_nonblock(listen_fd);

    // 1. 创建 epoll 实例:一个“前台”,帮你盯住所有 fd
    int epfd = epoll_create1(0);

    // 2. 把监听 fd 交给前台盯着:有新连接时通知我(EPOLLIN = 可读)
    epoll_event ev{};
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
    printf("listening on 8888 (epoll)\n");

    epoll_event events[1024];
    char buf[4096];
    while (true) {
        // 3. epoll_wait:没事就睡,一旦有 fd 就绪就醒来,返回就绪的个数
        int n = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < n; ++i) {
            int fd = events[i].data.fd;

            if (fd == listen_fd) {
                // 4a. 监听 fd 就绪 = 有新客人:把排队的连接全部接进来
                while (true) {
                    int conn_fd = accept(listen_fd, nullptr, nullptr);
                    if (conn_fd < 0) break;   // EAGAIN:已经接完了
                    set_nonblock(conn_fd);
                    epoll_event cev{};
                    cev.events = EPOLLIN;
                    cev.data.fd = conn_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
                }
            } else {
                // 4b. 普通连接就绪 = 客人说话了:读出来再写回去
                ssize_t r = read(fd, buf, sizeof(buf));
                if (r > 0) {
                    write(fd, buf, r);    // 简化:假设一次写得完,阶段 1 用 Buffer 解决
                } else if (r == 0 || errno != EAGAIN) {
                    // 客人挂断或出错:从 epoll 里移除并关闭
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
                    close(fd);
                }
            }
        }
    }
}
test_clients.py 27 行

下载 test_clients.py

# 测试 echo server:先同时建立 N 个连接,再挨个发一条消息,检查能不能收到原样的回显。
# 用法:python3 test_clients.py 名字 连接数,例如 python3 test_clients.py epoll 500
import socket
import sys

name = sys.argv[1]       # 只是打印时用的名字,比如 blocking 或 epoll
count = int(sys.argv[2])  # 要建立多少个连接

# 第一步:一口气建立 count 个连接,都先不发数据,模拟“很多人连着但不说话”
conns = []
for i in range(count):
    s = socket.create_connection(("127.0.0.1", 8888))
    s.settimeout(1)  # recv 最多等 1 秒,等不到就算失败
    conns.append(s)

# 第二步:挨个发一条不同的消息,看能不能收到一模一样的回显
ok = 0
for i, s in enumerate(conns):
    msg = f"hi{i}".encode()
    s.sendall(msg)
    try:
        if s.recv(100) == msg:
            ok += 1
    except socket.timeout:
        pass  # 1 秒内没收到回显

print(name, "echo ok:", ok, "/", count)

任务 7:课后练习(打印客户端地址、群聊、输出缓冲区)

4 个文件 · 出自 这篇教程

echo_addr.cpp 78 行

下载 echo_addr.cpp

// 练习 1:在 epoll echo 的基础上,打印每个客户端的 IP 和端口
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cerrno>
#include <cstdio>
#include <map>
#include <string>

static void set_nonblock(int fd) {
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

// 把 sockaddr_in 转成 "127.0.0.1:41844" 这样的字符串
static std::string to_ip_port(const sockaddr_in& a) {
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &a.sin_addr, ip, sizeof(ip));  // 二进制 IP -> 点分十进制字符串
    return std::string(ip) + ":" + std::to_string(ntohs(a.sin_port));  // 端口转回主机字节序
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8888);
    if (bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
    listen(listen_fd, 1024);
    set_nonblock(listen_fd);

    int epfd = epoll_create1(0);
    epoll_event ev{};
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
    printf("listening on 8888\n");

    std::map<int, std::string> peers;  // fd -> "ip:port",断开时还要打印
    epoll_event events[1024];
    char buf[4096];
    while (true) {
        int n = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < n; ++i) {
            int fd = events[i].data.fd;
            if (fd == listen_fd) {
                while (true) {
                    sockaddr_in peer{};
                    socklen_t len = sizeof(peer);  // 必须先设成结构体的大小,accept 会改写它
                    int conn_fd = accept(listen_fd, (sockaddr*)&peer, &len);
                    if (conn_fd < 0) break;
                    set_nonblock(conn_fd);
                    epoll_event cev{};
                    cev.events = EPOLLIN;
                    cev.data.fd = conn_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
                    peers[conn_fd] = to_ip_port(peer);
                    printf("new client %s fd=%d\n", peers[conn_fd].c_str(), conn_fd);
                }
            } else {
                ssize_t r = read(fd, buf, sizeof(buf));
                if (r > 0) {
                    write(fd, buf, r);
                } else if (r == 0 || errno != EAGAIN) {
                    printf("client %s fd=%d closed\n", peers[fd].c_str(), fd);
                    peers.erase(fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
                    close(fd);
                }
            }
        }
    }
}
chat_server.cpp 73 行

下载 chat_server.cpp

// 练习 2:群聊服务器。任何一个客户端发的消息,转发给其他所有客户端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cerrno>
#include <cstdio>
#include <set>
#include <string>

static void set_nonblock(int fd) {
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8888);
    if (bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
    listen(listen_fd, 1024);
    set_nonblock(listen_fd);

    int epfd = epoll_create1(0);
    epoll_event ev{};
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
    printf("chat server listening on 8888\n");

    std::set<int> clients;  // 当前在线的所有连接
    epoll_event events[1024];
    char buf[4096];
    while (true) {
        int n = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < n; ++i) {
            int fd = events[i].data.fd;
            if (fd == listen_fd) {
                while (true) {
                    int conn_fd = accept(listen_fd, nullptr, nullptr);
                    if (conn_fd < 0) break;
                    set_nonblock(conn_fd);
                    epoll_event cev{};
                    cev.events = EPOLLIN;
                    cev.data.fd = conn_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
                    clients.insert(conn_fd);
                    printf("user %d joined, %zu online\n", conn_fd, clients.size());
                }
            } else {
                ssize_t r = read(fd, buf, sizeof(buf));
                if (r > 0) {
                    // 在消息前面加上发送者的编号,再发给除了自己以外的所有人
                    std::string msg = "[user " + std::to_string(fd) + "] " + std::string(buf, r);
                    for (int other : clients) {
                        if (other != fd) write(other, msg.data(), msg.size());
                    }
                } else if (r == 0 || errno != EAGAIN) {
                    clients.erase(fd);
                    printf("user %d left, %zu online\n", fd, clients.size());
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
                    close(fd);
                }
            }
        }
    }
}
big_test.py 30 行

下载 big_test.py

# 测试“写不完”:先发 20 MB 数据,故意晚 1 秒才开始读回显,看最后收回来的是否完整
import socket
import threading
import time

SIZE = 20 * 1024 * 1024
data = bytes(i % 251 for i in range(SIZE))  # 有规律的内容,方便比对

s = socket.create_connection(("127.0.0.1", 8888))

def sender():
    s.sendall(data)

t = threading.Thread(target=sender)
t.start()
time.sleep(1)  # 这 1 秒里只发不收:服务器写回来的数据会把我们的接收缓冲区塞满

received = bytearray()
s.settimeout(2)
try:
    while len(received) < SIZE:
        chunk = s.recv(1 << 20)
        if not chunk:
            break
        received += chunk
except socket.timeout:
    pass
t.join(timeout=1)
print("sent", SIZE, "bytes, got back", len(received), "bytes,",
      "OK" if bytes(received) == data else "DATA LOST")
echo_buffered.cpp 112 行

下载 echo_buffered.cpp

// 练习 4:给每个连接加输出缓冲区,解决 write 写不完的问题
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cerrno>
#include <csignal>
#include <cstdio>
#include <map>
#include <string>

static void set_nonblock(int fd) {
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

static int epfd;
static std::map<int, std::string> outbuf;  // 每个连接还没发出去的数据

// 修改一个连接关心的事件:要不要关心 EPOLLOUT
static void watch_write(int fd, bool on) {
    epoll_event ev{};
    ev.events = on ? (EPOLLIN | EPOLLOUT) : EPOLLIN;
    ev.data.fd = fd;
    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}

static void close_conn(int fd) {
    outbuf.erase(fd);
    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
    close(fd);
}

// 尽量把输出缓冲区里的数据写出去;写不完的留在缓冲区里。出错返回 false
static bool flush(int fd) {
    std::string& out = outbuf[fd];
    while (!out.empty()) {
        ssize_t w = write(fd, out.data(), out.size());
        if (w > 0) {
            out.erase(0, w);  // 删掉已经写出去的部分
        } else if (w < 0 && errno == EAGAIN) {
            break;            // 内核发送缓冲区满了,等 EPOLLOUT 再写
        } else {
            return false;     // 真的出错了(比如对方已经关闭)
        }
    }
    return true;
}

int main() {
    signal(SIGPIPE, SIG_IGN);  // 往已关闭的连接写数据时,不要让 SIGPIPE 信号杀死进程

    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8888);
    if (bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
    listen(listen_fd, 1024);
    set_nonblock(listen_fd);

    epfd = epoll_create1(0);
    epoll_event ev{};
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
    printf("listening on 8888 (buffered)\n");

    epoll_event events[1024];
    char buf[65536];
    while (true) {
        int n = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < n; ++i) {
            int fd = events[i].data.fd;
            if (fd == listen_fd) {
                while (true) {
                    int conn_fd = accept(listen_fd, nullptr, nullptr);
                    if (conn_fd < 0) break;
                    set_nonblock(conn_fd);
                    epoll_event cev{};
                    cev.events = EPOLLIN;
                    cev.data.fd = conn_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
                    outbuf[conn_fd];  // 创建一个空的输出缓冲区
                }
                continue;
            }

            uint32_t e = events[i].events;
            if (e & EPOLLIN) {
                ssize_t r = read(fd, buf, sizeof(buf));
                if (r > 0) {
                    outbuf[fd].append(buf, r);  // 先放进输出缓冲区
                    if (!flush(fd)) { close_conn(fd); continue; }
                    if (!outbuf[fd].empty()) watch_write(fd, true);  // 没写完,关心可写事件
                } else if (r == 0 || errno != EAGAIN) {
                    close_conn(fd);
                    continue;
                }
            }
            if (e & EPOLLOUT) {
                // 内核发送缓冲区又有空间了,接着写
                if (!flush(fd)) { close_conn(fd); continue; }
                if (outbuf[fd].empty()) watch_write(fd, false);  // 写完了,不再关心可写
            }
        }
    }
}