Consolexin's blog Consolexin's blog
首页
  • 算法基础

    • 图论
    • 字符串
    • 动态规划
    • 二分
    • 滑动窗口
    • 排序
  • Project

    • CppServer
  • 相关书籍

    • 现代C++编程
  • 书籍

    • SQL必知必会
    • MySQL必知必会
分类
标签
归档
GitHub (opens new window)

Consolexinhun

小学生
首页
  • 算法基础

    • 图论
    • 字符串
    • 动态规划
    • 二分
    • 滑动窗口
    • 排序
  • Project

    • CppServer
  • 相关书籍

    • 现代C++编程
  • 书籍

    • SQL必知必会
    • MySQL必知必会
分类
标签
归档
GitHub (opens new window)
  • README
  • day01-从一个最简单的socket开始
  • day02-不要放过任何一个错误
  • day03-高并发还得用epoll
  • day04-来看看我们的第一个类
  • day05-epoll高级用法-Channel登场
  • day06-服务器与事件驱动核心类登场
  • day07-为我们的服务器添加一个Acceptor
  • day08-一切皆是类,连TCP连接也不例外
  • day09-缓冲区-大作用
  • day10-加入线程池到服务器
  • day11-完善线程池,加入一个简单的测试程序
  • day12-将服务器改写为主从Reactor多线程模式
  • day13-支持业务逻辑自定义、完善Connection类
  • day14-重构核心库、使用智能指针
  • day15-重构Connection、修改生命周期
  • day16-使用CMake工程化
  • day17-使用EventLoopThreadPool、移交EventLoop
  • day18-HTTP有限状态转换机
  • day19-创建HTTP响应,实现HTTP服务器
  • day20-定时器的创建使用
  • day21-服务器主动关闭连接
  • day22-初步涉及日志库,定义自己的输出流LogStream
  • day23-定义前端日志库,实现同步输出
  • day24-异步日志库
  • day25-更有效的缓冲区
  • day26-监听写事件
  • day27-处理静态文件,实现POST请求
  • day28-文件服务器的简单实现,文件的展示和下载
  • day29-文件的上传
  • day30-WebBench的测试
  • CppServer
consolexinhun
2025-04-20

day13-支持业务逻辑自定义、完善Connection类

# day13-支持业务逻辑自定义、完善Connection类

回顾之前的教程,可以看到服务器Echo业务的逻辑在Connection类中。如果我们需要不同的业务逻辑,如搭建一个HTTP服务器,或是一个FTP服务器,则需要改动Connection中的代码,这显然是不合理的。Connection类作为网络库的一部分,不应该和业务逻辑产生联系,业务逻辑应该由网络库用户自定义,写在server.cpp中。同时,作为一个通用网络库,客户端也可以使用网络库来编写相应的业务逻辑。今天我们需要完善Connection类,支持业务逻辑自定义。

首先来看看我们希望如何自定义业务逻辑,这是一个echo服务器的完整代码:

int main() {
  EventLoop *loop = new EventLoop();
  Server *server = new Server(loop);
  server->OnConnect([](Connection *conn) {  // 业务逻辑
    conn->Read();
    std::cout << "Message from client " << conn->GetSocket()->GetFd() << ": " << conn->ReadBuffer() << std::endl;
    if (conn->GetState() == Connection::State::Closed) {
      conn->Close();
      return;
    }
    conn->SetSendBuffer(conn->ReadBuffer());
    conn->Write();
  });
  loop->Loop(); // 开始事件循环
  delete server;
  delete loop;
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里新建了一个服务器和事件循环,然后以回调函数的方式编写业务逻辑。通过Server类的OnConnection设置lambda回调函数,回调函数的参数是一个Connection指针,代表服务器到客户端的连接,在函数体中可以书写业务逻辑。这个函数最终会绑定到Connection类的on_connect_callback_,也就是Channel类处理的事件(这个版本只考虑了可读事件)。这样每次有事件发生,事件处理实际上都在执行用户在这里写的代码逻辑。

关于Connection类的使用,提供了两个函数,分别是Write()和Read()。Write()函数表示将write_buffer_里的内容发送到该Connection的socket,发送后会清空写缓冲区;而Read()函数表示清空read_buffer_,然后将TCP缓冲区内的数据读取到读缓冲区。

在业务逻辑中,conn->Read()表示从客户端读取数据到读缓冲区。在发送回客户端之前,客户端有可能会关闭连接,所以需要先判断Connection的状态是否为Closed。然后将写缓冲区设置为和读缓冲区一样的内容conn->SetSendBuffer(conn->ReadBuffer()),最后调用conn->Write()将写缓冲区的数据发送给客户端。

可以看到,现在Connection类只有从socket读写数据的逻辑,与具体业务没有任何关系,业务完全由用户自定义。

在客户端我们也希望使用网络库来写业务逻辑,首先来看看客户端的代码:

int main() {
  Socket *sock = new Socket();
  sock->Connect("127.0.0.1", 1234);
  Connection *conn = new Connection(nullptr, sock);
  while (true) {
    conn->GetlineSendBuffer();
    conn->Write();
    if (conn->GetState() == Connection::State::Closed) {
      conn->Close();
      break;
    }
    conn->Read();
    std::cout << "Message from server: " << conn->ReadBuffer() << std::endl;
  }
  delete conn;
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意这里和服务器有很大的不同,之前设计的Connection类显然不能满足要求,所以需要完善Connection。

首先,这里没有服务器和事件循环,仅仅使用了一个裸的Connection类来表示从客户端到服务器的连接。所以此时Read()表示从服务器读取到客户端,而Write()表示从客户端写入到服务器,和之前服务器的Connection类方向完全相反。这样Connection就可以同时表示Server->Client或者Client->Server的连接,不需要新建一个类来区分,大大提高了通用性和代码复用。

其次,客户端Connection没有绑定事件循环,所以将第一个参数设置为nullptr表示不使用事件循环,这时将不会有Channel类创建来分配到EventLoop,表示使用一个裸的Connection。因此业务逻辑也不用设置服务器回调函数,而是直接写在客户端代码中。

另外,虽然服务器到客户端(Server->Client)的连接都使用非阻塞式socket IO(为了搭配epoll ET模式),但客户端到服务器(Client->Server)的连接却不一定,很多业务都需要使用阻塞式socket IO,比如我们当前的echo客户端。之前Connection类的读写逻辑都是非阻塞式socket IO,在这个版本支持了非阻塞式读写,代码如下:

void Connection::Read() {
  ASSERT(state_ == State::Connected, "connection state is disconnected!");
  read_buffer_->Clear();
  if (sock_->IsNonBlocking()) {
    ReadNonBlocking();
  } else {
    ReadBlocking();
  }
}
void Connection::Write() {
  ASSERT(state_ == State::Connected, "connection state is disconnected!");
  if (sock_->IsNonBlocking()) {
    WriteNonBlocking();
  } else {
    WriteBlocking();
  }
  send_buffer_->Clear();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ps.如果连接是从服务器到客户端,所有的读写都应采用非阻塞式IO,阻塞式读写是提供给客户端使用的。

至此,今天的教程已经结束了。教程里只会包含极小一部分内容,大量的工作都在代码里,请务必结合源代码阅读。在今天的教程中,我们完善了Connection类,将Connection类与业务逻辑完全分离,业务逻辑完全由用户自定义。至此,我们的网络库核心代码已经完全脱离了业务,成为一个真正意义上的网络库。今天我们也将Connection通用化,同时支持Server->Client和Client->Server,使其可以在客户端脱离EventLoop单独绑定socket使用,读写操作也都支持了阻塞式和非阻塞式两种模式。

到今天,本教程已经进行了一半,我们开发了一个真正意义上的网络库,使用这个网络库,只需要不到20行代码,就可以搭建一个echo服务器、客户端(完整程序在test目录)。但这只是一个最简单的玩具型网络库,需要做的工作还很多,在今后的教程里,我们会对这个网络库不断完善、不断提升性能,使其可以在生产环境中使用。

完整源代码:https://github.com/yuesong-feng/30dayMakeCppServer/tree/main/code/day13 (opens new window)

编辑 (opens new window)
上次更新: 2025/05/21, 06:42:57
day12-将服务器改写为主从Reactor多线程模式
day14-重构核心库、使用智能指针

← day12-将服务器改写为主从Reactor多线程模式 day14-重构核心库、使用智能指针→

最近更新
01
6-其他操作
05-20
02
4-联结
05-20
03
7-管理
05-20
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Consolexinhun | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×