目录
一、实现一个Reator模式的服务器
1.一些准备工作
在我们编写服务器前需要定义一些预备的头文件用于封装各种操作。
(1)下面的头文件可直接使用
err.hpp
#pragma once
#include<iostream>
enum errorcode
{
USAGE_ERROR = 1,
SOCKET_ERROR,
BIND_ERROR,
LISTEN_ERROR,
EPOLL_CREATE_ERROR
};
log.hpp
注意一下,下面使用logmessage打印的日志等级为DEBUG,我们都可以注释掉。
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>
//一个文件用于保存正常运行的日志,一个保存错误日志
#define LOG_FILE "./log.txt"
#define ERROR_FILE "./error.txt"
//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
//将运行等级转换为字符串
const char* to_string(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, ...)
{
//输出到屏幕
char logprefix[1024];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串
char logcontent[1024];
va_list arg;//可变参数列表
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout << logprefix << logcontent << std::endl;
//输出到文件中
//打开两个文件
FILE* log = fopen(LOG_FILE, "a");
FILE* err = fopen(ERROR_FILE, "a");
if(log != nullptr && err != nullptr)
{
FILE* cur = nullptr;
if(level == DEBUG || level == NORMAL || level == WARNING)
cur = log;
if(level == ERROR || level == FATAL)
cur = err;
if(cur)
fprintf(cur, "%s%s\n", logprefix, logcontent);
fclose(log);
fclose(err);
}
}
(2)一些头文件需要进行一些封装才能更适合当前代码的使用。
sock.hpp
//对原Sock进行修改,使监听套接字只维护在该类中,并增加一些配套成员函数
#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"
class Sock
{
private:
static const int backlog = 32;//全链接队列长度为32
static const int default_sock = -1;//初始化的默认套接字
public:
Sock()
:_listensock(default_sock)
{}
~Sock()
{
if(_listensock != default_sock)
close(_listensock);
}
void Socket()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(_listensock < 0)//创建套接字失败打印错误原因
{
logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
exit(SOCKET_ERROR);//退出
}
logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到
//打开端口复用保证程序退出后可以立即正常启动
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
void Bind(int port)
{
struct sockaddr_in local;//储存本地网络信息
local.sin_family = AF_INET;//通信方式为网络通信
local.sin_port = htons(port);//将网络字节序的端口号填入
local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
{
logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
exit(BIND_ERROR);//退出
}
logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到
}
void Listen()
{
//listen设置socket为监听模式
if(listen(_listensock, backlog) < 0) // 第二个参数backlog后面在填这个坑
{
logmessage(FATAL, "listen socket error");
exit(LISTEN_ERROR);
}
logmessage(NORMAL, "listen socket success");
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;//储存本地网络信息
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
if(sock < 0)
{
logmessage(ERROR, "accept fail");//接收新文件描述符失败
}
else
{
logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
*clientip = inet_ntoa(peer.sin_addr);
*clientport = ntohs(peer.sin_port);
}
return sock;
}
//返回监听套接字的文件描述符
int FD()
{
return _listensock;
}
//关闭监听套接字
int Close()
{
if(_listensock != default_sock)
close(_listensock);
}
private:
int _listensock;
};
util.hpp
//将原来写过的将套接字设为非阻塞的代码封装在Util类中
#pragma once
#include<iostream>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
class Util
{
public:
//将文件描述符设为非阻塞
static bool SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);//获取文件描述符的标志,该标志是一个位图结构
if(fl < 0)//获取失败
{
std::cerr << "fctnl:" << strerror(errno) << std::endl;//打印错误码
return false;
}
else
{
fcntl(fd, F_SETFL, fl | O_NONBLOCK);//将该文件描述符设为非阻塞
return true;
}
}
};
(3)下面的文件就需要我们自己实现了
epoller.hpp
这个文件定义了Epoller类,用于封装一些我们需要的epoll操作。(我们只实现了前四个函数,剩下的会在以后逐渐补齐)
//epoll的处理逻辑全部保存在该文件中
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/epoll.h>
#include"err.hpp"
#include"log.hpp"
const static int default_epfd = -1;
const static int size = 128;
class Epoller
{
public:
Epoller()
:_epfd(default_epfd)
{}
~Epoller()
{
if(_epfd != default_epfd)
close(_epfd);
}
//创建Epoll模型
void Create()
{
_epfd = epoll_create(size);
if(_epfd < 0)
{
//创建失败
logmessage(FATAL, "epoll_create error, error code: %d, error string: %s", errno, strerror(errno));
exit(EPOLL_CREATE_ERROR);
}
}
//向Epoll模型中增加需要关心的事件
bool AddEvent(int sock, uint32_t events)
{
//构建一个epoll_event结构体
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
//向epoll模型中插入该节点
int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
//返回值为0表示插入成功,否则失败
if(ret == 0)
return true;
else
return false;
}
//查看已经就绪的文件描述符
int Wait(struct epoll_event revs[], int num, int timeout)
{
int ret = epoll_wait(_epfd, revs, num, timeout);
return ret;
}
//对文件描述符做一定操作,我们只封装修改和删除结构体的代码
int Control(int sock, uint32_t events, int handle)
{
int ret = 0;
//用户要修改数据
if(handle == EPOLL_CTL_MOD)
{
//构建新的结构体覆盖旧的
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
ret = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
}
//用户要删除数据
else if(handle == EPOLL_CTL_DEL)
{
ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
}
//出错了
else
ret = -1;
return ret;
}
void Close()
{
if(_epfd != default_epfd)
close(_epfd);
}
private:
int _epfd;//epoll模型的文件描述符
};
2.初步编写服务器代码
(1)构建服务器类
对于整个服务器而言,我们将服务器的主要运行代码定义在tcpserver.hpp中。server.cc做为源文件,err.hpp、util.hpp、log.hpp、epoller.hpp、sock.hpp为各种封装组件。
首先,需要定义一个TcpServer类,新增initserver、start等函数。成员变量包括管理套接字操作的Sock变量,管理epoll操作的Epoller变量,还有一个端口号。
#pragma once
#include<iostream>
#include<poll.h>
#include<string>
#include<functional>
#include"log.hpp"
#include"Epoller.hpp"
#include"err.hpp"
#include"sock.hpp"
#include"util.hpp"
namespace tcpserver
{
static const int default_port = 8080;//默认端口号为8080
static const int num = 128;//设置元素数为2048,当然poll可监视的文件描述符无上限,也可以设计令其动态增长
static const int default_value = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素
class TcpServer
{
public:
//构造函数
TcpServer(int port = default_port)
:_port(port)
{}
//析构函数
~TcpServer()
{}
void initserver()
{}
void start()
{
while(1)
{}
}
private:
uint16_t _port;//进程的端口号
Sock _sock;//维护套接字的类
Epoller _epoller;//封装各种epoll接口的类
};
}
(2)对initserver进行初步编写
initserver的执行流程如下:
创建listen套接字、创建epoll模型、将套接字文件描述符设为非阻塞、将listen套接字加入模型。
void initserver()
{
//创建监听套接字
_sock.Socket();
_sock.Bind(_port);
_sock.Listen();
//创建epoll模型
_epoller.Create();
//加入按照ET模式工作的epoll模型的套接字必须是非阻塞的,所以还需要将listen套接字设为非阻塞
Util::SetNonBlock(_sock.FD());
//将listen套接字加入epoll模型,且模型按ET模式工作
_epoller.AddEvent(_sock.FD(), EPOLLIN | EPOLLET);
}
3.新建Connection类
(1)构建该类的原因
我们在之前的普通epoll服务器中使用了下面的代码:
这个代码实际上是有问题的,由于TCP面向字节流的特点,所以程序员需要自己解决读取完整报文的问题。也就是,我们进行一次读取并不能保证读到一个完整报文。而且该缓冲区是在栈上的,每次读完都会被清空。
所以,我们需要对每个套接字都各维护一个读缓冲区、一个写缓冲区,这样才能保证数据能够将报文保留下来,以便后续操作。
最终,我们决定再定义一个Connection类用于维护所有的套接字。其中既包括缓冲区,还包括可以处理读事件、写时间、异常事件的成员函数。这样,我们就可以允许我们自己在TcpServer类中定义函数传递给相应的套接字,实现不同的处理方式。
其实严格来讲listen套接字并不需要缓冲区,但是为了统一接口,我们也将其作为一个普通链接看待。
(2)管理相关类变量
服务器中往往会有很多链接,每个链接都有这样一个Connection变量,而这些变量都需要维护,所以我们在tcpserver的成员变量内维护一个哈希表保存这些变量。
由于我们加入了Connection的新对象,所以initserver还需要增加管理这些变量的内容。
最后我们决定再定义一个能够处理所有新增的套接字的函数AddConnection,将这些代码统一封装在里面。
定义一个AddConnection将套接字数据加入数据结构中进行管理,定义四个函数用于执行不同时间的处理。
(3)重新编写initserver
重新编写初始化函数,而且后期还会增加成员变量,依旧需要修改。
4.任务分发
(1)TcpServer添加成员变量
TcpServer中需要增加一个_revs结构体数组和一个_num变量。
前面我们也定义了num。
initserver中也要增加初始化代码。
析构函数也可以编写了。
(2)实现dispatcher和loop函数
我们将原来的start函数重命名为dispatcher。
在循环内使用loop函数封装代码。
(3)实现server.cc并进行试运行
server.cc
执行代码,和之前基本没有变化。但随着我们实现的推进,还需要对其进行不断地微调修改。有了源文件,我们就能试运行该程序了。
#include"tcpserver.hpp"
#include"err.hpp"
#include<memory>
using namespace std;
using namespace tcpserver;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
unique_ptr<TcpServer> p(new TcpServer());
p->initserver();
p->dispatcher();
return 0;
}
5.实现各个执行函数
实现各个处理函数,省略号表示上下还有代码。
class TcpServer
{
private:
......
//Connection变量是否存在
bool Connection_exist(int sock)
{
auto it = _connections.find(sock);
if(it == _connections.end())
return false;
else
return true;
}
//出现错误的处理函数
void Excepter(Connection* con)
{
logmessage(DEBUG, "Excepter in");
//出错了我们直接断开连接
//将数据从epoll模型中删除
_epoller.Control(con->_sock, 0, EPOLL_CTL_DEL);
//将该链接的Connection变量删除
_connections.erase(con->_sock);
//将该链接的文件描述符关闭
con->Close();
//删该除节点
delete con;
logmessage(DEBUG, "Excepter out");
}
//接收数据的处理函数
void Recver(Connection* con)
{
logmessage(DEBUG, "Recver in");
char buffer[1024];//定义缓冲区
while(1)//由于ET模式只有一次提醒机会,所以我们需要不断读取,直到无数据可读为止
{
ssize_t n = recv(con->_sock, buffer, sizeof(buffer)-1, 0);
if(n > 0)//读到了数据
{
buffer[n] = 0;//在末尾加上/0
con->_inbuffer += buffer;//追加到该链接的缓冲区
logmessage(NORMAL, "client# %s", buffer);
_func(con);//处理该链接的数据
}
else if (n == 0)//无数据可读
{
if(con->_excepter)
{
con->_excepter(con);
break;
}
}
else//出错
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;//没有数据就绪了,直接跳出循环
else if(errno == EINTR)
continue;//正在进行信号处理,继续读取等待处理完成即可
else//出错了,让Excepter处理错误,然后跳出循环
{
if(con->_excepter)
{
con->_excepter(con);
break;
}
}
}
}
logmessage(DEBUG, "Recver out");
}
//发送数据的处理函数
void Sender(Connection* con)
{
logmessage(DEBUG, "Sender in");
while(1)//将数据循环全部发送
{
int n = send(con->_sock, con->_outbuffer.c_str()
, con->_outbuffer.size(), 0);//先发送数据
if(n > 0)//此次发送数据成功
{
con->_outbuffer.erase(0, n);//删除缓冲区中已发送的数据
if(con->_outbuffer.empty())
{
//此次发送就已经将所有数据发完了
break;
}
//还有数据,继续循环发送
}
else//此次发送数据失败
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;//没有数据就绪了,直接跳出循环
else if(errno == EINTR)
continue;//正在进行信号处理,继续读取等待处理完成即可
else//出错了,让Excepter处理错误,然后跳出循环
{
if(con->_excepter)
{
con->_excepter(con);
break;
}
}
}
}
//如果数据没发完,需要让epoll对该描述符的写事件进行关心。
//如果发完了,要关闭对写事件的关心
if (!con->_outbuffer.empty())
con->_pts->EnableReadWrite(con, true, true);
else
con->_pts->EnableReadWrite(con, true, false);
logmessage(DEBUG, "Sender out");
}
//接收链接的处理函数,只用于listen套接字的读事件处理
void Accepter(Connection* con)
{
logmessage(DEBUG, "Accepter in");
//循环接收多个链接,直到没有链接接收时退出
while(1)
{
std::string clientip;
uint16_t clientport;
int err = 0;
//epoll负责等,accept直接接收,err负责将本次调用的错误码从函数中带出
int sock = _sock.Accept(&clientip, &clientport, &err);
if(sock > 0)//接收成功
{
//将链接管理起来
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(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
}
else//接收失败
{
if(err == EAGAIN || err == EWOULDBLOCK)
break;//没有数据就绪了,直接跳出循环
else if(err == EINTR)
continue;//正在进行信号处理,继续读取等待处理完成即可
else//出错了,跳出执行
break;
}
}
logmessage(DEBUG, "Accepter out");
}
.......
};
6.增加序列化与反序列化代码
我们直接使用之前写的网络计算器代码即可。
protocol.hpp
#pragma once
//#define MYSELF
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
//按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
//查找左侧的\r\n
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;//没找到表明数据有问题
//截取正文长度的字符串并转为整形
std::string text_len_string = package.substr(0, pos);
int text_len = std::stoi(text_len_string);
//截取正文放入储存正文的string里
*text = package.substr(pos + LINE_SEP_LEN, text_len);
return true;
}
class Request
{
public:
Request()
:_a(0)
,_b(0)
,_op(0)
{}
Request(int a, int b, char op)
:_a(a)
,_b(b)
,_op(op)
{}
//Request序列化
//Request结构体转化为字符串"a op b"
bool serialize(std::string *out)
{
#ifdef MYSELF
out->clear();//清空
//将变量转为字符串
std::string a_string = std::to_string(_a);
std::string b_string = std::to_string(_b);
*out = a_string;
*out += SEP;
*out += _op;
*out += SEP;
*out += b_string;
#else
Json::Value root;
root["first"] = _a;
root["second"] = _b;
root["oper"] = _op;
Json::FastWriter writer;
// Json::StyledWriter writer;
*out = writer.write(root);
#endif
return true;
}
//Request反序列化
//字符串"a op b"转化为Request结构体
bool deserialize(const std::string &in)
{
#ifdef MYSELF
auto left = in.find(SEP);//查找左侧的SEP
auto right = in.rfind(SEP);//查找右侧的SEP
if (left == std::string::npos || right == std::string::npos)
return false;//找不到,数据有问题
if (left == right)
return false;//只有一个SEP,数据有问题
if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
return false;//指向的不是一个位置,数据有问题
//按左闭右开的方式构造两个数字
std::string a_string = in.substr(0, left);
std::string b_string = in.substr(right + SEP_LEN);
//读取到的数字不能为空
if (a_string.empty() || b_string.empty())
return false;
//填入数据
_a = std::stoi(a_string);
_b = std::stoi(b_string);
_op = in[left + SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
_a = root["first"].asInt();
_b = root["second"].asInt();
_op = root["oper"].asInt();
#endif
return true;
}
public:
int _a;
int _b;
char _op;
};
class Response
{
public:
Response()
:_exitcode(0)
,_result(0)
{}
Response(int exitcode, int result)
:_exitcode(exitcode)
,_result(result)
{}
//Response序列化
//Response结构体转化为字符串"exitcode result"
bool serialize(std::string *out)
{
#ifdef MYSELF
out->clear();//清空
//将变量转为字符串
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;
}
//Response反序列化
//字符串"exitcode result"转化为Response结构体
bool deserialize(const std::string &in)
{
#ifdef MYSELF
auto mid = in.find(SEP);//查找中间的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:
int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
int _result; // 计算结果
};
//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024];//接收网络数据的字符串
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
if (n > 0)//读到了数据
{
buffer[n] = 0;
inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
if (pos == std::string::npos)
//如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
continue;//接着到最开始读数据直到读全
//执行至此时,报文"content_len"\r\n这部分一定读全了
//可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
//我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
//一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
//所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
if (inbuffer.size() < total_len)
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue;//报文没读全继续到上面读取
}
//此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
break;//准备处理报文,跳出循环
}
else//没读到数据
return false;
}
return true;
}
bool divonepackage(std::string &inbuffer, std::string *text)
{
*text = "";
// 分析处理
auto pos = inbuffer.find(LINE_SEP);
if (pos == std::string::npos)
return false;
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
if (inbuffer.size() < total_len)
return false;
// 至少有一个完整的报文
*text = inbuffer.substr(0, total_len);
inbuffer.erase(0, total_len);
return true;
}
server.cc
#include"tcpserver.hpp"
#include"protocol.hpp"
#include<memory>
using namespace std;
using namespace tcpserver;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}
//测试函数
// void send_back(Connection* con)
// {
// con->_outbuffer += con->_inbuffer;
// con->_inbuffer.erase(con->_inbuffer.begin(), con->_inbuffer.end());
// if (con->_sender)
// con->_sender(con);
// }
//计算器函数
const string ops = "+-*/%";
bool calculate(const Request &req, Response &resp)
{
// req已经有结构化完成的数据啦,你可以直接使用
resp._exitcode = OK;
resp._result = 0;
switch (req._op)
{
case '+':
resp._result = req._a + req._b;
break;
case '-':
resp._result = req._a - req._b;
break;
case '*':
resp._result = req._a * req._b;
break;
case '/':
{
if (req._b == 0)
resp._exitcode = DIV_ZERO;
else
resp._result = req._a / req._b;
}
break;
case '%':
{
if (req._b == 0)
resp._exitcode = MOD_ZERO;
else
resp._result = req._a % req._b;
}
break;
default:
resp._exitcode = OP_ERROR;
break;
}
return true;
}
//处理数据的函数
void handler(Connection* con)
{
printf("handler in\n");
string one_package;
while(divonepackage(con->_inbuffer, &one_package))//读取完整报文
{
string text;
//去掉报头
if(!delength(one_package, &text))
return;
//正文反序列化构造request
Request req;
if(!req.deserialize(text))
return;
//处理request得到response
Response resp;
calculate(req, resp);
//respone序列化得到正文
string result;
resp.serialize(&result);
//增加报头
con->_outbuffer += enlength(result);
//打印结果
cout << result << endl;
//发送
if (con->_sender)
con->_sender(con);
}
printf("handler out\n");
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
unique_ptr<TcpServer> p(new TcpServer(handler));
p->initserver();
p->dispatcher();
return 0;
}
7.运行
我们使用之前实现的网络计算器客户端作为客户端,连接服务器并实现计算处理。
二、总代码
1.服务器
epoller.hpp
//epoll的处理逻辑全部保存在该文件中
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/epoll.h>
#include"err.hpp"
#include"log.hpp"
const static int default_epfd = -1;
const static int size = 128;
class Epoller
{
public:
Epoller()
:_epfd(default_epfd)
{}
~Epoller()
{
if(_epfd != default_epfd)
close(_epfd);
}
//创建Epoll模型
void Create()
{
_epfd = epoll_create(size);
if(_epfd < 0)
{
//创建失败
logmessage(FATAL, "epoll_create error, error code: %d, error string: %s", errno, strerror(errno));
exit(EPOLL_CREATE_ERROR);
}
}
//向Epoll模型中增加需要关心的事件
bool AddEvent(int sock, uint32_t events)
{
//构建一个epoll_event结构体
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
//向epoll模型中插入该节点
int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
//返回值为0表示插入成功,否则失败
if(ret == 0)
return true;
else
return false;
}
//查看已经就绪的文件描述符
int Wait(struct epoll_event revs[], int num, int timeout)
{
int ret = epoll_wait(_epfd, revs, num, timeout);
return ret;
}
//对文件描述符做一定操作,我们只封装修改和删除结构体的代码
int Control(int sock, uint32_t events, int handle)
{
int ret = 0;
//用户要修改数据
if(handle == EPOLL_CTL_MOD)
{
//构建新的结构体覆盖旧的
struct epoll_event ev;
ev.events = events;
ev.data.fd = sock;
ret = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
}
//用户要删除数据
else if(handle == EPOLL_CTL_DEL)
{
ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
}
//出错了
else
ret = -1;
return ret;
}
void Close()
{
if(_epfd != default_epfd)
close(_epfd);
}
private:
int _epfd;//epoll模型的文件描述符
};
err.hpp
#pragma once
#include<iostream>
enum errorcode
{
USAGE_ERROR = 1,
SOCKET_ERROR,
BIND_ERROR,
LISTEN_ERROR,
EPOLL_CREATE_ERROR
};
log.hpp
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>
//一个文件用于保存正常运行的日志,一个保存错误日志
#define LOG_FILE "./log.txt"
#define ERROR_FILE "./error.txt"
//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
//将运行等级转换为字符串
const char* to_string(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, ...)
{
//输出到屏幕
char logprefix[1024];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串
char logcontent[1024];
va_list arg;//可变参数列表
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout << logprefix << logcontent << std::endl;
//输出到文件中
//打开两个文件
FILE* log = fopen(LOG_FILE, "a");
FILE* err = fopen(ERROR_FILE, "a");
if(log != nullptr && err != nullptr)
{
FILE* cur = nullptr;
if(level == DEBUG || level == NORMAL || level == WARNING)
cur = log;
if(level == ERROR || level == FATAL)
cur = err;
if(cur)
fprintf(cur, "%s%s\n", logprefix, logcontent);
fclose(log);
fclose(err);
}
}
protocol.hpp
#pragma once
//#define MYSELF
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
//按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
//查找左侧的\r\n
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;//没找到表明数据有问题
//截取正文长度的字符串并转为整形
std::string text_len_string = package.substr(0, pos);
int text_len = std::stoi(text_len_string);
//截取正文放入储存正文的string里
*text = package.substr(pos + LINE_SEP_LEN, text_len);
return true;
}
class Request
{
public:
Request()
:_a(0)
,_b(0)
,_op(0)
{}
Request(int a, int b, char op)
:_a(a)
,_b(b)
,_op(op)
{}
//Request序列化
//Request结构体转化为字符串"a op b"
bool serialize(std::string *out)
{
#ifdef MYSELF
out->clear();//清空
//将变量转为字符串
std::string a_string = std::to_string(_a);
std::string b_string = std::to_string(_b);
*out = a_string;
*out += SEP;
*out += _op;
*out += SEP;
*out += b_string;
#else
Json::Value root;
root["first"] = _a;
root["second"] = _b;
root["oper"] = _op;
Json::FastWriter writer;
// Json::StyledWriter writer;
*out = writer.write(root);
#endif
return true;
}
//Request反序列化
//字符串"a op b"转化为Request结构体
bool deserialize(const std::string &in)
{
#ifdef MYSELF
auto left = in.find(SEP);//查找左侧的SEP
auto right = in.rfind(SEP);//查找右侧的SEP
if (left == std::string::npos || right == std::string::npos)
return false;//找不到,数据有问题
if (left == right)
return false;//只有一个SEP,数据有问题
if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
return false;//指向的不是一个位置,数据有问题
//按左闭右开的方式构造两个数字
std::string a_string = in.substr(0, left);
std::string b_string = in.substr(right + SEP_LEN);
//读取到的数字不能为空
if (a_string.empty() || b_string.empty())
return false;
//填入数据
_a = std::stoi(a_string);
_b = std::stoi(b_string);
_op = in[left + SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
_a = root["first"].asInt();
_b = root["second"].asInt();
_op = root["oper"].asInt();
#endif
return true;
}
public:
int _a;
int _b;
char _op;
};
class Response
{
public:
Response()
:_exitcode(0)
,_result(0)
{}
Response(int exitcode, int result)
:_exitcode(exitcode)
,_result(result)
{}
//Response序列化
//Response结构体转化为字符串"exitcode result"
bool serialize(std::string *out)
{
#ifdef MYSELF
out->clear();//清空
//将变量转为字符串
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;
}
//Response反序列化
//字符串"exitcode result"转化为Response结构体
bool deserialize(const std::string &in)
{
#ifdef MYSELF
auto mid = in.find(SEP);//查找中间的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:
int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
int _result; // 计算结果
};
//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024];//接收网络数据的字符串
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
if (n > 0)//读到了数据
{
buffer[n] = 0;
inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
if (pos == std::string::npos)
//如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
continue;//接着到最开始读数据直到读全
//执行至此时,报文"content_len"\r\n这部分一定读全了
//可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
//我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
//一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
//所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
if (inbuffer.size() < total_len)
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue;//报文没读全继续到上面读取
}
//此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
break;//准备处理报文,跳出循环
}
else//没读到数据
return false;
}
return true;
}
bool divonepackage(std::string &inbuffer, std::string *text)
{
*text = "";
// 分析处理
auto pos = inbuffer.find(LINE_SEP);
if (pos == std::string::npos)
return false;
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
if (inbuffer.size() < total_len)
return false;
// 至少有一个完整的报文
*text = inbuffer.substr(0, total_len);
inbuffer.erase(0, total_len);
return true;
}
sock.hpp
//对原Sock进行修改,使监听套接字只维护在该类中,并增加一些配套成员函数
#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"
class Sock
{
private:
static const int backlog = 32;//全链接队列长度为32
static const int default_sock = -1;//初始化的默认套接字
public:
Sock()
:_listensock(default_sock)
{}
~Sock()
{
if(_listensock != default_sock)
close(_listensock);
}
void Socket()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(_listensock < 0)//创建套接字失败打印错误原因
{
logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
exit(SOCKET_ERROR);//退出
}
logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到
//打开端口复用保证程序退出后可以立即正常启动
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
void Bind(int port)
{
struct sockaddr_in local;//储存本地网络信息
local.sin_family = AF_INET;//通信方式为网络通信
local.sin_port = htons(port);//将网络字节序的端口号填入
local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
{
logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
exit(BIND_ERROR);//退出
}
logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到
}
void Listen()
{
//listen设置socket为监听模式
if(listen(_listensock, backlog) < 0) // 第二个参数backlog后面在填这个坑
{
logmessage(FATAL, "listen socket error");
exit(LISTEN_ERROR);
}
logmessage(NORMAL, "listen socket success");
}
int Accept(std::string *clientip, uint16_t *clientport, int* err)
{
struct sockaddr_in peer;//储存本地网络信息
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
printf("sock:%d", sock);
*err = errno;
if(sock > 0)
{
*clientip = inet_ntoa(peer.sin_addr);
*clientport = ntohs(peer.sin_port);
}
return sock;
}
//返回监听套接字的文件描述符
int FD()
{
return _listensock;
}
//关闭监听套接字
int Close()
{
if(_listensock != default_sock)
close(_listensock);
}
private:
int _listensock;
};
tcpserver.hpp
#pragma once
#include<iostream>
#include<poll.h>
#include<string>
#include<unordered_map>
#include<functional>
#include<assert.h>
#include"log.hpp"
#include"epoller.hpp"
#include"err.hpp"
#include"sock.hpp"
#include"util.hpp"
namespace tcpserver
{
class Connection;
class TcpServer;//前置声明
static const int default_port = 8080;//默认端口号为8080
static const int num = 64;//设置元素数为64,当然epoll可监视的文件描述符无上限,也可以设计令其动态增长
static const int default_value = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素
using func_t = std::function<void (Connection*)>;
class Connection
{
public:
//由于在之前的数据读取中,我们只将缓冲区创建在栈上
//而且TCP一次数据读取并不能保证能够读到一个完整报文
//所以我们构建一个Connection类维护每一个链接,也包括缓冲区这些结构
Connection(int sock, TcpServer *pts)
:_sock(sock)
,_pts(pts)
{}
~Connection()
{}
//注册处理方法
void Register(func_t r, func_t s, func_t e)
{
_recver = r;
_sender = s;
_excepter = e;
}
//关闭套接字
void Close()
{
close(_sock);
}
int _sock;//套接字
std::string _inbuffer;//输入缓冲区
std::string _outbuffer;//输出缓冲区
func_t _recver;//接收任务的处理函数
func_t _sender;//发送任务的处理函数
func_t _excepter;//异常任务的处理函数
TcpServer* _pts;//指向TcpServer类的指针,方便调用TcpServer的成员函数
};
class TcpServer
{
private:
void AddConnection(int sock, uint32_t events, func_t recver, func_t sender, func_t excepter)
{
if(events & EPOLLET)//筛选需要关心读事件的描述符,实际上这个判断不写也可以
Util::SetNonBlock(sock);//设为非阻塞
Connection* con = new Connection(sock, this);//为链接新建Connection对象
con->Register(recver, sender, excepter);//将该描述符可用的处理函数加入
bool ret = _epoller.AddEvent(sock, events);//将文件描述符加入到epoll模型中,允许epoll关心该描述符
assert(ret);
_connections.insert(std::pair<int, Connection*>(sock, con));
//_connections.insert(std::make_pair(sock, con));//将Connection对象插入哈希表管理
logmessage(NORMAL, "sock number: %d is in epoll and unorder_map", sock);
}
//Connection变量是否存在
bool Connection_exist(int sock)
{
auto it = _connections.find(sock);
if(it == _connections.end())
return false;
else
return true;
}
//封装分配任务的代码
void loop(int timeout)
{
int n = _epoller.Wait(_revs, _num, timeout);
for(int i = 0; i<n; ++i)//遍历_revs数组
{
int sock = _revs[i].data.fd;
uint32_t events = _revs[i].events;
//由于读写事件的处理函数中就已经能处理这些异常事件
//所以我们将异常事件全部转化为读写类型的事件,等待读写处理
if(events & EPOLLERR)//EPOLLERR表示该文件描述符出错
events |= (EPOLLIN | EPOLLOUT);
if(events & EPOLLHUP)//EPOLLERR表示对方已经断开链接
events |= (EPOLLIN | EPOLLOUT);
if(Connection_exist(sock))//该Connection数据得存在
{
//由于Connection变量中都有他们相关的处理函数
//所以不同套接字的_recver,_sender,_excepter都可能不一样
//这也支持了不同套接字的统一处理
if((events & EPOLLIN) && _connections[sock]->_recver)//读事件且有相关函数
_connections[sock]->_recver(_connections[sock]);
if((events & EPOLLOUT) && _connections[sock]->_sender)//写事件且有相关函数
_connections[sock]->_sender(_connections[sock]);
}
}
}
//出现错误的处理函数
void Excepter(Connection* con)
{
logmessage(DEBUG, "Excepter in");
//出错了我们直接断开连接
//将数据从epoll模型中删除
_epoller.Control(con->_sock, 0, EPOLL_CTL_DEL);
//将该链接的Connection变量删除
_connections.erase(con->_sock);
//将该链接的文件描述符关闭
con->Close();
//删该除节点
delete con;
logmessage(DEBUG, "Excepter out");
}
//接收数据的处理函数
void Recver(Connection* con)
{
logmessage(DEBUG, "Recver in");
char buffer[1024];//定义缓冲区
while(1)//由于ET模式只有一次提醒机会,所以我们需要不断读取,直到无数据可读为止
{
ssize_t n = recv(con->_sock, buffer, sizeof(buffer)-1, 0);
if(n > 0)//读到了数据
{
buffer[n] = 0;//在末尾加上/0
con->_inbuffer += buffer;//追加到该链接的缓冲区
logmessage(NORMAL, "client# %s", buffer);
_func(con);//处理该链接的数据
}
else if (n == 0)//无数据可读
{
if(con->_excepter)
{
con->_excepter(con);
break;
}
}
else//出错
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;//没有数据就绪了,直接跳出循环
else if(errno == EINTR)
continue;//正在进行信号处理,继续读取等待处理完成即可
else//出错了,让Excepter处理错误,然后跳出循环
{
if(con->_excepter)
{
con->_excepter(con);
break;
}
}
}
}
logmessage(DEBUG, "Recver out");
}
//发送数据的处理函数
void Sender(Connection* con)
{
logmessage(DEBUG, "Sender in");
while(1)//将数据循环全部发送
{
int n = send(con->_sock, con->_outbuffer.c_str()
, con->_outbuffer.size(), 0);//先发送数据
if(n > 0)//此次发送数据成功
{
con->_outbuffer.erase(0, n);//删除缓冲区中已发送的数据
if(con->_outbuffer.empty())
{
//此次发送就已经将所有数据发完了
break;
}
//还有数据,继续循环发送
}
else//此次发送数据失败
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;//没有数据就绪了,直接跳出循环
else if(errno == EINTR)
continue;//正在进行信号处理,继续读取等待处理完成即可
else//出错了,让Excepter处理错误,然后跳出循环
{
if(con->_excepter)
{
con->_excepter(con);
break;
}
}
}
}
//如果数据没发完,需要让epoll对该描述符的写事件进行关心。
//如果发完了,要关闭对写事件的关心
if (!con->_outbuffer.empty())
con->_pts->EnableReadWrite(con, true, true);
else
con->_pts->EnableReadWrite(con, true, false);
logmessage(DEBUG, "Sender out");
}
//接收链接的处理函数,只用于listen套接字的读事件处理
void Accepter(Connection* con)
{
logmessage(DEBUG, "Accepter in");
//循环接收多个链接,直到没有链接接收时退出
while(1)
{
std::string clientip;
uint16_t clientport;
int err = 0;
//epoll负责等,accept直接接收,err负责将本次调用的错误码从函数中带出
int sock = _sock.Accept(&clientip, &clientport, &err);
if(sock > 0)//接收成功
{
//将链接管理起来
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(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
}
else//接收失败
{
if(err == EAGAIN || err == EWOULDBLOCK)
break;//没有数据就绪了,直接跳出循环
else if(err == EINTR)
continue;//正在进行信号处理,继续读取等待处理完成即可
else//出错了,跳出执行
break;
}
}
logmessage(DEBUG, "Accepter out");
}
public:
//构造函数
TcpServer(func_t func, int port = default_port)
:_func(func)
,_port(port)
,_revs(nullptr)
{}
//析构函数
~TcpServer()
{
_sock.Close();
_epoller.Close();
if(_revs)
delete[] _revs;
}
void EnableReadWrite(Connection *con, bool readable, bool writeable)
{
uint32_t event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;
_epoller.Control(con->_sock, event, EPOLL_CTL_MOD);
}
void initserver()
{
//创建监听套接字
_sock.Socket();
_sock.Bind(_port);
_sock.Listen();
//创建epoll模型
_epoller.Create();
//将套接字加入epoll模型,同时也要为它新增一个Connection,并加入哈希表中
//我们所有新增的描述符统一使用AddConnection加入数据结构进行管理
AddConnection(_sock.FD(), EPOLLIN | EPOLLET,
std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
//对于listen套接字,我们只需要处理接收链接的读事件,所以我们使用包装的Accepter函数
//初始化数据
_num = num;
_revs = new struct epoll_event[_num];
}
//服务器开始运行不断通过Loop函数分发任务
void dispatcher()
{
int timeout = 1000;//设置等待时间
while(1)
{
loop(timeout);//这个分发任务的接口我们不想暴露出来
//logmessage(DEBUG, "time out...");//超时打印信息
}
}
private:
uint16_t _port;//进程的端口号
Sock _sock;//维护套接字的类
Epoller _epoller;//封装各种epoll接口的类
std::unordered_map<int, Connection*> _connections;
//将所有Connection对象管理在哈希表中,key值为文件描述符,value值为其Connection对象的指针
struct epoll_event* _revs;//epoll_wait返回的所有就绪的节点都放入_revs数组中
int _num;//数组可接收的最大节点数目
func_t _func;
};
}
util.hpp
//将原来写过的将套接字设为非阻塞的代码封装在Util类中
#pragma once
#include<iostream>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
class Util
{
public:
//将文件描述符设为非阻塞
static bool SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);//获取文件描述符的标志,该标志是一个位图结构
if(fl < 0)//获取失败
{
std::cerr << "fctnl:" << strerror(errno) << std::endl;//打印错误码
return false;
}
else
{
fcntl(fd, F_SETFL, fl | O_NONBLOCK);//将该文件描述符设为非阻塞
return true;
}
}
};
server.cc
#include"tcpserver.hpp"
#include"protocol.hpp"
#include<memory>
using namespace std;
using namespace tcpserver;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}
//测试函数
// void send_back(Connection* con)
// {
// con->_outbuffer += con->_inbuffer;
// con->_inbuffer.erase(con->_inbuffer.begin(), con->_inbuffer.end());
// if (con->_sender)
// con->_sender(con);
// }
//计算器函数
const string ops = "+-*/%";
bool calculate(const Request &req, Response &resp)
{
// req已经有结构化完成的数据啦,你可以直接使用
resp._exitcode = OK;
resp._result = 0;
switch (req._op)
{
case '+':
resp._result = req._a + req._b;
break;
case '-':
resp._result = req._a - req._b;
break;
case '*':
resp._result = req._a * req._b;
break;
case '/':
{
if (req._b == 0)
resp._exitcode = DIV_ZERO;
else
resp._result = req._a / req._b;
}
break;
case '%':
{
if (req._b == 0)
resp._exitcode = MOD_ZERO;
else
resp._result = req._a % req._b;
}
break;
default:
resp._exitcode = OP_ERROR;
break;
}
return true;
}
//处理数据的函数
void handler(Connection* con)
{
printf("handler in\n");
string one_package;
while(divonepackage(con->_inbuffer, &one_package))//读取完整报文
{
string text;
//去掉报头
if(!delength(one_package, &text))
return;
//正文反序列化构造request
Request req;
if(!req.deserialize(text))
return;
//处理request得到response
Response resp;
calculate(req, resp);
//respone序列化得到正文
string result;
resp.serialize(&result);
//增加报头
con->_outbuffer += enlength(result);
//打印结果
cout << result << endl;
//发送
if (con->_sender)
con->_sender(con);
}
printf("handler out\n");
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
unique_ptr<TcpServer> p(new TcpServer(handler));
p->initserver();
p->dispatcher();
return 0;
}
makefile
tcpserver: server.cc
g++ -o server server.cc -std=c++11 -ljsoncpp
.PHONY:clean
clean:
rm -f server
2.客户端
client.hpp
#pragma once
#include<iostream>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<errno.h>
#include<string.h>
#include<strings.h>
#include<istream>
#include<stdlib.h>
#include<stdio.h>
#include<memory>
#include<ctype.h>
#include"log.hpp"
#include"protocol.hpp"
#define NUM 1024
enum errorcode
{
USAGE_ERROR = 1,
SOCKET_ERROR,
BIND_ERROR,
CONNECT_ERROR
};
Request ParseLine(const std::string &line);
class tcpclient
{
public:
//构造函数
tcpclient(const std::string& ip, const uint16_t& port)
:_ip(ip)
,_port(port)
,_sock(-1)
{}
void initclient()
{
//创建套接字,创建失败打印错误原因
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock == -1)
{
logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
exit(SOCKET_ERROR);//退出
}
logmessage(NORMAL, "create socket success:%d", _sock);//创建套接字成功,打印让用户观察到
//客户端不需要显式绑定,该工作交给操作系统完成
}
//启动客户端进程
void run()
{
struct sockaddr_in local;
local.sin_family = AF_INET;//通信方式为网络通信
local.sin_port = htons(_port);//将网络字节序的端口号填入
local.sin_addr.s_addr = inet_addr(_ip.c_str());//填充结构体
//客户端连接服务器
if(connect(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)//客户端与服务器进行连接
{
logmessage(FATAL, "connect error");//connect失败属于最严重的错误
}
else
{
std::string line;
std::string inbuffer;
while (true)
{
std::cout << "mycal>>> ";
std::getline(std::cin, line); // 1+1
Request req = ParseLine(line); // "1+1"
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); // bug?? 不管
std::string package, text;
// "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;
}
}
}
Request ParseLine(const std::string &line)
{
// 建议版本的状态机!
//"1+1" "123*456" "12/0"
int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
int i = 0;
int cnt = line.size();
std::string left, right;
char op;
while (i < cnt)
{
switch (status)
{
case 0:
{
if(!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else left.push_back(line[i++]);
}
break;
case 1:
i++;
status = 2;
break;
case 2:
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);
}
//析构函数要释放不使用的文件描述符
~tcpclient()
{
if( _sock >= 0)
close(_sock);
}
private:
int _sock;//套接字文件描述符
std::string _ip;//服务器IP地址
uint16_t _port;//服务器的端口号
};
client.cc
#include"client.hpp"
using namespace std;
static void Usage(string proc)
{
printf("\nUsage:\n\t%s server_ip server_port\n\n", proc.c_str());
}
int main(int argc, char* argv[])
{
if(argc != 3)//如果没输入端口号和目的IP,argc保存的命令参数就不是三个,进程出错
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
uint16_t port = atoi(argv[2]);
string ip = argv[1];
unique_ptr<tcpclient> p(new tcpclient(ip, port));
p->initclient();
p->run();
return 0;
}
三、Reator模式
写了这么半天的代码,那到底什么是Reactor模式呢?
Reactor是一种服务器实现模式,它使用实践派发器和连接管理器实现服务器IO,是一种半同步半异步的IO服务器。
实际上我们此次实现的epoll服务器是一个不完全的Reactor服务器,epoll负责将数据放到Connection的_outbuffer中,再由其他函数处理数据。
但实际上的Reactor服务器只负责等待事件就绪,并对就绪的事件进行IO。而实际的数据处理会交给线程池或创建多进程由新进程处理。
它很类似与打地鼠游戏,有地鼠冒头就拿起锤子敲打,相当于有IO事件要处理就进行处理。
地鼠本身是怎么从下面来到这里的,我们不关心。同样,数据经过怎么样的处理,服务器也不关心,它只负责收发数据和异常处理。
总的来说,Reactor模式是目前服务器设计中最常用的模式,在实际开发中的使用非常多。还有一种设计模式叫做Proactor,它应用于少数特殊需求的服务器设计,有兴趣可以查找相关文章查询,我就不再详述了。