【项目篇】从零手写高并发服务器(三):日志宏与Buffer缓冲区模块

文章目录

从零手写高并发服务器(三):日志宏与Buffer缓冲区模块

💬 开篇:从这篇开始,我们正式进入服务器核心代码的编写。按照敏捷开发的思路,我们从最基础的模块开始,每个模块写完都能独立测试。本篇实现日志宏和Buffer缓冲区模块------这是所有后续模块的基础设施。

👍 点赞、收藏与分享:跟着敲一遍,你就能理解为什么需要用户态缓冲区,以及如何设计一个高效的缓冲区。

🚀 循序渐进:日志宏 → Buffer设计思路 → Buffer完整实现 → 测试验证。


一、日志宏的实现

1.1 为什么需要日志?

在开发服务器的过程中,我们需要大量的调试信息来观察程序运行状态。一个好的日志系统应该能够:

  • 显示时间戳,知道什么时候发生的
  • 显示线程ID,多线程环境下知道是哪个线程
  • 显示文件名和行号,快速定位问题
  • 支持不同的日志级别(INFO、DEBUG、ERROR)

1.2 日志宏实现

我们用宏来实现日志,这样可以自动获取 __FILE__ 和 __LINE__。

先创建我们的核心头文件:

bash 复制代码
cd ~/TcpServer/source
vim server.hpp

在文件开头写入日志宏:

cpp 复制代码
#ifndef __SERVER_HPP__
#define __SERVER_HPP__

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <cstring>
#include <ctime>
#include <functional>
#include <unordered_map>
#include <memory>
#include <pthread.h>

// ==================== 日志宏 ====================
#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL DBG

#define LOG(level, format, ...) do { \
    if (level >= DEFAULT_LOG_LEVEL) { \
        time_t t = time(NULL); \
        struct tm *ltm = localtime(&t); \
        char ts[32] = {0}; \
        strftime(ts, 31, "%H:%M:%S", ltm); \
        fprintf(stdout, "[%p %s %s:%d] " format "\n", (void*)pthread_self(), ts, __FILE__, __LINE__, ##__VA_ARGS__); \
    } \
} while(0)

#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

代码解析:

  • pthread_self():获取当前线程ID
  • strftime:格式化时间
  • __FILE__、__LINE__:编译器自动替换为当前文件名和行号
  • ##__VA_ARGS__:可变参数宏,## 的作用是当没有可变参数时,去掉前面的逗号

1.3 测试日志宏

在 server.hpp 末尾暂时加上 #endif,然后写个测试:

bash 复制代码
vim test_log.cpp
cpp 复制代码
#include "server.hpp"

int main() {
    INF_LOG("这是INFO日志");
    DBG_LOG("这是DEBUG日志,数字:%d", 100);
    ERR_LOG("这是ERROR日志,字符串:%s", "出错了");
    return 0;
}

编译运行:

bash 复制代码
g++ -std=c++11 test_log.cpp -o test_log -lpthread
./test_log
bash 复制代码
wsh@VM-16-2-ubuntu:~/TcpServer/source$ ./test_log 
[0x7f8a1b2c3740 14:32:15 test_log.cpp:4] 这是INFO日志
[0x7f8a1b2c3740 14:32:15 test_log.cpp:5] 这是DEBUG日志,数字:100
[0x7f8a1b2c3740 14:32:15 test_log.cpp:6] 这是ERROR日志,字符串:出错了

日志宏工作正常!删除测试文件,继续写Buffer。

bash 复制代码
rm test_log.cpp test_log

二、Buffer缓冲区模块

2.1 为什么需要用户态缓冲区?

在网络编程中,数据的收发存在几个问题:

问题1:TCP是字节流协议,没有消息边界

bash 复制代码
发送方发送:  "Hello" + "World"
接收方可能收到:
  情况1:"HelloWorld"(粘包)
  情况2:"Hel" + "loWorld"(拆包)
  情况3:"Hello" + "World"(正常)

问题2:一次read可能读不完,一次write可能写不完

bash 复制代码
发送缓冲区满了 → write返回,但数据没发完
接收缓冲区空了 → read返回,但数据没收完

解决方案:在用户态维护缓冲区

  • 接收缓冲区:把从socket读到的数据先存起来,等凑够一个完整的请求再处理
  • 发送缓冲区:把要发送的数据先存起来,等socket可写时再发送

2.2 Buffer设计思路

我们的Buffer采用线性缓冲区 + 动态扩容的设计:

bash 复制代码
Buffer内部结构:

  ┌─────────────────────────────────────────────────────┐
  │  已读空闲区  │     可读数据区      │   可写空闲区    │
  └─────────────────────────────────────────────────────┘
  ↑              ↑                     ↑                 ↑
  0          _reader_idx          _writer_idx        size()
  
  HeadIdleSize = _reader_idx
  ReadAbleSize = _writer_idx - _reader_idx
  TailIdleSize = size() - _writer_idx

