C++

虚函数表和虚函数表指针的创建

虚函数表实际是一个虚函数地址的数组,其指向为具体的代码区的位置。

  • 背景:用来实现多态机制
  • 生成:编译器编译的时候生成,由virtual关键字修饰。
  • 存放位置:可执行程序状态下,存放在磁盘;运行状态,存放在内存中。

虚函数默认参数的特殊行为

当虚函数有默认参数且被重载时,默认参数的值由调用时的静态类型决定,而不是运行时实际对象的类型。这是因为默认参数是编译时确定的,而虚函数调用是运行时动态绑定的。

class Base {
public:
    virtual void foo(int x = 10) { 
        cout << "Base::foo(" << x << ")" << endl;
    }
};

class Derived : public Base {
public:
    void foo(int x = 20) override { 
        cout << "Derived::foo(" << x << ")" << endl;
    }
};

int main() {
    Base* b = new Derived();
    b->foo();  // 输出:Derived::foo(10)
    delete b;
    return 0;
}

关键点:

  1. 虽然实际调用的是Derived::foo(),但使用的默认参数是Base::foo()的10
  2. 默认参数值在编译时根据指针类型(Base*)确定
  3. 虚函数调用在运行时根据实际对象类型(Derived)确定

最佳实践:

  • 避免在虚函数中使用默认参数
  • 如果必须使用,确保派生类和基类的默认参数一致
  • 可以使用非虚函数提供默认参数,调用虚函数实现:
class Base {
public:
    void foo(int x = 10) { do_foo(x); }
protected:
    virtual void do_foo(int x) { /*...*/ }
};
  • 虚拟内存分区:栈区(栈区之上时内核空间)、文件映射区、堆区、数据区(静态存储区)和代码区。 可执行程序中不同部分存储的内容如下:
可执行程序中(磁盘) 作用 对应虚拟内存分区(内存)
.bss 未初始化的或初始化为0的全局、静态变量 静态存储区
.data 初始化的全局、静态变量 静态存储区
.rodata 只读数据段和虚函数表 代码区
.text 具体代码表 代码区
  • 虚函数表指针的创建时机:
  1. 类对象构造的时候,把类的虚函数表地址赋值给vptr。
  2. 没有构造函数,编译器会生成默认的构造函数(有第一条的作用)。
  3. 继承的情况下(例如B继承A),先调用基类的构造函数,把A的虚函数表的地址赋值给vptr。再调用子类构造函数,把B的虚函数表的地址赋值给vptr。

虚函数表和虚函数表指针的关系

每个类只有一个虚函数表,类的不同对象通常来说虚函数表指针vptr(每个类实例有一个)是不一样的(自身地址不一样,指向的内容一样,为深拷贝。浅拷贝会导致一个置null后另一个类实例找不到虚函数表)。使用的时候需要显式地定义拷贝构造函数或重载赋值运算符。

默认拷贝构造函数何时生成

背景 不提供的话,位拷贝,全盘复制。

  • 危害1:堆上资源也会复制
  • 危害2:文件句柄,socket也复制 持有相同的对象资源,句柄,一者释放,另一者就拿不到数据。

触发默认构造函数时机

  • A b;
  • A a(b);
  • A a=b;
  • 函数传参,形参为类对象
  • 函数返回值为类对象(有返回值优化),无优化会多次调用。(C++11之后还会看是否有移动构造,然后看类有没有默认构造,再没有,报错)

生成时间 编译时生成。

  1. 类成员变量是一个类,该成员类有默认构造函数。
  2. 类继承自基类,基类有默认构造函数。
  3. 类成员有虚函数。
  4. 类继承自基类,基类有虚函数。

进程和线程的区别

本质:进程是资源费配的基本单位,线程是CPU调度的基本单位。

  • 并发性:切换效率。上下文切换(如redis to mysql),运行环境(寄存器,程序计数器,用户空间信息与内核空间,pcb)。如果同一个进程的线程切换,效率高,不用动用户空间的数据。
  • 内存:线程不具备独立分配虚拟内存的空间。进程有独立的地址空间。但线程有栈,pc,本地存储等独立空间。
  • 所属关系:线程依附于进程,进程可以有多个线程。
  • 健壮性:进程与进程之间隔离,独立的。线程共享一个进程,互相影响。进程健壮性高于线程。

系统调用的整个流程

  • 什么是系统调用 用户态进入内核态的一种形式。用户态需要操作到内核态的资源。 用户程序->函数库->系统调用->内核
  • 中断 系统调用是软中断(0x80)。中断号、中断向量表、中断处理程序。
  • 调用流程 触发中断->切换堆栈->执行中断程序->从中断处理程序返回(iret软中断返回)

