c++项目——聊天室——第一节
c++項(xiàng)目——聊天室——第一節(jié)
- 概述
- 引言
- 聊天室初步
- ????? 1 總體設(shè)計(jì)
- ????? 2 思路設(shè)計(jì)
- ????? 3 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)(細(xì)節(jié)設(shè)計(jì))
- ???????(1)分析消息協(xié)議部分
- ???????(2)分析客戶(hù)端
- ???????(3)分析服務(wù)器
- 聊天室1.0
- 運(yùn)行效果
- 聊天室1.0代碼簡(jiǎn)析(具體分析會(huì)在聊天室1.2的版本里)
- 客戶(hù)端
- 1、連接+讀服務(wù)器數(shù)據(jù)
- 2、向服務(wù)器寫(xiě)數(shù)據(jù)
- 服務(wù)器
- 1、接收客戶(hù)端消息
- 2、向客戶(hù)端寫(xiě)
- 有趣的點(diǎn)
- 異步循環(huán)調(diào)用為什么不爆棧?
- 為什么要使用self(shared_from_this)?
- 聊天室1.0 github地址:
- 參考文獻(xiàn)
概述
???????本節(jié)內(nèi)容是在 如何學(xué)習(xí)編程 之后進(jìn)一步由理論結(jié)合實(shí)踐去驗(yàn)證和加深該學(xué)習(xí)思想,為了方便起見(jiàn),不會(huì)再過(guò)多的闡述先驗(yàn)知識(shí),因此若是在閱讀過(guò)程中出現(xiàn)因先驗(yàn)知識(shí)不足而導(dǎo)致的難以理解的情況,請(qǐng)自行學(xué)習(xí)相關(guān)的先驗(yàn)知識(shí)。
???????因?yàn)閏++這門(mén)語(yǔ)言學(xué)習(xí)起來(lái)總有一定的難度,除了語(yǔ)言本身的原因以外,由于學(xué)的人相對(duì)較少,學(xué)精通的人更少,導(dǎo)致在推廣方面,無(wú)論是人數(shù)還是質(zhì)量都難以保證。
???????自己在剛開(kāi)始學(xué)c++的時(shí)候,出現(xiàn)過(guò)很多問(wèn)題,影響最大的還是以下幾點(diǎn):
- 1 資源相對(duì)較少,當(dāng)時(shí)自己找資源的能力也比較有限。
- 2 資源參差不齊,有些博主自己都沒(méi)有完全理解,寫(xiě)了一大堆專(zhuān)業(yè)術(shù)語(yǔ)出來(lái)裝逼的。
- 3 c++比較權(quán)威的書(shū)對(duì)于c++初學(xué)者不是很友好,因?yàn)闄?quán)威書(shū)籍很多先驗(yàn)知識(shí)都默認(rèn)你會(huì)了,但這也沒(méi)辦法。
???????看本篇文章的時(shí)候先看目錄,為了照顧c++小白有些地方寫(xiě)的比較細(xì)(俗話(huà)說(shuō)比價(jià)啰嗦),想看哪個(gè)部分直接目錄索引點(diǎn)過(guò)去即可。
???????寫(xiě)這篇文章除了有借此加深鞏固自己之前所學(xué)以外,也希望自己的文章可以幫助到更多的c++小白,讓越來(lái)越多的人喜歡上c++,愿c++經(jīng)久不衰。
引言
???????再次聲明,本篇文章是讓小白過(guò)渡到初學(xué)者的文章,因?yàn)楸救艘彩浅鯇W(xué)者,所以掌握的知識(shí)的全面程度和深度肯定是有限的,但學(xué)習(xí)本身就是不斷的擴(kuò)寬自己的廣度和深度,所以這很正常。就如同牛頓力學(xué)過(guò)度到量子力學(xué)一樣,牛頓力學(xué)沒(méi)有錯(cuò),量子力學(xué)也沒(méi)有錯(cuò),只是適用范圍不同罷了,或者說(shuō)量子力學(xué)的適用范圍更大,但不管怎么說(shuō),能在一定范圍內(nèi)正確解釋世界規(guī)律的,我覺(jué)得就是好理論。
???????先驗(yàn)知識(shí)聲明:在進(jìn)入聊天室的學(xué)習(xí)之前,必須要有一定的c++基礎(chǔ)知識(shí)和計(jì)算機(jī)相關(guān)的基礎(chǔ)知識(shí),沒(méi)有這些基礎(chǔ),什么牛鬼蛇神來(lái)了都沒(méi)有,就算是所謂的“天才、聰明人”,也只是通過(guò)類(lèi)比的方式,結(jié)合他自己之前類(lèi)似的經(jīng)歷推出來(lái)的(我對(duì)天才這個(gè)詞很反感,我覺(jué)得就是騙騙世人,給人們找借口的詞匯,如有不適敬請(qǐng)見(jiàn)諒),所以如果沒(méi)有掌握這些知識(shí),你看起來(lái)無(wú)比難受是很正常的事情。
???????具體的先驗(yàn)知識(shí):(其中黃色是必須掌握,淺色黑體是掌握了對(duì)細(xì)節(jié)的把握會(huì)更好)
??????? 1、c++基礎(chǔ)部分:類(lèi)初步(如構(gòu)造函數(shù)析構(gòu)函數(shù)、公有繼承私有繼承等)、STL基本使用(deque、vector、list、string、chrono時(shí)間庫(kù))、命名空間、枚舉、基本關(guān)鍵字(typedef、using)、const引用和值傳遞的區(qū)別、內(nèi)存對(duì)齊、右值引用和左值引用的區(qū)別、異常、c++11新特性(加強(qiáng)for循環(huán)、智能指針)還有其他c和c++相同的部分、c++多線(xiàn)程基礎(chǔ)。
??????? 2、計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ):c++asio網(wǎng)絡(luò)庫(kù)簡(jiǎn)單api使用、tcp報(bào)文格式、計(jì)算機(jī)網(wǎng)絡(luò)數(shù)據(jù)是如何從一臺(tái)主機(jī)上經(jīng)過(guò)5層模型(或者7層參考模型)到達(dá)另一臺(tái)主機(jī)的宏觀了解、同步異步的知識(shí)。
??????? 3、liunx基礎(chǔ):liunx最基本命令、liunx下cmake使用、liunx非常基本的腳本編寫(xiě)。
??????? 4、其他:像google protobuf等序列工具的使用、為什么要序列化、protobuf為什么快等。
??????? 細(xì)心的同學(xué)可能發(fā)現(xiàn)了,為啥我沒(méi)有把c++多線(xiàn)程標(biāo)記為必會(huì)呢?因?yàn)槲覀冞@個(gè)聊天室是一個(gè)循序漸進(jìn)的版本,因此如果沒(méi)到后面多線(xiàn)程的版本,不需要掌握c++多線(xiàn)程基礎(chǔ)當(dāng)然也可以駕馭。
??????? 還有就是,如果對(duì)c++內(nèi)存掌握程度高的話(huà),對(duì)一些細(xì)節(jié)的理解肯定還會(huì)更好,畢竟我們學(xué)習(xí)知識(shí)肯定是知其然還要知其所以然,再功利點(diǎn)說(shuō):遇到bug你也能知道原因然后快速定位去解決嘛。
聊天室初步
????? 1 總體設(shè)計(jì)
??????? 正所謂:兵馬未動(dòng),糧草先行;理論是用來(lái)更好指導(dǎo)實(shí)踐的。有一個(gè)好的架構(gòu)體系,或者說(shuō)在設(shè)計(jì)之初就考慮好很多東西的話(huà),對(duì)后面無(wú)論是出問(wèn)題還是迭代肯定都會(huì)更好解決。(可以結(jié)合 如何學(xué)習(xí)編程 提到的守恒思想去分析)。
??????? 無(wú)論是設(shè)計(jì)和分析問(wèn)題,首先要把握的就是他的核心,用哲學(xué)的話(huà)來(lái)說(shuō)就是:把握事物的主要矛盾。其實(shí)也就是把握事物的本質(zhì)。
??????? 聊天室聊天室,核心肯定是提供一個(gè)較為舒適的聊天服務(wù)。把握本質(zhì)以后,接下來(lái)我們做的事情是什么?——計(jì)算機(jī)分治思想,或者簡(jiǎn)單點(diǎn)說(shuō),把問(wèn)題分解。
??????? 其實(shí)細(xì)心的同學(xué)在生活中就可以觀察到:無(wú)論我們做任何事情,無(wú)形之中其實(shí)就已經(jīng)把這件事情分成若干字問(wèn)題進(jìn)行處理了。
??????? 比如在吃飯的時(shí)候,先拿起筷子,做好姿勢(shì)、選中要夾的菜、計(jì)算筷子到要夾的菜要走什么樣的路徑、夾中菜后把握怎么樣的力度可以不讓菜掉下來(lái)…
??????? 回到正題,那么我們?cè)?strong>如何把問(wèn)題分解呢——剪取不重要細(xì)節(jié)。借鑒或者類(lèi)比之前吃飯的例子,舒適的聊天室,本質(zhì)上就是多人之間進(jìn)行聊天,那我們先分析兩個(gè)人的情況,也先不管舒不舒適的問(wèn)題,那現(xiàn)在的問(wèn)題就變成了——兩個(gè)人的聊天室。
??????? 如果加入服務(wù)器——客戶(hù)端的模型思想:服務(wù)器用來(lái)接收和發(fā)送這兩個(gè)人的消息,客戶(hù)端負(fù)責(zé)(從命令行)接收消息,并交由服務(wù)器處理,同時(shí)還會(huì)接受來(lái)自服務(wù)器的消息。
??????? 這其實(shí)還是有點(diǎn)抽象,簡(jiǎn)單點(diǎn)說(shuō),舉個(gè)例子:A和B同學(xué)聊天,現(xiàn)在A同學(xué)想對(duì)B同學(xué)說(shuō) “ni hao”,那么簡(jiǎn)單至極有兩種方式:1 A直接給B發(fā)消息。 2 A給一個(gè)中轉(zhuǎn)站發(fā)消息,由這個(gè)中轉(zhuǎn)站給B發(fā)消息。
??????? 有的同學(xué)可能會(huì)說(shuō):“哎呀,那肯定是第一種了,第二種這么麻煩”。但是我們簡(jiǎn)化問(wèn)題的時(shí)候也不能忽略原本的內(nèi)容——也就是俗話(huà)說(shuō) 未雨綢繆。現(xiàn)在是兩個(gè)同學(xué)發(fā)消息,如果是五個(gè)同學(xué)、十個(gè)同學(xué)呢,這就不好處理了。所以目前我們就使用方法2。
??????? 到這,我們的1.0版本的聊天室已經(jīng)逐漸浮出水面了:A同學(xué)和中轉(zhuǎn)站建立連接,之后向中轉(zhuǎn)站建立連接;B同學(xué)和中轉(zhuǎn)站也建立連接,接受中轉(zhuǎn)站發(fā)送過(guò)來(lái)A的消息。
??????? 然后我們發(fā)現(xiàn),無(wú)論有多少個(gè)同學(xué),都要和這個(gè)中轉(zhuǎn)站建立連接,而每一位同學(xué)做的動(dòng)作都是差不多的——和中轉(zhuǎn)站建立連接、發(fā)送或接收中轉(zhuǎn)站的消息。
??????? 如果把中轉(zhuǎn)站換個(gè)名字——服務(wù)器。把每個(gè)同學(xué)的動(dòng)作邏輯換個(gè)名字——客戶(hù)端。
??????? 那么聊天室1.0的雛形就出來(lái)了——服務(wù)器用于和客戶(hù)端連接并收發(fā)消息;客戶(hù)端用于接收用戶(hù)輸入并收發(fā)服務(wù)器的消息。
????? 2 思路設(shè)計(jì)
??????? 下面我們就來(lái)逐步分析服務(wù)器和客戶(hù)端都是怎么設(shè)計(jì)的。
??????? 1 客戶(hù)端。客戶(hù)端由兩個(gè)方面組成:接收客戶(hù)輸入并把消息發(fā)送給服務(wù)器 和 接收由服務(wù)器發(fā)送給客戶(hù)端的消息。還是一樣,繼續(xù)分解問(wèn)題:先考慮接收客戶(hù)輸入并把消息發(fā)送給服務(wù)器怎么做?接受客戶(hù)輸入:可以用cin的getline接收,并把消息放到一個(gè)隊(duì)列里;把消息發(fā)送給服務(wù)器:借助c++asio網(wǎng)絡(luò)提供的api即可。再來(lái)看服務(wù)器發(fā)送給客戶(hù)端的消息怎么做?服務(wù)器發(fā)送給客戶(hù)端的數(shù)據(jù)通過(guò)網(wǎng)絡(luò)傳輸最開(kāi)始肯定是發(fā)到網(wǎng)卡上,但是對(duì)網(wǎng)卡的操作也太底層了,因此借助c++asio網(wǎng)絡(luò)庫(kù)——借助asio網(wǎng)絡(luò)庫(kù)的api接收服務(wù)端信息,并用隊(duì)列放到內(nèi)存,并用cout輸出即可。即:
- 接收客戶(hù)輸入并把消息發(fā)送給服務(wù)器:用cin的getline接收用戶(hù)輸入,并把消息放到一個(gè)隊(duì)列里,最后用asio庫(kù)api發(fā)給服務(wù)器;
- 接收由服務(wù)器發(fā)送給客戶(hù)端的消息:借助asio網(wǎng)絡(luò)庫(kù)的api接收服務(wù)端信息,并用隊(duì)列放到內(nèi)存,并用cout輸出。
??????? 2 服務(wù)器。服務(wù)器也是由兩個(gè)部分組成:接收客戶(hù)端消息 和 將聊天室消息發(fā)送給客戶(hù)端。因?yàn)楹涂蛻?hù)端有點(diǎn)類(lèi)似,這里直接給出結(jié)論,即:
- 接收客戶(hù)端消息:通過(guò)asio網(wǎng)絡(luò)庫(kù)api接收客戶(hù)端消息,并把所有消息都放到一個(gè)隊(duì)列里,;
- 將聊天室消息發(fā)送給客戶(hù)端:將隊(duì)列中的內(nèi)容借助asio網(wǎng)絡(luò)庫(kù)的api廣播(發(fā)送)給所有客戶(hù)端。
????? 3 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)(細(xì)節(jié)設(shè)計(jì))
???????上面的設(shè)計(jì)內(nèi)容部分算是結(jié)束了,但具體如何去設(shè)計(jì)類(lèi)和數(shù)據(jù)結(jié)構(gòu)還有待商榷。
???????(1)分析消息協(xié)議部分
???????還是一樣,將分治的思想融入進(jìn)來(lái),無(wú)論是客戶(hù)端還是服務(wù)器,最先要解決的,就是雙方要統(tǒng)一消息的格式,也就是我們常說(shuō)的協(xié)議。
???????因此我們?cè)O(shè)計(jì)一個(gè)chat_message類(lèi),用于存放消息,同時(shí)規(guī)定:消息結(jié)構(gòu)是 消息頭部 + 消息體的形式。消息頭部存放了消息體的長(zhǎng)度和消息類(lèi)型(比如是客戶(hù)端發(fā)送給服務(wù)器聊天的消息還是服務(wù)器發(fā)給客戶(hù)端的消息),而且是定長(zhǎng)的,這樣就可以通過(guò)頭部去處理消息體的內(nèi)容。
簡(jiǎn)單表示一下就是:
???????(2)分析客戶(hù)端
???????之后分析客戶(hù)端,cin的getline接收用戶(hù)輸入,同時(shí)還要有一個(gè)隊(duì)列,簡(jiǎn)單表示就是:
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_; };再分析一下要有什么函數(shù):
1 要有和服務(wù)器連接的函數(shù)——目前放在構(gòu)造函數(shù)里面。
2 要有接收函數(shù)——接收服務(wù)器發(fā)送的數(shù)據(jù)。
3 要有寫(xiě)出函數(shù)——向服務(wù)器發(fā)送自己的消息。
因此類(lèi)可以表示為:
chat_client { public://連接函數(shù)和接受函數(shù)都在構(gòu)造函數(shù)里面了//即chat_client(xxx) <==> connect + acceptchat_client(xxx); //有參構(gòu)造函數(shù)void write(const chat_message& msg); void close(); private:chat_message read_msg_;chat_message_queue write_msgs_; };???????(3)分析服務(wù)器
???????服務(wù)器除了要和客戶(hù)端連接chat_server,還要有一個(gè)聊天室chat_room接收消息,但是在廣播消息的時(shí)候,需要向每個(gè)客戶(hù)端都發(fā)消息,因此用chat_session表示接入進(jìn)來(lái)的客戶(hù)端,簡(jiǎn)單表示如下:
class chat_room{private:chat_message_queue recent_msgs_; }; class chat_session {private:chat_room& room_; //屬于哪個(gè)聊天室std::string m_name; //這里是這個(gè)session的名字chat_message read_msg_;chat_message_queue write_msgs_; }; class chat_server{public://有參構(gòu)造函數(shù)里包括了connect和readchat_server(xxx);private:chat_room room_; //管理所有的room };再分析一下需要有什么樣的函數(shù):
1 對(duì)于room來(lái)說(shuō),需要有客戶(hù)端加入到聊天室的join函數(shù)、需要有客戶(hù)端離開(kāi)的leave函數(shù)和向所有客戶(hù)端廣播的deliver函數(shù)。
2 所有的具體處理函數(shù)放在chat_session中,server只負(fù)責(zé)connect和read、room負(fù)責(zé)控制客戶(hù)加入退出和發(fā)送。到這其實(shí)已經(jīng)足夠,但為了封裝和可擴(kuò)展性,把server的read和room的發(fā)送放在了session里面做。即 read <=> session.start, deliver <=> session.deliver。
更完整的類(lèi)聲明如下:
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_; //屬于哪個(gè)聊天室std::string m_name; //這里是這個(gè)session的名字chat_message read_msg_;chat_message_queue write_msgs_; }; class chat_server{public://有參構(gòu)造函數(shù)里包括了connect和readchat_server(xxx);private:chat_room room_; //管理所有的room };當(dāng)然,因?yàn)橐Y(jié)合c++的asio庫(kù),所以聲明肯定還要更復(fù)雜一些,但那都是asio的東西,把握了這主體的東西對(duì)我們的編程來(lái)說(shuō)就足夠了。
聊天室1.0
下面來(lái)看看聊天室1.0的內(nèi)容:
一共分為5個(gè)文件:
??????? 1 chat_message.hpp、structHeader.h、structHeader.cpp用于存放消息格式的約定(協(xié)議)。
??????? 2 chat_server.cpp放服務(wù)器相關(guān)邏輯。
??????? 3 chat_client.cpp放客戶(hù)端相關(guān)邏輯。
(為了文件少一點(diǎn),也可以把structHeader的內(nèi)容合到chat_message.hpp里面)
至此完成的就是asio的例子程序完成的內(nèi)容。不過(guò)雖然功能一樣,但是我們把消息變成了type進(jìn)行了一個(gè)小改動(dòng),這樣讓我們的可擴(kuò)展性就提升了一些,我們?cè)诖嘶A(chǔ)上加入客戶(hù)端可以發(fā)送“綁定名字”的消息。
運(yùn)行效果
??????? 在代碼解析之前,先跑起來(lái),看一下運(yùn)行效果,爽一下。這樣后續(xù)對(duì)代碼的理解也會(huì)更容易一些。(windows上應(yīng)該也能跑,用的都是跨平臺(tái)的庫(kù),因?yàn)閣indows上用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??????? 編譯完以后是如下的效果:
??????? 之后先運(yùn)行server服務(wù)端
??????? 后面這個(gè)是端口號(hào),隨便給個(gè)正數(shù)即可。
??????? 之后再開(kāi)幾個(gè)客戶(hù)端,運(yùn)行客戶(hù)端
./client localhost 9999之后在客戶(hù)端輸入
BindName 要輸入的名字
或者
Chat 聊天內(nèi)容
??????? 即可發(fā)送消息給服務(wù)器,服務(wù)器會(huì)廣播給所有的客戶(hù)端,同時(shí)當(dāng)有新客戶(hù)端加入進(jìn)來(lái)的時(shí)候,服務(wù)器會(huì)將最近的100條消息發(fā)給這個(gè)新加進(jìn)來(lái)的客戶(hù)端(即接收歷史信息)。
效果如下:
BindName或者Chat發(fā)送要發(fā)送消息的類(lèi)型(可以支持中文)
??????? 當(dāng)然,這個(gè)可以使用BindName直接Chat,這里只是簡(jiǎn)單展示。
??????? 當(dāng)客戶(hù)端退出時(shí)(ctrl+d推出,不要ctrl+c太暴力了)(windows應(yīng)該是ctrl+z,就是結(jié)束getline輸入的命令),服務(wù)器會(huì)顯示客戶(hù)端退出的消息。當(dāng)客戶(hù)端再次連接時(shí),會(huì)看到歷史消息。
(visual多香,直接構(gòu)建項(xiàng)目搞定了)
聊天室1.0代碼簡(jiǎn)析(具體分析會(huì)在聊天室1.2的版本里)
??????? 關(guān)于頭文件的解析已經(jīng)在上面提過(guò)了,若是有所遺忘可以往上翻一下。
??????? 直接開(kāi)始具體介紹客戶(hù)端吧:
??????? 客戶(hù)端的主要函數(shù)和大致結(jié)構(gòu)在上面也已經(jīng)提過(guò),現(xiàn)在就是在之前提到的骨架上進(jìn)行“血肉填充”。
客戶(hù)端
1、連接+讀服務(wù)器數(shù)據(jù)
??????? 先看看構(gòu)造函數(shù):在客戶(hù)端構(gòu)造的時(shí)候就會(huì)進(jìn)行與服務(wù)器的連接建立,如果不希望這么做,可以把連接接口暴露出去,讓客戶(hù)端決定到底什么時(shí)候連接,這里為了簡(jiǎn)便就在構(gòu)造的時(shí)候連接了。
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){ //回調(diào)函數(shù)if (!ec){do_read_header();}}); }??????? 這里為什么要用異步呢?可以在連接的時(shí)候,在后臺(tái)準(zhǔn)備好和服務(wù)器對(duì)接的東西,比如游戲客戶(hù)端(LOL),要提前準(zhǔn)備圖形渲染、聲卡之類(lèi)的,就比較方便,然后這里調(diào)用了一個(gè)簡(jiǎn)單的回調(diào)函數(shù)。
??????? 簡(jiǎn)單點(diǎn)說(shuō),就是連接建立之后,服務(wù)器后面往客戶(hù)端發(fā)的消息就是要接收的消息了,解析消息的函數(shù)放在了do_read_header();里。
??????? 再來(lái)看看do_read_header()這個(gè)函數(shù):
??????? 調(diào)用了asio的異步讀(簡(jiǎn)單點(diǎn)理解就是:服務(wù)器通過(guò)網(wǎng)絡(luò)把數(shù)據(jù)發(fā)到了客戶(hù)端的網(wǎng)卡上面,然后客戶(hù)端從網(wǎng)卡上讀取服務(wù)器發(fā)送的消息。)
??????? 異步的方式是:在數(shù)據(jù)來(lái)之前,我這個(gè)線(xiàn)程或者進(jìn)程可以去干別的事情,等數(shù)據(jù)來(lái)了,你網(wǎng)卡告訴我,我再拷貝到內(nèi)存里面去。就相當(dāng)于你在等飛機(jī)或者火車(chē)的中間喝咖啡看電影一樣。
??????? 拿到數(shù)據(jù)之后,解析頭部,如果頭部合法,就讀數(shù)據(jù)包體。(我得通過(guò)頭部長(zhǎng)度來(lái)知道后面多少數(shù)據(jù)是屬于我這個(gè)數(shù)據(jù)包的)
??????? 繼續(xù)看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){//真正邏輯部分:判斷消息類(lèi)型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;}//循環(huán)讀包體,因?yàn)槭钱惒降?#xff0c;不會(huì)阻塞。(不會(huì)一直等著)do_read_header();}else{socket_.close();}});}??????? 邏輯真的很簡(jiǎn)單:異步讀——>分析數(shù)據(jù)類(lèi)型——>循環(huán)讀包頭。
2、向服務(wù)器寫(xiě)數(shù)據(jù)
??????? 寫(xiě)數(shù)據(jù)這個(gè)過(guò)程真的也很簡(jiǎn)單:從標(biāo)準(zhǔn)輸入讀(讀用戶(hù)輸入)——>解析輸入(是bindname還是chat還是非法)——>封裝成chat_message發(fā)出去。
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;//這里有點(diǎn)像迭代器,獲得line的輸入std::string input(line, line + std::strlen(line));std::string output;//都封裝到這個(gè)parseMessage里面,整個(gè)框架就可以復(fù)用了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非常簡(jiǎn)單,這里不過(guò)多說(shuō)明。這里封裝成一個(gè)函數(shù),增加了可復(fù)用性——當(dāng)解析邏輯改變時(shí),不用改變主邏輯結(jié)構(gòu)。
??????? 主要看看write這個(gè)函數(shù)是怎么做的吧:
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_是空的時(shí)候才進(jìn)行do_write,防止調(diào)用兩次do_writeif (!write_in_progress){do_write();}});}??????? post可以簡(jiǎn)單理解創(chuàng)建了一個(gè)事件,用post就可以交給io_context_去管理,當(dāng)然這不是特別主要,主要來(lái)分析一下它的具體實(shí)現(xiàn):
??????? 將消息插入到發(fā)送隊(duì)列的尾部,并調(diào)用do_write()函數(shù)進(jìn)行發(fā)送。
??????? 看看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();//沒(méi)寫(xiě)完就繼續(xù)寫(xiě)if (!write_msgs_.empty()){do_write();}}else{socket_.close();}});}??????? 是不是覺(jué)得很眼熟,簡(jiǎn)直和do_read_header如出一轍,不過(guò)他們的思想的確殊途同歸。
??????? 也是一樣:異步寫(xiě)——>將數(shù)據(jù)出隊(duì)列——>繼續(xù)回調(diào)do_write()。
??????? 到這里,客戶(hù)端的讀寫(xiě)邏輯基本上都已經(jīng)說(shuō)完了,服務(wù)器的實(shí)現(xiàn)邏輯和客戶(hù)端真的也很像,我們也來(lái)簡(jiǎn)單分析一下。
服務(wù)器
1、接收客戶(hù)端消息
??????? 同樣,在構(gòu)造函數(shù)里和客戶(hù)端建立連接,同時(shí)接收來(lái)自客戶(hù)端的消息。
chat_server(boost::asio::io_context& io_context,const tcp::endpoint& endpoint): acceptor_(io_context, endpoint){do_accept();}??????? 繼續(xù)看看do_accept:
void do_accept(){//這里異步連接一個(gè)新的客戶(hù)端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();}//這里可能會(huì)有錯(cuò)誤,但是服務(wù)器端的工作不能停//比如三次握手失敗了,失敗的邏輯在客戶(hù)端那邊處理,服務(wù)器不管,繼續(xù)監(jiān)聽(tīng)do_accept();});}??????? 簡(jiǎn)單一看,異步連接+start,好家伙和客戶(hù)端邏輯長(zhǎng)的不說(shuō)一摸一樣也是有八分像了。
??????? 邏輯:async_accept異步連接——>創(chuàng)建session(一個(gè)客戶(hù)端可以理解成是一個(gè)session)——>開(kāi)始接收數(shù)據(jù)(start)——>回調(diào)自己(do_accept)。
???????繼續(xù)看看start函數(shù):
void start(){room_.join(shared_from_this());do_read_header(); //讀報(bào)文頭部}???????加入到聊天室(為后面的寫(xiě)做鋪墊)+do_read_header。
???????一看到do_read_header懂得都懂,下一步肯定是do_read_body,直接看代碼吧:
void do_read_header(){//這里為了不被析構(gòu),所以搞了個(gè)這個(gè)內(nèi)容auto self(shared_from_this());//之后異步的去讀boost::asio::async_read(socket_,//把頭四個(gè)字節(jié)讀到buff里面去boost::asio::buffer(read_msg_.data(), chat_message::header_length),//第三個(gè)參數(shù)是一個(gè)函數(shù)指針,也就是一個(gè)回調(diào)函數(shù)[this, self](boost::system::error_code ec, std::size_t /*length*/){ //ec是error_code也就是模塊或者系統(tǒng)錯(cuò)誤,而且頭部信息合法//body長(zhǎng)度小于512if (!ec && read_msg_.decode_header()){do_read_body();}else{ //出錯(cuò)就斷開(kāi),這里智能指針引用計(jì)數(shù)為0room_.leave(shared_from_this());}});}???????稍微解釋一下,這個(gè)auto self(shared_from_this());和智能指針的引用計(jì)數(shù)相關(guān),這里不多做解釋,后面會(huì)提到,感興趣的同學(xué)可以先看看。
???????同樣也是異步讀+do_read_body()。
2、向客戶(hù)端寫(xiě)
???????大家可能會(huì)疑惑:服務(wù)器往客戶(hù)端寫(xiě)的部分在哪里呢?
???????還記得前面有一步,客戶(hù)端會(huì)加入到聊天室。在join的時(shí)候,服務(wù)器就會(huì)根據(jù)room的內(nèi)容,對(duì)客戶(hù)端進(jìn)行寫(xiě)操作。
前面就是加入到聊天室隊(duì)列的邏輯,真正的寫(xiě)操作在deliver里:
void deliver(const chat_message& msg){bool write_in_progress = !write_msgs_.empty();write_msgs_.push_back(msg);//第一次為空,只有為空的時(shí)候才會(huì)調(diào)用do_write//這里是防止調(diào)用兩次do writeif (!write_in_progress){do_write();}}大家看到do_write和客戶(hù)端的do_write一聯(lián)系,就很好理解了。
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){ //頭部信息寫(xiě)完了,就檢查是不是空的write_msgs_.pop_front();if (!write_msgs_.empty()){ //繼續(xù)寫(xiě)do_write();}}else{room_.leave(shared_from_this());}});}到這里,整個(gè)客戶(hù)端——服務(wù)器 聊天室的主要邏輯功能相信大家都有了一個(gè)大體的認(rèn)識(shí),如果想繼續(xù)深入細(xì)節(jié),可以在github上把代碼下載下來(lái)自己動(dòng)手跑一下,在獲得樂(lè)趣的同時(shí)也可以加深對(duì)代碼的理解。
在后續(xù)的聊天室1.1和聊天室1.2中會(huì)詳細(xì)介紹里面的具體細(xì)節(jié)。
有趣的點(diǎn)
???????聊天室1.0里面比較有趣的點(diǎn)——也就是一開(kāi)始我比較疑惑的點(diǎn)主要就是在異步回調(diào)這里:
???????為什么要用auto self(shared_from_this);呢?
???????回調(diào)的時(shí)候?yàn)槭裁床粫?huì)出現(xiàn)爆棧呢?(因?yàn)轭?lèi)似循環(huán)調(diào)用)
異步循環(huán)調(diào)用為什么不爆棧?
???????要想弄清楚這幾個(gè)問(wèn)題,最先要解決的問(wèn)題就是——回調(diào)函數(shù)究竟是什么?
???????如果按照普通的函數(shù)調(diào)用過(guò)程進(jìn)行思考,每調(diào)用一次函數(shù),會(huì)將函數(shù)調(diào)用處壓棧空間,類(lèi)似下圖所示:(為了表示簡(jiǎn)單就直接用函數(shù)名字代替參數(shù)入棧什么的了)
???????我們以服務(wù)器接收客戶(hù)端消息為例:假如是在一個(gè)線(xiàn)程里面,用同步的思想去考慮,這樣不斷的循環(huán)調(diào)用肯定會(huì)出現(xiàn)爆棧的情況。
???????自然而然的我們會(huì)想到——系統(tǒng)級(jí)的異步,比如說(shuō)linux的epoll是怎么做的呢?交給內(nèi)核去管理,讓內(nèi)核進(jìn)行回調(diào)通知。如果抽象點(diǎn)看:把內(nèi)核看成另一個(gè)線(xiàn)程或者說(shuō)另一個(gè)工作場(chǎng)景,就像我現(xiàn)在遇到問(wèn)題了,找了一個(gè)朋友吧事情交給他做,然后我繼續(xù)做我接下來(lái)的事情。就好像這兩件事工作在不同的線(xiàn)程里面。
???????之后帶著自己的疑惑和自己的思考去請(qǐng)教了一下前輩們,雖然異步的實(shí)現(xiàn)方式遠(yuǎn)沒(méi)有我們想的那么簡(jiǎn)單,但是這種思想是貫穿始終的,為了簡(jiǎn)單起見(jiàn)——我們姑且認(rèn)為是在兩個(gè)線(xiàn)程里面工作的。(前輩說(shuō)具體實(shí)現(xiàn)要看future)
???????帶著這個(gè)思路我們就解決了爆棧的問(wèn)題——當(dāng)運(yùn)行到do_read_header的時(shí)候,start開(kāi)了個(gè)線(xiàn)程給了do_read_header,它自己就執(zhí)行完畢了,函數(shù)返回,自然也就不會(huì)存在爆棧的問(wèn)題。
為什么要使用self(shared_from_this)?
???????其實(shí)要搞清楚這個(gè)需要對(duì)智能指針比較熟悉,在此之前,我們把這個(gè)auto的一條語(yǔ)句給它展開(kāi),或許對(duì)它的理解會(huì)更容易一些。
??????? 還是以服務(wù)器接收客戶(hù)端消息為例:展開(kāi)整條賦值表達(dá)式就是:
??????? 這個(gè)shared_from_this簡(jiǎn)單理解就是智能指針管理的this指針。
??????? 看到這里可能大概明白了這條語(yǔ)句的意思:給這個(gè)對(duì)象(this指向的就是當(dāng)前session對(duì)象)增加一個(gè)引用計(jì)數(shù)。
??????? 但是為什么需要增加一個(gè)引用計(jì)數(shù)呢?
??????? 還記得上面說(shuō)的思想:當(dāng)成兩個(gè)線(xiàn)程去看。也就是說(shuō)當(dāng)do_read_header在運(yùn)行的時(shí)候,可能出現(xiàn)start已經(jīng)運(yùn)行結(jié)束的情況:
???????而start的結(jié)束就意味著do_accept里面的if也結(jié)束了,意味著session的引用計(jì)數(shù)要減去1,假如沒(méi)有別的指針持有這個(gè)對(duì)象,這個(gè)對(duì)象就會(huì)被釋放了。
???????所以還記得do_read_header里面是怎么做的了嗎?
???????lambda表達(dá)式的捕獲是值捕獲,引用計(jì)數(shù)+1: 意味著async_read這個(gè)函數(shù)不結(jié)束,智能指針指向的這個(gè)session對(duì)象就不會(huì)釋放。保證了do_accept生成的chat_session的生命周期。
???????當(dāng)然為什么會(huì)出現(xiàn)這樣的疑惑主要還是因?yàn)閱尉€(xiàn)程的思想根深蒂固,只要日后多接觸多線(xiàn)程異步之類(lèi)的思想,習(xí)慣了以后就很容易理解了,這是個(gè)循序漸進(jìn)的過(guò)程,慢慢來(lái)就好。
聊天室1.0 github地址:
代碼很簡(jiǎn)單,完整代碼我把它放到了github上,后續(xù)我們會(huì)逐漸對(duì)他進(jìn)行更新和迭代,在循序漸進(jìn)中慢慢感受理論與實(shí)踐相結(jié)合的樂(lè)趣。
聊天室1.0
參考文獻(xiàn)
1 b站課程
2 boost-asio網(wǎng)絡(luò)庫(kù)
總結(jié)
以上是生活随笔為你收集整理的c++项目——聊天室——第一节的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 向日葵android自动退出,向日葵An
- 下一篇: C++矩阵求逆