c++项目——聊天室——第一节

  • 概述
  • 引言
  • 聊天室初步
    • 1 总体设计
    • 2 思路设计
    • 3 数据结构设计(细节设计)
      • (1)分析消息协议部分
      • (2)分析客户端
      • (3)分析服务器
  • 聊天室1.0
    • 运行效果
    • 聊天室1.0代码简析(具体分析会在聊天室1.2的版本里)
      • 客户端
      • 1、连接+读服务器数据
      • 2、向服务器写数据
      • 服务器
      • 1、接收客户端消息
      • 2、向客户端写
  • 有趣的点
    • 异步循环调用为什么不爆栈?
    • 为什么要使用self(shared_from_this)?
  • 聊天室1.0 github地址:
  • 参考文献

概述

本节内容是在 如何学习编程 之后进一步由理论结合实践去验证和加深该学习思想,为了方便起见,不会再过多的阐述先验知识,因此若是在阅读过程中出现因先验知识不足而导致的难以理解的情况,请自行学习相关的先验知识

因为c++这门语言学习起来总有一定的难度,除了语言本身的原因以外,由于学的人相对较少,学精通的人更少,导致在推广方面,无论是人数还是质量都难以保证。

自己在刚开始学c++的时候,出现过很多问题,影响最大的还是以下几点:

  • 1 资源相对较少,当时自己找资源的能力也比较有限。
  • 2 资源参差不齐,有些博主自己都没有完全理解,写了一大堆专业术语出来装逼的。
  • 3 c++比较权威的书对于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你也能知道原因然后快速定位去解决嘛。

聊天室初步

1 总体设计

正所谓:兵马未动,粮草先行;理论是用来更好指导实践的。有一个好的架构体系,或者说在设计之初就考虑好很多东西的话,对后面无论是出问题还是迭代肯定都会更好解决。(可以结合 如何学习编程 提到的守恒思想去分析)。

无论是设计和分析问题,首先要把握的就是他的核心,用哲学的话来说就是:把握事物的主要矛盾。其实也就是把握事物的本质。

聊天室聊天室,核心肯定是提供一个较为舒适的聊天服务。把握本质以后,接下来我们做的事情是什么?——计算机分治思想,或者简单点说,把问题分解。
        其实细心的同学在生活中就可以观察到:无论我们做任何事情,无形之中其实就已经把这件事情分成若干字问题进行处理了。
        比如在吃饭的时候,先拿起筷子,做好姿势、选中要夹的菜、计算筷子到要夹的菜要走什么样的路径、夹中菜后把握怎么样的力度可以不让菜掉下来…

回到正题,那么我们该如何把问题分解呢——剪取不重要细节。借鉴或者类比之前吃饭的例子,舒适的聊天室,本质上就是多人之间进行聊天,那我们先分析两个人的情况,也先不管舒不舒适的问题,那现在的问题就变成了——两个人的聊天室。
        如果加入服务器——客户端的模型思想:服务器用来接收和发送这两个人的消息,客户端负责(从命令行)接收消息,并交由服务器处理,同时还会接受来自服务器的消息。
        这其实还是有点抽象,简单点说,举个例子:A和B同学聊天,现在A同学想对B同学说 “ni hao”,那么简单至极有两种方式:1 A直接给B发消息。 2 A给一个中转站发消息,由这个中转站给B发消息。
        有的同学可能会说:“哎呀,那肯定是第一种了,第二种这么麻烦”。但是我们简化问题的时候也不能忽略原本的内容——也就是俗话说 未雨绸缪。现在是两个同学发消息,如果是五个同学、十个同学呢,这就不好处理了。所以目前我们就使用方法2。

到这,我们的1.0版本的聊天室已经逐渐浮出水面了:A同学和中转站建立连接,之后向中转站建立连接;B同学和中转站也建立连接,接受中转站发送过来A的消息。
        然后我们发现,无论有多少个同学,都要和这个中转站建立连接,而每一位同学做的动作都是差不多的——和中转站建立连接、发送或接收中转站的消息。
        如果把中转站换个名字——服务器。把每个同学的动作逻辑换个名字——客户端。
        那么聊天室1.0的雏形就出来了——服务器用于和客户端连接并收发消息;客户端用于接收用户输入并收发服务器的消息

2 思路设计

下面我们就来逐步分析服务器和客户端都是怎么设计的。

