想回家過年…
一、項(xiàng)目前置知識(shí)
1. websocketpp庫
1.1 http1.0/1.1和websocket協(xié)議
1.
a. http協(xié)議在Linux的學(xué)習(xí)部分我們就已經(jīng)學(xué)習(xí)過了,當(dāng)時(shí)http和https是一塊學(xué)的,我們當(dāng)時(shí)其實(shí)已經(jīng)了解了http的大部分知識(shí)內(nèi)容,比如http請(qǐng)求和響應(yīng)的格式,各自的報(bào)頭字段都有哪些,cookie和session機(jī)制,http1.1的長連接策略keep-alive,還有請(qǐng)求方法GET和POST等等知識(shí)內(nèi)容,這么看來http感覺已經(jīng)很優(yōu)秀了,為什么還要有websocket協(xié)議呢?
b. 其實(shí)http有一個(gè)致命的缺點(diǎn),就是無法支持服務(wù)器向客戶端主動(dòng)推送消息,傳統(tǒng)的CS通信方式都是一問一答的,即客戶端向服務(wù)器發(fā)送一個(gè)請(qǐng)求,服務(wù)器向客戶端反饋一個(gè)響應(yīng),而在最傳統(tǒng)的http1.0版本協(xié)議中,客戶端每和服務(wù)器進(jìn)行一次通信都需要建立一條TCP連接,當(dāng)瀏覽器訪問了服務(wù)器上的某個(gè)html網(wǎng)頁時(shí),此時(shí)就會(huì)在應(yīng)用層協(xié)議http的基礎(chǔ)上建立一條短連接,而http短連接其實(shí)就是tcp短鏈接,如果瀏覽器此時(shí)想要訪問web網(wǎng)頁中的其他資源,那就需要重新再向服務(wù)器發(fā)起一次http請(qǐng)求,以獲取到服務(wù)器上的對(duì)應(yīng)資源,此時(shí)原來的http連接就會(huì)自動(dòng)被斷開,然后重新建立一條短連接,這樣的方式非常的難受啊,因?yàn)橛脩粼L問某web資源時(shí),肯定不可能只訪問一個(gè)資源啊,他一定會(huì)向服務(wù)器發(fā)起多個(gè)http請(qǐng)求,獲取訪問多個(gè)web資源,那如果在傳統(tǒng)的http1.0協(xié)議下,就會(huì)頻繁的建立和斷開連接,這會(huì)很浪費(fèi)服務(wù)器的時(shí)間和網(wǎng)絡(luò)帶寬,因?yàn)閔ttp短連接其實(shí)就是tcp短連接,本來tcp是一個(gè)可靠的,高效的,有鏈接的協(xié)議,但結(jié)果http不會(huì)用,雙方通信一次就關(guān)閉掉了,這也太浪費(fèi)了!
c. 所以在http1.0之后,又推出了http1.1協(xié)議,也就是在請(qǐng)求報(bào)頭中添加了一個(gè)字段Connection:keep-alive,也就是http長連接,當(dāng)上層http連接建立成功后,下層的tcp連接不會(huì)在一次通信之后就斷開了,而是會(huì)在一段時(shí)間之后才斷開,在這段時(shí)間里面,雙方都可以使用該連接進(jìn)行資源的請(qǐng)求和獲取,或者是業(yè)務(wù)的請(qǐng)求和處理,確實(shí)是比以前要高效的多了,但http1.1依舊還存在一個(gè)問題,就是他的通信模式還是沒有變化的,也就是一問一答的通信模式,不過他已經(jīng)比原來的http1.0要高效很多了,省去了很多不必要的tcp連接建立和斷開,也減少浪費(fèi)帶寬。
2.
但在實(shí)際的用戶需求中,一問一答這樣的模式是遠(yuǎn)遠(yuǎn)無法適用于大多數(shù)場景的,就拿聊天這樣簡單的功能來說,用戶1是無法主動(dòng)將消息發(fā)送給用戶2的,因?yàn)樗麄儌z處于局域網(wǎng)中,而局域網(wǎng)中的ip地址是不唯一的,所以想要實(shí)現(xiàn)通信則必須借助中間的服務(wù)器角色,用戶1將消息發(fā)送給服務(wù)器,想要讓服務(wù)器將消息發(fā)送給用戶2,但這三臺(tái)機(jī)器應(yīng)用層都使用的是http協(xié)議啊,所以服務(wù)器無法將消息主動(dòng)推送給用戶2,只有說當(dāng)用戶2向服務(wù)器發(fā)送請(qǐng)求,詢問服務(wù)器,現(xiàn)在有沒有給我發(fā)送的消息???服務(wù)器此時(shí)才能將用戶1發(fā)送的消息以response的方式返回給用戶1。
這樣能通信嗎?當(dāng)然是可以的,但他的效率很低,因?yàn)橄胍蛻舳讼胍玫絼e人發(fā)給自己的消息,就必須不斷的輪詢服務(wù)器,看看服務(wù)器上有沒有發(fā)送給我的消息,如果有那就獲取,如果沒有那就繼續(xù)輪詢,這樣的效率非常低!因?yàn)榉?wù)器會(huì)將一部分的資源浪費(fèi)在不斷的回復(fù)輪詢這件事上,同時(shí)也很浪費(fèi)網(wǎng)絡(luò)資源。
所以,除了原來的http協(xié)議外,我們還需要一種能夠支持服務(wù)器向客戶端主動(dòng)推送消息的協(xié)議,這對(duì)服務(wù)器或客戶端來說,是非常重要的事情!
3.
websocket協(xié)議也是基于http協(xié)議的來實(shí)現(xiàn)的,他是網(wǎng)頁端和服務(wù)器保持長連接的一種消息推送機(jī)制。websocket之間的通信和TCP連接之間的通信非常的相似,websocket長連接其實(shí)也就是tcp長連接,即當(dāng)客戶端和服務(wù)器建立websocket長連接之后,雙方就會(huì)一直使用這個(gè)連接進(jìn)行通信,除非某一方主動(dòng)意愿的想要斷開連接,否則其他大部分正常情況連接都是不會(huì)斷開的,所以websocket和tcp是很相似的。
想要建立websocket長連接,其實(shí)還是需要借助http協(xié)議的,只不過在原本的http請(qǐng)求報(bào)頭中多加了一個(gè)額外的字段Upgrade:websocket,這樣就可以完成websocket協(xié)議的切換。
4.
下面是websocket協(xié)議切換的示意圖,需要注意的是,客戶端想和服務(wù)器進(jìn)行通信,包括協(xié)議切換的請(qǐng)求或者是任何的請(qǐng)求,都需要在三次握手建立連接的前提下進(jìn)行。
這里說一個(gè)知識(shí)點(diǎn),三次握手是不允許攜帶任何應(yīng)用層數(shù)據(jù)的(嚴(yán)格來說),原因其實(shí)和防止SYN洪水攻擊非常的相似,之前談?wù)揝YN洪水攻擊是在為什么是三次握手,而不是其他次握手這個(gè)問題(防止SYN洪水攻擊+最小成本驗(yàn)證全雙工通信信道)上討論的,今天這個(gè)問題的原因其實(shí)就是害怕一個(gè)客戶端就把服務(wù)器搞崩掉,如果在第一次握手中攜帶大量應(yīng)用層數(shù)據(jù),則服務(wù)器需要開辟內(nèi)存將收到的數(shù)據(jù)保存起來,并且需要維護(hù)建立好的連接,而此時(shí)客戶端并不認(rèn)為連接建立成功,或者壓根就不給你建立連接,就瘋狂的向服務(wù)器發(fā)送一次握手,并同時(shí)攜帶大量的數(shù)據(jù),這樣就會(huì)極大的消耗服務(wù)器上的資源,最終可能導(dǎo)致服務(wù)器宕機(jī)!而二次握手也是不能攜帶數(shù)據(jù)的,道理和前面的一樣,客戶端只在第一次握手發(fā)送的SYN報(bào)文段中加入大量的數(shù)據(jù),而第二次握手服務(wù)器發(fā)來的SYN報(bào)文段,客戶端也是可以選擇丟棄的,這么一來無論是一次握手,還是二次握手都是不允許攜帶數(shù)據(jù)的,但第三次握手其實(shí)是可以攜帶數(shù)據(jù)的,因?yàn)榇藭r(shí)客戶端已經(jīng)認(rèn)為連接建立成功了,雙方的消耗是同等的,而服務(wù)器的配置又比客戶端高,所以你單主機(jī)想要搞掉服務(wù)器是不大可能了。
但實(shí)際通信中,第三次握手也是不攜帶數(shù)據(jù)的,等到雙方連接都建立成功后,此時(shí)再攜帶數(shù)據(jù)看起來更合理一些,不過你要是強(qiáng)行想在第三次握手中攜帶數(shù)據(jù)也是可以的,只不過實(shí)際使用的時(shí)候大部分情況不會(huì)這么做。
5.
等到三次握手成功之后,雙方已經(jīng)建立好TCP連接了,此時(shí)客戶端只要發(fā)送一個(gè)攜帶Upgrade:Websocket的http請(qǐng)求即可,然后服務(wù)器返回一個(gè)101響應(yīng)狀態(tài)碼以及switch protocol的狀態(tài)碼描述,再配一個(gè)http/1.1組成一個(gè)狀態(tài)行,添加上其他的響應(yīng)報(bào)頭組織成一個(gè)響應(yīng)報(bào)文發(fā)送回客戶端,此時(shí)就可以完成websocket協(xié)議的切換。
后續(xù)CS雙方就可以使用websocket長連接進(jìn)行通信了,任何一方都可以主動(dòng)的給對(duì)方推送消息!非常的方便
三次握手會(huì)攜帶應(yīng)用層數(shù)據(jù)嗎?
1.2 websocketpp庫接口的前置認(rèn)識(shí)
1.
由于本項(xiàng)目使用了http和websocket兩種應(yīng)用層協(xié)議,而websocketpp這個(gè)網(wǎng)絡(luò)庫恰好支持了這兩種協(xié)議,所以我們使用了該庫作為本項(xiàng)目的依賴庫來實(shí)現(xiàn)http/websocket服務(wù)器。
2.
connection_hdl相當(dāng)于websocket連接的句柄,server是endpoint的子類,server也就是我們實(shí)例化服務(wù)器對(duì)象的一個(gè)類,所以想要搭建服務(wù)器必須了解endpoint里面聲明了哪些接口,timer_ptr是一個(gè)定時(shí)器對(duì)象指針,配合set_timer這個(gè)接口來使用,可以在服務(wù)器內(nèi)部設(shè)置定時(shí)任務(wù),這個(gè)接口在我們后面的session模塊中會(huì)用到,connection_ptr是websocket連接的智能指針管理對(duì)象,后面的各個(gè)通信模塊都會(huì)大量用到這個(gè)智能指針,connection類就是該對(duì)象所屬類,通常用來進(jìn)行http響應(yīng)的回復(fù),http請(qǐng)求內(nèi)容的獲取,以及websocket消息的推送,這個(gè)指針對(duì)象非常的重要。message_ptr是一個(gè)專門用來獲取websocket請(qǐng)求消息的指針對(duì)象,可以通過get_payload獲取websocket請(qǐng)求的有效載荷數(shù)據(jù)。
還有四個(gè)指定事件的回調(diào)函數(shù),當(dāng)服務(wù)器上特定事件被觸發(fā)時(shí),服務(wù)器對(duì)象會(huì)自動(dòng)調(diào)用這四個(gè)回調(diào)函數(shù),而這幾個(gè)回調(diào)函數(shù)的內(nèi)容是由程序員來編寫的,實(shí)現(xiàn)服務(wù)器對(duì)業(yè)務(wù)的處理邏輯,這四個(gè)函數(shù)中只有set_http_handler是設(shè)置http請(qǐng)求的回調(diào)函數(shù),其他三個(gè)都是用于處理websocket連接上消息的回調(diào)函數(shù)
3.
下面是connection類的實(shí)現(xiàn),從接口對(duì)應(yīng)的協(xié)議來劃分可以分為兩類,一類是http,一類是websocket,只有send一個(gè)接口是在websocket連接上發(fā)送消息的,其他的接口全是和http有關(guān)的。
4.
下面的這些都是websocketpp定義的一些日志等級(jí),http響應(yīng)狀態(tài)碼,websocket發(fā)送數(shù)據(jù)的類型等,日志這塊我們到時(shí)候?qū)戫?xiàng)目的時(shí)候會(huì)自己實(shí)現(xiàn),所以會(huì)將日志設(shè)置為none,表示禁止websocketpp打印所有日志。
至于websocket發(fā)送數(shù)據(jù)的類型,我們?cè)趯戫?xiàng)目的時(shí)候也不會(huì)做改動(dòng),直接使用text類型,發(fā)送json風(fēng)格的字符串響應(yīng)。
1.3 搭建一個(gè)http/websocket服務(wù)器
1.
上面說了那么多肯定沒啥用,干說咋可能學(xué)會(huì)呢,下面還是通過搭建一個(gè)服務(wù)器來熟悉websocketpp庫中接口的使用吧。
2.
搭建服務(wù)器其實(shí)可以分為兩個(gè)部分,一個(gè)是四種回調(diào)函數(shù)的實(shí)現(xiàn),一個(gè)是調(diào)用wssvr對(duì)象進(jìn)行服務(wù)器的各項(xiàng)功能初始化,第二個(gè)部分隱含了諸多的linux網(wǎng)絡(luò)的知識(shí)細(xì)節(jié),例如當(dāng)服務(wù)器宕機(jī)后立馬重啟依舊還可以綁定原來的端口號(hào),通過調(diào)用set_reuse_addr來實(shí)現(xiàn),不使用websocketpp庫所提供的日志輸出函數(shù),則可以通過調(diào)用接口set_access_channels(),傳遞一個(gè)none來實(shí)現(xiàn)。
3.
這里需要著重說一下bind的用法,bind有兩種用法。
一種是綁死參數(shù),這樣的用法下,bind生成的對(duì)象在傳參給包裝器時(shí),是不會(huì)影響類型的,也就是說你可以使用bind來傳遞任意的可調(diào)用對(duì)象給包裝器,而無需關(guān)心包裝器的類型是什么,bind綁定的可調(diào)用對(duì)象的類型又是什么,你想傳什么傳什么,在這種情況下,bind不影響傳遞參數(shù)時(shí),參數(shù)的類型是什么,只影響實(shí)際調(diào)用時(shí)的參數(shù)是什么,實(shí)際調(diào)用時(shí)候的傳參其實(shí)就是綁死的參數(shù)。
(這樣的用法比較少見,常見于某些API的包裝器參數(shù)功能無法滿足我們的需求,我們此時(shí)想讓這個(gè)包裝器在調(diào)用時(shí)按照我們所實(shí)現(xiàn)的一個(gè)函數(shù)去執(zhí)行,那么此時(shí)就可以采用綁死參數(shù)的方式來使用bind)
另一種是預(yù)留參數(shù)位置,等到bind生成的可調(diào)用對(duì)象被調(diào)用時(shí),再去傳參,bind提前用占位符來預(yù)留參數(shù)的位置。
下面的用例代碼就可以很好的說明,bind生成的可調(diào)用對(duì)象的類型是完全適配包裝器的,不管包裝器的類型是什么,bind生成的可調(diào)用對(duì)象都可以傳過去,并且無論最后你怎么給包裝器對(duì)象傳遞參數(shù),這都是徒勞的,因?yàn)閎ind已經(jīng)將可調(diào)用對(duì)象print的參數(shù)給綁死了,callback實(shí)際調(diào)用的就是print(“rygttm”)這個(gè)函數(shù)。
另一種用法就是下面的四個(gè)回調(diào)函數(shù)的設(shè)置,這幾個(gè)回調(diào)函數(shù)未來其實(shí)是由服務(wù)器自己去調(diào)用的,而不是我們來調(diào)用,當(dāng)服務(wù)器收到http請(qǐng)求,則服務(wù)器就會(huì)自動(dòng)調(diào)用我們所實(shí)現(xiàn)的http_handler類型的回調(diào)函數(shù),當(dāng)服務(wù)器收到websocket握手的請(qǐng)求,則在握手建立好之后會(huì)調(diào)用我們所實(shí)現(xiàn)的open_handler類型的回調(diào)函數(shù),其他兩個(gè)類型也是如此,這幾個(gè)類型都是包裝器類型重定義的,但在回調(diào)函數(shù)種,我們想用服務(wù)器類里面的某些接口來實(shí)現(xiàn)簡單的業(yè)務(wù)處理,所以我們希望把wssvr對(duì)象也傳到四個(gè)回調(diào)函數(shù)里面,而此時(shí)的做法就是通過bind來綁定部分參數(shù),其余服務(wù)器自己調(diào)用時(shí)傳遞的參數(shù)我們通過占位符給預(yù)留出來,讓服務(wù)器自己去傳參,我們不操這個(gè)心,這就是bind的第二個(gè)用法。
4.
這四個(gè)接口中重點(diǎn)實(shí)現(xiàn)http_callback和wsmessage_callback,在http_callback里面,我們打印一下http請(qǐng)求的幾個(gè)重要信息,然后給客戶端返回一個(gè)簡單的html頁面。
值得注意的是,http響應(yīng)的返回和websocket消息的發(fā)送所調(diào)用的API是不一樣的,我們只需要通過conn這個(gè)連接智能指針管理對(duì)象,調(diào)用set_body設(shè)置好響應(yīng)正文,調(diào)用append_header設(shè)置好響應(yīng)頭部字段,調(diào)用set_status設(shè)置好響應(yīng)狀態(tài)碼,然后服務(wù)器就會(huì)自動(dòng)構(gòu)建一個(gè)包括狀態(tài)行,響應(yīng)報(bào)頭,空行,響應(yīng)正文的完整的http響應(yīng)信息返回給客戶端!
而對(duì)于websocket消息的發(fā)送我們也是通過conn這個(gè)智能指針來發(fā)送的,發(fā)送的方式非常的簡單,只要調(diào)用send接口即可,第一個(gè)參數(shù)是要發(fā)送的websocket有效載荷數(shù)據(jù),第二個(gè)參數(shù)缺省值默認(rèn)是文本類型,我們可以傳也可以不傳這個(gè)參數(shù)。resp正文的內(nèi)容其實(shí)就是客戶端發(fā)送的消息,我們服務(wù)器這里做一個(gè)消息的回顯,回顯給客戶端,同時(shí)也把消息打印到服務(wù)器上看看消息內(nèi)容是什么,發(fā)送websocket數(shù)據(jù),可以看到調(diào)用的正好也是send接口。
5.
我們自己寫完服務(wù)器的四個(gè)回調(diào)函數(shù)的邏輯之后,接下來的三個(gè)接口應(yīng)該是不陌生的,其實(shí)就是監(jiān)聽端口號(hào),看是否有客戶端向我們服務(wù)器綁定的端口號(hào)發(fā)起了連接請(qǐng)求,如果有那就將三次握手后的連接加入到內(nèi)核監(jiān)聽隊(duì)列中,這個(gè)監(jiān)聽隊(duì)列的長度一般是5,我們也可以自己設(shè)置。
服務(wù)器調(diào)用start_accept就是將內(nèi)核監(jiān)聽隊(duì)列中已經(jīng)完成三次握手的連接拿上來,通過這個(gè)連接服務(wù)器就可以和客戶端進(jìn)行通信了,所以三次握手的過程和accept系統(tǒng)調(diào)用沒有任何關(guān)系,三次握手的過程是在listen過程中進(jìn)行的。
最后只要調(diào)用websocketpp庫中的server類中的接口run,就可以將服務(wù)器運(yùn)行起來了,到此就完成了wsserver.cc代碼的編寫。
6.
光實(shí)現(xiàn)一個(gè)服務(wù)端肯定還是不行的,http客戶端我們可以不用實(shí)現(xiàn),直接使用瀏覽器向服務(wù)器發(fā)起http請(qǐng)求就可以解決,但websocket客戶端必須由我們來實(shí)現(xiàn)了,我們需要自己編寫一個(gè)wsclient.html的前端頁面來充當(dāng)客戶端,通過在瀏覽器打開這個(gè)頁面來向服務(wù)器發(fā)起websocket連接建立的請(qǐng)求。
實(shí)現(xiàn)客戶端主要也是分為兩個(gè)部分,先通過new WebSocket向指定服務(wù)器發(fā)起websocket連接握手,當(dāng)服務(wù)器收到連接請(qǐng)求后,服務(wù)器會(huì)返回一個(gè)握手代表雙方websocket長連接建立成功,前端這邊會(huì)有一個(gè)連接的句柄,也就是let定義的ws_hdl,通過這個(gè)句柄來實(shí)現(xiàn)客戶端和服務(wù)器的websocket通訊,類似于服務(wù)器的四個(gè)回調(diào)函數(shù),前端這里也有ws_hdl被創(chuàng)建成功后的四個(gè)回調(diào)函數(shù),在onmessage回調(diào)函數(shù)的參數(shù)中,是有一個(gè)事件evt參數(shù)的,這個(gè)evt保存的是服務(wù)器返回的一個(gè)普通字符串,通過.data的方式就可以訪問到里面的內(nèi)容了,如果服務(wù)器返回的是json序列化之后的字符串,則我們需要先對(duì)e.data做json格式的解析,然后才能訪問到里面的內(nèi)容,但今天我們只是搭建一個(gè)樣例服務(wù)器,所以就不搞序列化反序列化那一套了,能夠?qū)崿F(xiàn)雙方的通信就可以了。
前端這里實(shí)現(xiàn)了一個(gè)輸入框和一個(gè)提交按鈕,我們同時(shí)為這個(gè)提交按鈕添加了一個(gè)點(diǎn)擊事件,用于向服務(wù)器發(fā)送,輸入框中用戶輸入的消息內(nèi)容,服務(wù)器會(huì)將我們發(fā)送的消息重新作為響應(yīng)返回到前端這里,前端的onmessage收到響應(yīng)事件后,會(huì)將消息內(nèi)容通過console.log打印到開發(fā)者工具的控制臺(tái)上,我們到時(shí)候通過fn+f12打開控制臺(tái)就可以看到這些日志消息了。前端這邊除了將消息以日志方式打印出來,還做了另一步操作,其實(shí)就是將輸入框中的消息內(nèi)容清空,通過id來獲取輸入框,然后將里面的值置為空串即可。
6.
在觀察實(shí)驗(yàn)現(xiàn)象前,需要說明一點(diǎn),我們今天所實(shí)現(xiàn)的前端頁面雖然確實(shí)是在linux機(jī)器上,但他不在wsserver里面,因?yàn)槲覀儧]有在里面搞一個(gè)web根目錄,將前端頁面放到web根目錄中,所以想要在瀏覽器中打開前端頁面,只能先將html文件放到win機(jī)器本地上,然后通過打開瀏覽器來訪問websocket服務(wù)器,以此來實(shí)現(xiàn)客戶端和服務(wù)器通信。
不過不用擔(dān)心,后面實(shí)現(xiàn)項(xiàng)目的時(shí)候,我們會(huì)將前端資源放到web根目錄下,瀏覽器直接請(qǐng)求服務(wù)器上的web資源即可,而無需以本地打開html文件的方式來與服務(wù)器進(jìn)行通訊。
通過下面的CS通信可以看到,服務(wù)器和客戶端成功以websocket連接的方式實(shí)現(xiàn)了通訊,這個(gè)前后端通信做的確實(shí)比較簡陋,等后面實(shí)現(xiàn)項(xiàng)目的時(shí)候,CS之間的交互會(huì)變得很多,到時(shí)候就可以更熟練的使用websocket進(jìn)行通訊了。
2. jsoncpp庫
1.
在網(wǎng)絡(luò)通信中,由于傳輸?shù)臄?shù)據(jù)往往是一個(gè)較大的集合,這個(gè)集合中會(huì)容納多種不同類型的數(shù)據(jù),所以通信雙方往往要對(duì)發(fā)送的數(shù)據(jù)做整合封裝和拆解,這兩個(gè)步驟用專業(yè)一點(diǎn)的詞匯來描述就是序列化和反序列化,雙方使用同一種方案來進(jìn)行序列化和反序列化,保證能夠?qū)?shù)據(jù)進(jìn)行合理正確的解析以及對(duì)數(shù)據(jù)打包發(fā)送。
這樣的序列化和反序列化方案,其實(shí)我們可以自己做,但一般我們不自己寫,因?yàn)閼?yīng)用層已經(jīng)有大佬幫我們寫好了,常用的例如xml,json,protobuf等等,本項(xiàng)目中用到的就是json這樣的序列化方案。
2.
json這個(gè)類重載了=和[ ]操作,這使得構(gòu)造一個(gè)包含多種數(shù)據(jù)類型的json對(duì)象變得非常的方便,使用json對(duì)象時(shí),只需要通過[ ]來使用即可,可以傳遞一個(gè)數(shù)組的下標(biāo),一個(gè)字符串等等。
StreamWriterBuilder這個(gè)類其實(shí)就是一個(gè)工廠類,通過這個(gè)工廠類能夠生產(chǎn)出一個(gè)StreamWriter對(duì)象,通過這個(gè)StreamWriter對(duì)象,我們就能夠進(jìn)行json格式的序列化。
json在進(jìn)行序列化時(shí),所調(diào)用的接口write,即將一個(gè)json對(duì)象序列化為一個(gè)json格式的字符串,然后這個(gè)字符串會(huì)被放到輸出流對(duì)象sout里面,我們一般傳遞的都是stringstream的對(duì)象,這個(gè)stringstream對(duì)象內(nèi)部有一個(gè)str()接口,通過這個(gè)接口我們就可以拿到string類型的可以發(fā)送到網(wǎng)絡(luò)中的字符串了。
CharReaderBuilder也是一個(gè)工廠類,通過這個(gè)工廠類能夠生產(chǎn)出一個(gè)CharReader對(duì)象,通過這個(gè)CharReader對(duì)象,就能夠進(jìn)行json格式字符串的反序列化。
json在反序列化時(shí),是通過parse接口來將json格式的字符串解析反序列化到Value 類型的root對(duì)象中,只不過我們需要傳入這個(gè)json格式的字符串的起始地址和末尾地址。
3.
下面是一個(gè)json序列化的案例代碼,幫助我們進(jìn)行基本的序列化實(shí)現(xiàn),首先定義一個(gè)Json::Value對(duì)象root,然后通過=和[ ]運(yùn)算符,向root里面填充需要發(fā)送到對(duì)端的字段,比如添加const string類型的字符串,int類型的整數(shù),向root中添加一個(gè)浮點(diǎn)數(shù)數(shù)組,數(shù)組的添加我們不能使用=運(yùn)算符,需要借助Json::Value類里面的append接口來實(shí)現(xiàn),不斷的調(diào)用append接口,即向數(shù)組中不斷的添加元素。
真正進(jìn)行序列化時(shí),我們需要先生產(chǎn)一個(gè)StreamWriter對(duì)象,然后調(diào)用write接口,將root和ss兩個(gè)對(duì)象傳遞進(jìn)去,調(diào)用成功后ss里面的str()就會(huì)返回一個(gè)string字符串,這個(gè)字符串就可以直接發(fā)送到網(wǎng)絡(luò)里面,通過網(wǎng)絡(luò)傳輸?shù)綄?duì)端主機(jī),別忘了釋放掉sw這個(gè)StreamWriter對(duì)象,因?yàn)檫@個(gè)對(duì)象的內(nèi)存是動(dòng)態(tài)開辟出來的,用完了,要記得還給操作系統(tǒng),否則會(huì)造成內(nèi)存泄漏。
其實(shí)在上面的序列化代碼里面,隱含了一部分C++的語法知識(shí),那就是單參數(shù)構(gòu)造,從庫文件里面我們可以看到,他只重載了一些基本類型到Json::Value類型的構(gòu)造函數(shù),為什么上面的代碼中能夠可以講18這個(gè)整形直接賦值給root呢?其實(shí)就是因?yàn)閹炖锩鎸?shí)現(xiàn)了下面的這些單參數(shù)構(gòu)造函數(shù),所謂的賦值可以細(xì)分為先通過參數(shù)構(gòu)造出一個(gè)value對(duì)象,然后拿著這個(gè)對(duì)象來進(jìn)行賦值給root對(duì)象,這個(gè)賦值的接口是庫里面實(shí)現(xiàn)了的,Value &operator=(const Value &other);,構(gòu)造出來的臨時(shí)對(duì)象剛好是一個(gè)常對(duì)象,正好可以傳遞。
4.
下面是反序列化的過程,首先實(shí)例化一個(gè)工廠類對(duì)象,通過這個(gè)對(duì)象生產(chǎn)出一個(gè)CharReader對(duì)象,然后調(diào)用parse接口進(jìn)行json格式字符串的反序列化,解析的過程可能會(huì)發(fā)生錯(cuò)誤(90%的正常情況下不會(huì)發(fā)生錯(cuò)誤),所以可以傳一個(gè)輸出型參數(shù)err,解析成功之后,root對(duì)象就是原生的發(fā)送方想要發(fā)送給我們的內(nèi)容了。
我們可以通過[“xxx”]來拿到對(duì)應(yīng)的value對(duì)象,但需要注意的是,如果想要拿到里面的值,我們還需要做一步類型轉(zhuǎn)換,因?yàn)閖son的[ ]重載函數(shù)返回的是jsonvalue對(duì)象,而不是我們想要的內(nèi)置類型,所以還需要進(jìn)行asInt,asCString,asFloat等接口的幫助,我們才能訪問到里面具體的值。
與序列化相同的是,最后別忘記釋放動(dòng)態(tài)開辟的內(nèi)存,否則會(huì)造成內(nèi)存泄露。
5.
調(diào)用我們實(shí)現(xiàn)的上面兩個(gè)函數(shù)之后,從打印結(jié)果可以看出,jsonvalue對(duì)象其實(shí)是一個(gè){}構(gòu)成的具有特定格式的一種對(duì)象,比如添加了換行符,制表符,包含的內(nèi)容采用了key : value的形式進(jìn)行組織,對(duì)于數(shù)組類型的數(shù)據(jù),value采用了[, , ,]的格式進(jìn)行組織。
我們反序列化上面json格式的字符串之后,打印內(nèi)容就是簡單的逐行打印。
3. mysqlclient庫
1.
由于本項(xiàng)目使用的是mysql數(shù)據(jù)庫來存儲(chǔ)玩家信息,所以在項(xiàng)目前置知識(shí)這里,我們還需要了解如何通過C風(fēng)格的API接口來操縱數(shù)據(jù)庫。
1> 首先需要初始化一個(gè)mysql的句柄,這個(gè)句柄是很常用的一個(gè)概念,像文件描述符,套接字,文件指針這些都可以稱之為句柄,你可以把他理解為一個(gè)魔法棒的存在(沒辦法這個(gè)太不好描述了),我們想要做某件事不能直接去做,而是需要借助句柄去做,比如你網(wǎng)絡(luò)通信,雙方能直接通信嗎?難道都用嗓子喊一聲?這肯定是不行的,在代碼層面上,我們就是通過socket套接字來完成通信的,比如對(duì)文件進(jìn)行讀取,寫入等操作,你是直接對(duì)硬盤上的某個(gè)文件操作嗎?其實(shí)不是的,我們是要在代碼層面上通過文件指針來完成這樣的操作的,mysql也是一樣的,在代碼層面上我們需要一個(gè)指針對(duì)象,通過這個(gè)指針對(duì)象來對(duì)數(shù)據(jù)庫進(jìn)行增刪查改,這個(gè)指針對(duì)象就是mysqlclient庫里面定義出來的MYSQL類型,后續(xù)所有的對(duì)數(shù)據(jù)庫的操作,都是通過這個(gè)類型的指針來完成的。
2> 初始化好句柄之后,下一步就是連接數(shù)據(jù)庫,這個(gè)句柄一定是要有操作對(duì)象的,沒有操作對(duì)象還玩什么啊,在數(shù)據(jù)庫這里,句柄的操作對(duì)象就是database,在文件中,操作對(duì)象就是文件,在網(wǎng)絡(luò)通訊中,操作對(duì)象就是socket連接,在今天的websocket協(xié)議通訊中,操作對(duì)象那就是websocket連接,道理是類似的。連接數(shù)據(jù)庫需要指定mysql所在的主機(jī)ip地址,mysqld服務(wù)的端口號(hào),database的名字,登錄數(shù)據(jù)庫服務(wù)的用戶名以及密碼等,mysql這樣的服務(wù)為了保證安全性,是不允許用戶跨網(wǎng)絡(luò)遠(yuǎn)程登錄的,必須要求在本地進(jìn)行登錄,所以ip地址就是我的云服務(wù)器本身的ip地址,那就是本地環(huán)回地址,至于端口號(hào),這個(gè)我們可以自己在mysql的配置文件中設(shè)置,如果沒有設(shè)置過的話,則默認(rèn)就是3306端口。當(dāng)然連接也有失敗的可能,在編寫項(xiàng)目類的代碼時(shí),日志輸出錯(cuò)誤信息是非常重要的一種調(diào)試手段,所以編寫用例代碼我們也延續(xù)這樣良好的代碼風(fēng)格,做好差錯(cuò)處理,因?yàn)閙ysql_real_connect接口是有可能調(diào)用失敗的,當(dāng)失敗時(shí),我們要在服務(wù)器上輸出錯(cuò)誤信息,確保后期好定位代碼中的錯(cuò)誤。
3> 連接數(shù)據(jù)庫成功之后,下一步就是設(shè)置字符集,客戶端和mysqld服務(wù)端要保證字符集是一致的,否則我們編寫的sql語句都有可能被服務(wù)端識(shí)別錯(cuò)誤,導(dǎo)致sql語句無法正常執(zhí)行,服務(wù)端默認(rèn)的編碼格式是utf8的,所以我們?cè)O(shè)置客戶端的編碼格式也是utf8,保證雙方是一致的編碼格式
4> 選擇要操作的數(shù)據(jù)庫,這個(gè)接口其實(shí)是比較雞肋的,因?yàn)椴僮鲾?shù)據(jù)庫的信息我們?cè)缭谡{(diào)用mysql_real_connect時(shí)就填充好了,所以這個(gè)接口我們就不調(diào)用了,什么都不做
2.
5> 接下來就是讓數(shù)據(jù)庫執(zhí)行對(duì)應(yīng)的sql語句了,sql語句共4類,只有select的執(zhí)行邏輯是不一樣的,因?yàn)閟elect需要把數(shù)據(jù)庫中查詢顯示到的信息展示在我們的終端上,而其他的更新,刪除,插入語句是不需要回顯的,執(zhí)行成功就是成功了
6> 針對(duì)select語句,MySQL也提供了對(duì)應(yīng)的API,例如mysql_store_result就是用來保存select語句查詢結(jié)果的,我們需要自己定義一個(gè)MYSQL_RES類型的指針,用來指向堆上mysql_store_result幫我們開辟好的一塊內(nèi)存,這塊內(nèi)存就是查詢結(jié)果。
mysql_num_rows用來獲取查詢結(jié)果中的條數(shù),mysql_num_fields用來獲取查詢結(jié)果中的列數(shù),因?yàn)镸ySQL的存儲(chǔ)格式是行列式的,所以就需要這兩個(gè)接口來獲取行數(shù)和列數(shù)。
在擁有res查詢結(jié)果和結(jié)果集的行數(shù)和列數(shù)之后,我們就可以遍歷結(jié)果集,將select查詢結(jié)果顯示到代碼終端上了,mysql_fetch_row是一個(gè)返回?cái)?shù)組的接口,你把res傳給他,他會(huì)依次逐行返回每行的結(jié)果,每行的結(jié)果就相當(dāng)于一個(gè)char**的數(shù)組,mysql_fetch_row會(huì)給我們返回這個(gè)數(shù)組的首地址,通過這個(gè)首地址+下標(biāo)索引,就可以拿到每行中所有的列字段值了。
7> 上面sql語句的執(zhí)行完畢之后,如果有查詢語句的話,千萬不要忘記釋放結(jié)果集,因?yàn)閞es這個(gè)指針指向的內(nèi)存是mysql_store_result幫我們動(dòng)態(tài)開辟出來的,所以一定要調(diào)用mysql_free_result來釋放結(jié)果集。最后我們也要釋放句柄,因?yàn)檫@個(gè)句柄管理的內(nèi)存也是mysql_init幫我們動(dòng)態(tài)開辟出來的,如果不釋放則會(huì)內(nèi)存泄露。
二、 項(xiàng)目設(shè)計(jì)
1. 項(xiàng)目模塊劃分
1.
項(xiàng)目總體其實(shí)可以劃分為三個(gè)模塊,一個(gè)是數(shù)據(jù)管理模塊,也就是進(jìn)行用戶信息的注冊(cè),存儲(chǔ)用戶的對(duì)戰(zhàn)信息等等,例如用戶名,密碼,總戰(zhàn)斗場次,勝利場次,天梯分?jǐn)?shù)等等信息都是靠數(shù)據(jù)管理模塊來維護(hù)的。
另一個(gè)是前端頁面模塊,這個(gè)模塊也是很重要的,當(dāng)前端頁面被瀏覽器獲取并運(yùn)行起來時(shí),他就是用戶直接接觸的一個(gè)模塊,用戶在頁面里進(jìn)行的所有操作,其實(shí)都是一個(gè)業(yè)務(wù)請(qǐng)求,這些業(yè)務(wù)請(qǐng)求都會(huì)被發(fā)送到服務(wù)器上,由服務(wù)器來對(duì)這些請(qǐng)求進(jìn)行業(yè)務(wù)邏輯處理,客戶端可能產(chǎn)生的業(yè)務(wù)請(qǐng)求有:register.html頁面的獲取,獲取好頁面后,用戶會(huì)輸入自己的用戶名和密碼,然后點(diǎn)擊提交按鈕進(jìn)行用戶的注冊(cè),點(diǎn)擊按鈕之后,注冊(cè)的請(qǐng)求就會(huì)被發(fā)送給服務(wù)器,服務(wù)器會(huì)通過數(shù)據(jù)管理模塊來判斷這個(gè)用戶名是否已經(jīng)存在,如果存在,則說明注冊(cè)請(qǐng)求失敗,服務(wù)器返回一個(gè)失敗的響應(yīng),如果注冊(cè)成功,則服務(wù)器返回一個(gè)login.html,用戶面前就是展示成登錄的頁面了,此時(shí)用戶就又可以輸入用戶名+密碼,點(diǎn)擊提交按鈕進(jìn)行登錄,當(dāng)?shù)卿浀恼?qǐng)求被發(fā)送到服務(wù)器后,服務(wù)器會(huì)檢驗(yàn)用戶是否存在,如果存在則判斷用戶名和密碼是否正確,如果正確說明登錄成功,此時(shí)應(yīng)該向用戶展示游戲大廳game_hall.html的頁面,進(jìn)入游戲大廳后,客戶端還要與服務(wù)器建立長連接,進(jìn)行對(duì)戰(zhàn)匹配的請(qǐng)求,如果對(duì)戰(zhàn)匹配成功,則還要跳轉(zhuǎn)到游戲房間頁面,在游戲房間中還要有下棋聊天等業(yè)務(wù)請(qǐng)求…
最后一個(gè)模塊就是項(xiàng)目的主體,也就是業(yè)務(wù)處理模塊,通過上面的前端模塊的分析,大概得有10多個(gè)業(yè)務(wù)請(qǐng)求吧,所以我們的服務(wù)器除了要能和客戶端進(jìn)行通信以外,還要能夠正確處理這些請(qǐng)求,這些處理的邏輯我們統(tǒng)稱為業(yè)務(wù)處理模塊。
下面是玩家用戶玩游戲的整個(gè)邏輯流程圖,值得注意的是,當(dāng)頁面切換時(shí),瀏覽器會(huì)主動(dòng)將原來的websocket連接斷開,以此來確保資源的釋放和網(wǎng)絡(luò)連接的正常關(guān)閉,所以當(dāng)頁面從游戲大廳跳轉(zhuǎn)到游戲房間時(shí),需要重新建立websocket連接,因?yàn)樵瓉淼倪B接已經(jīng)斷開了。
2.
但由于業(yè)務(wù)處理模塊非常的繁雜,所以業(yè)務(wù)處理模塊我們還要進(jìn)行細(xì)分,細(xì)分到每個(gè)子模塊功能的具體實(shí)現(xiàn)。
總共包括六個(gè)模塊的實(shí)現(xiàn),數(shù)據(jù)管理,session管理,在線用戶管理,匹配隊(duì)列管理,游戲房間管理,最后封裝實(shí)現(xiàn)服務(wù)器模塊。每個(gè)管理模塊實(shí)現(xiàn)的原因,以及其中的細(xì)節(jié),我們都放到每個(gè)模塊中進(jìn)行講解,這里先預(yù)熱一下,知道項(xiàng)目大概都實(shí)現(xiàn)了什么。
2. 實(shí)用工具類模塊
2.1 日志宏封裝
1.
由于在實(shí)現(xiàn)項(xiàng)目的時(shí)候,如果某些接口調(diào)用,或者邏輯有問題總是會(huì)進(jìn)行日志打印,以此來幫助我們進(jìn)行代碼的調(diào)試來定位錯(cuò)誤,所以為了方便后面進(jìn)行日志的輸出,我們這里封裝一個(gè)日志宏,通過宏函數(shù)來進(jìn)行調(diào)試信息或錯(cuò)誤信息的打印。
2.
time是一個(gè)用于獲取時(shí)間戳的一個(gè)函數(shù),即從1970年1月1日到現(xiàn)在過了多少秒,然后返回一個(gè)time_t類型的對(duì)象。
3.
localtime函數(shù)用于將time_t類型的對(duì)象轉(zhuǎn)換成一個(gè)結(jié)構(gòu)體類型struct tm,在這個(gè)結(jié)構(gòu)體內(nèi)部包含了許多的時(shí)間字段信息,例如秒,分,時(shí),天,月,年
4.
strftime函數(shù)用于將struct tm類型的對(duì)象指針進(jìn)行格式化輸出,將格式化后的內(nèi)容放到s緩沖區(qū)里面。格式化的形式有很多,我們就使用%H,M,S就可以了,分別代表當(dāng)前的時(shí)分秒。
5.
所以,通過上面三個(gè)函數(shù),我們就可以將當(dāng)前的時(shí)間信息輸出到一個(gè)char buffer緩沖區(qū)里面,但日志信息光有時(shí)間還是不夠的,還要有輸出的內(nèi)容,而C99恰好引入了新特性,允許宏中定義可變參數(shù),也就是. . .(點(diǎn)點(diǎn)點(diǎn))代表可變參數(shù),所以一個(gè)宏函數(shù)的實(shí)現(xiàn),只需要兩個(gè)參數(shù)就可以了,一個(gè)是format,代表格式化的字符串,另一個(gè)是. . . 代表格式化的字符串中等待傳遞的參數(shù)。
最后在調(diào)用fprintf,將格式化后的字符串輸出到顯示器文件上,也就是打印到屏幕終端上,在fprintf的第二個(gè)參數(shù)中可以看到,我們好像寫了三個(gè)字符串啊,以前我們使用printf的時(shí)候,好像只用到了一個(gè)字符串啊,這樣符合語法嗎?其實(shí)是沒問題的,在ANSI C標(biāo)準(zhǔn)中規(guī)定,在可變參數(shù)中,如果兩個(gè)常量字符串之間沒有逗號(hào)隔開的話,則這幾個(gè)常量字符串會(huì)自動(dòng)連接。第一個(gè)字符串中的第一個(gè)參數(shù),其實(shí)就是格式化輸出到buffer里面的時(shí)間信息,包含時(shí)分秒,第二個(gè)參數(shù)是預(yù)定義出來的宏__FILE__表示日志輸出所在的文件,第三個(gè)參數(shù)是__LINE__表示是文件中的第幾行輸出的內(nèi)容,format是調(diào)用LOG時(shí),調(diào)用者進(jìn)行的可變參數(shù)的控制,對(duì)應(yīng)傳遞的參數(shù)會(huì)傳給. . . ,我們用__VA_ARGS__就可以接收外部調(diào)用傳進(jìn)來的可變參數(shù)。
為什么要加一個(gè)##呢?主要是因?yàn)檎{(diào)用的時(shí)候,又可能只是簡單打印一串消息而已,不會(huì)傳可變參數(shù)進(jìn)來,那么此時(shí)__VA_ARGS__就是未定義的,調(diào)用fprintf就會(huì)出錯(cuò),而##的作用就是讓__VA_ARGS__和前面的__LINE__宏參數(shù)合并,當(dāng)調(diào)用者不傳可變參數(shù)的時(shí)候,LOG宏函數(shù)此時(shí)也不會(huì)出錯(cuò),因?yàn)橄喈?dāng)于沒有__VA_ARGS__這個(gè)參數(shù)。
6.
但是光有上面的宏函數(shù)還差點(diǎn)意思,日志宏應(yīng)該還要有日志等級(jí)的分類,例如normal debug error這樣的等級(jí),所以我們可以預(yù)定義出來一個(gè)默認(rèn)的日志等級(jí),表示只輸出當(dāng)前等級(jí)往上的所有等級(jí)的日志消息,只需要在原來的LOG里面多加一個(gè)level參數(shù),然后在實(shí)現(xiàn)中多加一個(gè)if邏輯條件判斷即可。
那每次調(diào)用LOG的時(shí)候我們都需要自己去傳一個(gè)日志等級(jí),這樣用起來感覺還是不方便,所以我們?cè)趯?duì)LOG做一層封裝,封裝出三個(gè)不同日志等級(jí)的宏函數(shù),分別為NLOG,DLOG,ELOG,這樣使用起來就比較方便了。
2.2 mysql_util
1.
在mysql_util這個(gè)類里面,封裝實(shí)現(xiàn)了靜態(tài)方法mysql_create,用于創(chuàng)建并初始化mysql句柄,以及設(shè)置好客戶端的字符集等工作。
2.
mysql_exec用于執(zhí)行mysql語句,但這個(gè)接口的封裝實(shí)現(xiàn)只能執(zhí)行插入,更新和刪除語句,因?yàn)橹挥羞@三個(gè)語句的執(zhí)行邏輯是一樣的,他們執(zhí)行成功后不用做任何額外的操作,但查詢語句卻需要執(zhí)行額外的操作,所以封裝實(shí)現(xiàn)時(shí),我們只封裝mysql_query這一個(gè)接口,如果調(diào)用者想要執(zhí)行查詢語句,則可以使用我們封裝的接口,如果想要將查詢的結(jié)果輸出顯示到自己的終端,則需要自己去實(shí)現(xiàn)保存結(jié)果集,遍歷結(jié)果集,釋放結(jié)果集等一系列操作。
mysql_destroy用于釋放銷毀mysql句柄。
2.3 json_util
1.
在json_util這里,封裝實(shí)現(xiàn)序列化和反序列化的靜態(tài)方法即可,在序列化接口里面,需要外部傳入一個(gè)root對(duì)象和一個(gè)str對(duì)象,在內(nèi)部我們會(huì)將root中的json格式的數(shù)據(jù)組織成為一個(gè)string對(duì)象,然后將這個(gè)對(duì)象賦值給str輸出型參數(shù),外部就可以拿到序列化后的字符串str了。
在內(nèi)部實(shí)現(xiàn)中,我們不在使用普通的指針來管理StreamWriter對(duì)象,而是使用智能指針unique_ptr來管理,這樣就不需要我們?cè)谑謩?dòng)釋放內(nèi)存了,當(dāng)智能指針銷毀時(shí),就會(huì)自動(dòng)釋放動(dòng)態(tài)申請(qǐng)的內(nèi)存。
在反序列化這里,也是需要外部傳入一個(gè)json格式的字符串str,然后內(nèi)部將str做反序列化,將反序列化后的json格式的value對(duì)象賦值給輸出型參數(shù)root中,外部就可以拿到反序列化后的value對(duì)象了。
與序列化相同的是,我們不在使用普通指針管理CharReader對(duì)象,也是采用智能指針unique_ptr來進(jìn)行管理,道理相同。
2.4 string_util
1.
由于后面在封裝實(shí)現(xiàn)服務(wù)器的時(shí)候,每次客戶端的請(qǐng)求我們都需要做會(huì)話的驗(yàn)證,而會(huì)話的驗(yàn)證離不開http請(qǐng)求頭部字段Cookie: ,我們需要獲取到cookie中的ssid字段,所以要對(duì)請(qǐng)求頭部中特點(diǎn)的字段作解析,拿到特定的值,所以在實(shí)用工具類這里在實(shí)現(xiàn)一個(gè)split函數(shù),用于進(jìn)行字符串的解析獲取。
下面是http請(qǐng)求頭部中Cookie字段的格式,內(nèi)容是以name=value的形式呈現(xiàn),多個(gè)值之間用分號(hào)+空格來區(qū)分開,所以如果想要拿到ssid的值,則必須進(jìn)行字符串解析。
(cookie中的值是服務(wù)器讓客戶端設(shè)置什么,cookie里面就攜帶什么的,比如客戶端和服務(wù)器建立http連接進(jìn)行登錄,登錄成功后,服務(wù)器會(huì)為該用戶建立一個(gè)session,這個(gè)session對(duì)應(yīng)的唯一標(biāo)識(shí)符ssid就是服務(wù)器返回的響應(yīng)頭部字段Set-Cookie中設(shè)置的,當(dāng)客戶端收到http響應(yīng)后,后續(xù)客戶端所有的請(qǐng)求字段中都會(huì)攜帶Cookie字段,無論是websocket請(qǐng)求還是http請(qǐng)求都會(huì)攜帶,所以服務(wù)器必須保證能夠獲取請(qǐng)求頭部字段中的Cookie字段,那么就一定要有能夠根據(jù)特定分隔符,解析字符串的能力)
2.
上面說的其實(shí)是有瑕疵的,比如我說后續(xù)客戶端所有的請(qǐng)求字段中都會(huì)攜帶Cookie字段,無論是http還是websocket請(qǐng)求,其實(shí)對(duì)于websocket請(qǐng)求來說,他的頭部字段中是壓根沒有Cookie字段的,因?yàn)閔ttp和websocket的報(bào)文格式是不一致的,怎么可能有Cookie字段,但為什么還能獲取到呢?
其實(shí)是因?yàn)樵诘谝淮螀f(xié)議切換請(qǐng)求后,websocketpp庫會(huì)將請(qǐng)求中攜帶的Cookie信息保存下來,將保存后的信息設(shè)置到connection這個(gè)類里面,我們調(diào)用connection類中的get_request_header來拿到Cookie字段的值時(shí),依舊是可以拿到的,所以后續(xù)即使是websocket請(qǐng)求,服務(wù)端也能夠通過get_request_header來拿到cookie信息,因?yàn)樵诘谝淮螀f(xié)議切換的http請(qǐng)求中,websocketpp庫已經(jīng)將cookie信息替我們保存起來了,供我們后續(xù)調(diào)用API來獲得這個(gè)cookie信息。
3.
在split實(shí)現(xiàn)這里,需要傳入的參數(shù)有三個(gè),一個(gè)是需要解析的字符串src,一個(gè)是解析時(shí)的分隔符sep,一個(gè)是解析后的內(nèi)容存放到輸出型參數(shù)res字符串?dāng)?shù)組中。
解析的方式也很簡單,我們定義兩個(gè)變量pos和idx,idx表示下一個(gè)分隔符的位置,pos表示當(dāng)前位置,搞一個(gè)while循環(huán),只要idx的值沒超過string::npos,那就一直向后查找,查找到分隔符后,判斷分隔符的位置是否和pos的位置相同,如果相同,那就說明pos位置本身就是分隔符,那么pos位置就應(yīng)該++向后挪動(dòng)1位,下次從新的pos位置開始查找,如果不同,那就直接調(diào)用substr進(jìn)行子串的截取,將截取后的子串放到res里面,然后直到整個(gè)字符串遍歷完畢后,循環(huán)結(jié)束,res中保存的就是以sep為分隔符,將src進(jìn)行截取,截取出來的子串內(nèi)容了。
2.5 file_util
1.
由于后續(xù)項(xiàng)目實(shí)現(xiàn)時(shí),客戶端會(huì)頻繁請(qǐng)求獲取服務(wù)器上的web前端資源,所以服務(wù)器需要在http_callback部分實(shí)現(xiàn)能夠?qū)⑶岸隧撁姘l(fā)送回客戶端的功能,而這一功能的實(shí)現(xiàn)就少不了文件讀取,服務(wù)器需要將文件內(nèi)容讀取到一個(gè)string中,然后服務(wù)器調(diào)用set_body這樣的函數(shù),將string內(nèi)容設(shè)置為響應(yīng)正文發(fā)送回客戶端,此時(shí)客戶端就會(huì)顯示出來一個(gè)前端網(wǎng)頁了。
所以文件讀取的功能我們也要在使用工具類模塊中實(shí)現(xiàn)一下,未來在處理前端請(qǐng)求web資源的業(yè)務(wù)時(shí),可以直接調(diào)用read接口將linux機(jī)器上實(shí)現(xiàn)的前端html頁面能夠返回給瀏覽器客戶端。
2.
read接口需要外部傳入兩個(gè)參數(shù),一個(gè)是輸入型參數(shù)文件名,一個(gè)是輸出型參數(shù)body,讀取文件后文件的內(nèi)容會(huì)被放到body里面,外部服務(wù)器在獲取到文件內(nèi)容后,就會(huì)將文件內(nèi)容返回給瀏覽器客戶端。C++操作文件的方式其實(shí)就是定義一個(gè)ifstream或ofstream對(duì)象,讀取文件是in,寫入文件是out,我們這里就定義一個(gè)ifstream的對(duì)象,以二進(jìn)制和讀取的方式來打開文件。
我們需要獲取一下文件的大小,這樣以便于提前resize開辟好body的空間大小,然后將讀取出來的文件內(nèi)容放到body里面。獲取文件的大小也是有技巧可言的,常見的一種方式就是先調(diào)用seekg,將文件讀取位置移動(dòng)到文件末尾處,然后調(diào)用tellg,拿到當(dāng)前的位置大小,拿到的這個(gè)位置大小,其實(shí)正好就是該文件的大小,獲取完文件大小后,不要再將文件讀取位置調(diào)整到開始。
有了文件大小之后,我們直接調(diào)用body.resize(filesize)進(jìn)行body的擴(kuò)容,然后調(diào)用ifs.read(),將文件的內(nèi)容以二進(jìn)制的形式存儲(chǔ)到body里面,最后記得將文件關(guān)閉即可,其實(shí)關(guān)閉的這一步,我們不搞也行,因?yàn)閕fs對(duì)象銷毀的時(shí)候,會(huì)自動(dòng)關(guān)閉文件,(這話可不是我說的,是C++ primer說的),如果你比較保守的話,不放心的話,也可以自己去手動(dòng)調(diào)用close來關(guān)閉文件。
3. 數(shù)據(jù)管理模塊
3.1 數(shù)據(jù)管理的設(shè)計(jì)
1.
數(shù)據(jù)管理這里的設(shè)計(jì)分為兩個(gè)部分,一個(gè)是數(shù)據(jù)庫中user表結(jié)構(gòu)的設(shè)計(jì),一個(gè)是項(xiàng)目代碼中user_table類的設(shè)計(jì)。用戶信息表這里,共創(chuàng)建6個(gè)字段,分別是用戶的唯一標(biāo)識(shí),也就是user_id,還有username,password,用戶的天梯分?jǐn)?shù),后續(xù)我們會(huì)根據(jù)天梯分?jǐn)?shù)的不同來判斷用戶的游戲等級(jí),例如1000 ~ 2000是青銅,2000 ~ 3000是白銀,3000 ~ 4000是黃金,用戶在匹配對(duì)戰(zhàn)時(shí),只能匹配到和自己游戲等級(jí)相同的玩家,還包括total_count總戰(zhàn)斗場次,win_count勝利場次。
當(dāng)用戶進(jìn)入到游戲大廳頁面時(shí),我們要展示出用戶的名稱,天梯分?jǐn)?shù),總戰(zhàn)斗場次,勝利場次等詳細(xì)信息。
2.
我們需要自己設(shè)計(jì)一個(gè)user_table類,這個(gè)類的主要功能是完成瀏覽器在向服務(wù)器發(fā)起的諸多請(qǐng)求中,涉及到訪問數(shù)據(jù)庫的操作,我們將這些操作接口全部封裝起來,方便后面服務(wù)器模塊進(jìn)行調(diào)用。
類成員變量是比較簡單的,因?yàn)槲覀円L問數(shù)據(jù)庫嘛,那肯定需要一個(gè)MySQL句柄,除此之外,其實(shí)我們還需要一把互斥鎖,因?yàn)閣ebsocketpp這個(gè)庫是多線程實(shí)現(xiàn)的,我們項(xiàng)目中的各個(gè)接口都有可能會(huì)在多線程的情況下被調(diào)用,所以只要涉及到共享資源的訪問,或者是其他的線程安全問題,我們都需要一把鎖來進(jìn)行保護(hù)。
有人可能會(huì)說,人家mysql提供的各個(gè)接口本身就是線程安全的啊!你搞個(gè)互斥鎖有什么意義呢?其實(shí)不然!當(dāng)我們調(diào)用mysql_query執(zhí)行sql語句時(shí),mysql_query本身確實(shí)是線程安全的,如果執(zhí)行的是增刪改這樣的sql語句也不會(huì)出現(xiàn)線程安全問題,但如果是查詢語句,此時(shí)就出現(xiàn)線程安全的問題了。
在查詢語句執(zhí)行后,我們是需要調(diào)用其他的API來進(jìn)行結(jié)果集的保存,遍歷,釋放等操作,在執(zhí)行mysql_store_result之前,上一條在數(shù)據(jù)庫中執(zhí)行的語句必須是select才行,但在多線程的情況下,你能保證執(zhí)行完select語句后,下一條語句執(zhí)行的一定是mysql_store_result嗎?我們的接口是可能會(huì)被多個(gè)線程調(diào)用的啊,有可能此時(shí)某個(gè)用戶在注冊(cè),那執(zhí)行的就是插入語句,但也有可能其他用戶在登錄,那執(zhí)行的就是查詢語句,所以你能保證select執(zhí)行之后,下一個(gè)執(zhí)行的API是mysql_store_result嗎?當(dāng)然是無法保證的!
每個(gè)API各自確確實(shí)實(shí)是一個(gè)原子操作,是線程安全的,但我們現(xiàn)在的需求是希望在查詢語句結(jié)束后,下一個(gè)執(zhí)行的MySQL API一定要是mysql_store_result,因?yàn)閙ysql句柄是只有一份的,mysql句柄是共享資源,多個(gè)線程都會(huì)訪問mysql句柄,我們希望mysql_query執(zhí)行select語句 + mysql_store_result這兩個(gè)操作合起來是一個(gè)原子操作,如何做到呢?那就只能通過加鎖來實(shí)現(xiàn),所以u(píng)ser_table類的成員變量除mysql句柄外還需要一把互斥鎖。
(你試想一下,如果不加鎖,A線程拿著句柄在執(zhí)行select語句,執(zhí)行完select語句后,B線程此時(shí)想要執(zhí)行insert語句,B線程搶過來這個(gè)句柄進(jìn)行insert語句的執(zhí)行,因?yàn)閙ysql_query是線程安全的,所以在執(zhí)行期間是不會(huì)有其他線程來打擾他的,A線程執(zhí)行select語句時(shí),也是同樣如此,現(xiàn)在B線程執(zhí)行完了,A線程又拿著這個(gè)句柄執(zhí)行mysql_store_result了,此時(shí)mysqld服務(wù)直接報(bào)錯(cuò),MySQL數(shù)據(jù)庫懵逼了,你上一條語句執(zhí)行的是insert語句啊,你現(xiàn)在要讓我執(zhí)行mysql_store_result,我給你保存?zhèn)€毛?。磕闵蠗l語句執(zhí)行的又不是select,此時(shí)mysqld服務(wù)直接就報(bào)錯(cuò)了。
因?yàn)閙ysql句柄是共享資源,所以A線程拿到進(jìn)入API執(zhí)行流程中,那此刻其他線程不能執(zhí)行任何的API,因?yàn)閙ysql API是線程安全的,如果是B線程拿到,那也是同樣如此,如果是C,D,E等線程也是這樣的,你們隨便拿不要緊,重要的是查詢語句和mysql_store_result合在一起得是原子操作啊!否則這就是有問題的?。。?/mark>
3.
需要我們實(shí)現(xiàn)的接口有構(gòu)造,析構(gòu),涉及到用戶動(dòng)態(tài)請(qǐng)求功能的處理接口有,insert,它可以幫助我們向數(shù)據(jù)庫中新增用戶的注冊(cè)信息,login負(fù)責(zé)對(duì)登錄的用戶進(jìn)行驗(yàn)證,看看數(shù)據(jù)庫中是否存在該用戶,如果存在,則比對(duì)用戶輸入的密碼是否正確,如果正確則說明登錄成功,同時(shí)login會(huì)以輸出型參數(shù)的方式來將數(shù)據(jù)庫中獲取到的用戶詳細(xì)信息返回給user變量里面,為什么要有這一步呢?主要是用戶登錄成功請(qǐng)求發(fā)起,發(fā)起的請(qǐng)求中會(huì)攜帶用戶的username和password這樣的信息,這些信息是要作為輸入型參數(shù)來告知login的,同時(shí)當(dāng)服務(wù)器處理完登錄請(qǐng)求后,外部其實(shí)是要為用戶創(chuàng)建session的,而創(chuàng)建session需要uid來進(jìn)行創(chuàng)建,所以這里的user就作為了輸入輸出型參數(shù)來使用,給外部返回一個(gè)用戶的詳細(xì)信息,外部想知道哪個(gè)信息字段值,只要使用json提供的[ ]重載即可使用。除此之外,還可以實(shí)現(xiàn)一些其他的輔助接口,例如通過用戶名來獲取用戶的詳細(xì)信息,通過用戶id來獲取用戶的詳細(xì)信息,因?yàn)楹竺嬖谟脩舸髲d展示用戶信息時(shí),我們是需要通過user_table類提供的API來獲取到用戶信息并展示的。此外在實(shí)現(xiàn)兩個(gè)接口,id對(duì)應(yīng)的某個(gè)用戶勝利時(shí),要在數(shù)據(jù)庫中更新用戶的信息,比如total_count++,win_count++,score+=30,當(dāng)然也少不了用戶失敗時(shí)的信息更新,所以再加一個(gè)loseAPI。
3.2 user_table類的實(shí)現(xiàn)
1.
構(gòu)造函數(shù)其實(shí)就是調(diào)用我們上面mysql_util里面實(shí)現(xiàn)的多個(gè)靜態(tài)方法,調(diào)用mysql_create進(jìn)行句柄的創(chuàng)建,析構(gòu)函數(shù)中進(jìn)行句柄的銷毀。
2.
在注冊(cè)信息這里,我們首先要判斷輸入型參數(shù)user中用戶信息的完整性,只有有一個(gè)不完整,則注冊(cè)信息失敗,如果全部完整,我們則編寫sql語句,進(jìn)行用戶信息的注冊(cè),sql語句需要sprintf進(jìn)行格式組織,將輸入型參數(shù)中的username和password字段拿到并格式化到sql語句中,最后調(diào)用工具類中的mysql_exec執(zhí)行語句即可。
3.
在登錄驗(yàn)證這里,其實(shí)要做的就是將數(shù)據(jù)庫中對(duì)應(yīng)的信息取出來同時(shí)進(jìn)行密碼的校驗(yàn),所以我們直接根據(jù)輸入型參數(shù)user中的用戶名和密碼字段,組織出具有篩選條件的查詢語句,在進(jìn)行查詢時(shí),如果能夠在數(shù)據(jù)庫中找到對(duì)應(yīng)的用戶信息,則我們需要將結(jié)果保存到本地,所以查詢和保存結(jié)果這兩步,必須是一個(gè)原子操作,那我們就進(jìn)行RAII風(fēng)格的加鎖控制。
在獲取到查詢結(jié)果集的行數(shù)之后,我們還需要進(jìn)行校驗(yàn),如果rowNum大于1,則說明用戶信息不唯一,如果小于1,則說明用戶信息不存在,只有等于1的時(shí)候,才是符合預(yù)期的,其實(shí)這里的校驗(yàn)也算是穩(wěn)一手的操作,99%的概率這里是不可能出錯(cuò)的。然后通過調(diào)用mysql_fetch_row遍歷結(jié)果集,將數(shù)據(jù)庫中的信息拿出來,把每個(gè)字段填充到user這個(gè)輸入輸出型參數(shù)當(dāng)中,最后釋放一下結(jié)果集就行。
4.
通過用戶名來獲取用戶詳細(xì)信息的邏輯和上面一模一樣,唯一不同的就是sql語句的篩選條件改動(dòng)了而已,這里也就不再贅述了。
道理相同,僅僅是改變了一下select的篩選條件,這里也不在贅述
5.
win和lose在實(shí)現(xiàn)時(shí),其實(shí)就是進(jìn)行數(shù)據(jù)庫信息的更新,編寫update語句即可,然后調(diào)用工具類中的mysql_exec執(zhí)行就完成函數(shù)的編寫了。
4. 在線用戶管理模塊
4.1 在線用戶管理的設(shè)計(jì)
1.
由于后期我們會(huì)通過用戶id,來獲取到用戶對(duì)應(yīng)的websocket連接,只有獲取到連接之后,服務(wù)器才能通過連接,將自己對(duì)于業(yè)務(wù)的處理結(jié)果發(fā)送給客戶端,比如說,在后面的游戲房間實(shí)現(xiàn)中,雙方下棋時(shí),如果有一方勝利,那么此時(shí)就應(yīng)該將誰勝利的消息廣播給房間中的雙方玩家,然后前端頁面會(huì)進(jìn)行檢測,看看服務(wù)器發(fā)送回來的消息中,勝利者是不是我自己,如果是我自己,那就應(yīng)該在頁面上顯示,我勝利了,如果不是我,那就應(yīng)該顯示我失敗了,所以必須實(shí)現(xiàn)一個(gè)能夠通過用戶id來獲取用戶對(duì)應(yīng)的websocket連接的API,這個(gè)API就是在線用戶管理模塊,也就是online_manager類中實(shí)現(xiàn)的。
在該類里面,不僅要有獲取游戲大廳用戶長連接的API,還應(yīng)該有獲取游戲房間用戶長連接的API,因?yàn)槲覀冎婪块g和大廳是兩個(gè)不同的頁面,使用的長連接也是不同的,所以獲取這兩個(gè)長連接的API也是不同的,兩者是解耦的。
2.
除了上面獲取連接的API之外,在線用戶管理還具有判斷一個(gè)用戶此時(shí)是否在線的功能,因?yàn)橛脩粲锌赡芡娴耐娴牟幌胪媪耍苯雨P(guān)閉前端頁面,那么后續(xù)服務(wù)器在進(jìn)行相關(guān)業(yè)務(wù)處理時(shí),就應(yīng)該進(jìn)行用戶是否在線的判斷,如果不在線,那么服務(wù)器就不提供相應(yīng)的服務(wù),如果在線,則繼續(xù)進(jìn)行業(yè)務(wù)處理。
3.
為了進(jìn)行上述功能的實(shí)現(xiàn),online_manager需要兩個(gè)哈希表來分別構(gòu)建用戶id和用戶對(duì)應(yīng)的websocket通信連接之間的映射關(guān)系,由于哈希表是共享資源,我們要對(duì)哈希表進(jìn)行插入和刪除,所以也需要一把互斥鎖來保證共享資源訪問的安全性。
需要實(shí)現(xiàn)的API有,當(dāng)websocket連接建立成功時(shí),將用戶加入到游戲大廳/游戲房間在線用戶管理中,當(dāng)websocket連接斷開時(shí),將用戶從游戲大廳/游戲房間在線用戶管理中移除,判斷當(dāng)前用戶是否還在游戲大廳/游戲房間中,通過uid來獲取用戶在游戲大廳/游戲房間中的長連接。
(由于connection_ptr這個(gè)類型是websocketpp庫里面的server類中定義的,所以我們提前typedef了一下這個(gè)server類,這樣使用起來會(huì)比較方便)
4.2 online_manager類的實(shí)現(xiàn)
1.
當(dāng)服務(wù)器與客戶端建立好websocket長連接之后,那就需要將用戶添加到在線用戶管理模塊中,而所謂的加入游戲大廳或房間的在線用戶管理,其實(shí)就是將uid和對(duì)應(yīng)的conn連接構(gòu)造成鍵值對(duì)插入到_hall_online_user或_room_online_user哈希表中,需要多說一嘴的是,插入鍵值對(duì)到哈希表中,是需要加鎖控制的,因?yàn)楣1硎枪蚕碣Y源,在多線程同時(shí)訪問下,如果不加鎖控制,可能會(huì)出現(xiàn)線程安全問題。
2.
當(dāng)服務(wù)器和客戶端websocket長連接斷開的時(shí)候,就需要從在線用戶管理中將用戶進(jìn)行移除,而所謂的移除,其實(shí)就是從哈希表中找到特定的鍵值對(duì),然后將鍵值對(duì)刪除就可以了。同樣的,由于涉及到對(duì)共享資源的訪問,我們也需要進(jìn)行加鎖控制。
3.
判斷用戶是否在在線用戶管理中,其實(shí)就是判斷uid對(duì)應(yīng)的迭代器是否存在,我們直接調(diào)用find查找uid對(duì)應(yīng)的迭代器,如果迭代器不為end(),那就說明當(dāng)前用戶確實(shí)在在線用戶管理中。同樣的,訪問共享資源,需要進(jìn)行加鎖控制。
4.
只要用戶在在線用戶管理中,那我們就可以通過迭代器的方式,找到uid對(duì)應(yīng)的connection_ptr,然后進(jìn)行返回即可,如果找不到,那我們就返回一個(gè)空的connection_ptr對(duì)象。
5. session管理模塊
5.1 HTTP的cookie&session機(jī)制
1.
在web開發(fā)里面,http是一種無狀態(tài)短連接的通信協(xié)議,也就是說,當(dāng)客戶端和服務(wù)器建立了一次http連接完成通信后,http連接就會(huì)斷開,下次客戶端想要訪問服務(wù)器的其他web資源時(shí),服務(wù)器是不知道你這個(gè)客戶端是誰的,服務(wù)器不知道你是誰,也不知道你現(xiàn)在登沒登錄,那服務(wù)器此時(shí)給客戶端提供服務(wù)就是不合理的,因?yàn)閔ttp是無狀態(tài)的啊,他不會(huì)保存任何客戶端的信息,但用戶有這樣的需求啊,比如你現(xiàn)在在B站的網(wǎng)頁端,你提交用戶名和密碼進(jìn)行登錄后,跳轉(zhuǎn)到B站的主頁面,你的登錄請(qǐng)求是http的,如果B站的服務(wù)器不報(bào)存你的任何信息,那當(dāng)你跳轉(zhuǎn)到B站的主頁面的時(shí)候,B站的服務(wù)器不認(rèn)識(shí)你啊,為啥要給你提供展示視頻等服務(wù)呢?還有一個(gè)例子,假設(shè)你現(xiàn)在已經(jīng)登錄好了,正訪問B站的視頻呢,然后你不小心把網(wǎng)頁關(guān)閉了,當(dāng)你重新打開時(shí),你希望B站的服務(wù)器認(rèn)識(shí)你嗎?你當(dāng)然希望啊,如果他不認(rèn)識(shí)你,你打開B站頁面后,又得重新輸入用戶名和密碼進(jìn)行登錄驗(yàn)證,你覺得這樣煩不煩???每次新打開頁面,我都需要輸入用戶名和密碼,煩都煩死了。
所以,即使http是無狀態(tài)的,但用戶需要他是有狀態(tài)的,那么服務(wù)器就會(huì)為每個(gè)用戶瀏覽器,都在后端中創(chuàng)建一個(gè)session會(huì)話對(duì)象(默認(rèn)狀態(tài)下,一個(gè)瀏覽器獨(dú)占后端服務(wù)器的一個(gè)session,不會(huì)出現(xiàn)你在一個(gè)瀏覽器中打開了多個(gè)標(biāo)簽頁訪問web資源,那么服務(wù)器就會(huì)為該網(wǎng)頁對(duì)應(yīng)創(chuàng)建多個(gè)session的這種情況),用來保存用戶的狀態(tài)信息(比如用戶的uid,用戶是登錄狀態(tài)還是未登錄狀態(tài)),讓服務(wù)器能夠具有識(shí)別當(dāng)前用戶是誰的能力!
2.
那服務(wù)器如何通過session校驗(yàn)當(dāng)前客戶端的狀態(tài)呢?其實(shí)除了后端session的創(chuàng)建之外,還需要一個(gè)cookie信息,當(dāng)客戶端訪問服務(wù)器進(jìn)行第一次登錄后,服務(wù)器此時(shí)就會(huì)為客戶端創(chuàng)建一個(gè)session,然后服務(wù)器會(huì)給客戶端返回一個(gè)http響應(yīng),響應(yīng)頭部字段中會(huì)有一個(gè)Set-Cookie字段,后面的值表示的就是服務(wù)器讓客戶端以后發(fā)送請(qǐng)求時(shí),在他自己的http請(qǐng)求頭部都設(shè)置一個(gè)Cookie字段,里面的值就是服務(wù)器的Set-Cookie設(shè)置的值
http響應(yīng)的Set-Cookie頭部字段
http請(qǐng)求的Cookie頭部字段
3.
但Set-Cookie的值應(yīng)該設(shè)置成什么呢?如果向下面的圖中所示,設(shè)置的消息內(nèi)容如果就直接是用戶的狀態(tài)信息的話,那么瀏覽器本地就需要保存一份包含用戶狀態(tài)信息的cookie文件,在后面的所有請(qǐng)求中,都去攜帶上用戶的登錄狀態(tài)信息,這樣確實(shí)可以保證服務(wù)器能夠識(shí)別客戶端,但安全性太低,因?yàn)閏ookie文件可能會(huì)被不法者盜取和篡改,不法者可能會(huì)冒充客戶端向服務(wù)器發(fā)起請(qǐng)求,同時(shí)這也會(huì)對(duì)用戶產(chǎn)生無法預(yù)料的影響,因?yàn)橛脩舻男畔⒖梢员蝗我獯鄹暮捅I取,通過cookie文件就可以拿到。
所以下面這樣的方式是不夠合理和安全的。
4.
此時(shí)就有大佬提出了解決方案,在cookie的基礎(chǔ)上引入session,形成cookie&session機(jī)制。即服務(wù)器來保存用戶的詳細(xì)狀態(tài)信息,而不是客戶端來保存,服務(wù)器為每一個(gè)已經(jīng)登錄的用戶創(chuàng)建一個(gè)唯一對(duì)應(yīng)的session,每一個(gè)會(huì)話都有自己的會(huì)話標(biāo)識(shí)符,也就是會(huì)話id,服務(wù)器返回的Set-Cookie字段中不再是用戶的詳細(xì)信息了,而是會(huì)話id,客戶端收到響應(yīng)后,會(huì)將ssid保存在自己本地的cookie文件中,后續(xù)每次請(qǐng)求服務(wù)器的頭部字段都會(huì)有Cookie信息,服務(wù)器只需要拿著請(qǐng)求中的ssid值在本地的session管理模塊中找一下,看看是否存在對(duì)應(yīng)的session,如果存在,則看一下用戶此時(shí)的狀態(tài)是什么,如果是合法的狀態(tài),那么服務(wù)器就會(huì)返回一個(gè)登錄成功的響應(yīng)信息,客戶端頁面就會(huì)發(fā)生跳轉(zhuǎn)。
(此時(shí)不法者就無法盜取到用戶的狀態(tài)信息了,因?yàn)橛脩舭l(fā)送的cookie信息中只有一個(gè)ssid啊,你要ssid有啥用啊,用戶信息泄露的問題就大大改善了,但如果不法者冒充用戶向服務(wù)器發(fā)起請(qǐng)求,這個(gè)問題是cookie&session解決不了的,此時(shí)需要配合其他策略來進(jìn)行解決,例如白名單,防火墻,異地登陸警告等等策略。況且這個(gè)問題也不應(yīng)該由cookie和session機(jī)制來解決,這是你網(wǎng)絡(luò)安全需要解決的問題,我就是個(gè)識(shí)別客戶端的機(jī)制,讓我解決這種問題干嘛啊。)
5.2 websocketpp庫中定時(shí)器的使用
1.
了解了cookie和session機(jī)制之后,我們先不急著實(shí)現(xiàn)服務(wù)器的session模塊,我們需要首先熟悉一下定時(shí)器的使用,這是很關(guān)鍵的,因?yàn)閟ession的銷毀其實(shí)就是一個(gè)定時(shí)任務(wù)。如果你登陸過后,不進(jìn)行任何的操作,session會(huì)一直永久保存在服務(wù)器嗎?當(dāng)然不會(huì),如果永久保存不銷毀的話,隨著登錄的用戶過多,那總有一天服務(wù)器扛不住可能就宕機(jī)了,所以session一定是有創(chuàng)建有銷毀的,當(dāng)你關(guān)閉頁面之后,session難道也要一直存在嗎?也是不會(huì)的,session可能在你關(guān)閉頁面后,會(huì)被保存一段時(shí)間,在一段時(shí)間之后,session就會(huì)定時(shí)銷毀了。
需要注意的是,在某些安全要求高的使用場景下,如果30s內(nèi)無操作,則session會(huì)自動(dòng)被銷毀,迫使用戶重新進(jìn)行登錄,還有一種情況是,為了安全性,可能用戶切換一個(gè)頁面,那其實(shí)就是切換一個(gè)websocket長連接,則服務(wù)器就會(huì)將原來的session立馬銷毀,重新創(chuàng)建一個(gè)新的session,這樣一般都是在安全性要求比較高的場景下進(jìn)行使用,只要換連接那就跟著換一個(gè)session
但本項(xiàng)目中沒有采取這樣高級(jí)別的安全方式,我們的項(xiàng)目在切換頁面后,使用的session還是原來的session,并沒有進(jìn)行更換。
2.
websocketpp庫中的endpoint類里面實(shí)現(xiàn)了一個(gè)set_timer接口,用于設(shè)置定時(shí)任務(wù),該接口的第一個(gè)參數(shù)duration表示多長ms時(shí)間之后執(zhí)行該定時(shí)任務(wù),第二個(gè)參數(shù)是一個(gè)包裝器類型,包裝的可調(diào)用對(duì)象的返回值是void,參數(shù)是一個(gè)庫里面定義的類型。
不過我們壓根不用理睬他是個(gè)啥包裝器類型,直接傳一個(gè)bind綁死參數(shù)的可調(diào)用對(duì)象就行,讓set_timer在規(guī)定時(shí)間之后,直接執(zhí)行我們自己傳入的可調(diào)用對(duì)象。
3.
下面的代碼希望大家不要感到陌生,其實(shí)這段代碼就是最開始我們搭建http/websocket服務(wù)器時(shí)的代碼,只不過在http_callback里面最后兩行,添加了一個(gè)定時(shí)任務(wù),也就是調(diào)用print函數(shù),上面我講過bind的用法,bind生成的可調(diào)用對(duì)象不影響類型,只影響實(shí)際調(diào)用時(shí)候的傳參,所以我們直接綁死print的參數(shù),那么在duration毫秒之后,set_timer就會(huì)自動(dòng)調(diào)用print函數(shù),我們無需管set_timer的第二個(gè)參數(shù)timer_handler是什么類型的包裝器,直接傳個(gè)綁死的可調(diào)用對(duì)象過去就行,那么在實(shí)際調(diào)用timer_handler類型的callback時(shí),傳任何參數(shù)都是沒有用的,他只會(huì)調(diào)用print(“rygttm”)這個(gè)函數(shù)。
4.
通過timer_ptr類型的tp指針接收set_timer的返回值后,如果我們想取消定時(shí)器,重新設(shè)置一波定時(shí)時(shí)間,比如我不想5000毫秒后執(zhí)行任務(wù)了,而是想在3000毫秒后執(zhí)行,那我們就需要將原先的定時(shí)任務(wù)取消,然后再重新調(diào)用set_timer設(shè)置新的定時(shí)任務(wù)。
而取消就需要借助timer_ptr類里面的cancel接口來實(shí)現(xiàn),但這個(gè)取消接口又特別的坑,它會(huì)導(dǎo)致定時(shí)任務(wù)被立即的執(zhí)行,下面在實(shí)現(xiàn)session管理模塊時(shí),我們還要對(duì)定時(shí)器被取消導(dǎo)致定時(shí)任務(wù)立即執(zhí)行,這樣的行為做特殊處理。
當(dāng)沒有取消定時(shí)任務(wù)時(shí),可以看到客戶端發(fā)起一次http請(qǐng)求后,服務(wù)器終端上在10s過后才會(huì)打印出rygttm,這表明在服務(wù)器的http_callback中,我們確實(shí)設(shè)置好了一個(gè)10s后執(zhí)行的定時(shí)任務(wù)。
當(dāng)我們?nèi)∠〞r(shí)任務(wù)之后,客戶端發(fā)起一次http請(qǐng)求,服務(wù)器調(diào)用http_callback都會(huì)立馬在終端上打印出來rygttm,由此可見,取消定時(shí)任務(wù)后,定時(shí)任務(wù)會(huì)立馬被執(zhí)行一次。
5.3 session的設(shè)計(jì)與實(shí)現(xiàn)
1.
一個(gè)會(huì)話應(yīng)該包含的信息有,這個(gè)會(huì)話本身的標(biāo)識(shí)符,也就是會(huì)話id,還應(yīng)該有用戶id,因?yàn)槊恳粋€(gè)session都是和一個(gè)用戶所關(guān)聯(lián)的,所以session中還要包含uid,表示這個(gè)session是哪個(gè)用戶的,還可以有一個(gè)用戶狀態(tài)字段,也就是表示用戶是unlogin還是login,這個(gè)字段其實(shí)有和沒有都行,因?yàn)槲覀冎粫?huì)為登陸成功的用戶創(chuàng)建會(huì)話,所以只有某個(gè)會(huì)話被創(chuàng)建,那么這個(gè)會(huì)話對(duì)應(yīng)的用戶狀態(tài)一定是已登錄的。每個(gè)會(huì)話都會(huì)有自己的定時(shí)任務(wù),例如多少s后銷毀,或者會(huì)話永久存在等等,那么會(huì)話一定是需要和定時(shí)器對(duì)象所關(guān)聯(lián)的,所以成員變量我們?cè)诩右粋€(gè)timer_ptr的定時(shí)器對(duì)象。
成員函數(shù)這里,其實(shí)實(shí)現(xiàn)的都是輔助接口,比如外部想獲取session中指定的信息時(shí),那么session就可以提供一些接口將指定信息進(jìn)行返回,傳給外部調(diào)用方,這些輔助接口的實(shí)現(xiàn)都很簡單,其實(shí)就是設(shè)置一些成員變量的值啦,或者返回成員變量什么的。
2.
set_user用于設(shè)置會(huì)話的成員變量_uid的值,get_uid用于獲取會(huì)話相對(duì)應(yīng)的用戶id,is_login用于判斷當(dāng)前會(huì)話對(duì)應(yīng)的用戶是否處于登錄狀態(tài),set_state用于設(shè)置會(huì)話對(duì)應(yīng)用戶的狀態(tài),set_timer用于設(shè)置會(huì)話對(duì)應(yīng)的定時(shí)器對(duì)象,這個(gè)接口其實(shí)就是由session_manager來調(diào)用的,get_timer用于獲取會(huì)話對(duì)應(yīng)的定時(shí)器對(duì)象,ssid用于返回會(huì)話id,構(gòu)造函數(shù)用于設(shè)置會(huì)話的ssid。
其實(shí)上面這些函數(shù)都是成對(duì)兒出現(xiàn)的,每一對(duì)兒都和成員變量所對(duì)應(yīng),說白了就是設(shè)置一下成員變量的值,然后獲取一下成員變量的值。
3.
由于session這個(gè)類比較簡單,所以設(shè)計(jì)和實(shí)現(xiàn)我放到一塊了,實(shí)現(xiàn)也是比較簡單的,大家看一眼就明白了。
5.4 session管理器的設(shè)計(jì)
1.
對(duì)于未來可能存在的多個(gè)session對(duì)象進(jìn)行管理,那我們肯定需要一個(gè)數(shù)據(jù)結(jié)構(gòu)來將多個(gè)session對(duì)象組織起來,為了更快的查找到特定的session對(duì)象,我們采用了哈希表這種數(shù)據(jù)結(jié)構(gòu)。
同時(shí)每個(gè)session都應(yīng)該被分配一個(gè)session id,所以session_manager的成員變量中還要有一個(gè)_next_ssid分配器,用于給每個(gè)session分配唯一的ssid,這個(gè)分配器聽起來特別高大上,但其實(shí)就是一個(gè)自增長的int類型值。
與之前的online_manager和user_table類都相同的是,這里涉及到對(duì)共享資源_next_ssid和_sessions的訪問,所以我們這里還需要加一把互斥鎖。
session管理器還需要給每個(gè)session添加定時(shí)任務(wù),所以我們還需要一個(gè)wsserver類對(duì)象,用于獲取server類中的set_timer接口,以此來設(shè)置會(huì)話的定時(shí)任務(wù)。
2.
可能會(huì)有人有疑問,為什么管理會(huì)話的智能指針是shared_ptr呢?unique_ptr不行嗎?
主要是因?yàn)閟ession管理器管理的不只有一個(gè)session,他需要通過哈希表將多個(gè)session組織起來,然后進(jìn)行管理。哈希表構(gòu)建會(huì)話id和會(huì)話智能指針之間的映射關(guān)系,那么向_sessions這個(gè)哈希表中插入鍵值對(duì)時(shí),當(dāng)然就會(huì)發(fā)生智能指針的拷貝了,哈希表有堆上的智能指針,函數(shù)棧幀里面有我們定義出來的session_ptr,因?yàn)閡nique_ptr是禁止拷貝的,所以就只能用shared_ptr來對(duì)session對(duì)象進(jìn)行管理。
當(dāng)session對(duì)象的引用計(jì)數(shù)變?yōu)?時(shí),session就會(huì)自動(dòng)被銷毀了。
3.
session_manager的構(gòu)造函數(shù)需要外部傳入一個(gè)wsserver類型的對(duì)象,create_session負(fù)責(zé)創(chuàng)建一個(gè)會(huì)話,需要外部傳入會(huì)話對(duì)應(yīng)用戶的uid,和用戶的狀態(tài),get_session_by_ssid用于通過ssid來獲取到會(huì)話管理指針,通過這個(gè)智能指針就可以拿到會(huì)話中所有的詳細(xì)信息,也就是session類里面的所有詳細(xì)信息。destroy_session用于銷毀session,其實(shí)所謂的銷毀,就是將哈希表中的鍵值對(duì)移除掉即可,釋放鍵值對(duì)在堆上對(duì)應(yīng)的內(nèi)存空間,而鍵值對(duì)里面不就有session_ptr嗎?該智能指針銷毀后,會(huì)以RAII的風(fēng)格釋放session所占用的內(nèi)存,因?yàn)閷?shí)際管理session的智能指針只有堆上這個(gè)還存在,其他的函數(shù)棧幀內(nèi)開辟的臨時(shí)的智能指針,在離開函數(shù)后都會(huì)被銷毀掉了,所以最后一定只剩一個(gè)session_ptr在堆上存放著。
set_session_expire_time就是設(shè)置會(huì)話的過期時(shí)間,即在指定時(shí)間段后,執(zhí)行destroy_session,完成session對(duì)象的釋放,這個(gè)接口實(shí)現(xiàn)起來是比較復(fù)雜的,append_already_session其實(shí)就是配合set_session_expire_time來實(shí)現(xiàn)會(huì)話的定時(shí)銷毀的,這個(gè)接口也是整個(gè)session_manager中最繁瑣的接口。
我們還預(yù)定義了兩個(gè)宏出來,分別代表session此刻是永久存在、session的過期銷毀時(shí)間,過期銷毀時(shí)間的初始值設(shè)置為了30000ms。
5.5 session_manager類的實(shí)現(xiàn)
1.
在構(gòu)造函數(shù)中,我們自己初始化_next_ssid的值,這個(gè)會(huì)話id分配器的值從1開始進(jìn)行分配。
創(chuàng)建session時(shí),我們上來就直接加鎖控制,因?yàn)橄旅娴拇a會(huì)涉及到對(duì)共享資源的訪問。我們將session會(huì)話對(duì)象開辟在堆上,用sp指針來進(jìn)行管理,然后調(diào)用session類的接口進(jìn)行會(huì)話相關(guān)信息的初始化,將會(huì)話狀態(tài),uid等字段填充好,最后將sp和_next_ssid構(gòu)成鍵值對(duì)插入到哈希表中,別忘了將_next_ssid進(jìn)行自增1,最后返回sp即可。
get_session_by_ssid也比較簡單,通過調(diào)用哈希表的find接口,即可找到ssid對(duì)應(yīng)的鍵值對(duì)是什么,如果找不到則返回一個(gè)空的智能指針對(duì)象,如果找到,則返回堆上的智能指針即可。
destroy_session的實(shí)現(xiàn)也很簡單,直接調(diào)用哈希表的erase接口進(jìn)行鍵值對(duì)的移除即可。
2.
設(shè)置會(huì)話的過期時(shí)間,其實(shí)分為四種情況,我們需要判斷會(huì)話原來有沒有定時(shí)刪除的任務(wù),有和沒有就會(huì)細(xì)分為兩種情況,在每種情況下面又都會(huì)細(xì)分兩個(gè)子情況,也就是看外部給set_session_expire_time傳入的時(shí)間參數(shù)是permanent永久,還是timeout。
所以總體的情況就會(huì)分為四種,對(duì)每一種情況都要有不同的處理。
有人可能會(huì)有疑問,咋能有這么多種狀態(tài)呢?你是不存心搞我啊?其實(shí)不然!
例如,當(dāng)用戶在登陸成功后,此時(shí)服務(wù)器會(huì)為用戶創(chuàng)建一個(gè)定時(shí)銷毀的會(huì)話,也就是說,如果在用戶登錄成功后,用戶遲遲不點(diǎn)擊一個(gè)提示框(前端alert顯示的登錄框),那么在30s之后,這個(gè)會(huì)話就會(huì)被銷毀掉,這也是為了安全起見,如果用戶點(diǎn)擊了那個(gè)提示框,頁面從登錄跳轉(zhuǎn)到游戲大廳,那么此時(shí)會(huì)話就應(yīng)該從定時(shí)銷毀變?yōu)橛谰么嬖?/strong>,因?yàn)檫B接此時(shí)會(huì)切換為websocket連接,后續(xù)服務(wù)器提供所有的業(yè)務(wù)處理之前,都要在websocket連接的基礎(chǔ)上,判斷會(huì)話是否存在,如果定時(shí)銷毀的話,服務(wù)器都找不到會(huì)話了,后續(xù)的業(yè)務(wù)處理的服務(wù)都提供不了了,當(dāng)游戲大廳頁面被關(guān)閉時(shí),我們又需要從永久存在變?yōu)槎〞r(shí)銷毀,還有一種情況是,用戶已經(jīng)登錄成功了,結(jié)果不小心把登錄頁面給關(guān)閉掉了,用戶那就重新輸入用戶名和密碼,重新進(jìn)行登錄,但此時(shí)用戶對(duì)應(yīng)的session已經(jīng)存在了啊,所以再次重新進(jìn)行登錄其實(shí)就是意味著刷新session定時(shí)銷毀的時(shí)間,從定時(shí)銷毀再到定時(shí)銷毀。
(其實(shí)在用戶登錄成功后,完全不需要再重新進(jìn)行登錄,只不過存在用戶反復(fù)登錄這樣的可能性,所以我們需要刷新定時(shí)銷毀的時(shí)間,但事實(shí)上,只要用戶登錄了一次,會(huì)話創(chuàng)建成功后,如果用戶不小心關(guān)閉了游戲大廳頁面或登錄頁面,也是沒有關(guān)系的,用戶可以直接再次請(qǐng)求游戲大廳頁面,只要重新請(qǐng)求這個(gè)過程的時(shí)間不超出定時(shí)銷毀的時(shí)間,那么是可以成功跳轉(zhuǎn)到游戲大廳頁面的,因?yàn)闀?huì)話在第一次登錄創(chuàng)建成功后,還沒有被銷毀。)
3.
第一個(gè)if else分支語句中,我們什么都不做就好,因?yàn)闀?huì)話被創(chuàng)建出來,你沒有向他添加任何定時(shí)任務(wù),那他默認(rèn)就是永久存在的。
第二個(gè)if else分支語句中,也很簡單,我們只需要通過調(diào)用_svr里面的set_timer接口,設(shè)置SESSION_TIMEOUT時(shí)間之后執(zhí)行銷毀session的任務(wù)函數(shù)即可,也就是調(diào)用destroy_session函數(shù),這里使用bind時(shí),也是采用綁死參數(shù)的方式來進(jìn)行,直接綁死參數(shù)this和ssid,則在SESSION_TIMEOUT時(shí)間之后,會(huì)話就會(huì)自動(dòng)被刪除。值得注意的是我們需要接收set_timer的返回值,也就是定時(shí)器對(duì)象,然后把這個(gè)定時(shí)器對(duì)象設(shè)置到會(huì)話的成員變量里面,表示這個(gè)會(huì)話現(xiàn)在已經(jīng)是有定時(shí)銷毀的任務(wù)了的。
第三個(gè)if else分支語句中,需要從定時(shí)刪除設(shè)置為永久存在,這里實(shí)現(xiàn)的時(shí)候,就比較麻煩了,因?yàn)槲覀冃枰热∠瓉頃?huì)話的定時(shí)刪除任務(wù),然后將會(huì)話搞成永久存在。
但是這里就有一個(gè)問題,取消原來的定時(shí)刪除任務(wù)會(huì)導(dǎo)致任務(wù)被立即執(zhí)行啊,那也就是說一旦cancel之后,會(huì)話就會(huì)被刪除了啊,那我們?cè)趺锤愠鰜硪粋€(gè)永久存在的會(huì)話呢?其實(shí)很簡單,我們?cè)偻鵢sessions這個(gè)哈希表里面添加一個(gè)鍵值對(duì)不就好了嗎?在函數(shù)的最開始部分,我們保存過當(dāng)前會(huì)話的會(huì)話句柄session_ptr sp啊,這是一個(gè)臨時(shí)對(duì)象,那現(xiàn)在我們重新構(gòu)建sp和ssid的映射關(guān)系,搞成一個(gè)鍵值對(duì),然后將鍵值對(duì)插入到_sessions里面不就行了嗎?
話說的一點(diǎn)問題都沒有,但是吧!這里還有一個(gè)bug,那就是cancel的任務(wù)確實(shí)會(huì)被執(zhí)行,但他不是被立馬執(zhí)行的,他是要等websocketpp庫里面統(tǒng)一挨個(gè)執(zhí)行定時(shí)任務(wù)隊(duì)列里面的定時(shí)任務(wù)時(shí),才會(huì)被執(zhí)行的!
所以有可能在我們添加新的鍵值對(duì)之后,cancle導(dǎo)致的定時(shí)任務(wù)destroy_session才會(huì)被執(zhí)行,那么此時(shí)就會(huì)導(dǎo)致我們剛剛立馬插入的鍵值對(duì)就被刪除掉了,那此時(shí)會(huì)話就沒有了,這就是一種錯(cuò)誤!所以一定不能立馬添加鍵值對(duì)!那怎么添加呢?通過設(shè)置定時(shí)任務(wù)來添加!
也就是調(diào)用set_timer,在0ms之后執(zhí)行append_already_session,這個(gè)定時(shí)任務(wù)的執(zhí)行,也不是立馬被執(zhí)行的,你可以理解為websocketpp庫里面有一個(gè)定時(shí)任務(wù)的隊(duì)列,set_timer的作用就是向這個(gè)隊(duì)列立馬添加定時(shí)執(zhí)行的函數(shù)元素,在等到真正執(zhí)行定時(shí)任務(wù)的時(shí)候,websocketpp會(huì)按照隊(duì)列的先后順序依次調(diào)用并執(zhí)行這些定時(shí)任務(wù),所以在設(shè)置append_already_session為定時(shí)任務(wù)后,那么該函數(shù)在被執(zhí)行時(shí),他的前一個(gè)定時(shí)任務(wù)元素,也就是cancle造成的destroy_session一定會(huì)先被執(zhí)行,那么此時(shí)的邏輯才是正確的!
(需要多說一嘴的是,在unordered_map中,如果我們插入具有相同的key的鍵值對(duì)時(shí),哈希表并不會(huì)報(bào)錯(cuò),而是會(huì)將新的鍵值對(duì)覆蓋掉原來舊的鍵值對(duì)?。?br>第四個(gè)if else分支語句中,需要從定時(shí)刪除設(shè)置為定時(shí)刪除,實(shí)現(xiàn)的方式就是在第三個(gè)分支語句的基礎(chǔ)上,多增加了一次的定時(shí)刪除任務(wù),先把原來的取消了,然后添加一個(gè)永久的會(huì)話,然后再給這個(gè)會(huì)話添加定時(shí)刪除任務(wù),最后別忘記把tmp這個(gè)定時(shí)器對(duì)象設(shè)置到session這個(gè)類的成員變量里面,通過調(diào)用會(huì)話句柄sp指向的set_timer接口來實(shí)現(xiàn)(一定要區(qū)分開兩個(gè)set_timer接口,我們自己實(shí)現(xiàn)的set_timer和websocketpp庫里面的set_timer重名了,但參數(shù)和返回值都是不一樣的,大家一定不要搞混了)。
4.
session類里面還需要一個(gè)接口,通過uid來判斷用戶的會(huì)話是否已經(jīng)存在了,如果存在,那就返回會(huì)話的句柄,如果不存在,那就返回一個(gè)空句柄。
(實(shí)現(xiàn)這個(gè)接口的原因,主要是服務(wù)器模塊處理登錄功能的時(shí)候,需要判斷用戶是否處于二次登錄狀態(tài),如果是二次登錄狀態(tài),并且第一次會(huì)話沒有過期,那么是不需要重新為用戶創(chuàng)建會(huì)話的,所以我們需要有一個(gè)接口來實(shí)現(xiàn)通過uid判斷會(huì)話管理模塊中會(huì)話是否存在這樣的功能。不過這樣的方式不太推薦,因?yàn)楸闅v的效率太低,正確的方式還是當(dāng)用戶反復(fù)登錄時(shí),每次登錄服務(wù)器都為用戶重新創(chuàng)建一個(gè)新的定時(shí)銷毀的session。)
(這個(gè)接口是我自己額外加進(jìn)去的,大家看個(gè)樂子就行,原生項(xiàng)目里面是沒有這個(gè)接口的,我這樣的想法也不太合適,刷新定時(shí)銷毀的過期時(shí)間不應(yīng)該在用戶反復(fù)登錄這里體現(xiàn),況且這樣的操作也不合理,而且還得遍歷哈希表,所以最好還是不要提供這個(gè)接口)
5.
下面我會(huì)為大家演示不同情況下會(huì)話的創(chuàng)建和銷毀過程,為了讓實(shí)驗(yàn)的進(jìn)度變得快些,我將SESSION_TIMEOUT設(shè)置為15000ms,也就是15s后會(huì)話定時(shí)刪除。
登錄成功,創(chuàng)建15s后定時(shí)銷毀的會(huì)話,我們15s無操作,跳轉(zhuǎn)到游戲大廳后,游戲大廳頁面會(huì)向服務(wù)器發(fā)起websocket長連接請(qǐng)求,服務(wù)器收到請(qǐng)求的第一件事情就是進(jìn)行會(huì)話驗(yàn)證,如果會(huì)話不存在,則跳轉(zhuǎn)回登錄頁面,進(jìn)行重新登錄,并以消息框的方式報(bào)錯(cuò),登錄過期請(qǐng)重新登錄。
頁面跳轉(zhuǎn)到游戲大廳后,長連接建立成功,則session變?yōu)橛谰么嬖?,?5s之后也可以看到,會(huì)話是不會(huì)被銷毀的。
進(jìn)入游戲大廳后,會(huì)話變?yōu)橛谰么嬖冢敲串?dāng)我們關(guān)閉游戲大廳頁面之后,會(huì)話就會(huì)從永久存在變?yōu)槎〞r(shí)銷毀,在服務(wù)器終端上可以看到15s過后會(huì)話被銷毀了。
在初次登錄成功后,剛創(chuàng)建的會(huì)話會(huì)保持15s的時(shí)間,在這段時(shí)間里,我們可以重新訪問游戲大廳,重新向服務(wù)器發(fā)起websocket長連接握手,此時(shí)會(huì)話就會(huì)從定時(shí)銷毀重新變?yōu)橛谰么嬖?,并且?5s之后,會(huì)話是不會(huì)被刪除的
第一次登錄成功后,服務(wù)器為我們創(chuàng)建了15s后銷毀的會(huì)話,此時(shí)我們將頁面關(guān)閉,重新進(jìn)行登錄,并且把這個(gè)過程控制在15s內(nèi)完成,那么原來的會(huì)話過期時(shí)間就會(huì)被刷新。
(上面這種情況大家看個(gè)樂子就行,項(xiàng)目中正確的刷新會(huì)話過期時(shí)間應(yīng)該是下面這種情況,上面我自己所說的這種情況,如果硬要實(shí)現(xiàn),當(dāng)然是可以實(shí)現(xiàn)的,但不推薦實(shí)現(xiàn)這個(gè),因?yàn)樾时容^低,我們需要在后端遍歷session管理器的所有鍵值對(duì),并且上面這樣的思想也是不合適的,正確的想法就應(yīng)該是用戶每一次登錄成功,服務(wù)器為用戶創(chuàng)建一次session,你把上次用戶登錄創(chuàng)建好的session給刷新保留下來,能提高多少服務(wù)器的效率???提高到?jīng)]多少,你后端還需要遍歷session管理器中的所有鍵值對(duì),整體服務(wù)器的效率還是降低的!
所以我上面敘述的這樣的處理方式看個(gè)樂子就行,下一篇博文講述封裝服務(wù)器模塊代碼時(shí)候,我會(huì)再說明一下登錄業(yè)務(wù)處理的邏輯,不要判斷處理這種反復(fù)登錄請(qǐng)求業(yè)務(wù),從而使用同一個(gè)session的情況。)
還有一種情況是,進(jìn)入游戲大廳后,前端會(huì)通過ajax發(fā)送http請(qǐng)求來獲取到用戶詳細(xì)信息并展示到前端頁面上,這個(gè)過程也會(huì)觸發(fā)刷新會(huì)話過期時(shí)間。
(這種情況是本項(xiàng)目中唯一體現(xiàn)出刷新定時(shí)銷毀session過期時(shí)間的情況!上面那種不算,僅僅是本人腦子里的一個(gè)小idea而已!)
從實(shí)驗(yàn)現(xiàn)象可以看到,前后兩次登錄用的是同一個(gè)session,第二次登錄刷新了第一次登錄所創(chuàng)建的session的定時(shí)銷毀時(shí)間。文章來源:http://www.zghlxwxcb.cn/news/detail-731840.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-731840.html
到了這里,關(guān)于【項(xiàng)目設(shè)計(jì)】網(wǎng)絡(luò)對(duì)戰(zhàn)五子棋(上)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!