本篇为聊天软件程序的服务器部分知识笔记。
一、后台运行子进程
1、signal(SIGCHLD, SIG_IGN)
:如果是以前台运行,那么可以用默认的处理方式,即signal(SIGCHLD, SIG_DFL)
,而如果是后台运行就要以忽略的方式处理它,在这种情况下会由系统来处理它,自动回收子进程防止其变成僵尸进程
2、fork()
:在当前位置创建一个子进程,并会返回两次,但对于任一进程都是返回一次,即父进程返回一次(返回子进程pid),子进程返回一次(返回0)
3、setsid()
:fork()执行后创建的子进程,其内存基本都是拷贝自父进程,而进程其实是一个会话,此时子进程的会话id是来自主进程的,所以需要为子进程设置一个新的会话id,防止父进程结束后(认为会话结束)回收父进程会话id下的所有子进程,同理也是不能使用网络执行服务器程序的原因。
二、木铎库
1、反应炉模式:
起初做服务器,一般是基于线程的架构,即thread-based architecture
,如下图所示
这种形式结构清晰、逻辑明了、可读性强,在一条线程内完成对用户的全部响应。但其缺点也是非常明显的,当用户量很大的时候,即使硬件层面支持了创建如此多的线程,那CPU在这些线程间的切换也是很大的消耗,甚至废掉。
基于这种问题,出现了事件驱动架构,即event-driven architecture
,这种架构把要使用CPU的内容定义为一个事件。如网络编程中客户端接入、数据读取、数据发送、断开连接。当有客户端接入事件发生时,就安排线程来处理接入事件,处理完成就移除该事件并等待下一次事件发生。
事件驱动架构的好处就是能控制CPU的利用率,同样以一万个用户量为例,前者需要开一万条线程分别处理用户的业务,而后者仅需几条或十几条线程即可,比如各三条线程处理客户端接入、连接关闭,各五条线程处理数据的接收解析、编码发送。这样没有被处理到的用户会在一个事件队列中等待被处理,而不是全部同时被处理但每个都处理不好。
可以用奶茶店理解上述内容。当有50人再买奶茶时,前者的做法是招聘了50名员工,一对一的分别去处理顾客的需求,结果50名员工全部挤在了点单机前互相争夺资源;而后者的做法则只需几名员工,一个负责点单,两个负责制作,一个负责出餐,还没点餐的顾客会有序排队等候,点餐完成后会从此队伍离开,进入取餐队伍等待发餐。
事件驱动架构的模式就是Reactor模式,即反应炉模式,如下图所示
2、反应炉模式的几个概念:
handle(句柄)
:具体的事件源,可以是文件描述符,也可以是网络套接字等。Synchronous Event Demultiplexer
:同步事件分离器。一般是系统的接口,如select、poll、epoll。这些东西将程序的状态由事件触发状态切换到事件处理状态,比如select会阻塞,直至select关注的某个handle产生事件。Event Handler
:事件处理器。这个元素里面一般包含一个回调函数,当handle上产生事件的时候,会调用此回调函数。Concrete Event Handler
:具体的事件处理器。注意这个一般是事件处理器的子类,会实现具体的回调完成业务逻辑。Initiation Dispatcher
:初始分发器。提供注册、删除与转发event handler的方法,当同步事件分离器发现某个handle上有事件发生时,就会通知初始分发器来调用事件处理器去处理事件。
3、工作流程
4、muduo::net::EventLoop
本小节为3的示例描述,通过几句伪代码详解工作流程。
// ******** 服务器
void onMessage(const muduo::net::TcpConnectionPtr& pConn,
muduo::net::Buffer* pBuf,
muduo::Timestamp time)
{
pConn->send(pBuf);
}
muduo::net::EventLoop loopServer; // 反应炉
muduo::net::InetAddress addr(9527);
muduo::net::TcpServer tcpServer(&loopServer, addr, "echo_server");
tcpServer.setMessageCallback(onMessage); // 初始分发器注册
tcpServer.start();
loopServer.loop(); // 事件循环 同步事件分离器
// ******** 客户端
void onConnection(const muduo::net::TcpConnectionPtr& pConn)
{
pConn->send("hello! I am client!");
}
void onMessageClient(const muduo::net::TcpConnectionPtr& pConn,
muduo::net::Buffer* pBuf,
muduo::Timestamp time)
{
std::cout<< pBuf->retrieveAllAsString() << std::endl;
}
muduo::net::EventLoop loopClient;
muduo::net::InetAddress addrServer("127.0.0.1", 9527);
muduo::net::TcpClient tcpClient(&loopClient, addrServer, "echo_client");
tcpClient.setConnectionCallback(onConnection);
tcpClient.setMessageCallback(onMessageClient);
tcpClient.connect();
loopClient.loop();
上述代码仅为说明用。
服务器注册了message的事件处理函数,并且其业务实现为将收到的消息再发送给发出者,然后进入同步事件分离器loop。
客户端注册了connection和message两个事件的处理函数,连接成功后向服务器发送消息,message内打印服务器返回的消息,然后进入同步事件分离器loop。
实际运行结果为,客户端输出“hello, I am client”。连接成功后,客户端向服务器发送该消息,在服务器对message的处理中是将收到的消息原封不动的再返回,继而来到了客户端对message的处理函数中,将其打印出来。
5、更高级的单例技巧
一般常见的单例模式都是将其声明成static变量,然后private其构造函数,如下
class A {
public:
static A& Instance() {
static A a;
return a;
}
private:
A();
~A();
}
#define oprA A::Instance()
虽然这种形式还有些逻辑不严谨的地方,但其实在一个团队里面也是能满足使用需要的。
其不严谨的地方在于,复制构造函数和赋值构造函数没有处理、类内仍可调用构造函数、可以被继承成为基类。
另外使用static实现单例的缺点还有:不能把静态变量放到头文件中,否则不同的cpp文件引入此头文件时会符号冲突;必须public接口释放类的内存。
而木铎库中提供了一种更优解的单例实现方法
Singleton
是一个模板类,首先其继承自noncopyable
,将复制构造函数和赋值构造函数删除掉了,而在Singleton
类中干脆连构造函数都删除掉了。
然后pthread_once
可保证进程内之创建一次,并且如果类T
如果没有析构,还会在main
函数之后调用destroy
函数释放内存,destroy
内还会校验类型是否是完整的。
这是在Singleton::init
中调用到的方法,用来判断是否需要本类去处理模板类型的释放问题。
这里面巧妙的运用了sizeof
不会执行括号内的代码的逻辑,即使两个版本的test都没有实现,但编译仍然正确,
以及在C++模板的SFINAE特性(substitution filed is not an error,替换失败不是错误),当类型有no_destroy
时,会匹配第一行的test
,此时test<T>
为char
的大小即1,否则test<T>
为int
的大小即4
三、服务器逻辑
//完整的main函数形式 env[]为环境变量
int main(int argc,char* argv[],char* env[])
{
// 1、信号处理
signal(SIGCHLD/*当产生一个子进程,父进程会受到信号*/, SIG_DFL);
signal(SIGPIPE/*管道*/, SIG_IGN);//网络中或管道操作产生的信号 不需要处理。 信号级别很高,如果不处理会直接挂掉 但又没什么用 所以用SIG_IGN忽略掉
signal(SIGINT, signal_exit);//中断
signal(SIGKILL, signal_exit);
signal(SIGTERM, signal_exit);//按下ctrl+C
signal(SIGILL, signal_exit);//非法指令错误
signal(SIGSEGV, signal_exit);//段错误
signal(SIGTRAP, signal_exit);//ctrl+break
signal(SIGABRT, signal_exit);//调用abort函数
std::cout << "chatServer is invoking..." << std::endl;
// 2、参数解析、后台运行
int ch = 0;
bool bIsDaemon = false;//是否是守护进程
while ((ch = getopt(argc, argv, "d")) != -1)//""内的内容为选项 可有多个内容 有参数的话在后缀: 多个参数或不确定也没有参数的话后缀::
{
switch (ch)
{
case 'd':
bIsDaemon = true;
break;
default:
show_help(argv[0]);
return -1;
}
}
if (bIsDaemon)
Daemon();// 后台运行
muduo::net::EventLoop loop;//事件循环
// 3、数据库模块初始化 单例
if (!Singleton<MySqlManager>::instance().Init("127.0.0.1", "root", "password", "dbName"))
{
std::cout << "database init error" << std::endl;
return -2;
}
// 4、用户信息初始化 单例
if (!Singleton<UserManager>::instance().Init())
{
std::cout << "load user failed!" << std::endl;
return -3;
}
// 5、服务器启动 单例
if (!Singleton<IMServer>::instance().Init("0.0.0.0", 9527, &loop))
{
std::cout << "Server Init Filed!" << std::endl;
return -1;
}
// 反应炉
loop.loop();
std::cout << "Good Bye!" << std::endl;
return 0;
}