1 客户端。客户端由两个方面组成:接收客户输入并把消息发送给服务器 和 接收由服务器发送给客户端的消息。还是一样,继续分解问题:先考虑接收客户输入并把消息发送给服务器怎么做?接受客户输入:可以用cin的getline接收,并把消息放到一个队列里;把消息发送给服务器:借助c++asio网络提供的api即可。再来看服务器发送给客户端的消息怎么做?服务器发送给客户端的数据通过网络传输最开始肯定是发到网卡上,但是对网卡的操作也太底层了,因此借助c++asio网络库——借助asio网络库的api接收服务端信息,并用队列放到内存,并用cout输出即可。即:

  • 接收客户输入并把消息发送给服务器:用cin的getline接收用户输入,并把消息放到一个队列里,最后用asio库api发给服务器;
  • 接收由服务器发送给客户端的消息:借助asio网络库的api接收服务端信息,并用队列放到内存,并用cout输出。

2 服务器。服务器也是由两个部分组成:接收客户端消息 和 将聊天室消息发送给客户端。因为和客户端有点类似,这里直接给出结论,即:

  • 接收客户端消息:通过asio网络库api接收客户端消息,并把所有消息都放到一个队列里,;
  • 将聊天室消息发送给客户端:将队列中的内容借助asio网络库的api广播(发送)给所有客户端。

3 数据结构设计(细节设计)

上面的设计内容部分算是结束了,但具体如何去设计类和数据结构还有待商榷。

(1)分析消息协议部分

还是一样,将分治的思想融入进来,无论是客户端还是服务器,最先要解决的,就是双方要统一消息的格式,也就是我们常说的协议

因此我们设计一个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];
};

(2)分析客户端

之后分析客户端,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 + acceptchat_client(xxx);  //有参构造函数void write(const chat_message& msg);  void close();
private:chat_message read_msg_;chat_message_queue write_msgs_;
};

(3)分析服务器

服务器除了要和客户端连接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和readchat_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和readchat_server(xxx);private:chat_room room_; //管理所有的room
};

当然,因为要结合c++的asio库,所以声明肯定还要更复杂一些,但那都是asio的东西,把握了这主体的东西对我们的编程来说就足够了。

聊天室1.0

下面来看看聊天室1.0的内容:

一共分为5个文件:
        1 chat_message.hpp、structHeader.h、structHeader.cpp用于存放消息格式的约定(协议)。
        2 chat_server.cpp放服务器相关逻辑。
        3 chat_client.cpp放客户端相关逻辑。

(为了文件少一点,也可以把structHeader的内容合到chat_message.hpp里面)
至此完成的就是asio的例子程序完成的内容。不过虽然功能一样,但是我们把消息变成了type进行了一个小改动,这样让我们的可扩展性就提升了一些,我们在此基础上加入客户端可以发送“绑定名字”的消息。

运行效果

在代码解析之前,先跑起来,看一下运行效果,爽一下。这样后续对代码的理解也会更容易一些。(windows上应该也能跑,用的都是跨平台的库,因为windows上用visual stdio比较easy,这里在linux上跑一下)

编译:

g++ -std=c++14 -pthread -I./ -L./ chat_client.cpp structHeader.cpp chat_message.hpp -o client
g++ -std=c++14 -pthread -I./ -L./ chat_server.cpp structHeader.cpp chat_message.hpp -o server

编译完以后是如下的效果:
        之后先运行server服务端

./server 9999

后面这个是端口号,随便给个正数即可。

之后再开几个客户端,运行客户端

./client localhost 9999

之后在客户端输入
BindName 要输入的名字
或者
Chat 聊天内容

即可发送消息给服务器,服务器会广播给所有的客户端,同时当有新客户端加入进来的时候,服务器会将最近的100条消息发给这个新加进来的客户端(即接收历史信息)。
效果如下:

BindName或者Chat发送要发送消息的类型(可以支持中文)

        当然,这个可以使用BindName直接Chat,这里只是简单展示。
        当客户端退出时(ctrl+d推出,不要ctrl+c太暴力了)(windows应该是ctrl+z,就是结束getline输入的命令),服务器会显示客户端退出的消息。当客户端再次连接时,会看到历史消息

(visual多香,直接构建项目搞定了)

聊天室1.0代码简析(具体分析会在聊天室1.2的版本里)

关于头文件的解析已经在上面提过了,若是有所遗忘可以往上翻一下。
        直接开始具体介绍客户端吧:
        客户端的主要函数和大致结构在上面也已经提过,现在就是在之前提到的骨架上进行“血肉填充”。

客户端

1、连接+读服务器数据

