一:网络缓冲区
下面是我写过关于网络缓冲区的两篇文章,一篇理论,一篇实战。这两篇的讲解都比本项目使用的难。
Linux下网络缓冲区------chainbuffer的具体设计-CSDN博客
我们在进行网络传输消息的时候,我们一般会直接使用系统的网络缓冲区,但是不利于我们进行管理和控制,于是我们直接手写网络缓冲区。我们看下面的类:其中包含存放数据的地址,和分配的大小,以及占用的地址或者说是占用的大小。这个类比较简单,我们很容易能看出来他们的职责。
对于他的构造函数和析构函数,都是比较简单也就是直接将数据进行初始化和删除数据的操作。对于下面的四个函数都是字面意思,返回一些需要的数据。
cpp
class DLL_MODIFIER CSimpleBuffer {
public:
CSimpleBuffer();
~CSimpleBuffer();
uchar_t *GetBuffer() { return buf_; }
uint32_t GetAllocSize() { return alloc_size_; }
uint32_t GetWriteOffset() { return write_offset_; }
void IncWriteOffset(uint32_t len) { write_offset_ += len; }
void Extend(uint32_t len);
uint32_t Write(void *buf, uint32_t len);
uint32_t Read(void *buf, uint32_t len);
private:
uchar_t *buf_; //开辟空间
uint32_t alloc_size_; //分配大小
uint32_t write_offset_; //写的偏移量
};
对于这个扩展函数来说,我们一开始就开辟一块内存,但是当我们继续来数据的时候,我们这个开辟的空间不够用,于是我们需要在这个基础上进行扩展内存。在扩展的时候我们要多开辟一点内存,防止内存溢出。
cpp
void CSimpleBuffer::Extend(uint32_t len) { //扩展缓冲区
alloc_size_ = write_offset_ + len; //在这里我们是使用之前使用的和加上新开辟的,组成新的缓冲区。
alloc_size_ += alloc_size_ >> 2; // increase by 1/4 allocate size //每次还额外增加四分之一的空间。
uchar_t *new_buf = (uchar_t *)realloc(buf_, alloc_size_); //在原先的内存块上扩展内存,如果无法扩展,\
则分配个新的内存块,并将数据复制到新的内存块中。
buf_ = new_buf;
}
我们在写数据的时候,肯定要先判断我们的空间是否够,如果不够那么就扩展呗。对于读取数据比较简单。
cpp
uint32_t CSimpleBuffer::Write(void *buf, uint32_t len) { //每次写之前都要看看空间够不够,不够的话就扩展。
if (write_offset_ + len > alloc_size_) {
Extend(len);
}
if (buf) {
memcpy(buf_ + write_offset_, buf, len); //将buf_ + write_offset_ 位置开始,将buf中的数据拷贝到buf_ + write_offset_位置开始,拷贝len个字节。
}
write_offset_ += len;
return len;
}
uint32_t CSimpleBuffer::Read(void *buf, uint32_t len) { //将数据读出来。
if (0 == len)
return len;
if (len > write_offset_)
len = write_offset_;
if (buf)
memcpy(buf, buf_, len);
write_offset_ -= len;
memmove(buf_, buf_ + len, write_offset_);
return len;
}
二:main函数,将之前写的一些基础组件进行组合
在main函数中我们一共有八个部分,第一二部分就是我们要加载的数据库配置文件以及要监听的端口和地址。我们先加载它,我们这个加载的函数并不是自己写的,使用别人写好的,会使用就可以了。三四就是数据库开始初始化,五六就是网络模块的初始化,七八就是开始循环等待客户端的连接。
cpp
void http_callback(void *callback_data, uint8_t msg, uint32_t socket_handle,
void *pParam) {
UNUSED(callback_data);
UNUSED(pParam);
if (msg == NETLIB_MSG_CONNECT) {
// 这里是不是觉得很奇怪,为什么new了对象却没有释放?
// 实际上对象在被Close时使用delete this的方式释放自己
CHttpConn *pConn = new CHttpConn();
pConn->OnConnect(socket_handle);
} else {
LogError("!!!error msg:{}", msg);
}
}
int main(int argc, char *argv[]) {
// 默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,
// 或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。
// SIG_IGN 忽略信号的处理程序
signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号
int ret = 0;
// 1.获取配置文件路径
//配置文件中就是服务端监听的端口,比如本地和8081,还有加载的数据库的配置信息。
char *str_tc_http_server_conf = NULL;
if(argc > 1) {
str_tc_http_server_conf = argv[1]; // 指向配置文件路径
} else {
str_tc_http_server_conf = (char *)"tc_http_server.conf";
}
printf("conf file path: %s\n", str_tc_http_server_conf);
// 2.读取配置文件
//2.1 解析配置文件
CConfigFileReader config_file; //读取配置文件 开源文件
if(config_file.ParseConf(str_tc_http_server_conf) != 0) {
std::cout << str_tc_http_server_conf << " no exist, please check conf file\n";
return -1;
}
// 2.2读取配置文件字段
// 日志级别
char *log_level = config_file.GetConfigName("log_level"); //读取日志设置级别
if (!log_level) {
LogError("config item missing, exit... log_level:{}", log_level);
return -1;
}
DLog::SetLevel(log_level); //设置日志打印级别 spdlog
// http监听地址和端口
char *http_listen_ip = config_file.GetConfigName("HttpListenIP");
char *str_http_port = config_file.GetConfigName("HttpPort"); //8081 -- nginx.conf,当前服务的端口
// 检测监听ip和端口是否存在
if (!http_listen_ip || !str_http_port) {
LogError("config item missing, exit... ip:{}, port:{}", http_listen_ip, str_http_port);
return -1;
}
uint16_t http_port = atoi(str_http_port);
// 打印提示而已
LogInfo("main into"); //单例模式 日志库 spdlog 我们这里已经define了,在log里面,这里可以直接使用
//3.初始化 redis 连接池,内部也会读取读取配置文件tc_http_server.conf
CacheManager::SetConfPath(str_tc_http_server_conf); //设置配置文件路径
CacheManager *cache_manager = CacheManager::getInstance();
if (!cache_manager) {
LogError("CacheManager init failed");
return -1;
}
//4.初始化 mysql 连接池,内部也会读取读取配置文件tc_http_server.conf
CDBManager::SetConfPath(str_tc_http_server_conf); //设置配置文件路径
CDBManager *db_manager = CDBManager::getInstance();
if (!db_manager) {
LogError("DBManager init failed");
return -1;
}
//5.reactor网络模型, 这里只对Windows系统有作用,Linux环境调用这个函数实际没有做什么
ret = netlib_init();
if (ret == NETLIB_ERROR) {
LogError("netlib_init failed");
return ret;
}
//6.监听指定的IP和端口,并绑定accept新连接后的回调函数http_callback, 这里主要是我们http端口
ret = netlib_listen(http_listen_ip, http_port, http_callback, NULL); //就是普通的网络监听
//创建socket,服务端接听,然后设置客户端的cb,然后onwrite,执行callback。
if (ret == NETLIB_ERROR) {
LogError("listen {}:{} failed", http_listen_ip, http_port);
return ret;
}
LogInfo("server start listen on:For http://{}:{}", http_listen_ip, http_port);
LogInfo("now enter the event loop...");
//7.将当前进程id写入文件server.pid, 可以直接cat这个文件获取进程id
WritePid();
//8.进入epoll_wait触发的循环流程
netlib_eventloop(1);
return 0;
}
我们重点讲一下,这个回调函数:http_callback。
我们看一下这个回调函数是在哪里传入进去的,他是在第六个位置,我们的网络在监听的时候,当客户端连接上来给他配置的回调函数。我们在回想一下这个回调函数会在哪里被执行?由于他是客户端自带的一个回调函数,那么我们的回调函数被执行就是在触发写事件的时候。
那么在这个回调函数中我们看到创建了HTTP的conn。那我们来看看这个类都写了什么。因为我们这里先看整体的main函数,因此先只看onconnect函数,这个函数到底做了什么。
cpp
class CHttpConn : public CRefObject {
public:
//构造函数
CHttpConn();
//析构函数
virtual ~CHttpConn();
// 获取当前自定义的唯一handle
uint32_t GetConnHandle() { return conn_handle_; }
// 返回对端ip
char *GetPeerIP() { return (char *)peer_ip_.c_str(); }
//发送数据,最终调用socket的send
int Send(void *data, int len);
//关闭连接,最终调用socket的close
void Close();
// accept收到新连接fd,触发OnConnect的调用,并把socket fd传递进来 //在接到新连接之后,执行回调函数,会传入socket的fd。不信自己去accept去看。
void OnConnect(net_handle_t socket_handle);
// 可读事件触发回调OnRead
void OnRead();
// 可写事件触发回调OnWrite
void OnWrite();
// 关闭事件触发OnClose
void OnClose();
// 数据发送完毕回调OnWriteComlete,自己业务处理的
void OnWriteComlete();
private:
// 账号注册处理
void _HandleRegisterRequest(string &url, string &post_data);
// 账号登陆处理
void _HandleLoginRequest(string &url, string &post_data);
//获取我的文件列表
void _HandleMyfilesRequest(string &url, string &post_data);
protected:
//socket fd
net_handle_t socket_handle_;
// 业务自定义唯一标识
uint32_t conn_handle_;
//当前socket写缓存是否有空间,=ture当前socket写缓存已满,=false当前socket写缓存有空间可以写
bool busy_;
//连接状态
uint32_t state_;
//保存对端ip
std::string peer_ip_;
//保存对端端口
uint16_t peer_port_;
//缓存从socket读取的数据
CSimpleBuffer in_buf_;
//缓存还没有发送出去的数据
CSimpleBuffer out_buf_;
// http解析
CHttpParserWrapper http_parser_;
};
首先我们写类的函数的时候,我们先看一下其他重要的函数,因为这个类是一个HTTP的连接,所以和basesocket一样都需要一个寻找这个类的函数,以及存放整体的一个容器。然后我们在这个http连接中在传入一个callback,用于使用这个http的socket全部信息。
cpp
CHttpConn *FindHttpConnByHandle(uint32_t handle);
typedef hash_map<uint32_t, CHttpConn *> HttpConnMap_t;
// conn_handle 从0开始递增,可以防止因socket handle重用引起的一些冲突
static uint32_t g_conn_handle_generator = 0;
static HttpConnMap_t g_http_conn_map;
CHttpConn *FindHttpConnByHandle(uint32_t conn_handle) {
CHttpConn *pConn = NULL;
HttpConnMap_t::iterator it = g_http_conn_map.find(conn_handle);
if (it != g_http_conn_map.end()) {
pConn = it->second;
}
return pConn;
}
// 连接的处理函数
void httpconn_callback(void *callback_data, uint8_t msg, uint32_t handle,
uint32_t uParam, void *pParam) {
NOTUSED_ARG(uParam);
NOTUSED_ARG(pParam);
// convert void* to uint32_t, oops
uint32_t conn_handle = *((uint32_t *)(&callback_data)); //获取连接标识
CHttpConn *pConn = FindHttpConnByHandle(conn_handle); //根据连接标识找到对应的http对象
if (!pConn) { //如果没有则返回
// LogWarn("no find conn_handle: {}", conn_handle);
return;
}
pConn->AddRef(); //添加引用计数
switch (msg) {
case NETLIB_MSG_READ: //可读事件
pConn->OnRead();
break;
case NETLIB_MSG_WRITE: //可写事件
pConn->OnWrite();
break;
case NETLIB_MSG_CLOSE:
pConn->OnClose(); //关闭事件
break;
default:
LogError("!!!httpconn_callback error msg:{}", msg);
//fix me,可以考虑如果这里触发,直接关闭该连接
// pConn->Close();
break;
}
pConn->ReleaseRef(); //释放引用计数,如果数据发送完毕,对应的http 连接在这个位置为析构对象
}
也就是说,我们在与服务端进行连接的时候,我们在这个连接中又创建了一个http的类,在这个连接中,使用这个类进行通信。
到目前位置,我们只讲解了一些基础的组件,以及将这些组件组合起来了,这样我们就可以正常的接收外部的连接了。我们还没有讲一些业务相关的代码,会在以后进行讲解。0voice · GitHub