核心操作:

操作 说明
写入数据 写到 _writer_idx 位置,然后 _writer_idx 后移
读取数据 从 _reader_idx 位置读,然后 _reader_idx 后移
空间不足 先尝试移动数据到头部,不够再扩容

2.3 Buffer完整实现

继续编辑 server.hpp,在日志宏后面添加Buffer类:

cpp 复制代码
// ==================== Buffer缓冲区 ====================
#define BUFFER_DEFAULT_SIZE 1024

class Buffer {
private:
    std::vector<char> _buffer;  // 使用vector进行内存空间管理
    uint64_t _reader_idx;       // 读偏移
    uint64_t _writer_idx;       // 写偏移
public:
    Buffer() : _reader_idx(0), _writer_idx(0), _buffer(BUFFER_DEFAULT_SIZE) {}
    
    char* Begin() { return &*_buffer.begin(); }
    
    // 获取当前写入起始地址
    char* WritePosition() { return Begin() + _writer_idx; }
    
    // 获取当前读取起始地址
    char* ReadPosition() { return Begin() + _reader_idx; }
    
    // 获取缓冲区末尾空闲空间大小--写偏移之后的空闲空间
    uint64_t TailIdleSize() { return _buffer.size() - _writer_idx; }
    
    // 获取缓冲区起始空闲空间大小--读偏移之前的空闲空间
    uint64_t HeadIdleSize() { return _reader_idx; }
    
    // 获取可读数据大小 = 写偏移 - 读偏移
    uint64_t ReadAbleSize() { return _writer_idx - _reader_idx; }
    
    // 将读偏移向后移动
    void MoveReadOffset(uint64_t len) { 
        if (len == 0) return; 
        // 向后移动的大小,必须小于可读数据大小
        assert(len <= ReadAbleSize());
        _reader_idx += len;
    }
    
    // 将写偏移向后移动
    void MoveWriteOffset(uint64_t len) {
        // 向后移动的大小,必须小于当前后边的空闲空间大小
        assert(len <= TailIdleSize());
        _writer_idx += len;
    }
    
    // 确保可写空间足够(整体空闲空间够了就移动数据,否则就扩容)
    void EnsureWriteSpace(uint64_t len) {
        // 如果末尾空闲空间大小足够,直接返回
        if (TailIdleSize() >= len) { return; }
        // 末尾空闲空间不够,则判断加上起始位置的空闲空间大小是否足够
        // 够了就将数据移动到起始位置
        if (len <= TailIdleSize() + HeadIdleSize()) {
            // 将数据移动到起始位置
            uint64_t rsz = ReadAbleSize();  // 把当前数据大小先保存起来
            std::copy(ReadPosition(), ReadPosition() + rsz, Begin());  // 把可读数据拷贝到起始位置
            _reader_idx = 0;      // 将读偏移归0
            _writer_idx = rsz;    // 将写位置置为可读数据大小
        } else {
            // 总体空间不够,则需要扩容,不移动数据,直接给写偏移之后扩容足够空间即可
            // 2倍扩容,而不是「刚好够」
            uint64_t new_size = _buffer.size();
            while (new_size < _writer_idx + len) {
                new_size *= 2;
            }
            DBG_LOG("RESIZE %ld", new_size);
            _buffer.resize(new_size);
        }
    }
    
    // 写入数据
    void Write(const void *data, uint64_t len) {
        // 1. 保证有足够空间,2. 拷贝数据进去
        if (len == 0) return;
        EnsureWriteSpace(len);
        const char *d = (const char *)data;
        std::copy(d, d + len, WritePosition());
    }
    
    void WriteAndPush(const void *data, uint64_t len) {
        Write(data, len);
        MoveWriteOffset(len);
    }
    
    void WriteString(const std::string &data) {
        return Write(data.c_str(), data.size());
    }
    
    void WriteStringAndPush(const std::string &data) {
        WriteString(data);
        MoveWriteOffset(data.size());
    }
    
    void WriteBuffer(Buffer &data) {
        return Write(data.ReadPosition(), data.ReadAbleSize());
    }
    
    void WriteBufferAndPush(Buffer &data) { 
        WriteBuffer(data);
        MoveWriteOffset(data.ReadAbleSize());
    }
    
    // 读取数据
    void Read(void *buf, uint64_t len) {
        // 要求要获取的数据大小必须小于可读数据大小
        assert(len <= ReadAbleSize());
        std::copy(ReadPosition(), ReadPosition() + len, (char*)buf);
    }
    
    void ReadAndPop(void *buf, uint64_t len) {
        Read(buf, len);
        MoveReadOffset(len);
    }
    
    std::string ReadAsString(uint64_t len) {
        // 要求要获取的数据大小必须小于可读数据大小
        assert(len <= ReadAbleSize());
        std::string str;
        str.resize(len);
        Read(&str[0], len);
        return str;
    }
    
    std::string ReadAsStringAndPop(uint64_t len) {
        assert(len <= ReadAbleSize());
        std::string str = ReadAsString(len);
        MoveReadOffset(len);
        return str;
    }
    
    // 查找换行符位置
    char* FindCRLF() {
        char *res = (char*)memchr(ReadPosition(), '\n', ReadAbleSize());
        return res;
    }
    
    // 获取一行数据
    std::string GetLine() {
        char *pos = FindCRLF();
        if (pos == NULL) {
            return "";
        }
        // +1是为了把换行字符也取出来
        return ReadAsString(pos - ReadPosition() + 1);
    }
    
    std::string GetLineAndPop() {
        std::string str = GetLine();
        MoveReadOffset(str.size());
        return str;
    }
    
    // 清空缓冲区
    void Clear() {
        // 只需要将偏移量归0即可
        _reader_idx = 0;
        _writer_idx = 0;
    }
};

2.4 测试Buffer模块

在 test 目录下创建测试文件:

bash 复制代码
cd ~/TcpServer/test
vim buffer_test.cpp
cpp 复制代码
#include "../source/server.hpp"

int main() {
    Buffer buf;
    
    // 测试1:基本写入和读取
    std::string s1 = "Hello World";
    buf.WriteStringAndPush(s1);
    DBG_LOG("写入后,可读数据大小:%ld", buf.ReadAbleSize());
    
    std::string s2 = buf.ReadAsStringAndPop(buf.ReadAbleSize());
    DBG_LOG("读取的数据:%s", s2.c_str());
    DBG_LOG("读取后,可读数据大小:%ld", buf.ReadAbleSize());
    
    // 测试2:按行读取
    buf.Clear();
    std::string lines = "line1\nline2\nline3\n";
    buf.WriteStringAndPush(lines);
    
    DBG_LOG("--- 按行读取测试 ---");
    while (buf.ReadAbleSize() > 0) {
        std::string line = buf.GetLineAndPop();
        if (line.empty()) break;
        // 去掉换行符打印
        if (line.back() == '\n') line.pop_back();
        DBG_LOG("读取一行:%s", line.c_str());
    }
    
    // 测试3:扩容测试
    buf.Clear();
    DBG_LOG("--- 扩容测试 ---");
    for (int i = 0; i < 2000; i++) {
        buf.WriteStringAndPush("A");
    }
    DBG_LOG("写入2000个字符后,可读数据大小:%ld", buf.ReadAbleSize());
    
    return 0;
}

编译运行:

bash 复制代码
g++ -std=c++11 buffer_test.cpp -o buffer_test -lpthread
./buffer_test
bash 复制代码
wsh@VM-16-2-ubuntu:~/TcpServer/test$ ./buffer_test 
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:8] 写入后,可读数据大小:11
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:11] 读取的数据:Hello World
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:12] 读取后,可读数据大小:0
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:18] --- 按行读取测试 ---
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:23] 读取一行:line1
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:23] 读取一行:line2
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:23] 读取一行:line3
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:28] --- 扩容测试 ---
[0x7f8a1b2c3740 14:45:22 ../source/server.hpp:89] RESIZE 2024
[0x7f8a1b2c3740 14:45:22 buffer_test.cpp:32] 写入2000个字符后,可读数据大小:2000

Buffer模块测试通过!可以看到:

  • 基本读写正常
  • 按行读取正常
  • 超过默认1024大小时自动扩容

三、提交代码

bash 复制代码
cd ~/TcpServer
git add .
git commit -m "实现日志宏和Buffer缓冲区模块"
git push

四、本篇总结

本篇我们实现了两个基础模块:

模块 功能
日志宏 带时间戳、线程ID、文件行号的日志输出
Buffer 用户态缓冲区,支持读写、按行读取、自动扩容

当前 server.hpp 结构:

cpp 复制代码
// 日志宏
#define LOG(...)
#define INF_LOG(...)
#define DBG_LOG(...)
#define ERR_LOG(...)

// Buffer类
class Buffer { ... };

💬 下一篇预告:实现Socket模块,封装套接字的创建、绑定、监听、连接、收发等操作。

相关推荐
@兽血沸腾23 分钟前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
承渊政道4 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
讯捷蓝达6 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
数字新视界8 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落8 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
lisanmengmeng9 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
严肃小白兔9 小时前
Cloudflare 更新 API Token 管理:从最小权限到自动化凭据安全
网络·安全·自动化
Android系统攻城狮9 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
上火的金鱼妹10 小时前
k8s生产实际问题排查(网关,数据库读写等)
运维·服务器
孙克旭_10 小时前
k8s基础组件与创建资源
运维·docker·容器·kubernetes