本节内容是在 如何学习编程 之后进一步由理论结合实践去验证和加深该学习思想,为了方便起见,不会再过多的阐述先验知识,因此若是在阅读过程中出现因先验知识不足而导致的难以理解的情况,请自行学习相关的先验知识。
因为c++这门语言学习起来总有一定的难度,除了语言本身的原因以外,由于学的人相对较少,学精通的人更少,导致在推广方面,无论是人数还是质量都难以保证。
自己在刚开始学c++的时候,出现过很多问题,影响最大的还是以下几点:
所以除了有借此加深巩固自己之前所学以外,也希望自己的文章可以帮助到更多的c++小白,让越来越多的人喜欢上c++,愿c++经久不衰。
再次声明,本篇文章是让小白过渡到初学者的文章,因为本人也是初学者,所以掌握的知识的全面程度和深度肯定是有限的,但学习本身就是不断的扩宽自己的广度和深度,所以这很正常。就如同牛顿力学过度到量子力学一样,牛顿力学没有错,量子力学也没有错,只是适用范围不同罢了,或者说量子力学的适用范围更大,但不管怎么说,能在一定范围内正确解释世界规律的,我觉得就是好理论。
先验知识声明:在进入聊天室的学习之前,必须要有一定的c++基础知识和计算机相关的基础知识,没有这些基础,什么牛鬼蛇神来了都没有,就算是所谓的“天才、聪明人”,也只是通过类比的方式,结合他自己之前类似的经历推出来的(我对天才这个词很反感,我觉得就是骗骗世人,给人们找借口的词汇,如有不适敬请见谅),所以如果没有掌握这些知识,你看起来无比难受是很正常的事情。
具体的先验知识:(其中黄色是必须掌握,浅色黑体是掌握了对细节的把握会更好)
1、c++基础部分:类初步(如构造函数析构函数、公有继承私有继承等)、STL基本使用(deque、vector、list、string、chrono时间库)、命名空间、枚举、基本关键字(typedef、using)、const引用和值传递的区别、内存对齐、右值引用和左值引用的区别、异常、c++11新特性(加强for循环、智能指针)还有其他c和c++相同的部分、c++多线程基础。
2、计算机网络基础:c++asio网络库简单api使用、tcp报文格式、计算机网络数据是如何从一台主机上经过5层模型(或者7层参考模型)到达另一台主机的宏观了解。
3、liunx基础:liunx最基本命令、liunx下cmake使用、liunx非常基本的脚本编写。
4、其他:像google protobuf等序列工具的使用、为什么要序列化、protobuf为什么快等。
细心的同学可能发现了,为啥我没有把c++多线程标记为必会呢?因为我们这个聊天室是一个循序渐进的版本,因此如果没到后面多线程的版本,不需要掌握c++多线程基础当然也可以驾驭。
还有就是,如果对c++内存掌握程度高的话,对一些细节的理解肯定还会更好,毕竟我们学习知识肯定是知其然还要知其所以然,再功利点说:遇到bug你也能知道原因然后快速定位去解决嘛。
正所谓:兵马未动,粮草先行;理论是用来更好指导实践的。有一个好的架构体系,或者说在设计之初就考虑好很多东西的话,对后面无论是出问题还是迭代肯定都会更好解决。(可以结合 如何学习编程 提到的守恒思想去分析)。
无论是设计和分析问题,首先要把握的就是他的核心,用哲学的话来说就是:把握事物的主要矛盾。其实也就是把握事物的本质。
聊天室聊天室,核心肯定是提供一个较为舒适的聊天服务。把握本质以后,接下来我们做的事情是什么?——计算机分治思想,或者简单点说,把问题分解。
其实细心的同学在生活中就可以观察到:无论我们做任何事情,无形之中其实就已经把这件事情分成若干字问题进行处理了。
比如在吃饭的时候,先拿起筷子,做好姿势、选中要夹的菜、计算筷子到要夹的菜要走什么样的路径、夹中菜后把握怎么样的力度可以不让菜掉下来…
回到正题,那么我们该如何把问题分解呢——剪取不重要细节。借鉴或者类比之前吃饭的例子,舒适的聊天室,本质上就是多人之间进行聊天,那我们先分析两个人的情况,也先不管舒不舒适的问题,那现在的问题就变成了——两个人的聊天室。
如果加入服务器——客户端的模型思想:服务器用来接收和发送这两个人的消息,客户端负责(从命令行)接收消息,并交由服务器处理,同时还会接受来自服务器的消息。
这其实还是有点抽象,简单点说,举个例子:A和B同学聊天,现在A同学想对B同学说 “ni hao”,那么简单至极有两种方式:1 A直接给B发消息。 2 A给一个中转站发消息,由这个中转站给B发消息。
有的同学可能会说:“哎呀,那肯定是第一种了,第二种这么麻烦”。但是我们简化问题的时候也不能忽略原本的内容——也就是俗话说 未雨绸缪。现在是两个同学发消息,如果是五个同学、十个同学呢,这就不好处理了。所以目前我们就使用方法2。
到这,我们的1.0版本的聊天室已经逐渐浮出水面了:A同学和中转站建立连接,之后向中转站建立连接;B同学和中转站也建立连接,接受中转站发送过来A的消息。
然后我们发现,无论有多少个同学,都要和这个中转站建立连接,而每一位同学做的动作都是差不多的——和中转站建立连接、发送或接收中转站的消息。
如果把中转站换个名字——服务器。把每个同学的动作逻辑换个名字——客户端。
那么聊天室1.0的雏形就出来了——服务器用于和客户端连接并收发消息;客户端用于接收用户输入并收发服务器的消息。
下面我们就来逐步分析服务器和客户端都是怎么设计的。
1 客户端。客户端由两个方面组成:接收客户输入并把消息发送给服务器 和 接收由服务器发送给客户端的消息。还是一样,继续分解问题:先考虑接收客户输入并把消息发送给服务器怎么做?接受客户输入:可以用cin的getline接收,并把消息放到一个队列里;把消息发送给服务器:借助c++asio网络提供的api即可。再来看服务器发送给客户端的消息怎么做?服务器发送给客户端的数据通过网络传输最开始肯定是发到网卡上,但是对网卡的操作也太底层了,因此借助c++asio网络库——借助asio网络库的api接收服务端信息,并用队列放到内存,并用cout输出即可。即:
2 服务器。服务器也是由两个部分组成:接收客户端消息 和 将聊天室消息发送给客户端。因为和客户端有点类似,这里直接给出结论,即:
上面的设计内容部分算是结束了,但具体如何去设计类和数据结构还有待商榷。
还是一样,将分治的思想融入进来,无论是客户端还是服务器,最先要解决的,就是双方要统一消息的格式,也就是我们常说的协议。
因此我们设计一个chat_message类,用于存放消息,同时规定:消息结构是 消息头部 + 消息体的形式。消息头部存放了消息体的长度和消息类型(比如是客户端发送给服务器聊天的消息还是服务器发给客户端的消息),而且是定长的,这样就可以通过头部去处理消息体的内容。
简单表示一下就是:
struct Header{
int bodySize;
int type;
};
enum MessageType {
MT_BIND_NAME = 1,
MT_CHAT_INFO = 2,
MT_ROOM_INFO = 3,
};
class chat_message {
Header m_header;
char data[header_length + max_body_length];
};
之后分析客户端,cin的getline接收用户输入,同时还要有一个队列,简单表示就是:
while (std::cin.getline(line, chat_message::max_body_length + 1)){
chat_message msg;
auto type = 0;
std::string input(line, line + std::strlen(line));
std::string output;
if(parseMessage(input,&type,output)){
msg.setMessage(type, output.data(), output.size());
c.write(msg);
}
下面是chat_client的表示:
chat_client {
chat_message read_msg_;
chat_message_queue write_msgs_;
};
再分析一下要有什么函数:
1 要有和服务器连接的函数——目前放在构造函数里面。
2 要有接收函数——接收服务器发送的数据。
3 要有写出函数——向服务器发送自己的消息。
因此类可以表示为:
chat_client {
public:
//连接函数和接受函数都在构造函数里面了
//即chat_client(xxx) <==> connect + accept
chat_client(xxx); //有参构造函数
void write(const chat_message& msg);
void close();
private:
chat_message read_msg_;
chat_message_queue write_msgs_;
};
服务器除了要和客户端连接chat_server,还要有一个聊天室chat_room接收消息,但是在广播消息的时候,需要向每个客户端都发消息,因此用chat_session表示接入进来的客户端,简单表示如下:
class chat_room{
private:
chat_message_queue recent_msgs_;
};
class chat_session {
private:
chat_room& room_; //属于哪个聊天室
std::string m_name; //这里是这个session的名字
chat_message read_msg_;
chat_message_queue write_msgs_;
};
class chat_server{
public:
//有参构造函数里包括了connect和read
chat_server(xxx);
private:
chat_room room_; //管理所有的room
};
再分析一下需要有什么样的函数:
1 对于room来说,需要有客户端加入到聊天室的join函数、需要有客户端离开的leave函数和向所有客户端广播的deliver函数。
2 所有的具体处理函数放在chat_session中,server只负责connect和read、room负责控制客户加入退出和发送。到这其实已经足够,但为了封装和可扩展性,把server的read和room的发送放在了session里面做。即 read <=> session.start, deliver <=> session.deliver。
更完整的类声明如下:
class chat_room{
public:
void join(chat_session_ptr);
void leave(chat_session_ptr);
void deliver(const chat_message&);
private:
chat_message_queue recent_msgs_;
};
class chat_session {
public:
void start();
void deliver(const chat_message& msg);
private:
chat_room& room_; //属于哪个聊天室
std::string m_name; //这里是这个session的名字
chat_message read_msg_;
chat_message_queue write_msgs_;
};
class chat_server{
public:
//有参构造函数里包括了connect和read
chat_server(xxx);
private:
chat_room room_; //管理所有的room
};
当然,因为要结合c++的asio库,所以声明肯定还要更复杂一些,但那都是asio的东西,把握了这主体的东西对我们的编程来说就足够了。
下面来看看聊天室1.0的内容:
一共分为5个文件:
1 chat_message.hpp、structHeader.h、structHeader.cpp用于存放消息格式的约定(协议)。
2 chat_server.cpp放服务器相关逻辑。
3 chat_client.cpp放客户端相关逻辑。
至此完成的就是asio的例子程序完成的内容。不过虽然功能一样,但是我们把消息变成了type进行了一个小改动,这样让我们的可扩展性就提升了一些,我们在此基础上加入客户端可以发送“绑定名字”的消息。
当然代码很简单,完整代码我把它放到了github上,后续我们会逐渐对他进行更新和迭代,在循序渐进中慢慢感受理论与实践相结合的乐趣。
聊天室1.0
1 b站课程
2 boost-asio网络库