25.Reactor

预备知识

std::bind

c 复制代码
template <class Fn, class... Args>
  /* unspecified */ bind (Fn&& fn, Args&&... args);

解释: std::bind(&TcpServer::Accepter, this, std::placeholders::_1)

这段代码使用了 C++11 中的 std::bind 函数,它用于创建函数对象(也称为函数包装器或绑定器),以便稍后可以将其用作回调函数或传递给其他函数。让我逐步解释这段代码:

  1. std::bind:这是 std::bind 函数的调用,表示创建一个函数对象。

  2. &TcpServer::Accepter:这是一个成员函数指针,指向 TcpServer 类中的名为 Accepter 的成员函数。& 操作符用于获取函数的地址。

  3. this:这是一个指向当前对象的指针,通常在类的成员函数中使用,表示当前对象的实例。

  4. std::placeholders::_1std::placeholders::_1 是一个占位符,用于表示稍后将传递的参数。在这种情况下,它表示将在调用函数对象时传递的第一个参数。std::placeholders::_1 可以与绑定函数的参数位置相对应,以将参数正确地传递给目标函数。

所以,这行代码的作用是创建一个函数对象,它将在调用时执行 TcpServer 类中的 Accepter 成员函数,传递当前对象指针 this 作为第一个参数,并等待稍后传递的其他参数(如果有的话)。

通常,这种技术用于创建回调函数,以便将成员函数用作事件处理程序或异步操作的回调。这允许您在使用 C++ 中的回调机制时更加灵活。

演示代码

c 复制代码
// bind example
#include <iostream>     // std::cout
#include <functional>   // std::bind

// a function: (also works with function object: std::divides<double> my_divide;)
double my_divide (double x, double y) {return x/y;}

struct MyPair {
  double a,b;
    
  double multiply() 
  {
      return a*b;
  }
};

int main () {
  using namespace std::placeholders;    // adds visibility of _1, _2, _3,...

  // binding functions:
  auto fn_five = std::bind (my_divide,10,2);               // returns 10/2
  std::cout << fn_five() << '\n';                          // 5

  auto fn_half = std::bind (my_divide,_1,2);               // returns x/2
  std::cout << fn_half(10) << '\n';                        // 5

  auto fn_invert = std::bind (my_divide,_2,_1);            // returns y/x
  std::cout << fn_invert(10,2) << '\n';                    // 0.2

  auto fn_rounding = std::bind<int> (my_divide,_1,_2);     // returns int(x/y)
  std::cout << fn_rounding(10,3) << '\n';                  // 3

    // 初始化了它的两个成员变量a和b.  a被设置为10,而b被设置为2。
  MyPair ten_two {10,2};

   // binding members:
    // 绑定了MyPair中multiply成员函数(绑定类内成员)
    // bound_member_fn(ten_two),需要将实例作为参数传递过去
    // 如果不在调用时将实例传参,那么就在绑定时进行传参,如std::bind (&MyPair::a,ten_two)
  auto bound_member_fn = std::bind (&MyPair::multiply,_1); // returns x.multiply()
  std::cout << bound_member_fn(ten_two) << '\n';           // 20

   // 绑定了ten_two实例中的a值
  auto bound_member_data = std::bind (&MyPair::a,ten_two); // returns ten_two.a
  std::cout << bound_member_data() << '\n';                // 10

  return 0;
}

函数名被视为函数指针或地址

  • 在大多数编程语言中,函数名本身可以被视为函数的地址或指针。这意味着您可以使用函数名来引用或传递函数,就像您可以使用指向函数的指针一样。这是因为函数名通常表示函数在内存中的起始地址。

  • 在 C 和 C++ 中,函数名可以被视为指向函数的指针。您可以将函数名赋值给函数指针,然后通过函数指针来调用函数。例如:

c 复制代码
#include <stdio.h>

void myFunction() {
    printf("Hello, World!\n");
}

int main() {
    void (*functionPointer)() = myFunction; // 函数指针指向 myFunction
    functionPointer(); // 调用函数
    return 0;
}
  • 在上面的示例中,myFunction 是函数的名称,但它可以被赋值给函数指针 functionPointer,然后通过 functionPointer() 调用函数。

  • 这种函数名作为函数地址的特性在许多编程语言中也适用,但在不同语言和编译器中的具体实现方式可能有所不同。值得注意的是,在某些语言和运行时环境中,这种行为可能不成立,但在 C 和 C++ 中,它通常是有效的。

EPOLLERR都会触发那些错误

在 Linux 中,EPOLLERR 用于表示发生异常事件的情况,这包括一系列错误和异常情况。以下是可能触发 EPOLLERR 的情况示例以及操作系统可能采取的处理方式:

  1. 套接字错误 :如果与套接字通信的过程中发生错误,如连接被重置、连接超时、对方关闭连接等,会触发 EPOLLERR。操作系统通常会在 EPOLLERR 事件上通知应用程序,应用程序可以在事件处理程序中关闭套接字、记录错误信息或采取其他适当的处理。

  2. 文件描述符关闭 :如果尝试在已关闭的文件描述符上进行 I/O 操作,将触发 EPOLLERR。操作系统将通知应用程序,应用程序可以在事件处理程序中关闭文件描述符并释放资源。

  3. 管道错误 :在管道通信中,如果尝试读取或写入管道时发生错误,例如管道满或空,将触发 EPOLLERR。操作系统通常会通知应用程序,应用程序可以在事件处理程序中处理管道错误,释放资源或重试操作。

  4. 信号中断 :某些操作可能被信号中断,例如 SIGPIPE 信号。当这些信号中断 I/O 操作时,会触发 EPOLLERR。操作系统会通知应用程序,应用程序可以处理信号、记录错误信息或执行相关操作。

  5. 套接字错误标志 :有时,可以使用 SO_ERROR 选项来获取套接字上的错误标志。如果套接字上发生错误,这将触发 EPOLLERR 事件。操作系统会通知应用程序,并应用程序可以检查错误标志以获取详细信息。

操作系统不会自动解决这些异常情况,而是将它们通知给应用程序,以便应用程序可以采取适当的措施,如关闭套接字、释放资源、记录错误信息或重新尝试操作。这允许应用程序有更多的控制权,以适应不同的应用需求。通常,应用程序会在 EPOLLERR 事件的处理程序中根据具体情况采取必要的措施,以确保应用程序的稳定性。

EPOLLERR和EPOLLHUP的区别

EPOLLERREPOLLHUP 是两种不同的 epoll 事件标志,它们有以下区别:

  1. EPOLLERR

    • 表示错误事件:EPOLLERR 表示发生了错误事件,通常与文件描述符上的错误相关。
    • 触发条件:EPOLLERR 在以下情况下触发,如套接字连接失败、连接重置、连接超时、对方关闭连接等错误情况。
    • 处理方式:通常,当 EPOLLERR 触发时,应用程序需要采取适当的错误处理措施,例如关闭套接字、记录错误信息或执行错误处理操作。
  2. EPOLLHUP

    • 表示挂起事件:EPOLLHUP 表示发生了挂起事件,通常与连接的一方关闭相关。
    • 触发条件:EPOLLHUP 在以下情况下触发,如连接的一方关闭连接,即连接的对方已关闭。
    • 处理方式:通常,当 EPOLLHUP 触发时,应用程序需要采取适当的处理措施,如关闭套接字或执行资源清理。

总结来说,EPOLLERR 主要用于检测文件描述符上的错误情况,而 EPOLLHUP 主要用于检测连接的一方已关闭。这两个事件标志可以帮助应用程序在 epoll 模型中处理异常情况和连接关闭。

三目运算符

三目运算符(也称为条件运算符)是一种在 C 语言中用于创建条件表达式的特殊语法结构。它的一般形式如下:

c 复制代码
condition ? expression_if_true : expression_if_false
  • condition 是一个布尔表达式,如果为真(非零),则执行 expression_if_true;否则,执行 expression_if_false

以下是一个简单的示例,演示如何使用三目运算符:

c 复制代码
#include <stdio.h>

int main() {
    int a = 5;
    int b = 10;

    // 使用三目运算符来比较 a 和 b 的大小
    int max = (a > b) ? a : b;
    
    printf("The maximum value is: %d\n", max);

    return 0;
}

在上述示例中,我们比较了 ab 的大小。如果 a 大于 b,则 max 被赋值为 a;否则,max 被赋值为 b。最后,我们打印出了最大的值。

三目运算符是一种紧凑的方式来处理简单的条件情况,但在处理更复杂的条件逻辑时,可能更倾向于使用传统的 if 语句。

Reactor

