日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

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

發布時間:2023/12/20 c/c++ 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c++项目——聊天室——第一节 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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發消息。
??????? 有的同學可能會說:“哎呀,那肯定是第一種了,第二種這么麻煩”。但是我們簡化問題的時候也不能忽略原本的內容——也就是俗話說 未雨綢繆?,F在是兩個同學發消息,如果是五個同學、十個同學呢,這就不好處理了。所以目前我們就使用方法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++项目——聊天室——第一节的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。