俗話說的好,架構歷來都不是一蹴而就的,沒有什麼架構一開始設計就是最終版本,其中須要通過不少步驟的變化,今天咱們就從一個最簡單的例子來看看,究竟架構這個東西是怎麼變的。
我將從一個最簡單的聊天室的功能來實現,看看這樣一個提及來好像很簡單但的功能,咱們須要考慮哪些問題。html
我使用golang實現,從0開始實現,須要藉助的是websocket來實現即時,基礎知識本身補一下,這裏不作過多贅述。git
即時聊天室包含功能(這裏寫出的功能假設就是產品經理告訴咱們的):
一、所用用戶能鏈接聊天室
二、鏈接成功的用戶能向聊天室發送消息
三、全部成功鏈接的用戶能收到聊天室的消息github
爲了簡化,咱們暫定只有一個房間,由於即便要求須要多個房間和一個房間差很少;而後咱們簡化消息存儲,咱們默認也不持久化消息,由於消息的持久化就會涉及各類數據庫操做還有分頁查詢,這裏暫時不作考慮。golang
那麼你必定奇怪了,這些都沒了,那整個實現還有啥難度?你大能夠本身先想想若是是你,你會怎麼樣去實現。web
下文中我會用C表明客戶端,S表明服務端
(本文爲了展現架構的演變,若是你能想到更好的架構或者一開始就直接想到最終版本,那麼證實你已經有不少的經驗積累了,給大佬遞茶)數據庫
各個版本和測試客戶端全部的代碼都已經上傳github,若是有須要請查看,https://github.com/LinkinStars/simple-chatroom編程
第一個版本確定是最簡單的版本,咱們就筆直朝着目標走。
咱們知道websocket能實現最基本的通訊。
客戶端發送消息,服務端接收消息,C -> S
服務端發送消息,客戶端接收消息,S -> C安全
那麼聊天室就是:不少C發消息給S
S將全部收到的消息發給每個C
那麼咱們的第一個架構就很容易想到是這樣子的:服務器
咱們在服務端維護一個鏈接池,鏈接池中保存了鏈接的用戶,每當服務端收到一個消息以後,就遍歷一遍鏈接池,將這個消息發送給全部鏈接池中的人。流程圖以下:
微信
那麼下面,咱們用代碼來實現一下
首先定義Room裏面有一個鏈接池

而後咱們寫一個處理websocket的方法

最後寫一個羣發消息,遍歷鏈接池,發送消息

補全其餘部分,就完成了,這就是咱們第一個版本,而後咱們用一個測試的html測試一下

嗯,完成啦~我真棒,真簡單
固然不可能那麼簡單!!!還有不少問題!
針對於第一個版本,那麼存在的問題還有
一、咱們發現,當用戶斷開鏈接的時候,鏈接池裏面這個鏈接沒有被移除,因此消息發送的時候會報錯,並且鏈接池會一直變大。
二、用戶不少,遍歷發送消息是一個耗時的操做,不該該被阻塞
針對這兩個問題改動以下:
一、當發送消息失敗,證實鏈接已經斷開,因此從鏈接池中移除鏈接
二、羣發消息改成gorutinue
因此V1.1修改以下


到此爲止,第一個版本就到這裏了,由於聰明的你應該已經發現這樣設計的架構存在一個巨大的問題...
若是你有必定的併發編程的經驗就會發現,上面版本有一個很危險的併發操做,那就是鏈接池。
咱們假設一種狀況,當一個協程正在遍歷鏈接池發送消息的時候,另一個協程把其中一些鏈接刪除了,還有一個協程把新的鏈接加進去了,這樣的操做就是傳說中的併發問題。
並且對於websocket來講還有一個問題,就是若是併發去對同一個鏈接發送消息的話就會出現panic: concurrent write to websocket connection這樣的異常,由於是panic因此問題就很是大了。
併發問題怎麼解決?不少人會說,簡單,加鎖就完事了



加完了,搞定,這下沒問題了吧。這就是版本2。由於加入了鎖機制,因此併發安全保證了,可是
新的問題又出現了,咱們若是咱們在發送消息的方法中加入延時,模擬出發送消息網絡不正常的狀況
time.Sleep(time.Second * 2)
那麼你就會發現,當新的用戶加入的時候,由於當前還有消息正在發送,因此致使新加入的用戶沒有辦法獲取到鎖,也就沒法發送消息
那怎麼辦呢?
而後順便說一下,由於鎖的是room在必定併發的程度上仍是有可能出現異常

我在開發golang的時候有這樣一個信念,有鎖的地方必定能用channel優化,從而面向併發編程,雖然並不是絕對,可是golang提供的channel不少狀況下都能將鎖給替換掉,從而換取出性能的提高,具體怎麼作呢?
首先咱們想一下有哪些地方能夠利用channel進行解耦
一、第一次鏈接,咱們將鏈接扔進一個信道中去
二、斷開鏈接,咱們將要刪除的鏈接扔進一個信道中去
三、發送消息,咱們每一個鏈接對象都有一個信道,只須要將消息寫入這個信道就能發送消息
因此咱們從新調整一下架構,圖以下:

而後咱們看看代碼上面如何實現:
首先定義一個客戶端

包含一個鏈接和一個發送消息的專用信道
而後定義客戶端的兩個方法

當從websocket中獲取到信息的時候,將消息丟到chatRoom的總髮送信道中去,由chatRoom去羣發。
當本身的send信道中有消息時,將消息經過websocket發送給客戶端。
同時當發送或者接收消息出現異常,將本身發送給取消註冊的信道,由chatRoom去移除註冊信息。
而後定義聊天室