Connection()

  • socket套接字在通信的时候,每个sock在内核都会创建接收缓冲区和发送缓冲区,这样的缓冲区常常开辟在堆上,不会像临时变量char buffer[1024]随着栈帧空间的销毁而销毁,这能更好的存储网络中收到的数据 和 即将要发送到网络中的数据,如果用栈上的空间来存储网络收发的数据,则数据极有可能被销毁掉,因为只要变量所在栈帧销毁,则变量中的数据在下次变量重新开辟时,就会由原来存储的网络数据变为未初始化过的随机数据了。
  • 所以为了让每个sock都有自己的收发缓冲区,不再使用原来编写服务器时,用一个char buffer[1024]来存储sock上的网络数据,而是改用一个Connection结构体来代表一个通信的sock,这个结构体内部包含通信的套接字描述符_sock,以及sock所对应的_inbuffer和_outbuffer。
  • 除此之外,该结构体还包括了三个回调方法_recver,_sender,_excepter,分别表示sock对应的读方法,写方法,异常方法,func_t是一个包装器类型,包装内容为函数指针,返回值是void,参数是Connection指针类型。
  • 该结构体还包括了一个额外的服务器类型的指针TcpServer *tsp_; 如果在Connection的回调方法中,想要调用一下TcpServer类中的方法时,这个回指指针会帮我们拿到TcpServer中的方法。
  • Connection还实现了两个函数,一个是注册函数,一个是关闭sock的函数,注册函数用于将外部实现的sock对应的读方法,写方法,异常方法,注册到sock所在的结构体Connection中。

  • 版权声明:本文为CSDN博主「rygttm」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
  • 原文链接:https://blog.csdn.net/erridjsis/article/details/132548615
c 复制代码
// 把从每个套接字读到的数据都放入到这个套接字对应的缓冲区当中
// 这样就可以将每次从套接字中保存的数据保存到缓冲区中(缓冲区是在堆上开辟空间)
// 避免了临时变量char buffer[1024]随着栈帧空间的销毁而销毁的问题
// 等检测到缓冲区(缓冲区是在堆上开辟空间)上有完整报文,再交付一个完整报文给应用层
class Connection
{
public:
    Connection(int sock, TcpServer *tsp) : sock_(sock), tsp_(tsp)
    {
    }

    void Register(func_t r, func_t s, func_t e)
    {
        recver_ = r;
        sender_ = s;
        excepter_ = e;
    }

    ~Connection()
    {
    }

    void Close()
    {
        close(sock_);
    }

public:
    int sock_;              // 套接字文件描述符
    std::string inbuffer_;  // 输入缓冲区
    std::string outbuffer_; // 输出缓冲区

    // 对应这个套接字的读方法,写方法,异常方法
    func_t recver_;   // 从sock_读
    func_t sender_;   // 向sock_写
    func_t excepter_; // 处理sock_ IO的时候上面的异常事件

    TcpServer *tsp_;

    // 代表对应sock最近被访问的时间(有兴趣可以实现一下)
    // uint64_t lasttime;
};

InitServer()

  • 给AddConnection传参时,用到了一个C++11的知识,就是bind绑定的使用,一般情况下,如果将包装器包装的函数指针类型传参给包装器类型时,是没有任何问题的,因为包装器本质就是一个仿函数,内部调用了被包装的对象的方法,所以传参是没有任何问题的。
  • 但如果要是在类内传参,那就有问题了,会出现类型不匹配的问题。为什么是类型不匹配呢?因为在类内调用类内方法时,其实是通过this指针来调用的,如果你直接将Accepter方法传给AddConnection,两者类型是不匹配的。因为Accepter的第一个参数是this指针,正确的做法是利用包装器的适配器bind来进行传参,bind将Accepter进行绑定,前两个参数为绑定的对象类型 和 给绑定的对象所传的参数。因为Accepter第一个参数是this指针,所以第一个参数就可以固定传this,后面的一个参数不应该是现在传,而应该是调用Accepter方法的时候再传,只有这样才能在类内将类成员函数指针传给包装器类型。
c 复制代码
void AddConnection(int sock, uint32_t events, func_t recver, func_t sender, func_t excepter)
{
    // 1. 首先要为该sock创建Connection,并初始化,并添加到connections_
    // 如果这个套接字是ET模式(也就是events & EPOLLET为真),那么我们需要将套接字文件描述符设置为非阻塞
    if (events & EPOLLET)
        Util::SetNonBlock(sock);

    // 创建套接字文件描述符对应的Connection对象
    Connection *conn = new Connection(sock, this);

    // 2. 给对应的sock设置对应回调处理方法
    conn->Register(recver, sender, excepter);

    // 3. 其次将sock与它要关心的事件"写透式"注册到epoll中,让epoll帮我们关心
    // 也就是将sock与它要关心的事添加到epoll模型中
    bool r = epoller_.AddEvent(sock, events);
    assert(r);
    (void)r;

    // 后续连接的每个文件描述符都有一个对应的Connection对象
    // 使用connections_来管理这些Connection对象
    // 4. 将kv添加到connections_
    // 将sock作为key值,Connection对象作为value值
    // 将其添加到std::unordered_map<int, Connection *> connections_中
    // 将这个Connection管理起来,我们可以通过sock快速查找到对应的Connection对象
    connections_.insert(std::pair<int, Connection *>(sock, conn));

    logMessage(DEBUG, "add new sock : %d in epoll and unordered_map", sock);
}

void InitServer()
{
    // 1. 创建socket
    sock_.Socket();
    // 1.1 将服务器的ip地址和port端口号都bind绑定
    sock_.Bind(port_);
    // 1.2 设置服务器为监听状态
    sock_.Listen();

    // 2. 构建Epoll模型
    epoller_.Create();

    // 3. a.将目前唯一的一个sock(也就是监听套接字),添加到epoller中,
    // b.添加之前需要先将对应的fd设置成为非阻塞
    // c.listensock_也是一个socket啊,也要看做成为一个Connection
    // d.std::bind(&TcpServer::Accepter, this, std::placeholders::_1)
    // 就是将成员函数Accepter与Connection进行绑定
    // listensock对应的读方法就是Accepter,写方法和异常方法设置为nullptr
    AddConnection(sock_.Fd(), EPOLLIN | EPOLLET,
                  std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);

    // revs_这是一个指向 `struct epoll_event` 结构的指针,用于存储已触发事件的信息。
    // epoll_event结构包含有关已触发事件的详细信息,包括文件描述符和事件类型。
    // revs_指向struct epoll_event[num]数组,
    // 因此可以传参给struct epoll_event revs[]
    revs_ = new struct epoll_event[num];
    num_ = num;
}

事件派发器

c 复制代码
// 判断sock对应的Connection对象是否在unordermap中
// 因为如果发生异常,sock对应的Connection对象会被从unordermap中移除
// 所以为了安全起见,再进行读写之前,都要检查这个文件描述符是否还在unordermap中
bool IsConnectionExists(int sock)
{
    auto iter = connections_.find(sock);
    return iter != connections_.end();
}

void Loop(int timeout)
{
    // 使用epoller_.Wait获取已经就绪的事件
    int n = epoller_.Wait(revs_, num_, timeout);
    for (int i = 0; i < n; i++)
    {
        // 从就绪数组中拿到对应的文件描述符
        int sock = revs_[i].data.fd;
        // 拿到对应的文件描述符对应的就绪事件
        uint32_t events = revs_[i].events;

        /*
        if ((events & EPOLLIN) && sock == sock_.Fd())
        {
            // 如果sock_.fd()的返回值是监听套接字
            // 因此如果满足这个条件就说明监听套接字的读事件已经就绪
            // 那么调用对应的读取方法读取套接字中的数据
            connections_[sock]->recver_(connections_[sock]);

            // sock对应的Connection对象中就有读取方法,我们需要调用这个方法来进行读取
            // 通过connections_[sock]就可以拿到unordermap中sock对应的Connection对象
            // 再用过Connection对象调用recver_成员函数
            // 而recver_成员函数,就是void Accepter(Connection *conn)传参过去的
            // 因此recver_的参数为connections_[sock]
        }
        if ((events & EPOLLIN) && sock != sock_.Fd())
        {
            // 如果sock_.fd()的返回值是普通套接字
            // 因此如果满足这个条件就说明普通套接字的读事件已经就绪
            // 那么调用对应的读取方法读取套接字中的数据
            connections_[sock]->recver_(connections_[sock]);

            // 对应普通的成员函数,对应的读取方法是Recver函数
            // 而recver_成员函数,就是Recver函数传参过去的
        }

        综上,只要是套接字就绪,不论是监听套接字还是普通套接字,我们都调用
        connections_[sock]->recver_(connections_[sock]);读取套接字中的数据
        */

        // 将所有的异常问题,全部转化 成为读写问题(因为读写过程中,本身就可能会有异常事件)
        // 在recver_或者sender_函数中,检测到异常问题,则会调用对应处理异常的函数Excepter
        // EPOLLERR表示对应的文件描述符发生错误
        // EPOLLHUP表示对应的文件描述符被挂起
        if (events & EPOLLERR)
            events |= (EPOLLIN | EPOLLOUT);
        if (events & EPOLLHUP)
            events |= (EPOLLIN | EPOLLOUT);

        // 如果要对套接字进行读取,要保证三点
        // 1.读事件就绪,也就是events & EPOLLIN为真
        // 2.套接字sock在unordermap中被管理,IsConnectionExists(sock)为真
        // 3.确保程序员将这个套接字所有事件对应的回调方法传递到了sock对应的Connection对象中
        // 如果connections_[sock]->recver_为真,那么recver_存在,recver_就是这个函数的地址
        if ((events & EPOLLIN) && IsConnectionExists(sock) && connections_[sock]->recver_)
            connections_[sock]->recver_(connections_[sock]);
        if ((events & EPOLLOUT) && IsConnectionExists(sock) && connections_[sock]->sender_)
            connections_[sock]->sender_(connections_[sock]);
    }
}

// 事件派发器
void Dispatcher()
{
    int timeout = 1000;
    while (true)
    {
        Loop(timeout);
        // logMessage(DEBUG, "time out ...");
    }
}

回调函数

Accepter()

c 复制代码
// Accepter就是套接字文件描述符对应的读方法
void Accepter(Connection *conn)
{
    for (;;)
    {
        std::string clientip;
        uint16_t clientport;

        // err是一个输出型参数
        // 获取新连接,如果发生错误将错误码输出到err
        // 系统调用接口accept()
        // 1.如果获取新连接成功,则返回值大于或等于0
        // 2.出现错误,返回值为-1,(错误信息可能为EAGAIN 或 EWOULDBLOCK等)
        // 3.被信号中断,它可能会返回 -1,并设置 errno 为 EINTR
        int err = 0;
        int sock = sock_.Accept(&clientip, &clientport, &err);
        if (sock > 0)
        {
            // 代码运行到这里说明获取新连接成功,得到对应的套接字文件描述符
            // 因此,使用 AddConnection()函数为套接字文件描述符
            // 1.首先要为该sock创建Connection;
            // 如果这个套接字是ET模式,那么我们需要将套接字文件描述符设置为非阻塞
            // 2.给对应的sock设置对应回调处理方法;因此通过bind传递了三个方法
            // 3.也就是将sock与它要关心的事添加到epoll模型中
            // 4.将sock作为key值,Connection对象作为value值,将其添加到unordermap中
            AddConnection(
                sock, EPOLLIN | EPOLLET,
                std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                std::bind(&TcpServer::Excepter, this, std::placeholders::_1));

            logMessage(DEBUG, "get a new link, info: [%s:%d]", clientip.c_str(), clientport);
        }
        else
        {
            // 如果代码运行到这里,那么说明出现了错误
            // 出现错误,返回值为-1,(错误信息可能为EAGAIN 或 EWOULDBLOCK等)
            // 1.如果错误码为err == EAGAIN || err == EWOULDBLOCK,
            // 说明只是将文件描述符中的数据读取完了,非阻塞返回的错误码,只需要跳出循环即可
            // 2.如果错误码为err == EINTR,说明读取时,被信号中断了,那么继续循环读取即可
            if (err == EAGAIN || err == EWOULDBLOCK)
                break;
            else if (err == EINTR)
                continue;
            else
                // 如果代码运行到了这里说明是真的发生了其他错误
                // 错误信息已经在sock_.Accept(&clientip, &clientport, &err)中被打印了
                break;
        }
    }
}

Recver()

c 复制代码
void Recver(Connection *conn)
{
    char buffer[1024];
    while (true)
    {
        // conn->sock_读取对应套接字中的数据
        // 0:通常表示阻塞接收,直到有数据可用
        // 此时conn->sock_的读事件一定是就绪的
        ssize_t s = recv(conn->sock_, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            // 返回值大于等于 0:表示成功接收的字节数,即接收操作成功完成。
            buffer[s] = 0;

            // 将读到的数据入队列,conn->inbuffer_是在堆上面开辟的空间
            // 不会随栈空间的销毁,数据被销毁
            conn->inbuffer_ += buffer;
            // 在 C++ 中,std::string 类型是一个非平凡复制类型,
            // 它包含动态分配的内存来存储字符串数据,因此无法直接通过可变参数列表传递。
            // 可变参数列表通常用于传递基本数据类型(如整数、字符、指针等)和 POD(Plain Old Data)类型。
            // 因此必须使用c_str()将其转化为c字符串
            logMessage(DEBUG, "\n%s", conn->inbuffer_.c_str());

            // 读取到数据之后,直到service_判断出conn->inbuffer_中至少有一个完整的报文
            // 开始对完整的报文数据表进行处理
            service_(conn); // 在main.cc中实现
        }
        else if (s == 0)
        {
            // 0:表示连接已关闭
            if (conn->excepter_)
            {
                conn->excepter_(conn);
                // 处理完异常,必须要返回
                // 否则,处理完异常后,会接着循环,那么就会再一次处理异常
                // 这样就会使同一块内存空间被释放两次,造成段错误
                return;
            }
        }
        else
        {
            // 读取发生错误
            // errno == EAGAIN || errno == EWOULDBLOCK如果为真,
            // 代表数据读取完毕,因为文件描述符是非阻塞的,所以超时返回了
            // errno == EINTR 为真,代表读取被信号中断了,继续读取就可以
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;
            else if (errno == EINTR)
                continue;
            else
            {
                // 读取出现错误
                // 调用conn->excepter_异常函数来处理
                if (conn->excepter_)
                {
                    conn->excepter_(conn);
                    return;
                }
            }
        }
    } // while
}

Sender()

c 复制代码
// 将Connection *conn中对应的文件描述符,添加到epoll中,
// 让epoll帮我们关心它的读事件或者写事件是否就绪(根据我们的传参来决定)
void EnableReadWrite(Connection *conn, bool readable, bool writeable)
{
    uint32_t event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;
    epoller_.Control(conn->sock_, event, EPOLL_CTL_MOD);
}


void Sender(Connection *conn)
{
    while (true)
    {
        // 第四个参数0:通常表示阻塞发送,直到数据被成功发送或发生错误。
        // 对应的文件描述符conn->sock_是非阻塞的,可以循环执行send()函数,直到将发送缓冲区的数据发完
        // 当发送缓冲区中没有数据向套接字发送时send()函数会自动返回的,
        // 当非阻塞套接字上发送缓冲区已满,错误码EAGAIN会被返回
        ssize_t s = send(conn->sock_, conn->outbuffer_.c_str(), conn->outbuffer_.size(), 0);
        logMessage(DEBUG, "for fanhui value:%d", s);
        if (s >= 0)
        {
            // 当返回值大于等于 0:表示成功发送的字节数,即发送操作成功完成。
            // 如果conn->outbuffer_.empty()为真,说明发送缓冲区中的数据被发送完了
            if (conn->outbuffer_.empty())
            {
                break;
            }
            else
                conn->outbuffer_.erase(0, s);
        }
        else
        {
            // -1:表示发送操作失败。
            // 在这种情况下,您可以使用 errno 来获取具体的错误信息,
            // 例如连接重置、连接超时、套接字错误等。
            // 常见的错误码包括 EAGAIN(非阻塞套接字上发送缓冲区已满)
            // 和 EINTR(操作被信号中断)等
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;
            else if (errno == EINTR)
                continue;
            else
            {
                // 发送出现了真正的错误,调用处理异常的函数来进行处理
                if (conn->excepter_)
                {
                    conn->excepter_(conn);
                    return;
                }
            }
        }
    }

    // 如果没有发送完毕,需要对对应的sock开启对写事件的关心
    // 如果发完了,我们要关闭对写事件的关心
    // conn->tsp_就是TcpServer对象,
    // Connection对象中包含了指针TcpServer *tsp_
    // 在tcpServer中,创建sock对应的Connection对象时,就将TcpServer *tsp_进行了初始化
    if (!conn->outbuffer_.empty())
        conn->tsp_->EnableReadWrite(conn, true, true);
    else
        conn->tsp_->EnableReadWrite(conn, true, false);
}

Excepter

c 复制代码
void Excepter(Connection *conn)
{
    logMessage(DEBUG, "Excepter begin");
    // 将异常的文件描述符,从epoll模型中移除
    epoller_.Control(conn->sock_, 0, EPOLL_CTL_DEL);
    // 关闭这个套接字文件描述符
    conn->Close();

    // 将conn->sock_对应的键对值从unordermap中移除
    connections_.erase(conn->sock_);

    logMessage(DEBUG, "关闭%d 文件描述符的所有的资源", conn->sock_);

    // 释放sock对应的Connection对象占用的资源
    delete conn;
}

服务器代码

makefile

c 复制代码
LD=-DMYSELF
TcpServer: main.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp ${LD}
.PHONY:clean
clean:
	rm -f TcpServer

Err.hpp

c 复制代码
#pragma once

#include <iostream>

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    EPOLL_CREATE_ERR
};

Log.hpp

c 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char * to_levelstr(int level)
{
    switch(level)
    {
        case DEBUG : return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default : return nullptr;
    }
}

void logMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logprefix[NUM];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
        to_levelstr(level), (long int)time(nullptr), getpid());

    char logcontent[NUM];
    va_list arg;
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);

    std::cout << logprefix << logcontent << std::endl;
}

Protocol.hpp

c 复制代码
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
// 不敢使用sizeof(),因为其包含\0这个字符,而strlen()不会
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

// 将报头和报文的正文内容进行拼接,中间用\r\n进行分割
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
    // 报头中存放的就是报文正文内容所占字节的大小
    // 需要将其转化为字符串,才可以与正文内容进行拼接
    std::string send_string = std::to_string(text.size());

    // 将报头和报文的正文部分进行拼接,它们之前只用分隔符进行分割
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    // 将其返回
    return send_string;
}

// 去掉完整报文的报头,通过输出参数std::string *text
// 返回报文的正文部分
// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    // "content_len"\r\n"exitcode result"\r\n
    // package.find(LINE_SEP)拿到报头右侧的分隔符的位置
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    // package.substr(0, pos)拿到报头字符串
    std::string text_len_string = package.substr(0, pos);
    // 将报头字符串转化为数字,其对应的就是报文正文的长度,即text_len
    int text_len = std::stoi(text_len_string);

    // package.substr(pos + LINE_SEP_LEN, text_len)拿到报文正文的内容
    *text = package.substr(pos + LINE_SEP_LEN, text_len);

    // 去掉报头成功,则返回真
    return true;
}

// 当服务器或者客户端收到请求消息时,需要将网络中的消息先进行反序列化
// 再进行处理
class Request
{
public:
    // 无参构造
    Request() : x(0), y(0), op(0)
    {
    }

    Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)
    {
    }

    // 序列化
    // 1. 自己写序列化
    // 2. 用现成的系统提供的序列化或者反序列化的接口
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        // 将*out进行清空
        *out = "";

        // 结构化 -> "x op y";
        // x是数字,使用接口to_string()将数字转化为字符串
        // op本身就是char类型,因此不需要进行转化
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        // 将所有的字符串进行拼接,且字符之间使用SEP(空格)进行分割
        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;
#else
        // 定义一个json对象
        Json::Value root;
        // x是数字,将x与first配对,存储到json中
        // 那么josn会将x转化为字符串
        root["first"] = x;
        root["second"] = y;
        root["oper"] = op;

        // 两种方法都可以将root中的数据进行序列化
        Json::FastWriter writer;
        // Json::StyledWriter writer;

        // 将root传入,writer.write(root)会自动将root中的内容进行序列化
        // writer.write(root)返回的是一个string类型的对象
        *out = writer.write(root);
#endif
        return true;
    }


    // 反序列化
    // "x op y";
    // const std::string &in, in里面存放的去掉报头的报文正文
    // 将其进行反序列化
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        // "x op y" -> 结构化
        // in.find(SEP),找到的是左操作数x右边的分隔符
        auto left = in.find(SEP);

        // in.rfind(SEP),找到的是右操作数y左边的分隔符
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;

        // SEP_LEN 是分隔符SEP的长度
        if (right - (left + SEP_LEN) != 1)
            return false;

        // in.substr(0, left),分割的字符串是左闭右开的区间
        // [0, 2) [start, end) , start(起始位置),
        // end - start(从起始位置分割end - start个字符串)
        std::string x_string = in.substr(0, left);         // 分割出来的做操作数
        std::string y_string = in.substr(right + SEP_LEN); // 分割出来的右操作数

        // 保证x_string不为空串
        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;

        // 将左操作数和右操作数,从字符串转化为数字
        x = std::stoi(x_string);
        y = std::stoi(y_string);

        // 操作符op的下标为left + SEP_LEN
        op = in[left + SEP_LEN];

#else
        Json::Value root;
        Json::Reader reader;

        // 将in这个流中读取的数据放入到root中
        reader.parse(in, root);

        // 根据键对值,拿到相应的操作数和操作符
        // asInt()的返回值是一个数字
        x = root["first"].asInt();
        y = root["second"].asInt();

        // 整数赋值给char类型的op,会自动转化为一个字符
        op = root["oper"].asInt();
#endif

        return true;
    }

public:
    // "x op y"
    int x;
    int y;
    char op;
};




// 当服务器或者客户端处理完收到的请求的数据之后,
// 需要将处理后的结果构建成可以发送给网络的响应
class Response
{
public:
    Response() : exitcode(0), result(0)
    {
    }

    Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
    {
    }

    // 序列化
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        *out = "";
        // 将退出码,和处理结果转化为字符串
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        // 将退出码,和处理结果转化的字符串进行拼接
        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = exitcode;
        root["result"] = result;

        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    // 反序列化
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        // "exitcode result"
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        // 保证退出码和处理结果不是空字符串
        if (ec_string.empty() || res_string.empty())
            return false;

        // 将退出码和处理结果从字符串转化为数字
        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);

#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        exitcode = root["exitcode"].asInt();
        result = root["result"].asInt();
#endif

        // 反序列化成功,则返回真
        return true;
    }

public:
    // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int exitcode;
    int result; // 计算结果
};


// 解析一个报文
// "content_len"\r\n"x op y"\r\n    
bool ParseOnePackage(std::string &inbuffer, std::string *text)
{
    // 将*text指向的空间置空
    *text = "";
    // 分析处理
    auto pos = inbuffer.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    // text_len_string是正文长度
    std::string text_len_string = inbuffer.substr(0, pos);

    // 将正文长度对应的字符串转化为数字
    int text_len = std::stoi(text_len_string);

    // 报文的总长度 = 报头长度+正文长度+2*分割符长度
    int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;

    // 如果inbuffer.size() < total_len,说明inbuffer此时没有一个完整的报文
    if (inbuffer.size() < total_len)
        return false;

    // 至少有一个完整的报文
    *text = inbuffer.substr(0, total_len);

    // 将报文拿走后,从缓冲区inbuffer中删除
    inbuffer.erase(0, total_len);

    return true;
}

Util.hpp

c 复制代码
#pragma once

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

class Util
{
public:
    // 将文件描述符设置为非阻塞
    static bool SetNonBlock(int fd)
    {
        int fl = fcntl(fd, F_GETFL);
        if (fl < 0) return false;
        fcntl(fd, F_SETFL, fl | O_NONBLOCK);
        return true;
    }
};

Epoller.hpp

c 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/epoll.h>
#include "Err.hpp"
#include "Log.hpp"

const static int defaultepfd = -1;
const static int size = 128;

class Epoller
{
public:
    Epoller() : epfd_(defaultepfd)
    {
    }

    ~Epoller()
    {
        if (epfd_ != defaultepfd)
            close(epfd_);
    }

public:
    void Create()
    {
        // 创建epoll模型
        epfd_ = epoll_create(size);
        if (epfd_ < 0)
        {
            logMessage(FATAL, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));
            exit(EPOLL_CREATE_ERR);
        }
    }

    // 添加文件描述符到epoll模型中
    // user -> kernel (从用户添加到内核)
    bool AddEvent(int sock, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;

        int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, sock, &ev);
        // 添加成功,则返回值为0
        return n == 0;
    }

    // 从epoll模型中拿就绪事件
    // kernel -> user
    // revs[]:这部分表示一个结构体数组。revs 是一个数组的名称,用于存储 struct epoll_event 类型的结构体。
    // 这个数组通常用于保存多个事件的信息。
    int Wait(struct epoll_event revs[], int num, int timeout)
    {
        // int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
        // events指针,指向一个存储struct epoll_event的数组
        // int maxevents,代表这个数组的大小
        int n = epoll_wait(epfd_, revs, num, timeout);
        return n;
    }

    // 给特定套接字修改读事件关心,或者写事件关心,
    // 或者移除特定套接字
    // EPOLL_CTL_MOD 是一个标志,用于告诉 epoll 实例要修改已注册事件的配置。
    // EPOLL_CTL_DEL 是一个标志,用于告诉 epoll 实例要删除已注册的事件。
    bool Control(int sock, uint32_t event, int action)
    {
        int n = 0;
        if (action == EPOLL_CTL_MOD)
        {
            struct epoll_event ev;
            ev.events = event;
            ev.data.fd = sock;

            // 成功操作:如果 epoll_ctl 操作成功执行,它将返回 0。
            // 这表示已成功添加、修改或删除事件,并且没有发生错误。
            // 出现错误:如果 epoll_ctl 操作失败,它将返回 -1,
            // 并且错误码将被设置为适当的错误值。
            // 您可以使用 errno 全局变量来获取具体的错误信息。
            n = epoll_ctl(epfd_, action, sock, &ev);
        }
        else if (action == EPOLL_CTL_DEL)
        {
            n = epoll_ctl(epfd_, action, sock, nullptr);
        }
        else
            n = -1;

        return n == 0;
    }

    // 关闭epoll模型对应的文件描述符
    void Close()
    {
        if (epfd_ != defaultepfd)
            close(epfd_);
    }