先看看构造函数:在客户端构造的时候就会进行与服务器的连接建立,如果不希望这么做,可以把连接接口暴露出去,让客户端决定到底什么时候连接,这里为了简便就在构造的时候连接了。

chat_client(boost::asio::io_context& io_context,const tcp::resolver::results_type& endpoints): io_context_(io_context),socket_(io_context){ do_connect(endpoints);}

看看do_connect怎么做的:

void do_connect(const tcp::resolver::results_type& endpoints){boost::asio::async_connect(socket_, endpoints,[this](boost::system::error_code ec, tcp::endpoint){ //回调函数if (!ec){do_read_header();}});
}

这里为什么要用异步呢?可以在连接的时候,在后台准备好和服务器对接的东西,比如游戏客户端(LOL),要提前准备图形渲染、声卡之类的,就比较方便,然后这里调用了一个简单的回调函数。

简单点说,就是连接建立之后,服务器后面往客户端发的消息就是要接收的消息了,解析消息的函数放在了do_read_header();里。
        再来看看do_read_header()这个函数:

void do_read_header(){boost::asio::async_read(socket_,boost::asio::buffer(read_msg_.data(), chat_message::header_length),[this](boost::system::error_code ec, std::size_t /*length*/){if (!ec && read_msg_.decode_header()){do_read_body();}else{ socket_.close();}});}

调用了asio的异步读(简单点理解就是:服务器通过网络把数据发到了客户端的网卡上面,然后客户端从网卡上读取服务器发送的消息。)
        异步的方式是:在数据来之前,我这个线程或者进程可以去干别的事情,等数据来了,你网卡告诉我,我再拷贝到内存里面去。就相当于你在等飞机或者火车的中间喝咖啡看电影一样。
        拿到数据之后,解析头部,如果头部合法,就读数据包体。(我得通过头部长度来知道后面多少数据是属于我这个数据包的)

继续看do_read_body()

void do_read_body(){boost::asio::async_read(socket_,boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),[this](boost::system::error_code ec, std::size_t /*length*/){if (!ec){//真正逻辑部分:判断消息类型if(read_msg_.body_length() == sizeof(RoomInformation) &&read_msg_.type() == MT_ROOM_INFO) {const RoomInformation *info =reinterpret_cast<const RoomInformation*>(read_msg_.body());std::cout << "client: '";assert(info->name.nameLen <= sizeof(info->name.name));std::cout.write(info->name.name, info->name.nameLen);
std::cout << "  says : '";assert(info->chat.infoLen <= sizeof(info->chat.information));std::cout.write(info->chat.information, info->chat.infoLen);std::cout << std::endl;}//循环读包体,因为是异步的,不会阻塞。(不会一直等着)do_read_header();}else{socket_.close();}});}

逻辑真的很简单:异步读——>分析数据类型——>循环读包头

2、向服务器写数据

写数据这个过程真的也很简单:从标准输入读(读用户输入)——>解析输入(是bindname还是chat还是非法)——>封装成chat_message发出去

char line[chat_message::max_body_length + 1];while (std::cin.getline(line, chat_message::max_body_length + 1)){chat_message msg;auto type = 0;//这里有点像迭代器,获得line的输入std::string input(line, line + std::strlen(line));std::string output;//都封装到这个parseMessage里面,整个框架就可以复用了if(parseMessage(input,&type,output)){msg.setMessage(type, output.data(), output.size());c.write(msg);std::cout << "write message for server " << output.size() << std::endl;}}

解析parseMessage和封装setMessage非常简单,这里不过多说明。这里封装成一个函数,增加了可复用性——当解析逻辑改变时,不用改变主逻辑结构

主要看看write这个函数是怎么做的吧:

void write(const chat_message& msg){ boost::asio::post(io_context_,[this, msg]() //这里msg是值拷贝,而不是值引用{ //这里和chat message中的deliver处理是一样的bool write_in_progress = !write_msgs_.empty();write_msgs_.push_back(msg);//只有write_msgs_是空的时候才进行do_write,防止调用两次do_writeif (!write_in_progress){do_write();}});}

post可以简单理解创建了一个事件,用post就可以交给io_context_去管理,当然这不是特别主要,主要来分析一下它的具体实现:
        将消息插入到发送队列的尾部,并调用do_write()函数进行发送。

看看do_write:

void do_write(){boost::asio::async_write(socket_,boost::asio::buffer(write_msgs_.front().data(),write_msgs_.front().length()),[this](boost::system::error_code ec, std::size_t /*length*/){if (!ec){write_msgs_.pop_front();//没写完就继续写if (!write_msgs_.empty()){do_write();}}else{socket_.close();}});}

是不是觉得很眼熟,简直和do_read_header如出一辙,不过他们的思想的确殊途同归。
        也是一样:异步写——>将数据出队列——>继续回调do_write()

到这里,客户端的读写逻辑基本上都已经说完了,服务器的实现逻辑和客户端真的也很像,我们也来简单分析一下。

服务器

1、接收客户端消息

同样,在构造函数里和客户端建立连接,同时接收来自客户端的消息。

chat_server(boost::asio::io_context& io_context,const tcp::endpoint& endpoint): acceptor_(io_context, endpoint){do_accept();}

继续看看do_accept:

void do_accept(){//这里异步连接一个新的客户端acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket){if (!ec){auto session = std::make_shared<chat_session>(std::move(socket), room_);session->start();}//这里可能会有错误,但是服务器端的工作不能停//比如三次握手失败了,失败的逻辑在客户端那边处理,服务器不管,继续监听do_accept();});}

简单一看,异步连接+start,好家伙和客户端逻辑长的不说一摸一样也是有八分像了。
        逻辑:async_accept异步连接——>创建session(一个客户端可以理解成是一个session)——>开始接收数据(start)——>回调自己(do_accept)。

继续看看start函数:

void start(){room_.join(shared_from_this());do_read_header(); //读报文头部}

加入到聊天室(为后面的写做铺垫)+do_read_header。

一看到do_read_header懂得都懂,下一步肯定是do_read_body,直接看代码吧:

void do_read_header(){//这里为了不被析构,所以搞了个这个内容auto self(shared_from_this());//之后异步的去读boost::asio::async_read(socket_,//把头四个字节读到buff里面去boost::asio::buffer(read_msg_.data(), chat_message::header_length),//第三个参数是一个函数指针,也就是一个回调函数[this, self](boost::system::error_code ec, std::size_t /*length*/){ //ec是error_code也就是模块或者系统错误,而且头部信息合法//body长度小于512if (!ec && read_msg_.decode_header()){do_read_body();}else{ //出错就断开,这里智能指针引用计数为0room_.leave(shared_from_this());}});}

稍微解释一下,这个auto self(shared_from_this());和智能指针的引用计数相关,这里不多做解释,后面会提到,感兴趣的同学可以先看看。
       同样也是异步读+do_read_body()。

void do_read_body(){//这里的目的和上面一样auto self(shared_from_this());boost::asio::async_read(socket_,//也是一样,把body的内容读到buff里面,错位了四个字节boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),[this, self](boost::system::error_code ec, std::size_t /*length*/){if (!ec){//handleMessage负责处理body里面的内容,处理完以后继续异步读headerhandleMessage();do_read_header();}else{room_.leave(shared_from_this());}});}

2、向客户端写

大家可能会疑惑:服务器往客户端写的部分在哪里呢?
       还记得前面有一步,客户端会加入到聊天室。在join的时候,服务器就会根据room的内容,对客户端进行写操作。

//chat_room函数实现
void chat_room::join(chat_session_ptr session)
{sessions_.insert(session);std::cout << "one client join the room" << std::endl;for (const auto& msg: recent_msgs_)session->deliver(msg);
}

前面就是加入到聊天室队列的逻辑,真正的写操作在deliver里:

void deliver(const chat_message& msg){bool write_in_progress = !write_msgs_.empty();write_msgs_.push_back(msg);//第一次为空,只有为空的时候才会调用do_write//这里是防止调用两次do writeif (!write_in_progress){do_write();}}

大家看到do_write和客户端的do_write一联系,就很好理解了。

void do_write(){auto self(shared_from_this());boost::asio::async_write(socket_,boost::asio::buffer(write_msgs_.front().data(),write_msgs_.front().length()),[this, self](boost::system::error_code ec, std::size_t /*length*/){if (!ec){ //头部信息写完了,就检查是不是空的write_msgs_.pop_front();if (!write_msgs_.empty()){ //继续写do_write();}}else{room_.leave(shared_from_this());}});}

到这里,整个客户端——服务器 聊天室的主要逻辑功能相信大家都有了一个大体的认识,如果想继续深入细节,可以在github上把代码下载下来自己动手跑一下,在获得乐趣的同时也可以加深对代码的理解。

在后续的聊天室1.1和聊天室1.2中会详细介绍里面的具体细节。

有趣的点

聊天室1.0里面比较有趣的点——也就是一开始我比较疑惑的点主要就是在异步回调这里:
       为什么要用auto self(shared_from_this);呢?
       回调的时候为什么不会出现爆栈呢?(因为类似循环调用)

异步循环调用为什么不爆栈?

要想弄清楚这几个问题,最先要解决的问题就是——回调函数究竟是什么?
       如果按照普通的函数调用过程进行思考,每调用一次函数,会将函数调用处压栈空间,类似下图所示:(为了表示简单就直接用函数名字代替参数入栈什么的了)

       我们以服务器接收客户端消息为例:假如是在一个线程里面,用同步的思想去考虑,这样不断的循环调用肯定会出现爆栈的情况。
       自然而然的我们会想到——系统级的异步,比如说linux的epoll是怎么做的呢?交给内核去管理,让内核进行回调通知。如果抽象点看:把内核看成另一个线程或者说另一个工作场景,就像我现在遇到问题了,找了一个朋友吧事情交给他做,然后我继续做我接下来的事情。就好像这两件事工作在不同的线程里面。
       之后带着自己的疑惑和自己的思考去请教了一下前辈们,虽然异步的实现方式远没有我们想的那么简单,但是这种思想是贯穿始终的,为了简单起见——我们姑且认为是在两个线程里面工作的。(前辈说具体实现要看future)

带着这个思路我们就解决了爆栈的问题——当运行到do_read_header的时候,start开了个线程给了do_read_header,它自己就执行完毕了,函数返回,自然也就不会存在爆栈的问题。

为什么要使用self(shared_from_this)?

其实要搞清楚这个需要对智能指针比较熟悉,在此之前,我们把这个auto的一条语句给它展开,或许对它的理解会更容易一些。
        还是以服务器接收客户端消息为例:展开整条赋值表达式就是:

std::shared_ptr<chat_session> self = shared_from_this();

这个shared_from_this简单理解就是智能指针管理的this指针。
        看到这里可能大概明白了这条语句的意思:给这个对象(this指向的就是当前session对象)增加一个引用计数。

但是为什么需要增加一个引用计数呢?
        还记得上面说的思想:当成两个线程去看。也就是说当do_read_header在运行的时候,可能出现start已经运行结束的情况:

void do_accept(){acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket){if (!ec){//这里是session的生命周期auto session = std::make_shared<chat_session>(std::move(socket), room_);session->start();} // start结束,这里智能指针的引用计数要减去1do_accept();});}void start(){room_.join(shared_from_this());do_read_header(); //用上面的思想:这条语句就是丢到另一个线程里去//丢完之后start就运行结束了}

而start的结束就意味着do_accept里面的if也结束了,意味着session的引用计数要减去1,假如没有别的指针持有这个对象,这个对象就会被释放了。
       所以还记得do_read_header里面是怎么做的了吗?

void do_read_header(){//我觉得写auto还是有点憨批,本来就是强类型语言//除非心里及其清楚这个auto代表着什么//std::shared_ptr<chat_session> self(shared_from_this());std::shared_ptr<chat_session> self = shared_from_this();boost::asio::async_read(socket_,boost::asio::buffer(read_msg_.data(), chat_message::header_length),//lambda表达式捕获类型是值捕获[this, self](boost::system::error_code ec, std::size_t /*length*/){ //......});}

lambda表达式的捕获是值捕获,引用计数+1: 意味着async_read这个函数不结束,智能指针指向的这个session对象就不会释放。保证了do_accept生成的chat_session的生命周期。

当然为什么会出现这样的疑惑主要还是因为单线程的思想根深蒂固,只要日后多接触多线程异步之类的思想,习惯了以后就很容易理解了,这是个循序渐进的过程,慢慢来就好。

聊天室1.0 github地址:

代码很简单,完整代码我把它放到了github上,后续我们会逐渐对他进行更新和迭代,在循序渐进中慢慢感受理论与实践相结合的乐趣。
聊天室1.0

参考文献

1 b站课程
2 boost-asio网络库

c++项目——聊天室——第一节相关推荐

  1. Android小项目————聊天室(UI篇)

    Android小项目----聊天室(UI篇) 一.前言 这是所做的第二个android项目,主要目的对暑假所学的java和android知识点进行复习巩固和实践,由于知识所限,目前这个聊天室并不是很完 ...

  2. 暑假项目聊天室(0)--源码

    引言 暑假留校结束了,暑假中最重要的一个项目聊天室,在最终的努力下还是完成了,下面就来分析一下我写的聊天室; 文件 main.c 服务端主函数 main_cli.c 客户端主函数和客户端收发线程函数 ...

  3. 网络编程项目(聊天室项目)

    一.实现目标 一个在Linux下可以使用的聊天软件,要求至少实现如下功能: 1. 采用Client/Server架构 2. Client A 登陆聊天服务器前,需要注册自己的ID和密码 3. 注册成功 ...

  4. Linux C小项目 —— 聊天室

    多线程的聊天室 服务器端: 实现多用户群体聊天功能(人数上限可设置): 每个用户所发送的消息,其他已连接服务器的用户均可以收到: 用户输入"bye"退出,服务器输入"qu ...

  5. Java小项目——聊天室(多线程版本)

    目录 1. 前言 2. 功能实现 3. 模块划分 4. 功能分析 4.1 前期分析 4.2 具体实现 5. 使用技术 6. 代码 1. 前言 之前写过单线程版本的聊天室,这次对之前的版本进行扩展与优化 ...

  6. Android小项目——聊天室

    聊天室简介 简单介绍 更换图标 网络权限 登录界面 activity_main.xml MainActivity.java 选择头像 activity_choose_picture.xml Choos ...

  7. 《Python多人游戏项目实战》第一节 简单的方块移动

    目录 1.1 设置游戏窗口 1.2 绘制一个方块 1.3 编写服务端代码 1.4 完善客户端代码 1.5 完整代码下载地址 在本节,我们将通过一个简单的方块移动程序进入多人联机游戏的大门.每个玩家打开 ...

  8. 带你玩转区块链--以太坊基础、发币、基于智能合约实现彩票项目-第二章-第一节【以太坊篇】

    意义: 在上一节知识学习中,我们已经了解如何实现一个基础区块链,并重构了BTC关键代码.对比传统的中心化项目,区块链项目拥有很多优势,如:追溯性.不可传篡改性.在中心化项目中的网络协议是:[数据层-- ...

  9. C++小项目(聊天室)——select模型+mysql+花生壳端口映射打造一个可以用外网连接的小qq

    成品展示: B站视频链接 这个小软件是我初学网络编程写的小玩具,记录一下,等学完完成端口模型再利用完成端口写别的好玩的软件,看的课程是这个老师,真的强烈推荐,课程28块钱,老师讲的巨棒,很细,很适合新 ...

  10. JavaSE项目:聊天室

    聊天室项目: 分析: /*** 小项目:聊天室步骤分析;* 客户端:* main:* 01> 使用TCP编程,创建客户端的Socket对象; * 02> 创建键盘输入对象,用来输入用户名; ...

最新文章

  1. extmail电子邮件系统
  2. C#获取进程的CPU使用率
  3. 在Windows 7 下使用Visual Studio 2010 编写自动申请管理员权限运行的程序
  4. 打印二叉树的所有路径
  5. 很少有人能把CDN说的这么有趣了
  6. j2ee可以用于前端开发吗_用于J2EE开发的Cloud IDE
  7. pc游戏手柄测试软件,《原神》PC版技术性开发测试,游戏手柄操作更佳爽快
  8. php iis 重启服务,重新启动IIS即可生效
  9. 计算机网络实验一VLAN间路由
  10. C/C++探秘(1)
  11. 《CSS禅意花园》明日上市
  12. 【Vue】Vue中mixins的使用方法及实际项目应用详解
  13. 《信号与系统》第一章 信号与系统概述
  14. C#中Guid.ToString (String)五种格式,以及将32位的GUID转为16位及其他格式
  15. HTML中的文本标签及样式
  16. windowXP快速关机法
  17. Java百度地图教程:创建地图应用与基本API的使用
  18. Python使用format输出时还想输出‘{‘,‘}‘的方法
  19. 如何用 BearyChat 将 ChatOps 带到你的日常工作中
  20. FPGA学习——基于Verilog实现的多功能时钟

热门文章

  1. 百度竞价软件测试面试,【百度SEM基础试题】百度推广竞价专员基础知识测试
  2. 玉米社:百度竞价关键词“否定”与“精确否定”的区别
  3. win10 下安装wampserver 的几个坑
  4. Java2实用教程第五版+第七章习题答案
  5. 140809暑期培训
  6. 双核不可阻挡!首款双核处理器Tegra2详解
  7. 上计算机课睡觉检讨书400,课堂睡觉200字检讨书
  8. 关于 JWT Token 自动续期的解决(根据其他文献参考写的)
  9. python填空题_《Python程序设计》题库 - 填空题
  10. Take me to your heart(English Song)