// 示例:通过`syscall`指令或软中断触发
int main() {
    write(1, "Hello", 5);  // 调用write系统调用
}; 
x86架构示例(通过0x80中断)
mov eax, 4     ; 系统调用号(sys_write
mov ebx, 1     ; 文件描述符
mov ecx, msg   ; 缓冲区地址
mov edx, len   ; 数据长度
// 系统调用表示例(Linux内核)
void *sys_call_table[] = {
    [0] = sys_restart_syscall,
    [1] = sys_exit,
    [4] = sys_write,  // 对应示例中的write调用
    //...
};
// sys_write内核函数简化逻辑
asmlinkage long sys_write(
    unsigned int fd, 
    const char __user *buf, 
    size_t count
) {
    struct file *file = fget(fd);
    return vfs_write(file, buf, count, &file->f_pos);
}
// 通过iret指令恢复现场
ENTRY(ret_from_sys_call)
    RESTORE_ALL
    iret
  • 如果使用阻塞的io且io未就绪,将线程或者进程切换,运行态->阻塞态

网络相关概念

MTU和MSS

  • MTU(Maximum Transmission Unit): 最大传输单元,指网络接口层能够传输的最大数据包大小,单位为字节。以太网默认MTU为1500字节。
  • MSS(Maximum Segment Size): 最大分段大小,指TCP报文段中数据部分的最大长度,由MTU减去IP和TCP头部长度(通常40字节)得到,默认1460字节。
  • 关系:MSS = MTU - IP头(20字节) - TCP头(20字节)
  • 作用:MTU/MSS用于控制数据包大小,避免IP分片,提高传输效率。

TCP粘包问题

  • 定义:TCP是字节流协议,接收方无法区分消息边界,多个应用层数据包可能被粘在一起接收。
  • 原因
    1. Nagle算法将多个小包合并发送
    2. 接收方缓冲区中数据堆积
  • 解决方案
    1. 固定长度消息
    2. 特殊分隔符(如\n)
    3. 消息头包含长度字段(常用)

TCP可靠传输机制

  • 序列号和确认应答
    • 每个字节都有唯一序列号
    • 接收方发送ACK确认收到的数据
    • 选择性确认(SACK)可提高效率
  • 超时重传
    • 发送数据后启动定时器
    • 超时未收到ACK则重传
    • 动态计算RTO(重传超时时间)
  • 流量控制
    • 滑动窗口机制
    • 接收方通过窗口大小通告可用缓冲区
    • 零窗口探测防止死锁
  • 拥塞控制
    • 慢启动:指数增长窗口
    • 拥塞避免:线性增长窗口
    • 快速重传:收到3个重复ACK立即重传
    • 快速恢复:避免过度降低发送速率
// 简化的TCP发送端伪代码
void tcp_send() {
    while(has_data_to_send) {
        send_packet(next_seq);
        start_timer(next_seq);
        next_seq += packet_size;
        
        if(received_ack) {
            adjust_window_size();
            stop_timer(acked_seq);
        } else if(timeout) {
            retransmit_packet();
            adjust_congestion_window();
        }
    }
}

线程池手撕

 #include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>

class ThreadPool {
public:
    explicit ThreadPool(size_t threads = std::thread::hardware_concurrency()) {
        for(size_t i = 0; i < threads; ++i)
            workers.emplace_back([this] {
                for(;;) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        condition.wait(lock, [this]{ return stop || !tasks.empty(); });
                        if(stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
    }

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
        using return_type = decltype(f(args...));
        
        auto task = std::make_shared<std::packaged_task<return_type()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
        
        std::future<return_type> res = task->get_future();
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            if(stop)
                throw std::runtime_error("enqueue on stopped ThreadPool");
            tasks.emplace([task](){ (*task)(); });
        }
        condition.notify_one();
        return res;
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for(std::thread &worker : workers)
            worker.join();
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop = false;
};

移动语义

C++11引入了移动语义,可以减少拷贝构造函数的调用,提高效率。 对象移动时,会将对象内的资源转移到另一个对象,而不是拷贝。避免资源的重新分配。

  • 移动构造函数:将右值对象内的资源转移到左值对象内。
  • 移动赋值运算符:将右值对象内的资源转移到左值对象内。
class A {
public:
    A(A&& other) noexcept {
        std::cout << "move constructor" << std::endl;
        // 资源转移
        data = other.data;
        other.data = nullptr;
    }
    A& operator=(A&& other) noexcept {
        std::cout << "move assignment operator" << std::endl;
        if(this!= &other) {
            // 资源转移
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
    A(const A& other) {
        std::cout << "copy constructor" << std::endl;
        // 拷贝构造
        data = new int(other.data);
    }
    A& operator=(const A& other) {
        std::cout << "copy assignment operator" << std::endl;
        if(this!= &other) {
            // 拷贝赋值
            delete data;
            data = new int(other.data);
        }
        return *this;
    }
    ~A() {
        std::cout << "destructor" << std::endl;
        delete data;
    }
    int* data;
};

int main() {
    A a1(10);
    A a2(std::move(a1)); // 移动构造
    A a3 = std::move(a2); // 移动赋值
    return 0;
}

输出:

move constructor
move constructor
move assignment operator
destructor
destructor

移动语义在stl中的应用

  • std::move:将左值转换为右值。
  • std::forward:将参数类型转换为右值引用。
  • std::move_if_noexcept:判断类型是否支持移动语义。
  • std::swap:交换两个对象内的资源。

完美转发

在模板中,将模板参数完美地转发到内部函数(内部调用)参数中去。转发时保证被转发的参数的值属性不变。

template<typename F, typename... Args>
auto invoke(F&& f, Args&&... args) -> decltype(std::forward<F>(f)(std::forward<Args>(args)...)) {
    return std::forward<F>(f)(std::forward<Args>(args)...);
}

  • 借用万能引用,模板参数T或者auto,即需要类型推导,来接受左右属性。
  • 应用折叠规则,模板参数T&&,即右值引用。T为左值或者左值引用,转为左值引用。T为右值或者右值引用,转为右值引用。(例如int& &&->int&,int&& &&->int&&,int &&->int &)
  • 无论是左值引用还是右值引用,都是左值,具名。左值和右值是指对象是否拥有资源的这个本质(或者叫属性),而不是对象的值。
  • std::forward(v):T为左值引用,v将转化为T类型的左值。T为右值引用,v将转化为T类型的右值。去除了引用,转化为T类型的对象。

智能指针

  • 指针管理困境:
    • 资源释放问题:指针失效,资源泄露。
    • 资源管理问题:资源泄露,资源管理复杂。(重复释放,释放时机不确定,释放顺序不确定)
  • 如何解决:智能指针。RAII(Resource Acquisition Is Initialization)思想。根据对象的生命周期进行资源的控制。
  • shared_ptr实现原理:
    • 引用计数:每个对象维护一个引用计数,当引用计数为0时,释放资源。
    • 自动释放:智能指针管理的资源,在离开作用域时自动释放。
    • 异常安全:智能指针保证异常安全,即使在构造函数或析构函数中抛出异常,也能保证资源的正确释放。
  • 常见智能指针:
    • unique_ptr:独占所有权,只能有一个指针指向对象。
    • shared_ptr:共享所有权,多个指针指向对象。
    • weak_ptr:弱引用,指向shared_ptr对象,但不影响引用计数。

单例模式C++11之后的最佳解决————Meyers’ Singleton

对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。

在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。

而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。

Meyers Singleton的实现:

class Singleton
{
private:
	Singleton() { };
	~Singleton() { };
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
public:
	static Singleton& getInstance() 
        {
		static Singleton instance;
		return instance;
	}
};

模板工厂模式

  • 该模式用来封装和管理类的创建,终极目的是为了解耦,实现创建者和调用者的分离。

template <class ProductClass, typename... ArgType>
void* objCreateFunc(ArgType... arg)
{
    return new ProductClass(arg...);
}

#ifndef ReflectRegister
#    define ReflectRegister(ProductClass, ...) \
        static int __type##ProductClass =      \
            ReflectFactory::registerObjCreateFunc(#ProductClass, (void*)&objCreateFunc<ProductClass, ##__VA_ARGS__>);
#endif

class ReflectFactory
{
public:
    template <class BaseClass, typename... ArgType>
    static BaseClass* createObj(const std::string& className, ArgType... arg)
    {
        typedef BaseClass* (*_CreateFactory)(ArgType...);

        auto& _funcMap = getObjCreateFuncMap();
        auto iFind = _funcMap.find(className);
        if (iFind == _funcMap.end())
            return nullptr;
        else
            return reinterpret_cast<_CreateFactory>(_funcMap[className])(arg...);
    }

    static int registerObjCreateFunc(const std::string& className, void* func)
    {
        getObjCreateFuncMap()[className] = func;
        return 0;
    }

private:
    static std::unordered_map<std::string, void*>& getObjCreateFuncMap()
    {
        static std::unordered_map<std::string, void*> s_classRefectionMap;
        return s_classRefectionMap;
    }
};

  • 这里reinterpret_cast<_CreateFactory>(_funcMap[className])(arg...);转换的时void* objCreateFunc(ArgType... arg)这个函数指针,将它的返回值变成了父类指针,但是具体执行的时候return new ProductClass(arg...);还是创建了子类对象,实现了多态!

环形缓冲区

环形缓冲区是一种数据结构,它可以实现在一段连续的内存中存储和读取数据,但是只能以一种循环的方式来访问。

环形缓冲区的实现原理:

  • 环形缓冲区由一个缓冲区数组和两个指针组成。
  • 缓冲区数组中存储的数据是环形的,即缓冲区数组的首尾相连。
  • 两个指针:
    • read_ptr:指向当前读入缓冲区的位置。
    • write_ptr:指向下一个待写入缓冲区的位置。
  • 读操作:
    • 读操作从缓冲区中读取数据,首先从read_ptr指向的位置开始读取,直到缓冲区的末尾,然后从缓冲区的开头开始读取,直到read_ptr指向的位置。
    • 读操作完成后,read_ptr指向下一个待读取的位置。
  • 写操作:
    • 写操作向缓冲区中写入数据,首先将数据写入write_ptr指向的位置,然后将write_ptr指向下一个位置。
    • 如果写操作导致缓冲区的末尾被覆盖,则将缓冲区的开头到末尾的数据拷贝到缓冲区的开头,然后将write_ptr指向缓冲区的开头。
    • 写操作完成后,write_ptr指向下一个待写入的位置。

环形缓冲区的优点:

  • 环形缓冲区可以实现数据的循环读写,即使缓冲区的容量不足,也可以循环使用。
  • 环形缓冲区可以实现数据的异步读写,即使读写操作发生在不同的线程中,也可以保证数据的完整性。
  • 环形缓冲区可以实现数据的缓冲,即使数据生产的速度远远大于消费的速度,也可以缓冲数据。

手撕ringbuffer

#include <iostream>
#include <mutex>
#include <condition_variable>

template<typename T>
class RingBuffer {
public:
    RingBuffer(size_t size) : m_size(size), m_buffer(new T[size]), m_read_pos(0), m_write_pos(0) {}
    ~RingBuffer() { delete[] m_buffer; }

    bool push(const T& data) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (isFull()) {
            return false;
        }
        m_buffer[m_write_pos] = data;
        m_write_pos = (m_write_pos + 1) % m_size;
        m_cv.notify_one();
        return true;
    }

    bool pop(T& data) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (isEmpty()) {
            return false;
        }
        data = m_buffer[m_read_pos];
        m_read_pos = (m_read_pos + 1) % m_size;
        m_cv.notify_one();
        return true;
    }

    bool peek(T& data) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (isEmpty()) {
            return false;
        }
        data = m_buffer[m_read_pos];
        return true;
    }

    bool isFull() const {
        return (m_write_pos + 1) % m_size == m_read_pos;
    }

    bool isEmpty() const {
        return m_read_pos == m_write_pos;
    }

private:
    size_t m_size;
    T* m_buffer;
    size_t m_read_pos;
    size_t m_write_pos;
    std::mutex m_mutex;
    std::condition_variable m_cv;
};

内存对齐的ringbuffer

#include <iostream>
#include <mutex>
#include <condition_variable>

template<typename T, size_t ALIGNMENT = 64>
class AlignedRingBuffer {
public:
    AlignedRingBuffer(size_t size) : m_size(size), m_buffer(new char[size * sizeof(T) + ALIGNMENT]), m_read_pos(0), m_write_pos(0) {
        m_buffer = (T*)(((size_t)m_buffer + ALIGNMENT - 1) & ~(ALIGNMENT - 1));
    }
    ~AlignedRingBuffer() { delete[] m_buffer; }

    bool push(const T& data) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (isFull()) {
            return false;
        }
        *(T*)(m_buffer + m_write_pos) = data;
        m_write_pos = (m_write_pos + 1) % m_size;
        m_cv.notify_one();
        return true;
    }

    bool pop(T& data) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (isEmpty()) {
            return false;
        }
        data = *(T*)(m_buffer + m_read_pos);
        m_read_pos = (m_read_pos + 1) % m_size;
        m_cv.notify_one();
        return true;
    }

    bool peek(T& data) {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (isEmpty()) {
            return false;
        }
        data = *(T*)(m_buffer + m_read_pos);
        return true;
    }

    bool isFull() const {
        return (m_write_pos + 1) % m_size == m_read_pos;
    }

    bool isEmpty() const {
        return m_read_pos == m_write_pos;
    }

private:
    size_t m_size;
    char* m_buffer;
    size_t m_read_pos;
    size_t m_write_pos;
    std::mutex m_mutex;
    std::condition_variable m_cv;
};

缓存一致性

缓存一致性(Cache Coherency)是指多个CPU或多个处理器共享同一块内存,当其中一个处理器对内存进行修改时,其他处理器需要知道这个修改,并将其缓存中的数据更新。缓存一致性保证了共享内存的正确性,是计算机系统的重要组成部分。

缓存一致性协议:MESI协议,基于总线嗅探,实现了十五的串行化。

  • M(Modified):缓存行中的数据被修改,但还没有被写回主存。
  • E(Exclusive):缓存行中的数据被独占使用,其他处理器不能访问。
  • S(Shared):缓存行中的数据可以被多个处理器共享。
  • I(Invalid):缓存行中的数据无效,需要从主存中重新加载。

MESI协议的特点:

  • 缓存行的大小一般为64字节。
  • 缓存行的状态变化必须通过总线嗅探来通知其他处理器。
  • 缓存行的状态变化必须遵循先行发生原则。
  • 缓存行的状态变化必须遵循容许延迟的原则。
  • 缓存行的状态变化必须遵循写回策略。
  • 缓存行的状态变化必须遵循写直达的原则。
  • 缓存行的状态变化必须遵循写回拒绝的原则。
  • 缓存行的状态变化必须遵循失效拒绝的原则。

CPU 在读写数据的时候,都是在 CPU Cache 读写数据的,原因是 Cache 离 CPU 很近,读写性能相比内存高出很多。对于 Cache 里没有缓存 CPU 所需要读取的数据的这种情况,CPU 则会从内存读取数据,并将数据缓存到 Cache 里面,最后 CPU 再从 Cache 读取数据。

而对于数据的写入,CPU 都会先写入到 Cache 里面,然后再在找个合适的时机写入到内存,那就有「写直达」和「写回」这两种策略来保证 Cache 与内存的数据一致性:

  • 写直达,只要有数据写入,都会直接把数据写入到内存里面,这种方式简单直观,但是性能就会受限于内存的访问速度。
  • 写回,当缓存行中的数据被修改,并通知其他 CPU 后,缓存行的状态就会变成「M」,这时缓存行中的数据就被标记为「脏」,然后 CPU 就开始将缓存行中的数据写入到内存中,写入完成后,缓存行的状态就会变成「S」,其他 CPU 就可以从内存中读取数据了。写回,对于已经缓存在 Cache 的数据的写入,只需要更新其数据就可以,不用写入到内存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到内存里,这种方式在缓存命中率高的情况,性能会更好。 当今 CPU 都是多核的,每个核心都有各自独立的 L1/L2 Cache,只有 L3 Cache 是多个核心之间共享的。所以,我们要确保多核缓存是一致性的,否则会出现错误的结果。

要想实现缓存一致性,关键是要满足 2 点:

  • 第一点是写传播,也就是当某个 CPU 核心发生写入操作时,需要把该事件广播通知给其他核心。
  • 第二点是事物的串行化,这个很重要,只有保证了这个,次啊能保障我们的数据是真正一致的,我们的程序在各个不同的核心上运行的结果也是一致的。

基于总线嗅探机制的 MESI 协议,就满足上面了这两点,因此它是保障缓存一致性的协议。 MESI 协议,是已修改、独占、共享、已实现这四个状态的英文缩写的组合。整个 MSI 状态的变更,则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心。

定时器

定时器是操作系统提供的一种机制,用于在指定的时间点后执行某个任务。


#include <sys/epoll.h>
#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>

using namespace std;

struct TimerNodeBase
{
	time_t expire;
	int64_t id;
};

// TimerNode 继承 TimerNodeBase
struct TimerNode:public TimerNodeBase
{
	// C++ 11特性,使用函数对象。降低拷贝消耗,提高效率
	using Callback = std::function<void(const TimerNode &node)>;
	Callback func;

	// 构造函数,只构造一次
	TimerNode(int64_t id,time_t expire,Callback func):func(func){
		this->id = id;
		this->expire = expire;

	}
};

// 基类引用,多态特性
bool operator<(const TimerNodeBase &lhd, const TimerNodeBase &rhd)
{
	if (lhd.expire < rhd.expire)
		return true;
	else if (lhd.expire > rhd.expire)
		return false;
	return lhd.id < rhd.id;
}

class Timer
{
public:
	static time_t GetTick()
	{
		/* C++ 11时间库chrono */
		//表示一个具体时间
		auto sc = chrono::time_point_cast<chrono::milliseconds>(chrono::steady_clock::now());
		auto tmp = chrono::duration_cast<chrono::milliseconds>(sc.time_since_epoch());
		return tmp.count();
	}

	TimerNodeBase AddTimer(time_t msec, TimerNode::Callback func)
	{
		time_t expire = GetTick() + msec;
		//避免拷贝、移动构造
		auto ele = timermap.emplace(GenID(), expire, func);

		return static_cast<TimerNodeBase>(*ele.first);
	}

	bool DelTimer(TimerNodeBase &node)
	{
		// C++ 14新特性,不在需要传一个key对象,传递一个key的等价值
		auto iter = timermap.find(node);
		if (iter != timermap.end())
		{
			timermap.erase(iter);
			return true;
		}
		return false;
	}

	bool CheckTimer()
	{
		auto iter = timermap.begin();

		if (iter != timermap.end() && iter->expire <= GetTick())
		{
			iter->func(*iter);
			timermap.erase(iter);
			return true;
		}
		return false;
	}

	time_t TimeToSleep()
	{
		auto iter = timermap.begin();
		if (iter == timermap.end())
			return -1;//没有定时任务,设置epoll一直阻塞。
		time_t diss = iter->expire - GetTick();

		return diss > 0 ? diss : 0;
	}
private:
	static int64_t GenID()
	{
		return gid++;
	}

	static int64_t gid;
	set<TimerNode, std::less<>> timermap;
};

int64_t Timer::gid = 0;

#define EPOLL_EV_LENFTH	1024


int main()
{
	int epfd = epoll_create(1);

	unique_ptr<Timer> timer = make_unique<Timer>();

	int i = 0;
	timer->AddTimer(1000, [&](const TimerNode &node) {
		cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
	});

	timer->AddTimer(1000, [&](const TimerNode &node) {
		cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
	});

	timer->AddTimer(3000, [&](const TimerNode &node) {
		cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
	});

	auto node = timer->AddTimer(2100, [&](const TimerNode &node) {
		cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
	});

	timer->DelTimer(node);

	cout << "now time:" << Timer::GetTick() << endl;

	epoll_event evs[EPOLL_EV_LENFTH] = { 0 };

	while (1)
	{
		int nready = epoll_wait(epfd, evs, EPOLL_EV_LENFTH, timer->TimeToSleep());
		for (int i = 0; i < nready; i++)
		{
			/*处理IO事件*/
		}

		// timer检测和处理
		while (timer->CheckTimer());
	}

	return 0;
}

IO多路复用

IO多路复用是指通过一种机制,一个进程可以监视多个文件句柄fd,一旦某个文件句柄状态改变(比如可读,可写,网络数据到达等),能够通知程序进行相应的读写操作。

select/poll/epoll都是IO多路复用的机制,本质上都是同步I/O模型,都需要在读写事件就绪后自己负责读写,也就是说都需要自己处理读写事件。

select的缺点:

  • 单个进程能够监视的文件描述符数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核解决。
  • 对于大量连接的服务器,需要维护大量的描述符,内存消耗大。
  • 效率不高,每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时是比较大的。

select与poll的区别在于:

  • poll使用了pollfd结构体,而不是bitmap,减少了内存消耗,且select的bitmap直接在fd置位不可重用,需要每次重置它。poll选择修改pollfd的revents字段(之后可以置0,就可以重用)。
  • 大小没有了1024的限制,可以处理更多的连接。

epoll的优点:

  • 没有最大描述符限制,能打开的FD上限远大于1024。
  • 效率很高,只有活跃的连接才会调用回调函数,不会浪费CPU资源。
  • 内存使用效率高,不会随着FD数目的增加而线性增长。
  • 采用事件驱动模型,只需要注册一次事件,之后只要该事件发生,内核会通知进程。
  • epfd用户态和内核态共享。置位->重排

简易内存池


#include <cstdlib>

// 每个内存块的前4/8字节用于存储链表指针
struct MemoryBlock {
    MemoryBlock* next;
};

class FixedMemoryPool {
private:
    MemoryBlock* free_list;      // 空闲链表指针
    size_t       block_size;     // 实际供用户使用的块大小
    void*        pool_begin;     // 初始内存池首地址
    void*        pool_end;       // 初始内存池结束地址

public:
    // 初始化预分配内存块
    FixedMemoryPool(size_t user_block_size, int num_blocks) {
        block_size = user_block_size;
        
        // 总内存 = 块数量 * (用户需求大小 + 指针大小)
        size_t total_size = num_blocks * (block_size + sizeof(MemoryBlock*));
        pool_begin = malloc(total_size);
        pool_end = (char*)pool_begin + total_size;

        // 初始化空闲链表
        free_list = static_cast<MemoryBlock*>(pool_begin);
        MemoryBlock* current = free_list;
        
        for(int i=0; i<num_blocks-1; ++i) {
            current->next = reinterpret_cast<MemoryBlock*>(
                (char*)current + block_size + sizeof(MemoryBlock*)
            );
            current = current->next;
        }
        current->next = nullptr;
    }

    // 分配内存
    void* alloc() {
        if(!free_list) return nullptr; // 可扩展为自动扩容
        
        MemoryBlock* block = free_list;
        free_list = free_list->next;
        return (void*)(block + 1); // 返回指针后移一个指针大小的位置
    }

    // 回收内存
    void dealloc(void* ptr) {
        if(!ptr) return;
        
        // 指针回退得到完整的MemoryBlock结构
        MemoryBlock* block = static_cast<MemoryBlock*>(ptr) - 1;
        block->next = free_list;
        free_list = block;
    }

    ~FixedMemoryPool() {
        free(pool_begin);
    }
};

设计原则SOLID

  • 单一职责原则 一个类应该只有一个因其他变化的原因,一个类应该只负责一个职责或者功能。
  • 开闭原则 软件实体应该是可扩展的,但是不可修改的。我们应该在不修改现有代码的情况下添加新的功能。对扩展开放,对修改关闭。
  • 里氏替换原则 子类型必须能够替换掉它们的基类型。
  • 接口隔离原则 客户端不应该被强迫依赖它们不适用的接口。
  • 依赖倒置原则 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。底层细节实现可以根据情况进行替换,高层调用的是抽象。

内存对其原则

TCP滑动窗口实现细节

滑动窗口工作原理

  1. 发送方维护一个发送窗口,包含:
    • 已发送待确认的数据
    • 可以发送但未发送的数据
  2. 接收方维护一个接收窗口,通告剩余缓冲区大小
  3. 窗口滑动条件:
    • 收到新的ACK确认
    • 接收方窗口更新
// 简化的滑动窗口实现
class TcpSlidingWindow {
private:
    uint32_t send_base;    // 发送窗口起始序号
    uint32_t next_seq;     // 下一个要发送的序号
    uint32_t window_size;  // 当前窗口大小
    std::map<uint32_t, Packet> sent_packets; // 已发送未确认的数据包

public:
    void on_ack_received(uint32_t ack_num) {
        // 移动发送窗口
        while (send_base < ack_num) {
            sent_packets.erase(send_base);
            send_base++;
        }
    }

    void on_window_update(uint32_t new_window) {
        window_size = new_window;
    }

    bool can_send() const {
        return (next_seq - send_base) < window_size;
    }
};

拥塞控制算法实现

1. 慢启动算法

  • 窗口大小从1个MSS开始
  • 每收到一个ACK,窗口大小增加1个MSS
  • 指数增长直到达到阈值(ssthresh)

2. 拥塞避免算法

  • 窗口大小线性增长
  • 每RTT时间窗口增加1个MSS
  • 公式:cwnd = cwnd + 1/cwnd

3. 快速重传算法

  • 收到3个重复ACK立即重传丢失报文
  • 无需等待超时

4. 快速恢复算法

  • 重传后不将窗口降为1
  • 将窗口设置为ssthresh + 3*MSS
  • 进入拥塞避免阶段
class TcpCongestionControl {
private:
    uint32_t cwnd;          // 拥塞窗口大小
    uint32_t ssthresh;      // 慢启动阈值
    uint32_t dup_ack_count; // 重复ACK计数器

public:
    void on_packet_loss() {
        ssthresh = cwnd / 2;
        cwnd = 1; // 慢启动
        dup_ack_count = 0;
    }

    void on_dup_ack() {
        dup_ack_count++;
        if (dup_ack_count == 3) {
            // 快速重传
            retransmit_packet();
            ssthresh = cwnd / 2;
            cwnd = ssthresh + 3; // 快速恢复
        }
    }

    void on_new_ack() {
        if (cwnd < ssthresh) {
            // 慢启动阶段
            cwnd += 1; // 指数增长
        } else {
            // 拥塞避免阶段
            cwnd += 1.0 / cwnd; // 线性增长
        }
        dup_ack_count = 0;
    }
};

(1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

(3) 结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节

TCP与UDP协议对比

1. 基本概念

TCP (传输控制协议)

  • 面向连接的协议
  • 提供可靠的、有序的数据传输
  • 具有流量控制和拥塞控制机制
  • 基于字节流的传输方式,分段,大于MSS的包会被拆分

UDP (用户数据报协议)

  • 无连接的协议
  • 提供不可靠的数据传输服务
  • 传输速度快但无法保证数据到达
  • 基于数据报的传输方式,不分段,不保证包的顺序

2. 主要区别

特性 TCP UDP
连接方式 面向连接(三次握手建立连接) 无连接
可靠性 可靠传输(确认、重传机制) 不可靠传输
数据顺序 保证数据按序到达 不保证顺序
流量控制 有(滑动窗口)
拥塞控制 有(多种算法)
传输效率 相对较低 相对较高
头部开销 20-60字节 8字节
双工性 全双工 可以是全双工

3. 应用场景

TCP典型应用

  • Web浏览(HTTP/HTTPS)
  • 文件传输(FTP)
  • 电子邮件(SMTP/POP3/IMAP)
  • 远程登录(SSH/Telnet)

UDP典型应用

  • 视频/音频流媒体
  • 实时在线游戏
  • DNS查询
  • VoIP(如Skype)
  • 广播/多播应用

4. 选择建议

  • 需要可靠数据传输时选择TCP
  • 需要低延迟、可容忍少量数据丢失时选择UDP
  • 实时性要求高的应用通常选择UDP
  • 需要确保数据完整性的应用选择TCP

5. 总结

TCP和UDP各有优缺点,没有绝对的优劣之分,关键是根据应用场景的需求选择合适的协议。现代网络应用中,很多场景会同时使用两种协议,发挥各自的优势。

6. 在UDP上实现可靠传输

虽然UDP本身不提供可靠性保证,但可以在应用层实现类似TCP的可靠传输机制。以下是常见的实现方案:

6.1 基本实现思路

  1. 序列号机制:为每个数据包分配唯一序列号
  2. 确认应答机制:接收方返回ACK确认收到的数据
  3. 超时重传机制:未收到ACK时重传数据
  4. 流量控制:通过滑动窗口控制发送速率
  5. 拥塞控制:实现简单的拥塞避免算法

6.2 实现示例

class ReliableUDP {
private:
    uint32_t next_seq;      // 下一个发送序号
    uint32_t ack_seq;       // 已确认序号
    std::map<uint32_t, Packet> unacked_packets; // 未确认数据包
    std::mutex mtx;
    
    // 重传定时器
    void start_retransmit_timer(uint32_t seq) {
        std::thread([this, seq](){
            std::this_thread::sleep_for(std::chrono::milliseconds(RTO));
            std::lock_guard<std::mutex> lock(mtx);
            if (unacked_packets.count(seq)) {
                resend_packet(seq);
            }
        }).detach();
    }

public:
    void send(const std::string& data) {
        std::lock_guard<std::mutex> lock(mtx);
        Packet packet = create_packet(next_seq++, data);
        unacked_packets[packet.seq] = packet;
        udp_send(packet);
        start_retransmit_timer(packet.seq);
    }

    void on_ack_received(uint32_t ack_seq) {
        std::lock_guard<std::mutex> lock(mtx);
        // 移除已确认的数据包
        while (!unacked_packets.empty() && 
               unacked_packets.begin()->first <= ack_seq) {
            unacked_packets.erase(unacked_packets.begin());
        }
        this->ack_seq = ack_seq;
    }
};

6.3 开源实现参考

  1. QUIC:Google开发的基于UDP的可靠传输协议
  2. UDT:高性能UDP数据传输协议
  3. ENET:游戏网络库,提供可靠UDP传输
  4. RUDP:可靠UDP协议实现

6.4 注意事项

  1. 性能开销:可靠性机制会增加延迟和CPU使用率
  2. 实现复杂度:需要处理各种边界条件和异常情况
  3. 不适合所有场景:简单应用可能不需要完整可靠性
  4. 缓冲区管理:需要合理设置发送和接收缓冲区大小

6.5 选择建议

  • 需要低延迟但又要一定可靠性的场景适合UDP+应用层可靠性
  • 完全可靠性要求高的场景建议直接使用TCP
  • 实时性要求极高的场景可以使用原生UDP