private:
    int epfd_;
};

Sock.hpp

c 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"

// 表示全连接队列中最多有32+1个连接
// 具体请看tcp相关实验
const static int backlog = 32;

const static int defaultsock = -1;

class Sock
{
public:
    Sock():_listensock(defaultsock)
    {}

    ~Sock()
    {
        if(_listensock != defaultsock) close(_listensock);
    }

public:
    int Socket()
    {
        // 1. 创建socket文件套接字对象
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "create socket error");
            exit(SOCKET_ERR);
        }
        logMessage(NORMAL, "create socket success: %d", _listensock);

        // int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
        // sockfd:套接字描述符,指定要设置选项的套接字
        // level:选项的级别,通常是 SOL_SOCKET 表示通用套接字选项。
        // 设置 SO_REUSEADDR 选项,允许地址重用
        // optval:一个指向包含选项值的缓冲区的指针。
        // optlen:指定选项值的长度。
        int opt = 1;

        // 我们将 `SO_REUSEADDR` 选项设置为1,从而启用了地址重用功能。
        // 这可以让套接字在绑定地址时可以重用之前被关闭的套接字的地址,
        // 而不会因为 TIME_WAIT 状态而无法绑定。
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    void Bind(int port)
    {
        // 2. bind绑定自己的网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind socket error");
            exit(BIND_ERR);
        }
        logMessage(NORMAL, "bind socket success");
    }

    void Listen()
    {
        // 3. 设置socket 为监听状态
        if (listen(_listensock, backlog) < 0)
        {
            logMessage(FATAL, "listen socket error");
            exit(LISTEN_ERR);
        }
        logMessage(NORMAL, "listen socket success");
    }

    // 获取新链接
    int Accept(std::string *clientip, uint16_t *clientport, int *err)
    {
        // 在struct sockaddr_in结构体中,存在端口号和IP地址
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        // 将获取的套接字的文件描述符返回
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
        *err = errno;
        
        // sock < 0, 获取套接字失败
        if (sock < 0)
            logMessage(ERROR, "accept error, next");
        else
        {
            logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?
            // 获取套接字成功,通过输入输出型参数来查看客户端的端口号和IP地址
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }

    // 通过成员函数获取私有的成员变量
    int Fd()
    {
        return _listensock;
    }

    // 关闭监听套接字
    void Close()
    {
        if(_listensock != defaultsock) close(_listensock);
    }

private:
    int _listensock;
};

TcpServer.hpp

c 复制代码
#pragma once

#include <iostream>
#include <cassert>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
#include "Sock.hpp"
#include "Err.hpp"
#include "Epoller.hpp"
#include "Util.hpp"
#include "Protocol.hpp"

namespace tcpserver
{
    // 因为这两个类,一直在交叉引用,所以一定要事先进行声明
    class Connection;
    class TcpServer;

    static const uint16_t defaultport = 8080;
    static const int num = 64;

    using func_t = std::function<void(Connection *)>;

    // 把从每个套接字读到的数据都放入到这个套接字对应的缓冲区当中
    // 这样就可以将每次从套接字中保存的数据保存到缓冲区中(缓冲区是在堆上开辟空间)
    // 避免了临时变量char buffer[1024]随着栈帧空间的销毁而销毁的问题
    // 等检测到缓冲区(缓冲区是在堆上开辟空间)上有完整报文,再交付一个完整报文给应用层
    class Connection
    {
    public:
        Connection(int sock, TcpServer *tsp) : sock_(sock), tsp_(tsp)
        {
        }

        void Register(func_t r, func_t s, func_t e)
        {
            recver_ = r;
            sender_ = s;
            excepter_ = e;
        }

        ~Connection()
        {
        }

        void Close()
        {
            close(sock_);
        }

    public:
        int sock_;              // 套接字文件描述符
        std::string inbuffer_;  // 输入缓冲区
        std::string outbuffer_; // 输出缓冲区

        // 对应这个套接字的读方法,写方法,异常方法
        func_t recver_;   // 从sock_读
        func_t sender_;   // 向sock_写
        func_t excepter_; // 处理sock_ IO的时候上面的异常事件

        TcpServer *tsp_;

        // 代表对应sock最近被访问的时间(有兴趣可以实现一下)
        // uint64_t lasttime;
    };

    ///
    ///

    class TcpServer // Reactor
    {
    private:
        void Recver(Connection *conn)
        {
            char buffer[1024];
            while (true)
            {
                // conn->sock_读取对应套接字中的数据
                // 0:通常表示阻塞接收,直到有数据可用
                // 此时conn->sock_的读事件一定是就绪的
                ssize_t s = recv(conn->sock_, buffer, sizeof(buffer) - 1, 0);
                if (s > 0)
                {
                    // 返回值大于等于 0:表示成功接收的字节数,即接收操作成功完成。
                    buffer[s] = 0;

                    // 将读到的数据入队列,conn->inbuffer_是在堆上面开辟的空间
                    // 不会随栈空间的销毁,数据被销毁
                    conn->inbuffer_ += buffer;
                    // 在 C++ 中,std::string 类型是一个非平凡复制类型,
                    // 它包含动态分配的内存来存储字符串数据,因此无法直接通过可变参数列表传递。
                    // 可变参数列表通常用于传递基本数据类型(如整数、字符、指针等)和 POD(Plain Old Data)类型。
                    // 因此必须使用c_str()将其转化为c字符串
                    logMessage(DEBUG, "\n%s", conn->inbuffer_.c_str());

                    // 读取到数据之后,直到service_判断出conn->inbuffer_中至少有一个完整的报文
                    // 开始对完整的报文数据表进行处理
                    service_(conn);
                }
                else if (s == 0)
                {
                    // 0:表示连接已关闭
                    if (conn->excepter_)
                    {
                        conn->excepter_(conn);
                        // 处理完异常,必须要返回
                        // 否则,处理完异常后,会接着循环,那么就会再一次处理异常
                        // 这样就会使同一块内存空间被释放两次,造成段错误
                        return;
                    }
                }
                else
                {
                    // 读取发生错误
                    // errno == EAGAIN || errno == EWOULDBLOCK如果为真,
                    // 代表数据读取完毕,因为文件描述符是非阻塞的,所以超时返回了
                    // errno == EINTR 为真,代表读取被信号中断了,继续读取就可以
                    if (errno == EAGAIN || errno == EWOULDBLOCK)
                        break;
                    else if (errno == EINTR)
                        continue;
                    else
                    {
                        // 读取出现错误
                        // 调用conn->excepter_异常函数来处理
                        if (conn->excepter_)
                        {
                            conn->excepter_(conn);
                            return;
                        }
                    }
                }
            } // while
        }

        void Sender(Connection *conn)
        {
            while (true)
            {
                // 第四个参数0:通常表示阻塞发送,直到数据被成功发送或发生错误。
                // 对应的文件描述符conn->sock_是非阻塞的,可以循环执行send()函数,直到将发送缓冲区的数据发完
                // 当发送缓冲区中没有数据向套接字发送时send()函数会自动返回的,
                // 当非阻塞套接字上发送缓冲区已满,错误码EAGAIN会被返回
                ssize_t s = send(conn->sock_, conn->outbuffer_.c_str(), conn->outbuffer_.size(), 0);
                logMessage(DEBUG, "for fanhui value:%d", s);
                if (s >= 0)
                {
                    // 当返回值大于等于 0:表示成功发送的字节数,即发送操作成功完成。
                    // 如果conn->outbuffer_.empty()为真,说明发送缓冲区中的数据被发送完了
                    if (conn->outbuffer_.empty())
                    {
                        break;
                    }
                    else
                        conn->outbuffer_.erase(0, s);
                }
                else
                {
                    // -1:表示发送操作失败。
                    // 在这种情况下,您可以使用 errno 来获取具体的错误信息,
                    // 例如连接重置、连接超时、套接字错误等。
                    // 常见的错误码包括 EAGAIN(非阻塞套接字上发送缓冲区已满)
                    // 和 EINTR(操作被信号中断)等
                    if (errno == EAGAIN || errno == EWOULDBLOCK)
                        break;
                    else if (errno == EINTR)
                        continue;
                    else
                    {
                        // 发送出现了真正的错误,调用处理异常的函数来进行处理
                        if (conn->excepter_)
                        {
                            conn->excepter_(conn);
                            return;
                        }
                    }
                }
            }

            // 如果没有发送完毕,需要对对应的sock开启对写事件的关心
            // 如果发完了,我们要关闭对写事件的关心
            // conn->tsp_就是TcpServer对象,
            // Connection对象中包含了指针TcpServer *tsp_
            // 在tcpServer中,创建sock对应的Connection对象时,就将TcpServer *tsp_进行了初始化
            if (!conn->outbuffer_.empty())
                conn->tsp_->EnableReadWrite(conn, true, true);
            else
                conn->tsp_->EnableReadWrite(conn, true, false);
        }

