项目复习:从封装epoll_server到实现reactor服务器(part2)
- 项目复习:从封装epoll_server到实现reactor服务器(part2)
仓库:https://github.com/Yufccode/Reactor-based-HyperWebServer/
这一部分不多说,直接进入代码编写,后面有需要解释的特性,再解释。
基本结构搭建好
tcp_server.hpp
#ifndef __YUFC_TCP_SERVER__
#define __YUFC_TCP_SERVER__
#include <iostream>
#include <string>
class tcp_server {
private:
public:
};
#endif
为什么上面我们写的epoll的recv是不正确的?
我们好好的看一下这一份代码。
void epoll_recver(int sock) {
// 读取这个普通套接字里面的内容
// 1. 读取数据
// 2. 处理数据
char buffer[10240];
size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
// 假设这里就是读到了一个完整的报文
buffer[n] = 0;
__handler_request(buffer); // 进行回调!
} else if (n <= 0) {
// 对端关闭文件描述符
// 让epoll不再关注这个文件描述符
// 一定要先从epoll中去掉,才能close文件描述符
bool res = __epoll::control_epoll(__epoll_fd, EPOLL_CTL_DEL, sock, 0);
assert(res); // 保证是成功的,因为一般来说都是成功的,所以直接assert
(void)res;
close(sock);
if (n == 0)
logMessage(NORMAL, "client %d quit, me quit too ...", sock);
else if (n < 0)
logMessage(NORMAL, "client recv %d error, close error sock", sock);
}
}
调用recv的时候读取的是完整的报文吗?不知道,所以我们buffer[10240]里面的也是不完整的报文。
此时能够直接交付给__handler_request()做我们的业务逻辑吗?不可以!这样是错的!
但是,我们上节课写的epoll服务器,也没有对应的缓冲区让我们暂时存放,等报文完整才__handler_request(),所以,上面的epoll_recver是错误的。
而且,一个epoll_server可能有成百上千的sock,每一个sock,都需要缓冲区才行!
sock要封装了,要维护缓冲区
所以要维护!
tcp_server.hpp
class connection {
public:
using func_t = std::function<void(connection*)>;
public:
connection();
~connection();
public:
int __sock; // io的文件描述符
func_t __recv_callback;
func_t __send_callback;
func_t __except_callback;
std::string __in_buffer; // 输入缓冲区(暂时没有处理二进制流)
std::string __out_buffer; // 输出缓冲区
tcp_server* tsvr; // 回指指针
};
每一个文件描述符,都要封装!因为每一个文件描述符,都有可能有三种时间,读事件,写事件,异常事件。
简单来说就是,我们通过封装func_t
,后面这个sock需要读的时候,就去调用__recv_callback
。后面的同理!
这三个函数对象,都要从用户层传过来!
当然,输入输出缓冲区也是需要的!目前的用string做缓冲区,暂时没有办法处理二进制流,但是文本是可以的。
tcp_server* tsvr;
这个指针也很好理解,毕竟我这个connector
总要知道是哪个服务器在调用我吧。
然后初始化什么的这里就不说了,看代码就行。然后我也写了一个void set_callback(func_t recv_cb, func_t send_cb, func_t except_cb)
来设置这个sock封装(connector)的回调。
封装epoll(1)
这次封装和上次就不同了,这次我们把epoll的所有动作都封装到epoll.hpp里面去,包括epollfd什么的,我们的tcp_server就不管这个epoll里面的事情了,tcp_server只负责调用接口!
class tcp_server {
public:
const static int gport = 8080;
private:
int __listen_sock;
int __port;
__epoll __poll; // 这里直接维护一个epoll
然后,其实在很多真正的开发场景中,多路转接的方法都是可以选的,一般就是写一个虚基类class poll {}
,里面的函数就是纯虚的,然后去继承epoll, select, poll 这些。
所以对于tcp_server, 我们多路转接都叫poll比较好,因为也不一定要用epoll。
继续先写tcp_server
tcp_server(int port = gport)
: __port(gport) {
// 1. 创建listensock
__listen_sock = Sock::Socket();
Sock::Bind(__listen_sock, __port);
Sock::Listen(__listen_sock);
// 2. 创建多路转接对象
__poll.create_poll();
// 3. 封装listensock, 因为以后在tcpserver里面,已经不会再有裸露的sock了,都是要封装的
connection* conn = new connection(__listen_sock);
conn->set_callback(read_callback, nullptr, nullptr);
conn->__tsvr = this;