代码中小知识点的解释

boost::asio::io_context 的作用

boost::asio::io_context 是 Boost.Asio 库中的一个核心组件,
主要用于管理异步事件和任务的执行。
它在网络编程、并发编程和异步操作中发挥着重要作用。

主要作用

  1. 事件循环

    • io_context 提供了一个事件循环,可以调度和处理异步操作。当有事件发生时(例如,I/O 操作完成、定时器触发等),它会调用相应的回调函数。
  2. 异步操作管理

    • 它允许用户注册各种异步操作,如网络连接、读取/写入数据、定时器等,并在这些操作完成时自动调用相应的回调函数。
  3. 多线程支持

    • io_context 可以在多个线程中运行,允许并行处理多个事件。这使得应用程序能够充分利用多核处理器。
  4. 任务调度

    • 除了处理 I/O 操作,io_context 还可以用来调度其他类型的任务,例如定时任务或延迟任务。
  5. 资源管理

    • 它负责管理与异步操作相关的资源,确保在不再需要时能够正确释放资源。

使用示例

以下是一个简单的使用 io_context 的示例:

cpp
#include <iostream>
#include <boost/asio.hpp>

using namespace boost::asio;

void print() {
    std::cout << "Hello, World!" << std::endl;
}

int main() {
    io_context io;

    // 创建一个定时器
    deadline_timer timer(io, boost::posix_time::seconds(5));
    
    // 设置定时器到期时的处理函数
    timer.async_wait([](const boost::system::error_code& /*e*/) {
        print();
    });

    io.run();  // 进入事件循环
    return 0;
}

在这个例子中,我们创建了一个定时器,设置它在 5 秒后触发,并通过 io_context 的事件循环来处理这项异步操作。

小结
总之,boost::asio::io_context 是构建高效、可扩展的异步应用程序的重要工具,特别是在网络和 I/O 操作的上下文中。它使得处理复杂的异步逻辑变得更加简洁和高效。

using作用

创建别名

简化代码:使用 using 可以避免每次使用 tcp 类型时都要写出完整的命名空间路径。例如,后续代码中可以直接使用 tcp,而不需要写作 boost::asio::ip::tcp。

结构体/类别名:

using 还可以用于定义类型别名,例如:
cpp
using MyInt = int;

使用 using 简化函数指针

cpp
#include <iostream>

// 使用 using 简化函数指针类型
using FunctionPointer = void(*)(int);

void myFunction(int x) {
    std::cout << "Value: " << x << std::endl;
}

int main() {
    FunctionPointer ptr = &myFunction; // 使用更简洁的定义
    ptr(42);
    return 0;
}

C++ memset用法

用法示例
1. 初始化数组
cpp
#include <iostream>
#include <cstring>

int main() {
    int arr[5];

    // 使用 memset 将数组所有元素初始化为 0
    memset(arr, 0, sizeof(arr));

    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " "; // 输出为 0 0 0 0 0
    }
    return 0;
}
2. 初始化结构体
cpp
#include <iostream>
#include <cstring>

struct MyStruct {
    int a;
    double b;
};

int main() {
    MyStruct s;

    // 使用 memset 将结构体所有成员初始化为 0
    memset(&s, 0, sizeof(s));

    std::cout << "a: " << s.a << ", b: " << s.b << std::endl; // 输出 a: 0, b: 0
    return 0;
}
3. 设置特定值
注意:memset 只适合设置字节值,因此它通常不适合于设置非字符类型的值。
例如,如果你想将整数数组的值设置为某个特定非零值,应该谨慎使用。

cpp
#include <iostream>
#include <cstring>

int main() {
    int arr[5];

    // 错误示范:使用 memset 将所有元素设置为 1 (实际上会把每个字节都设置为 1)
    memset(arr, 1, sizeof(arr));

    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " "; // 输出不确定的结果
    }
    return 0;
}

注意事项
数据类型:memset 按字节操作,不适合用于设置非字符类型的值(如整型、浮点型等)为非零值。
未初始化的内存:如果你对一个类或结构体使用 memset,请注意这可能会影响到包含虚函数或非平坦数据成员的对象,因为 memset 会直接操作内存,不会调用构造函数。

enum

class Session
{
private:
	enum {max_length=1024};
	char data[max_length];
};
在 C++ 中,enum 定义的都是整数常量,
即使它们被赋予了字符或其他类型的值。

使用字符值时,它们会被视为对应的 ASCII 整数值。
如果需要使用这些常量,您可以将它们转换回相应的字符类型。

size_t

  1. 定义
    类型:size_t 是一个无符号整型(unsigned integer),其确切的位数依赖于平台和编译器,通常为 16、32 或 64 位。
  2. 特点
    无符号:由于 size_t 是无符号类型,因此它不可以表示负数。这使得它非常适合用于表示内存大小、数组索引等总是非负的值。

