聊天软件项目笔记(二)

本篇为聊天软件程序的服务器部分知识笔记。

一、后台运行子进程

在这里插入图片描述
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、反应炉模式的几个概念:

  1. handle(句柄):具体的事件源,可以是文件描述符,也可以是网络套接字等。
  2. Synchronous Event Demultiplexer:同步事件分离器。一般是系统的接口,如select、poll、epoll。这些东西将程序的状态由事件触发状态切换到事件处理状态,比如select会阻塞,直至select关注的某个handle产生事件。
  3. Event Handler:事件处理器。这个元素里面一般包含一个回调函数,当handle上产生事件的时候,会调用此回调函数。
  4. Concrete Event Handler:具体的事件处理器。注意这个一般是事件处理器的子类,会实现具体的回调完成业务逻辑。
  5. 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;
}
  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值