        void Excepter(Connection *conn)
        {
            logMessage(DEBUG, "Excepter begin");
            // 将异常的文件描述符,从epoll模型中移除
            epoller_.Control(conn->sock_, 0, EPOLL_CTL_DEL);
            // 关闭这个套接字文件描述符
            conn->Close();

            // 将conn->sock_对应的键对值从unordermap中移除
            connections_.erase(conn->sock_);

            logMessage(DEBUG, "关闭%d 文件描述符的所有的资源", conn->sock_);

            // 释放sock对应的Connection对象占用的资源
            delete conn;
        }

        // 判断sock对应的Connection对象是否在unordermap中
        // 因为如果发生异常,sock对应的Connection对象会被从unordermap中移除
        // 所以为了安全起见,再进行读写之前,都要检查这个文件描述符是否还在unordermap中
        bool IsConnectionExists(int sock)
        {
            auto iter = connections_.find(sock);
            return iter != connections_.end();
        }

        void Loop(int timeout)
        {
            // 使用epoller_.Wait获取已经就绪的事件
            int n = epoller_.Wait(revs_, num_, timeout);
            for (int i = 0; i < n; i++)
            {
                // 从就绪数组中拿到对应的文件描述符
                int sock = revs_[i].data.fd;
                // 拿到对应的文件描述符对应的就绪事件
                uint32_t events = revs_[i].events;

                /*
                if ((events & EPOLLIN) && sock == sock_.Fd())
                {
                    // 如果sock_.fd()的返回值是监听套接字
                    // 因此如果满足这个条件就说明监听套接字的读事件已经就绪
                    // 那么调用对应的读取方法读取套接字中的数据
                    connections_[sock]->recver_(connections_[sock]);

                    // sock对应的Connection对象中就有读取方法,我们需要调用这个方法来进行读取
                    // 通过connections_[sock]就可以拿到unordermap中sock对应的Connection对象
                    // 再用过Connection对象调用recver_成员函数
                    // 而recver_成员函数,就是void Accepter(Connection *conn)传参过去的
                    // 因此recver_的参数为connections_[sock]
                }
                if ((events & EPOLLIN) && sock != sock_.Fd())
                {
                    // 如果sock_.fd()的返回值是普通套接字
                    // 因此如果满足这个条件就说明普通套接字的读事件已经就绪
                    // 那么调用对应的读取方法读取套接字中的数据
                    connections_[sock]->recver_(connections_[sock]);

                    // 对应普通的成员函数,对应的读取方法是Recver函数
                    // 而recver_成员函数,就是Recver函数传参过去的
                }

                综上,只要是套接字就绪,不论是监听套接字还是普通套接字,我们都调用
                connections_[sock]->recver_(connections_[sock]);读取套接字中的数据
                */

                // 将所有的异常问题,全部转化 成为读写问题(因为读写过程中,本身就可能会有异常事件)
                // 在recver_或者sender_函数中,检测到异常问题,则会调用对应处理异常的函数Excepter
                // EPOLLERR表示对应的文件描述符发生错误
                // EPOLLHUP表示对应的文件描述符被挂起
                if (events & EPOLLERR)
                    events |= (EPOLLIN | EPOLLOUT);
                if (events & EPOLLHUP)
                    events |= (EPOLLIN | EPOLLOUT);

                // 如果要对套接字进行读取,要保证三点
                // 1.读事件就绪,也就是events & EPOLLIN为真
                // 2.套接字sock在unordermap中被管理,IsConnectionExists(sock)为真
                // 3.确保程序员将这个套接字所有事件对应的回调方法传递到了sock对应的Connection对象中
                // 如果connections_[sock]->recver_为真,那么recver_存在,recver_就是这个函数的地址
                if ((events & EPOLLIN) && IsConnectionExists(sock) && connections_[sock]->recver_)
                    connections_[sock]->recver_(connections_[sock]);
                if ((events & EPOLLOUT) && IsConnectionExists(sock) && connections_[sock]->sender_)
                    connections_[sock]->sender_(connections_[sock]);
            }
        }

        // Accepter就是套接字文件描述符对应的读方法
        void Accepter(Connection *conn)
        {
            for (;;)
            {
                std::string clientip;
                uint16_t clientport;

                // err是一个输出型参数
                // 获取新连接,如果发生错误将错误码输出到err
                // 系统调用接口accept()
                // 1.如果获取新连接成功,则返回值大于或等于0
                // 2.出现错误,返回值为-1,(错误信息可能为EAGAIN 或 EWOULDBLOCK等)
                // 3.被信号中断,它可能会返回 -1,并设置 errno 为 EINTR
                int err = 0;
                int sock = sock_.Accept(&clientip, &clientport, &err);
                if (sock > 0)
                {
                    // 代码运行到这里说明获取新连接成功,得到对应的套接字文件描述符
                    // 因此,使用 AddConnection()函数为套接字文件描述符
                    // 1.首先要为该sock创建Connection;
                    // 如果这个套接字是ET模式,那么我们需要将套接字文件描述符设置为非阻塞
                    // 2.给对应的sock设置对应回调处理方法;因此通过bind传递了三个方法
                    // 3.也就是将sock与它要关心的事添加到epoll模型中
                    // 4.将sock作为key值,Connection对象作为value值,将其添加到unordermap中
                    AddConnection(
                        sock, EPOLLIN | EPOLLET,
                        std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                        std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                        std::bind(&TcpServer::Excepter, this, std::placeholders::_1));

                    logMessage(DEBUG, "get a new link, info: [%s:%d]", clientip.c_str(), clientport);
                }
                else
                {
                    // 如果代码运行到这里,那么说明出现了错误
                    // 出现错误,返回值为-1,(错误信息可能为EAGAIN 或 EWOULDBLOCK等)
                    // 1.如果错误码为err == EAGAIN || err == EWOULDBLOCK,
                    // 说明只是将文件描述符中的数据读取完了,非阻塞返回的错误码,只需要跳出循环即可
                    // 2.如果错误码为err == EINTR,说明读取时,被信号中断了,那么继续循环读取即可
                    if (err == EAGAIN || err == EWOULDBLOCK)
                        break;
                    else if (err == EINTR)
                        continue;
                    else
                        // 如果代码运行到了这里说明是真的发生了其他错误
                        // 错误信息已经在sock_.Accept(&clientip, &clientport, &err)中被打印了
                        break;
                }
            }
        }

        void AddConnection(int sock, uint32_t events, func_t recver, func_t sender, func_t excepter)
        {
            // 1. 首先要为该sock创建Connection,并初始化,并添加到connections_
            // 如果这个套接字是ET模式,那么我们需要将套接字文件描述符设置为非阻塞
            if (events & EPOLLET)
                Util::SetNonBlock(sock);

            // 创建套接字文件描述符对应的Connection对象
            Connection *conn = new Connection(sock, this);

            // 2. 给对应的sock设置对应回调处理方法
            conn->Register(recver, sender, excepter);

            // 3. 其次将sock与它要关心的事件"写透式"注册到epoll中,让epoll帮我们关心
            // 也就是将sock与它要关心的事添加到epoll模型中
            bool r = epoller_.AddEvent(sock, events);
            assert(r);
            (void)r;

            // 4. 将kv添加到connections_
            // 将sock作为key值,Connection对象作为value值
            // 将其添加到std::unordered_map<int, Connection *> connections_中
            // 将这个Connection管理起来,我们可以通过sock快速查找到对应的Connection对象
            connections_.insert(std::pair<int, Connection *>(sock, conn));

            logMessage(DEBUG, "add new sock : %d in epoll and unordered_map", sock);
        }

    public:
        TcpServer(func_t func, uint16_t port = defaultport) 
        : service_(func), port_(port), revs_(nullptr)
        {
        }

        void InitServer()
        {
            // 1. 创建socket
            sock_.Socket();
            sock_.Bind(port_);
            sock_.Listen();

            // 2. 构建Epoll
            epoller_.Create();

            // 3. a.将目前唯一的一个sock(也就是监听套接字),添加到epoller中,
            // b.添加之前需要先将对应的fd设置成为非阻塞
            // c.listensock_也是一个socket啊,也要看做成为一个Connection
            // d.std::bind(&TcpServer::Accepter, this, std::placeholders::_1)
            // 就是将成员函数Accepter与Connection进行绑定
            AddConnection(sock_.Fd(), EPOLLIN | EPOLLET,
                          std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);

            // revs_这是一个指向 `struct epoll_event` 结构的指针,用于存储已触发事件的信息。
            // epoll_event结构包含有关已触发事件的详细信息,包括文件描述符和事件类型。
            // revs_指向struct epoll_event[num]数组,
            // 因此可以传参给struct epoll_event revs[]
            revs_ = new struct epoll_event[num];
            num_ = num;
        }

        // 将Connection *conn中对应的文件描述符,添加到epoll中,
        // 让epoll帮我们关心它的读事件或者写事件是否就绪(根据我们的传参来决定)
        void EnableReadWrite(Connection *conn, bool readable, bool writeable)
        {
            uint32_t event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;
            epoller_.Control(conn->sock_, event, EPOLL_CTL_MOD);
        }

        // 事件派发器
        void Dispatcher()
        {
            int timeout = 1000;
            while (true)
            {
                Loop(timeout);
                // logMessage(DEBUG, "time out ...");
            }
        }

        ~TcpServer()
        {
            // 关闭监听套接字
            sock_.Close();
            // 关闭epoll模型对应的文件描述符
            epoller_.Close();
            // 释放存储就绪事件的数组revs_
            if (nullptr == revs_)
                delete[] revs_;
        }

    private:
        // Sock对象,通过其来调用sock的所有函数接口
        Sock sock_;

        // tcp服务器的端口号
        uint16_t port_;

        // epoll 模型的调用
        Epoller epoller_;

        // 我们将套接字文件描述符和其对应的缓冲区等做了封装
        // 也就是说每一个套接字文件描述符都对应一个Connection对象
        // 如果客户端有大量的人进行连接,那么就会存在大量的Connection
        // 因此我们需要用unordered_map<int, Connection *>对大量的Connection对象进行管理
        std::unordered_map<int, Connection *> connections_;

        // revs_这是一个指向 `struct epoll_event` 结构的指针,用于存储已触发事件的信息。
        // epoll_event结构包含有关已触发事件的详细信息,包括文件描述符和事件类型。
        // revs_指向struct epoll_event[num]数组,
        // 因此可以传参给struct epoll_event revs[]
        struct epoll_event *revs_;

        // 代表struct epoll_event[num] 数组的大小
        int num_;

        // 解析报文,并逻辑处理的回调函数
        func_t service_;
    };
}

main.cc

c 复制代码
#include "TcpServer.hpp"
#include <memory>

using namespace tcpserver;

static void usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port"
              << "\n\n";
}

bool cal(const Request &req, Response &resp)
{
    // req已经有结构化完成的数据啦,你可以直接使用
    resp.exitcode = OK;
    resp.result = OK;

    switch (req.op)
    {
    case '+':
        resp.result = req.x + req.y;
        break;
    case '-':
        resp.result = req.x - req.y;
        break;
    case '*':
        resp.result = req.x * req.y;
        break;
    case '/':
    {
        if (req.y == 0)
            resp.exitcode = DIV_ZERO;
        else
            resp.result = req.x / req.y;
    }
    break;
    case '%':
    {
        if (req.y == 0)
            resp.exitcode = MOD_ZERO;
        else
            resp.result = req.x % req.y;
    }
    break;
    default:
        resp.exitcode = OP_ERROR;
        break;
    }

    return true;
}



void calculate(Connection *conn)
{
    // 使用ParseOnePackage()函数
    // 从inbuffer中读取一个完整的报文
    std::string onePackage;
    while (ParseOnePackage(conn->inbuffer_, &onePackage))
    {
        // 使用deLength()将这个完整报文的报头去掉
        // 并将报文的正文输出到reqStr中
        std::string reqStr;
        if (!deLength(onePackage, &reqStr))
            return;
        std::cout << "去掉报头的正文:\n"
                  << reqStr << std::endl;

        // 2. 拿到正文后,对请求Request进行反序列化
        // 2.1 得到一个结构化的请求对象
        Request req;
        if (!req.deserialize(reqStr))
            return;

        // cal()函数将数据处理后,将响应结果输出到resp中
        Response resp;
        cal(req, resp);

        // 将响应正文进行序列化
        // 将序列化的数据输出到respStr中
        std::string respStr;
        resp.serialize(&respStr);

        // 5. 然后我们在发送响应
        // 5.1 给正文添加报头构建成为一个完整的报文
        conn->outbuffer_ += enLength(respStr);

        std::cout << "--------------result: " << conn->outbuffer_ << std::endl;
    }

    // 调用sock对应的发送方法进行发送
    if (conn->sender_)
        conn->sender_(conn);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));
    tsvr->InitServer();
    tsvr->Dispatcher();

    return 0;
}

客户端代码

makefile

c 复制代码
cc=g++

calclient:calClient.cc
	$(cc) -o $@ $^ -std=c++11 -ljsoncpp 

.PHONY:clean
clean:
	rm -f calclient 

protocol.hpp

c 复制代码
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>

#define SEP " "
// 不敢使用sizeof(),因为其包含\0这个字符,而strlen()不会
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

// 将报头和报文的正文内容进行拼接,中间用\r\n进行分割
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
    // 报头中存放的就是报文正文内容所占字节的大小
    std::string send_string = std::to_string(text.size());

    // 将报头和报文的正文部分进行拼接,它们之前只用分隔符进行分割
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    // 将其返回
    return send_string;
}

// 去掉完整报文的报头,通过输出参数std::string *text
// 返回报文的正文部分
// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    // "content_len"\r\n"exitcode result"\r\n
    // package.find(LINE_SEP)拿到报头右侧的分隔符的位置
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    // package.substr(0, pos)拿到报头字符串
    std::string text_len_string = package.substr(0, pos);
    // 将报头字符串转化为数字,其对应的就是报文正文的长度,即text_len
    int text_len = std::stoi(text_len_string);

    // package.substr(pos + LINE_SEP_LEN, text_len)拿到报文正文的内容
    *text = package.substr(pos + LINE_SEP_LEN, text_len);

    // 去掉报头成功,则返回真
    return true;
}

// 当服务器或者客户端收到请求消息时,需要将网络中的消息先进行反序列化
// 再进行处理
class Request
{
public:
    // 无参构造
    Request() : x(0), y(0), op(0)
    {
    }

    Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)
    {
    }

    // 序列化
    // 1. 自己写序列化
    // 2. 用现成的系统提供的序列化或者反序列化的接口
    bool serialize(std::string *out)
    {
        // 将*out进行清空
        *out = "";

        // 结构化 -> "x op y";
        // x是数字,使用接口to_string()将数字转化为字符串
        // op本身就是char类型,因此不需要进行转化
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        // 将所有的字符串进行拼接,且字符之间使用SEP(空格)进行分割
        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;
        return true;
    }

    // 反序列化
    // "x op y";
    // const std::string &in, in里面存放的去掉报头的报文正文
    // 将其进行反序列化
    bool deserialize(const std::string &in)
    {
        // "x op y" -> 结构化
        // in.find(SEP),找到的是左操作数x右边的分隔符
        auto left = in.find(SEP);

        // in.rfind(SEP),找到的是右操作数y左边的分隔符
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;

        // SEP_LEN 是分隔符SEP的长度
        if (right - (left + SEP_LEN) != 1)
            return false;

        // in.substr(0, left),分割的字符串是左闭右开的区间
        // [0, 2) [start, end) , start(起始位置),
        // end - start(从起始位置分割end - start个字符串)
        std::string x_string = in.substr(0, left);         // 分割出来的做操作数
        std::string y_string = in.substr(right + SEP_LEN); // 分割出来的右操作数

        // 保证x_string不为空串
        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;

        // 将左操作数和右操作数,从字符串转化为数字
        x = std::stoi(x_string);
        y = std::stoi(y_string);

        // 操作符op的下标为left + SEP_LEN
        op = in[left + SEP_LEN];

        return true;
    }

public:
    // "x op y"
    int x;
    int y;
    char op;
};


// 当服务器或者客户端处理完收到的请求的数据之后,
// 需要将处理后的结果构建成可以发送给网络的响应
class Response
{
public:
    Response() : exitcode(0), result(0)
    {}

    Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
    {}

    // 序列化
    bool serialize(std::string *out)
    {
        *out = "";
        // 将退出码,和处理结果转化为字符串
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        // 将退出码,和处理结果转化的字符串进行拼接
        *out = ec_string;
        *out += SEP;
        *out += res_string;
        return true;
    }

    // 反序列化
    bool deserialize(const std::string &in)
    {
        // "exitcode result"
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        // 保证退出码和处理结果不是空字符串
        if (ec_string.empty() || res_string.empty())
            return false;

        // 将退出码和处理结果从字符串转化为数字
        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);

        // 反序列化成功,则返回真
        return true;
    }

public:
    // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int exitcode;
    int result; // 计算结果
};


// 从套接字对应的文件描述符sock中进行读取
// 如下是多个报文的字节流
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
    // 定义应用层的缓冲区,来存放报文
    char buffer[1024];
    while (true)
    {
        // 从sock读取sizeof(buffer) - 1个字节并将其存放到buffer中
        // 经过多次循环,就可以将sock中的内容全部读取
        // 并返回读取内容的字节个数
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            // 给字符串的末尾添加结束符,即\0
            buffer[n] = 0;
            // 将读取的所有数据放入到string类型的inbuffer中
            inbuffer += buffer;

            // 分析处理
            // "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n
            // inbuffer.find(LINE_SEP)就是报头"content_len"右侧的分隔符的位置
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == std::string::npos)
                continue;  // 如果没有找到,那么就返回继续读取,再重新找

            // inbuffer.substr(0, pos),获取到报头
            // 报头存放就是报文正文内容所占的字节数,即text_len_string
            std::string text_len_string = inbuffer.substr(0, pos);

            // 将报文正文内容所占的字节数,由字符串转为数字
            int text_len = std::stoi(text_len_string);

            // total_len 报文的总长度
            // text_len_string.size()是报头的长度
            // 2 * LINE_SEP_LEN + text_len 报文正文和两个分割符长度的总和
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            
            // text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();
            std::cout << "处理前#inbuffer: \n"<< inbuffer << std::endl;

            // inbuffer.size()小于total_len,说明inbuffer没有一个完整的报文
            // 那么就返回,继续从sock读取,直到inbuffer.size() > total_len
            if (inbuffer.size() < total_len)
            {
                std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
                continue;
            }

            // 至少有一个完整的报文
            // 分割出一个完整报文
            *text = inbuffer.substr(0, total_len);

            // 从inbuffer删除掉分割出的报文
            inbuffer.erase(0, total_len);

            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;

            break;
        }
        else
            // 如果n小于等于0说明对方关闭了sock文件描述符对应的文件,因此返回错误
            return false;
    }
    return true;
}

calClient.hpp

c 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "protocol.hpp"

#define NUM 1024

class calClient
{
public:
    // 客户端的构造函数
    calClient(const std::string &serverip, const uint16_t &serverport)
        : _sock(-1), _serverip(serverip), _serverport(serverport)
    {
    }

    // 初始化客户端
    void initClient()
    {
        // 1. 创建套接字socket
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            // 创建套接字失败,打印错误信息
            std::cerr << "socket create error" << std::endl;
            exit(2);
        }

        // 2. tcp的客户端要不要bind?要的!
        // 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
        // 3. 要不要listen?不要!
        // 4. 要不要accept? 不要!
        // 5. 要什么呢? 要发起链接!
    }

    // 启动客户端
    void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            // 客户端向服务器发起链接失败,则打印错误信息
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            // 链接成功,客户端和服务端进行通信
            std::string line;
            std::string inbuffer;
            while (true)
            {
                std::cout << "mycal>>> ";
                // 将输入流的数据,放入到应用层的缓冲区line中
                std::getline(std::cin, line);  // 1+1

                // ParseLine(line)拿到了输入数据构建的请求
                Request req = ParseLine(line); // "1+1"

                // 将数据序列化之后,通过输出参数std::string content返回
                // 此时content也就是要发送的报文的正文
                std::string content; // 输出型参数
                req.serialize(&content);

                // 给报文添加报头并返回
                std::string send_string = enLength(content);
                std::cout << "sendstring:\n"<< send_string << std::endl;

                // 将序列化之后的报文发送
                send(_sock, send_string.c_str(), send_string.size(), 0); 

                // 读取服务器计算数据后的返回结果
                // std::string package,输出型参数,返回完整的报文
                // std::string text,输出型参数,返回报文正文
                std::string package, text;

                // 从套接字的文件描述符sock中读取一个完整的报文
                //  "content_len"\r\n"exitcode result"\r\n
                if (!recvPackage(_sock, inbuffer, &package))
                    continue;

                // 分离报文报头和报文正文
                if (!deLength(package, &text))
                    continue;

                // 报文正文包含如下内容
                // "exitcode result"
                // 从网络中拿到的数据,因此需要先进行反序列化
                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp.exitcode << std::endl;
                std::cout << "result: " << resp.result << std::endl;
            }
        }
    }

    // 客户端的析构函数
    ~calClient()
    {
        // 关闭客户端套接字对应的文件描述符
        if (_sock >= 0)
            close(_sock);
    } 

    Request ParseLine(const std::string &line)
    {
        // 建议版本的状态机!
        //"1+1" "123*456" "12/0"
        int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
        int i = 0;

        // line.size()是输入左操作数、右操作数、操作符和分隔符的总长度
        int cnt = line.size();
        std::string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                // 状态码为0,表明当前位置是在操作符之前
                // isdigit(line[i])判断line[i]是不是数字,是数字则返回真
                if (!isdigit(line[i]))
                {
                    // 此时line[i]一定是操作符
                    op = line[i];
                    // 1:碰到了操作符,因此将状态码改为1
                    status = 1;
                }
                else
                    // 此时line[i] 一定是左操作数
                    left.push_back(line[i++]);
            }
            break;
            case 1:
                // 状态码为1,说明line[i]一定是操作符,因此进行++
                i++;
                // ++ 之后line[i]一定为右操作数,因此将状态码变为2
                status = 2;
                break;
            case 2:
                // 此时line[i]一定为右操作数
                right.push_back(line[i++]);
                break;
            }
        }
        std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;
        
        // 将拿到的数据,构建为一个请求并返回
        return Request(std::stoi(left), std::stoi(right), op);
    }

private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

calClient.cc

c 复制代码
#include "calClient.hpp"
#include <memory>

using namespace std;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<calClient> tcli(new calClient(serverip, serverport));
    tcli->initClient();
    tcli->start();
    return 0;
}

演示结果

/ 客户端的析构函数

~calClient()

{

// 关闭客户端套接字对应的文件描述符

if (_sock >= 0)

close(_sock);

}

复制代码
Request ParseLine(const std::string &line)
{
    // 建议版本的状态机!
    //"1+1" "123*456" "12/0"
    int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
    int i = 0;

    // line.size()是输入左操作数、右操作数、操作符和分隔符的总长度
    int cnt = line.size();
    std::string left, right;
    char op;
    while (i < cnt)
    {
        switch (status)
        {
        case 0:
        {
            // 状态码为0,表明当前位置是在操作符之前
            // isdigit(line[i])判断line[i]是不是数字,是数字则返回真
            if (!isdigit(line[i]))
            {
                // 此时line[i]一定是操作符
                op = line[i];
                // 1:碰到了操作符,因此将状态码改为1
                status = 1;
            }
            else
                // 此时line[i] 一定是左操作数
                left.push_back(line[i++]);
        }
        break;
        case 1:
            // 状态码为1,说明line[i]一定是操作符,因此进行++
            i++;
            // ++ 之后line[i]一定为右操作数,因此将状态码变为2
            status = 2;
            break;
        case 2:
            // 此时line[i]一定为右操作数
            right.push_back(line[i++]);
            break;
        }
    }
    std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;
    
    // 将拿到的数据,构建为一个请求并返回
    return Request(std::stoi(left), std::stoi(right), op);
}

private:

int _sock;

std::string _serverip;

uint16_t _serverport;

};

复制代码
## calClient.cc

```c
#include "calClient.hpp"
#include <memory>

using namespace std;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<calClient> tcli(new calClient(serverip, serverport));
    tcli->initClient();
    tcli->start();
    return 0;
}

演示结果

外链图片转存中...(img-0W8siaSo-1743663033793)

相关推荐
我命由我1234510 分钟前
C++ - 头文件基础(常用标准库头文件、自定义头文件、头文件引入方式、防止头文件重复包含机制)
服务器·c语言·开发语言·c++·后端·visualstudio·visual studio code
w236173460111 分钟前
Linux常用基础命令应用
linux·服务器·php
别致的影分身13 分钟前
Protobuf 的快速使用(四)
服务器·网络·c++
White の algo15 分钟前
【Linux系统】linux下的软件管理
linux·运维·服务器
松树戈17 分钟前
Ubuntu挂载HDD迁移存储PostgreSQL数据
linux·ubuntu·postgresql
矛取矛求20 分钟前
Linux 系统安装与优化全攻略:打造高效开发环境
linux·运维·服务器
李匠202429 分钟前
C++学习之LINUX网络编程-套接字通信基础
c++·学习
一把年纪学编程29 分钟前
linux 安装 mysql记录
linux·运维·mysql
Pacify_The_North1 小时前
【C++进阶五】list深度剖析
开发语言·c++·算法·list
虾球xz1 小时前
游戏引擎学习第206天
c++·学习·游戏引擎