【项目篇】从零手写高并发服务器(三):日志宏与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模块,封装套接字的创建、绑定、监听、连接、收发等操作。

相关推荐
~莫子2 小时前
Docker镜像构建
运维·docker·容器
lucia_zl2 小时前
linux收集进程性能数据
linux·运维·chrome
董可伦2 小时前
Flink DataStream2Table 总结
服务器·python·flink
amcomputer2 小时前
服务器数据如何实现备份同步?
运维·服务器
Cc琎2 小时前
api接口分布在多台服务器, 如何同步用户的每日请求次数
java·运维·服务器·redis·php
小码吃趴菜2 小时前
服务器预约系统linux小项目-第一节课
运维·服务器
搬山境KL攻城狮3 小时前
ssh密钥对使用
运维·ssh
IpdataCloud4 小时前
金融风控管理如何借助IP离线库提升效率?多语言工程实践
网络·网络协议·tcp/ip
道亦无名4 小时前
Linux下是STM32的编译修改配置文件tensorflow
linux·运维