golang從簡單的即時聊天來看架構演變


前言

俗話說的好,架構從來都不是一蹴而就的,沒有什么架構一開始設計就是最終版本,其中需要經過很多步驟的變化,今天我們就從一個最簡單的例子來看看,究竟架構這個東西是怎么變的。
我將從一個最簡單的聊天室的功能來實現,看看這樣一個說起來好像很簡單但的功能,我們需要考慮哪些問題。

我使用golang實現,從0開始實現,需要借助的是websocket來實現即時,基礎知識自己補一下,這里不做過多贅述。

 

功能描述

即時聊天室包含功能(這里寫出的功能假設就是產品經理告訴我們的):
1、所用用戶能連接聊天室
2、連接成功的用戶能向聊天室發送消息
3、所有成功連接的用戶能收到聊天室的消息

為了簡化,我們暫定只有一個房間,因為即使要求需要多個房間和一個房間差不多;然后我們簡化消息存儲,我們默認也不持久化消息,因為消息的持久化就會涉及各種數據庫操作還有分頁查詢,這里暫時不做考慮。

那么你一定奇怪了,這些都沒了,那整個實現還有啥難度?你大可以自己先想一想如果是你,你會怎么樣去實現。

下文中我會用C代表客戶端,S代表服務端
(本文為了展示架構的演變,如果你能想到更好的架構或者一開始就直接想到最終版本,那么證明你已經有很多的經驗積累了,給大佬遞茶)

 

各個版本和測試客戶端所有的代碼都已經上傳github,如果有需要請查看,https://github.com/LinkinStars/simple-chatroom

 

版本1

第一個版本肯定是最簡單的版本,我們就筆直朝着目標走。
我們知道websocket能實現最基本的通信。
客戶端發送消息,服務端接收消息,C -> S
服務端發送消息,客戶端接收消息,S -> C

那么聊天室就是:很多C發消息給S
S將所有收到的消息發給每一個C
那么我們的第一個架構就很容易想到是這樣子的:

我們在服務端維護一個連接池,連接池中保存了連接的用戶,每當服務端收到一個消息之后,就遍歷一遍連接池,將這個消息發送給所有連接池中的人。流程圖如下:

那么下面,我們用代碼來實現一下
首先定義Room里面有一個連接池

然后我們寫一個處理websocket的方法

最后寫一個群發消息,遍歷連接池,發送消息

補全其他部分,就完成了,這就是我們第一個版本,然后我們用一個測試的html測試一下

嗯,完成啦~我真棒,真簡單

當然不可能那么簡單!!!還有很多問題!
針對於第一個版本,那么存在的問題還有
1、我們發現,當用戶斷開連接的時候,連接池里面這個連接沒有被移除,所以消息發送的時候會報錯,而且連接池會一直變大。
2、用戶很多,遍歷發送消息是一個耗時的操作,不應該被阻塞

針對這兩個問題改動如下:
1、當發送消息失敗,證明連接已經斷開,所以從連接池中移除連接
2、群發消息改為gorutinue

 

版本1.1

所以V1.1修改如下

到此為止,第一個版本就到這里了,因為聰明的你應該已經發現這樣設計的架構存在一個巨大的問題...

 

版本2

如果你有一定的並發編程的經驗就會發現,上面版本有一個很危險的並發操作,那就是連接池。

  • 連接池的並發操作: 新的用戶進來需要添加入連接池 如果用戶斷開連接需要移出連接池 每次發送消息需要遍歷連接池

我們假設一種情況,當一個協程正在遍歷連接池發送消息的時候,另外一個協程把其中一些連接刪除了,還有一個協程把新的連接加進去了,這樣的操作就是傳說中的並發問題。

而且對於websocket來說還有一個問題,就是如果並發去對同一個連接發送消息的話就會出現panic: concurrent write to websocket connection這樣的異常,因為是panic所以問題就非常大了。

並發問題怎么解決?很多人會說,簡單,加鎖就完事了



加完了,搞定,這下沒問題了吧。這就是版本2。因為加入了鎖機制,所以並發安全保證了,但是

新的問題又出現了,我們如果我們在發送消息的方法中加入延時,模擬出發送消息網絡不正常的情況
time.Sleep(time.Second * 2)
那么你就會發現,當新的用戶加入的時候,因為當前還有消息正在發送,所以導致新加入的用戶沒有辦法獲取到鎖,也就無法發送消息
那怎么辦呢?

然后順便說一下,因為鎖的是room在一定並發的程度上還是有可能出現異常

版本3

我在開發golang的時候有這樣一個信念,有鎖的地方一定能用channel優化,從而面向並發編程,雖然並非絕對,但是golang提供的channel很多情況下都能將鎖給替換掉,從而換取出性能的提升,具體怎么做呢?
首先我們想一下有哪些地方可以利用channel進行解耦
1、第一次連接,我們將連接扔進一個信道中去
2、斷開連接,我們將要刪除的連接扔進一個信道中去
3、發送消息,我們每個連接對象都有一個信道,只需要將消息寫入這個信道就能發送消息