跨平台性:size_t 的大小与系统架构(32-bit 或 64-bit)有关。在 32 位系统中,size_t 通常是 32 位;在 64 位系统中,它通常是 64 位。

boost::asio::ip::tcp::endpoint

endpoint 类表示一个 TCP 端点,即一个网络地址(IP 地址)与一个端口号的组合。

// 创建 TCP 端点,绑定到 localhost 上的 8080 端口
m_endpoint(boost::asio::ip::address::from_string("127.0.0.1"),8080);
	
//使用特定的 IPv4 地址,可以这样做:
m_endpoint(boost::asio::ip::address_v4::from_string("127.0.0.1"), port);
	
//监听所有可用的网络接口(即所有 IP 地址),可以使用:
m_endpoint(boost::asio::ip::address_v4::any(),port);

map中被移除的数据会被销毁吗map中被移除的数据会被销毁吗

在 C++ 中,当从 std::map 中移除元素时,该元素的内存管理和销毁行为取决于元素的类型以及它们是如何存储的。以下是一些关键点:

  1. 基本类型和对象
    如果 std::map 存储的是基本数据类型(如 int, double 等)或对象(如自定义类的实例),当你使用 erase() 移除元素时,该元素会被销毁,其占用的内存会被释放。
cpp
std::map<int, MyClass> myMap;
// 添加元素
myMap[1] = MyClass();

// 移除元素
myMap.erase(1); // MyClass 对象会被销毁
  1. 智能指针
    如果 std::map 存储的是智能指针(如 std::shared_ptr 或 std::unique_ptr),当你移除元素时,智能指针本身会被销毁,但指针所管理的对象的生命周期将由智能指针控制。

对于 std::shared_ptr,只有当所有引用计数降到零时,对象才会被销毁。

cpp
std::map<int, std::shared_ptr<MyClass>> myMap;
myMap[1] = std::make_shared<MyClass>();

myMap.erase(1); // 智能指针被销毁,如果没有其他引用,则 MyClass 对象也会被销毁
对于 std::unique_ptr,则直接删除所管理的对象。

cpp
std::map<int, std::unique_ptr<MyClass>> myMap;
myMap[1] = std::make_unique<MyClass>();

myMap.erase(1); // Unique_ptr 被销毁,因此 MyClass 对象也会被销毁
  1. 原始指针
    如果 std::map 存储的是原始指针,移除元素只会删除指针的存储,不会调用其指向的对象的析构函数,因此需要手动管理这些对象的生命周期,以避免内存泄漏。
cpp
std::map<int, MyClass*> myMap;
myMap[1] = new MyClass();

myMap.erase(1); // 仅删除指针,不会销毁 MyClass 对象; 需要手动 delete

总结
自动销毁: 基本类型和对象会在移除时被销毁。
智能指针: 会根据引用计数自动管理资源。
原始指针: 需要手动管理,避免内存泄漏。

当同时使用cin>>,getline()时

、当同时使用cin>>,getline()时,需要注意的是,在cin>>输入流完成之后,getline()之前,需要通过

str="\n";
getline(cin,str);
的方式将回车符作为输入流cin以清除缓存,如果不这样做的话,在控制台上就不会出现getline()的输入提示,而直接跳过,因为程序默认地将之前的变量作为输入流。

    int age;
    cout<<"Please enter an integer value as your age: ";
    cin>>age;
    cout<<"Your ager is: "<<age<<".\n";
    
    string mystr;
    cout<<"What's your name? "<<endl;
    mystr="\n";
    getline(cin,mystr);
 
    getline(cin,mystr);
    cout<<"Hello,"<<mystr<<".\n";

cin.getline(valueData, max_length)

长度限制:
(这段话不对-->)
如果输入超过 max_length - 1 个字符,getline 会截断输入,并且不添加 '\0',这时输入缓冲区中的多余字符将保留,因此需要处理这种情况。
(<--这段话不对)

int main()
{
    
    char totalData[5];
    if (cin.getline(totalData , 5)) {
        cout << "成功" << endl;
        cout << totalData<< endl;
    }

    //if (std::cin.fail()) {
    //    std::cin.clear(); // 清除错误标志
    //    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 清空缓冲区
    //}
    char temp[max_length];
    cin.getline(temp, max_length);

    int a;
    cin >> a;
    cout << a << endl;
    cout << totalData << endl;
    
    if (totalData[4] == '\0') { cout << "true" << endl; }
    return 0;
}

GTP说的不对,我自己实测,cin.getline(totalData , 5)返回false,而后如果再使用 输入 会出错,
但是totalData[4] == '\0'为true,
即使 totalData 数据中有'\0',但是后续输入会出问题,所以要清除缓冲区(在下面的 清除缓冲区)

换行符:
如果在输入时遇到换行符,它会被视为终止符,getline 会在读取到换行符之前停止。
如果你在使用 cin.getline(valueData, max_length) 时输入了换行符,函数会在遇到换行符时停止读取,并且会在读取的字符串末尾添加一个空字符 '\0'。

清除缓冲区:
如果在前一次输入中使用了 cin.getline 而没有处理完输入(例如用户输入了太多字符),可能需要手动清除输入缓冲区以避免影响后续的输入操作。

if (std::cin.fail()) {
        std::cin.clear(); // 清除错误标志
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 清空缓冲区
    }

代码实现过程中的一些问题

智能指针实现伪闭包

up讲的官方案例其实是没有隐患的(至少我认为没有,虽然up一直说有隐患),但是实际的开发生产中,不会按照官方案例案例的方式来写,所以按照实际的开发生产的方式来写,会造成服务器崩溃。
崩溃的原因是:
客户端发送信息到服务器,立即将客户端关闭,此时服务器会触发异步读和异步写(哪个先触发是不确定的),但是读和写都会使得error.value()!=0,先触发的操作会将session进行delete操作,这个资源已经被释放了,另一个晚触发的操作访问资源时必然会造成错误(即使另一个操作没有操作资源,也会造成delete两次),而后使得服务器崩溃。
解决办法:
利用智能指针(如 std::shared_ptr 和 std::weak_ptr),我们可以构造一个伪闭包,延长某个会话(session)的生命周期。

server类中的map主要是为了管理连接,实现伪闭包后,有没有map都不会影响到伪闭包的作用,map在后期的还是有用的(我自己想的:客户端之间信息的传递,应该会用到)

增加发送队列实现服务器全双工

up的代码用的是async_write
up说调用async_write发送数据时,如果底层缓冲区已经满了,导致部分数据未发送,而后再次调用async_write时,会造成数据混乱。
但是GTP说TCP确保数据的顺序和完整性,所以不会造成数据混乱。

emmm。。。使用async_write时确实有可能造成数据没有按照期望的顺序发送出去, 但是up的说法可能不是很正确,GTP说法更加准确:第一个写操作可能在第二个写操作开始后才完成。

服务器目前是应答式的,要将其改为 全双工的 但是我感觉把handle_read和handle_write里面的内容稍微调整就可以了,但是这样还是没有解决数据发送顺序的问题。

还是按照up的来吧!

处理粘包问题

增加发送队列,保证第一次发送操作完成后,第二次发送才进行,但是数据可能会粘连到一起。

服务器保证第一次发送操作完成后,第二次发送才进行,
并且使用了async_write,我自己感觉发送的时候应该是不会造成粘包问题,比如服务器要发送两条信息给客户端:ABC abc
如果服务器没有进行相关处理,那么发送的时候就有可能是: AB Ca bc
但是对于客户端接收数据,还是有可能会造成粘包,客户端应该也需要进行处理吧??

服务器接收

对于粘包问题,我采用了简易的处理方式,但是代码方面并没有按照up的来写。
存在的问题:
客户端发送 2+5 字节
服务器接收到 2字节后,就会继续读5字节,此时客户端发送的信息出现丢失,也就是剩余信息不足5字节,此时服务器就会一直 异步 等待这个客户端给它足够的信息,但是这个客户端并没有信息了,所以,如果能设置一个超时就好了,或者是async_read自带超时?或者可以设置参数?
另外一个就是,服务接收不到该客户端的足够的信息时,该客户端又发送一个新的信息,这样应该会导致服务器读取后续数据出错,还是说我想多了??
我看up的简易处理粘包,也没有考虑这个问题
可能是:如果信息丢失,服务器应该是会触发读错误,所以应该不需要考虑(服务接收不到该客户端的足够的信息时,该客户端又发送一个新的信息,这样应该会导致服务器读取后续数据出错,)服务器触发读错误后,该会话会在服务器中被移除,考虑太多了,不想了

jsoncpp在程序中的使用

逻辑系统

没有逻辑系统时,网络层在收到信息时,信息在网络层进行解析,在信息处理完毕后,调用send函数中,将要发送的信息加入网络中的发送队列中,然后发送到客户端。虽然 处理信息 的速度很快,但是依旧会占用时间(无论网络层是单线程还是多线程,都会使其效率降低),并且网络层中处理逻辑,使得程序模块的耦合性增大,不利于代码模块化,也不利于后期程序的维护。

加入逻辑层后,网络层在收到信息后,将其投送给逻辑层中的逻辑队列,逻辑层会将逻辑队列中数据取出并进行处理,然后再将处理好的数据放入网络层中的发送队列,由网络层将数据发送到客户端。

逻辑层为单线程,一开始我也想,如果把逻辑层设计为多线程,会不会大大提高逻辑层处理效率,但是up说在取出数据时,需要反复的加锁解锁,效率和单线程应该差不多。

设计逻辑系统后,大大提高了网络层的并发性,并且降低了模块的耦合度。