register用於處理註冊
unregister用於處理移除註冊
clientsPool這裏更換爲map,方便移除
send是總髮送消息信道,用於羣發消息
而後定義處理websocket方法

當前第一次來的時候就建立客戶端,而後啓動客戶端的讀取和發送方法,而且將本身發給註冊信道
最後最重要的就是如何去調度處理chatRoom中全部的管道,咱們使用select

當有註冊的時候就註冊,當有離開的時候就刪除,當須要發送消息的時候,消息會發送給每個client各自的send信道由它們本身發送。
這樣就成功實現了使用channel代替了原來的鎖

當前羣發消息和客戶的加入退出就基本不受到影響了,隨時能夠加入和退出,一旦加入就會收到消息。
一切看似很完美吧,其實還有些bug,咱們建立一些客戶端進行壓測試試看。
編寫壓測代碼以下,由於壓測就是建立不少客戶端發送消息,這裏就很少作贅述了


而後會發現,測試的過程當中,若是你啓動一個網頁版本的客戶端發現,你的消息發不出去了。這是爲何呢?
原來咱們以前在處理全部管道中任務的時候當處理髮送消息的時候有問題,雖然send是一個有緩衝的通道,可是當緩衝滿的時候,那麼就會阻塞,沒法向裏面再發送消息,須要等待send裏面的消息被消費,可是若是send裏面的消息要被消費,前提就是要輪到這個消息被髮送,因而形成了循環等待,必定意義上的死鎖。(有點繞,你須要理一理)

因此咱們須要修改一下代碼,修復這個bug,當消息沒法寫入send信道的時候,那就直接將這個消息拋棄(雖然這樣處理好像不太科學),由於要不就是這個用戶已經斷開鏈接,要不就是這個用戶的緩衝信道已經佔滿了。以下:

其實在作的過程當中就發現了一些問題,一個問題同一個用戶若是不停的發送消息,那麼一方面是會對服務器形成壓力,另外一方面對於別的用戶來講這是一種騷擾,因此咱們須要限制用戶發送消息的頻率。這裏爲了測試方便,針對於同一個用戶1秒內只能發送一條消息,這樣從必定程度上也減小了併發問題的出現。
改動很是簡單,以下:

咱們啓動多個客戶端定時的發送一些消息進行測試,5個客戶端下每1ms發送一條消息,本機測試下來沒有問題。(固然這個版本)
那麼到如今咱們已經實際了聊天室的基本功能,對於一個最簡單的聊天來講已經足夠了,可是由於咱們簡化了不少細節,因此存在不少優化的地方,下面列舉幾個地方能夠作後續的優化和升級。
一、消息持久化,當前消息發送以後若是當時用戶不在線就沒法收到,這樣對於用戶來講實際上是很難受的,因此消息須要進行持久化,而持久化就會有不少方案,保存消息的方式,以及保存消息的時間,不能由於保存消息而影響即時性。以及用戶再次登陸以後須要將以後保存的消息返回給用戶。
二、消息id,咱們如今發送消息的時候是不帶消息id的,可是其實做爲消息自己,消息的發送須要保證冪等性,相同的消息(消息id相同)不該該發送屢次,因此消息id的生成,如何保證消息不重複也是須要考慮的。
三、消息不丟失,消息持久化,網絡異常都有可能致使消息丟失,如何保證消息不丟失呢?
四、密集型消息分發,當用戶人數不少,當前會建立不少的協程去分發消息,人一多確定就不行了,並且人一多,一臺機器確定不夠,那麼分佈式維護鏈接池等等架構的調整就須要進行了。
五、心跳保活,鏈接一段時間以後,因爲網絡的緣由或者別的緣由,可能會致使鏈接中斷的狀況出現,因此通過一段時間就須要發送一些消息保持鏈接。相似PING\PONG
六、鑑權,這個簡單,當前任何用戶連上就能發送消息,理論上來講,其實須要通過鑑權以後才能發送消息。
七、消息加密,如今消息都是明文傳輸的,這樣傳遞消息實際上是不安全的,因此加密傳輸消息也是後期能夠考慮的,同時消息的壓縮也是。
這些後續的擴展就要你來思考一下了,如何去實現。設計的時候你也能夠參考不少現實中已經存在的一些例子來幫助你思考。在咱們實現的時候也沒有藉助任何的中間件,因此你能夠後期考慮使用一些中間件來完成分佈式等要求,如mq等。
是否是看到這裏發現只是簡單的一個即時聊天后面的架構擴展都是很是可怕的,若是真的要作到像微信或者qq那樣隨意的單聊和羣聊,而且解決各類併發問題還有不少路要走。
若是你有一些本身的想法,也歡迎在下面留言討論。
這裏其實想說明的並非如何去設計一個IM,想要真正說明的是一個架構師如何進行演變的,其中須要考慮到哪些問題,這些問題又是如何被解決的。其中須要經歷不斷的測試,調整,測試,調整。還想說明的是,架構沒有好和壞,只有適合與否,對於一個小的項目來講就沒有必要用大架構,合適的纔是最好的。
最後,也確定有人想了解一些大型的聊天im的架構,這裏有幾篇博客我認爲寫的很不錯,能夠參考一下。
下面這兩篇是對一些大型架構的說明
https://alexstocks.github.io/html/pubsub.html
https://alexstocks.github.io/html/im.html
下面是一些github上的項目
https://github.com/alberliu/goim
這個項目比較簡單,容易理解,文檔介紹詳細解釋了不少概念,具體使用nsq來實現消息的轉發
https://github.com/Terry-Mao/goim
這個項目相對複雜,運用到的東西就比較多,須要必定的理解,同時擴展性就相對不錯
做者:LinkinStar