所以我們重新調整一下架構,圖如下:

然后我們看看代碼上面如何實現:

首先定義一個客戶端

包含一個連接和一個發送消息的專用信道
然后定義客戶端的兩個方法

當從websocket中獲取到信息的時候,將消息丟到chatRoom的總發送信道中去,由chatRoom去群發。
當自己的send信道中有消息時,將消息通過websocket發送給客戶端。
同時當發送或者接收消息出現異常,將自己發送給取消注冊的信道,由chatRoom去移除注冊信息。

然后定義聊天室

register用於處理注冊
unregister用於處理移除注冊
clientsPool這里更換為map,方便移除
send是總發送消息信道,用於群發消息

然后定義處理websocket方法

當前第一次來的時候就創建客戶端,然后啟動客戶端的讀取和發送方法,並且將自己發給注冊信道

最后最重要的就是如何去調度處理chatRoom中所有的管道,我們使用select

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

當前群發消息和客戶的加入退出就基本不受到影響了,隨時可以加入和退出,一旦加入就會收到消息。
一切看似很完美吧,其實還有些bug,我們創建一些客戶端進行壓測試試看。

 

版本3.1

編寫壓測代碼如下,因為壓測就是創建很多客戶端發送消息,這里就不多做贅述了

然后會發現,測試的過程中,如果你啟動一個網頁版本的客戶端發現,你的消息發不出去了。這是為什么呢?
原來我們之前在處理所有管道中任務的時候當處理發送消息的時候有問題,雖然send是一個有緩沖的通道,但是當緩沖滿的時候,那么就會阻塞,無法向里面再發送消息,需要等待send里面的消息被消費,但是如果send里面的消息要被消費,前提就是要輪到這個消息被發送,於是造成了循環等待,一定意義上的死鎖。(有點繞,你需要理一理)

所以我們需要修改一下代碼,修復這個bug,當消息無法寫入send信道的時候,那就直接將這個消息拋棄(雖然這樣處理好像不太科學),因為要不就是這個用戶已經斷開連接,要不就是這個用戶的緩沖信道已經占滿了。如下:

 

版本3.2

其實在做的過程中就發現了一些問題,一個問題同一個用戶如果不停的發送消息,那么一方面是會對服務器造成壓力,另一方面對於別的用戶來說這是一種騷擾,所以我們需要限制用戶發送消息的頻率。這里為了測試方便,針對於同一個用戶1秒內只能發送一條消息,這樣從一定程度上也減少了並發問題的出現。

改動非常簡單,如下:

我們啟動多個客戶端定時的發送一些消息進行測試,5個客戶端下每1ms發送一條消息,本機測試下來沒有問題。(當然這個版本)

 

后續版本

那么到現在我們已經實際了聊天室的基本功能,對於一個最簡單的聊天來說已經足夠了,但是因為我們簡化了很多細節,所以存在很多優化的地方,下面列舉幾個地方可以做后續的優化和升級。

1、消息持久化,當前消息發送之后如果當時用戶不在線就無法收到,這樣對於用戶來說其實是很難受的,所以消息需要進行持久化,而持久化就會有很多方案,保存消息的方式,以及保存消息的時間,不能因為保存消息而影響即時性。以及用戶再次登錄之后需要將之后保存的消息返回給用戶。

2、消息id,我們現在發送消息的時候是不帶消息id的,但是其實作為消息本身,消息的發送需要保證冪等性,相同的消息(消息id相同)不應該發送多次,所以消息id的生成,如何保證消息不重復也是需要考慮的。

3、消息不丟失,消息持久化,網絡異常都有可能導致消息丟失,如何保證消息不丟失呢?

4、密集型消息分發,當用戶人數很多,當前會創建很多的協程去分發消息,人一多肯定就不行了,而且人一多,一台機器肯定不夠,那么分布式維護連接池等等架構的調整就需要進行了。

5、心跳保活,連接一段時間之后,由於網絡的原因或者別的原因,可能會導致連接中斷的情況出現,所以經過一段時間就需要發送一些消息保持連接。類似PING\PONG

6、鑒權,這個簡單,當前任何用戶連上就能發送消息,理論上來說,其實需要經過鑒權之后才能發送消息。

7、消息加密,現在消息都是明文傳輸的,這樣傳遞消息其實是不安全的,所以加密傳輸消息也是后期可以考慮的,同時消息的壓縮也是。

這些后續的擴展就要你來思考一下了,如何去實現。設計的時候你也可以參考很多現實中已經存在的一些例子來幫助你思考。在我們實現的時候也沒有借助任何的中間件,所以你可以后期考慮使用一些中間件來完成分布式等要求,如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

轉載請注明出處:https://www.cnblogs.com/linkstar/p/10776994.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM