用 Go 編寫一個簡單的 WebSocket 推送服務

用 Go 編寫一個簡單的 WebSocket 推送服務

本文中代碼可以在 github.com/alfred-zhon… 獲取。

背景

最近拿到需求要在網頁上展示報警信息。以往報警信息都是通過短信,微信和 App 推送給用戶的,現在要讓登錄用戶在網頁端也能實時接收到報警推送。

依稀記得以前工作的時候遇到過類似的需求。因為以前的瀏覽器標準比較陳舊,并且那時用 Java 較多,所以那時候解決這個問題就用了 Comet4J。具體的原理就是長輪詢,長鏈接。但現在畢竟 html5 流行開來了,IE 都被 Edge 接替了,再用以前這種技術就顯得過時。

很早以前就聽過 WebSocket 的大名,但因為那時很多用戶的瀏覽器還不支持,所以對這個技術也就是淺嘗輒止,沒有太深入研究過。現在趁著項目需要,就來稍微深入了解一下。

websocket 簡介

以往瀏覽器要獲取服務端數據,都是通過發送 HTTP 請求,然后等待服務端回應的。也就是說瀏覽器端一直是整個請求的發起者,只有它主動,才能獲取到數據。而要讓瀏覽器一側能夠獲取到服務端的實時數據,就需要不停地向服務端發起請求。雖然大多數情況下并沒有獲取到實際數據,但這大大增加了網絡壓力,對于服務端來說壓力也直線上升。

后來我們學會了使用長連接 + 長輪詢的方式。換句話說,也就是延長 HTTP 請求的存在時間,盡量保持 HTTP 連接。雖然這在一定程度上降低了不少壓力,但仍然需要不停地進行輪詢,也做不到真正的實時性。(借用一張圖)

隨著 HTML5 的到來,WebSocket 在 2011 年被定為標準(詳情請參見 RFC 6455)。

借用 《Go Web 編程》的話。WebSocket 采用了一些特殊的報頭,使得瀏覽器和服務器只需要做一個握手的動作,就可以在瀏覽器和服務器之間建立一條連接通道。且此連接會保持在活動狀態,你可以使用 JavaScript 來向連接寫入或從中接收數據,就像在使用一個常規的 TCP Socket 一樣。它解決了 Web 實時化的問題。

由于 WebSocket 是全雙工通信,所以當建立了 WebSocket 連接之后,接下來的通信就類似于傳統的 TCP 通信了。客戶端和服務端可以相互發送數據,不再有實時性的問題。

開發包的選擇

在 Go 官方的 SDK 中,并不包含對 WebSocket 的支持,所以必須使用第三方庫。

要使用 Golang 開發 WebSocket,選擇基本就在 x/net/websocket 和 gorilla/websocket 之間。《Go Web 編程》一書中的例子使用了 x/net/websocket 作為開發包,而且貌似它也更加官方且正式。而實際根據我在網上查詢得到的反饋看來,并非如此。x/net/websocket 貌似 Bug 較多,且較為不穩定,問題解決也并不及時。相比之下,gorilla/websocket 則更加優秀。

還有對于 Gorilla web toolkit 組織的貢獻,必須予以感謝。?。其下不僅有 WebSocket 的實現,也有一些其他工具。歡迎大家使用并且能夠給予反饋或貢獻。

推送服務實現

基本原理

項目初步設計如下:

server 啟動以后會注冊兩個 Handler。

  • websocketHandler 用于提供瀏覽器端發送 Upgrade 請求并升級為 WebSocket 連接。
  • pushHandler 用于提供外部推送端發送推送數據的請求。

瀏覽器首先連接 websocketHandler (默認地址為 ws://ip:port/ws)升級請求為 WebSocket 連接,當連接建立之后需要發送注冊信息進行注冊。這里注冊信息中包含一個 token 信息。server 會對提供的 token 進行驗證并獲取到相應的 userId(通常來說,一個 userId 可能同時關聯許多 token),并保存維護好 token, userId 和 conn(連接)之間的關系。

推送端發送推送數據的請求到 pushHandler(默認地址為 ws://ip:port/push),請求中包含了 userId 字段和 message 字段。server 會根據 userId 獲取到所有此時連接到該 server 的 conn,然后將 message 一一進行推送。

由于推送服務的實時性,推送的數據并沒有也不需要進行緩存。

代碼詳解

我在此處會稍微講述一下代碼的基本構成,也順便說說 Go 語言中一些常用的寫法和模式(本人也是從其他語言轉向 Go 語言,畢竟 Go 語言也相當年輕。所以有建議的話,敬請提出。)。由于 Go 語言的發明人和一些主要維護者大都來自于 C/C++ 語言,所以 Go 語言的代碼也更偏向于 C/C++ 系。

首先先看一下 Server 的結構:

// Server defines parameters for running websocket server.
type Server struct {// Address for server to listen onAddr string// Path for websocket request, default "/ws".WSPath string// Path for push message, default "/push".PushPath string// Upgrader is for upgrade connection to websocket connection using// "github.com/gorilla/websocket".//// If Upgrader is nil, default upgrader will be used. Default upgrader is// set ReadBufferSize and WriteBufferSize to 1024, and CheckOrigin always// returns true.Upgrader *websocket.Upgrader// Check token if it's valid and return userID. If token is valid, userID// must be returned and ok should be true. Otherwise ok should be false.AuthToken func(token string) (userID string, ok bool)// Authorize push request. Message will be sent if it returns true,// otherwise the request will be discarded. Default nil and push request// will always be accepted.PushAuth func(r *http.Request) boolwh *websocketHandlerph *pushHandler
}
復制代碼

PS: 由于我整個項目的注釋都是用英文寫的,所以見諒了,希望不妨礙閱讀。

這里說一下 Upgrader *websocket.Upgrader,這是 gorilla/websocket 包的對象,它用來升級 HTTP 請求。

如果一個結構體參數過多,通常不建議直接初始化,而是使用它提供的 New 方法。這里是:

// NewServer creates a new Server.
func NewServer(addr string) *Server {return &Server{Addr:     addr,WSPath:   serverDefaultWSPath,PushPath: serverDefaultPushPath,}
}
復制代碼

這也是 Go 語言對外提供初始化方法的一種常見用法。

然后 Server 使用 ListenAndServe 方法啟動并監聽端口,與 http 包的使用類似:

// ListenAndServe listens on the TCP network address and handle websocket
// request.
func (s *Server) ListenAndServe() error {b := &binder{userID2EventConnMap: make(map[string]*[]eventConn),connID2UserIDMap:    make(map[string]string),}// websocket request handlerwh := websocketHandler{upgrader: defaultUpgrader,binder:   b,}if s.Upgrader != nil {wh.upgrader = s.Upgrader}if s.AuthToken != nil {wh.calcUserIDFunc = s.AuthToken}s.wh = &whhttp.Handle(s.WSPath, s.wh)// push request handlerph := pushHandler{binder: b,}if s.PushAuth != nil {ph.authFunc = s.PushAuth}s.ph = &phhttp.Handle(s.PushPath, s.ph)return http.ListenAndServe(s.Addr, nil)
}
復制代碼

這里我們生成了兩個 Handler,分別為 websocketHandlerpushHandlerwebsocketHandler 負責與瀏覽器建立連接并傳輸數據,而 pushHandler 則處理推送端的請求。可以看到,這里兩個 Handler 都封裝了一個 binder 對象。這個 binder 用于維護 token <-> userID <-> Conn 的關系:

// binder is defined to store the relation of userID and eventConn
type binder struct {mu sync.RWMutex// map stores key: userID and value of related slice of eventConnuserID2EventConnMap map[string]*[]eventConn// map stores key: connID and value: userIDconnID2UserIDMap map[string]string
}
復制代碼

websocketHandler

具體看一下 websocketHandler 的實現。

// websocketHandler defines to handle websocket upgrade request.
type websocketHandler struct {// upgrader is used to upgrade request.upgrader *websocket.Upgrader// binder stores relations about websocket connection and userID.binder *binder// calcUserIDFunc defines to calculate userID by token. The userID will// be equal to token if this function is nil.calcUserIDFunc func(token string) (userID string, ok bool)
}
復制代碼

很簡單的結構。websocketHandler 實現了 http.Handler 接口:

// First try to upgrade connection to websocket. If success, connection will
// be kept until client send close message or server drop them.
func (wh *websocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {wsConn, err := wh.upgrader.Upgrade(w, r, nil)if err != nil {return}defer wsConn.Close()// handle Websocket requestconn := NewConn(wsConn)conn.AfterReadFunc = func(messageType int, r io.Reader) {var rm RegisterMessagedecoder := json.NewDecoder(r)if err := decoder.Decode(&rm); err != nil {return}// calculate userID by tokenuserID := rm.Tokenif wh.calcUserIDFunc != nil {uID, ok := wh.calcUserIDFunc(rm.Token)if !ok {return}userID = uID}// bindwh.binder.Bind(userID, rm.Event, conn)}conn.BeforeCloseFunc = func() {// unbindwh.binder.Unbind(conn)}conn.Listen()
}
復制代碼

首先將傳入的 http.Request 轉換為 websocket.Conn,再將其分裝為我們自定義的一個 wserver.Conn(封裝,或者說是組合,是 Go 語言的典型用法。記住,Go 語言沒有繼承,只有組合)。然后設置了 ConnAfterReadFuncBeforeCloseFunc 方法,接著啟動了 conn.Listen()AfterReadFunc 意思是當 Conn 讀取到數據后,嘗試驗證并根據 token 計算 userID,然乎 bind 注冊綁定。BeforeCloseFunc 則為 Conn 關閉前進行解綁操作。

pushHandler

pushHandler 則容易理解。它解析請求然后推送數據:

// Authorize if needed. Then decode the request and push message to each
// realted websocket connection.
func (s *pushHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {if r.Method != http.MethodPost {w.WriteHeader(http.StatusMethodNotAllowed)return}// authorizeif s.authFunc != nil {if ok := s.authFunc(r); !ok {w.WriteHeader(http.StatusUnauthorized)return}}// read requestvar pm PushMessagedecoder := json.NewDecoder(r.Body)if err := decoder.Decode(&pm); err != nil {w.WriteHeader(http.StatusBadRequest)w.Write([]byte(ErrRequestIllegal.Error()))return}// validate the dataif pm.UserID == "" || pm.Event == "" || pm.Message == "" {w.WriteHeader(http.StatusBadRequest)w.Write([]byte(ErrRequestIllegal.Error()))return}cnt, err := s.push(pm.UserID, pm.Event, pm.Message)if err != nil {w.WriteHeader(http.StatusInternalServerError)w.Write([]byte(err.Error()))return}result := strings.NewReader(fmt.Sprintf("message sent to %d clients", cnt))io.Copy(w, result)
}
復制代碼

Conn

Conn (此處指 wserver.Conn) 為 websocket.Conn 的包裝。

// Conn wraps websocket.Conn with Conn. It defines to listen and read
// data from Conn.
type Conn struct {Conn *websocket.ConnAfterReadFunc   func(messageType int, r io.Reader)BeforeCloseFunc func()once   sync.Onceid     stringstopCh chan struct{}
}
復制代碼

最主要的方法為 Listen()

// Listen listens for receive data from websocket connection. It blocks
// until websocket connection is closed.
func (c *Conn) Listen() {c.Conn.SetCloseHandler(func(code int, text string) error {if c.BeforeCloseFunc != nil {c.BeforeCloseFunc()}if err := c.Close(); err != nil {log.Println(err)}message := websocket.FormatCloseMessage(code, "")c.Conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))return nil})// Keeps reading from Conn util get error.
ReadLoop:for {select {case <-c.stopCh:break ReadLoopdefault:messageType, r, err := c.Conn.NextReader()if err != nil {// TODO: handle read error maybebreak ReadLoop}if c.AfterReadFunc != nil {c.AfterReadFunc(messageType, r)}}}
}
復制代碼

主要設置了當 websocket 連接關閉時的處理和不停地讀取數據。

文中很難全面地描述整個代碼的運作流程,像具體閱讀代碼,請前往 github.com/alfred-zhon… 獲取。

后記

代碼我已經進行了一定的測試,也已經在正式環境中運行了一段時間。但是代碼可能仍然不夠穩定,所以在使用過程中出現問題,也實屬正常。隨意隨時歡迎大家給我提 issues 或者 PRs。

參考

  • 《Go Web 編程》 --- astaxie
  • Web 通信 之 長連接、長輪詢(long polling) --- hoojo
  • Gorilla web toolkit

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/390794.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/390794.shtml
英文地址,請注明出處:http://en.pswp.cn/news/390794.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

leetcode 231. 2 的冪

給你一個整數 n&#xff0c;請你判斷該整數是否是 2 的冪次方。如果是&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 如果存在一個整數 x 使得 n 2x &#xff0c;則認為 n 是 2 的冪次方。 示例 1&#xff1a; 輸入&#xff1a;n 1 輸出&#xff1a;tr…

Java概述、環境變量、注釋、關鍵字、標識符、常量

Java語言的特點 有很多小特點&#xff0c;重點有兩個開源&#xff0c;跨平臺 Java語言是跨平臺的 Java語言的平臺 JavaSE JavaME--Android JavaEE DK,JRE,JVM的作用及關系(掌握) (1)作用 JVM&#xff1a;保證Java語言跨平臺 &#xff0…

寫游戲軟件要學什么_為什么要寫關于您所知道的(或所學到的)的內容

寫游戲軟件要學什么Im either comfortably retired or unemployed, I havent decided which. What I do know is that I am not yet ready for decades of hard-won knowledge to lie fallow. Still driven to learn new technologies and to develop new projects, I see the …

leetcode 342. 4的冪

給定一個整數&#xff0c;寫一個函數來判斷它是否是 4 的冪次方。如果是&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 整數 n 是 4 的冪次方需滿足&#xff1a;存在整數 x 使得 n 4x 示例 1&#xff1a; 輸入&#xff1a;n 16 輸出&#xff1a;true …

梯度反傳_反事實政策梯度解釋

梯度反傳Among many of its challenges, multi-agent reinforcement learning has one obstacle that is overlooked: “credit assignment.” To explain this concept, let’s first take a look at an example…在許多挑戰中&#xff0c;多主體強化學習有一個被忽略的障礙&a…

三款功能強大代碼比較工具Beyond compare、DiffMerge、WinMerge

我們經常會遇到需要比較同一文件的不同版本&#xff0c;特別是代碼文件。如果人工去對比查看&#xff0c;勢必費時實力還會出現紕漏和錯誤&#xff0c;因此我們需要借助一些代碼比較的工具來自動完成這些工作。這里介紹3款比較流行且功能強大的工具。 1. Beyond compare這是一款…

shell腳本_Shell腳本

shell腳本In the command line, a shell script is an executable file that contains a set of instructions that the shell will execute. Its main purpose is to reduce a set of instructions (or commands) in just one file. Also it can handle some logic because it…

大數據與Hadoop

大數據的定義 大數據是指無法在一定時間內用常規軟件工具對其內容進行抓取、管理和處理的數據集合。 大數據的概念–4VXV 1,數據量大&#xff08;Volume&#xff09;2,類型繁多&#xff08;Variety &#xff09;3,速度快時效高&#xff08;Velocity&#xff09;4,價值密度低…

Arm匯編指令學習

ARM指令格式 ARM指令格式解析 opcode: 指令助記符,例如,MOV ,ADD,SUB等等 cond&#xff1a;指令條件碼表.下面附一張圖 {S}:是否影響CPSR的值. {.W .N}:指令寬度說明符,無論是ARM代碼還是Thumb&#xff08;armv6t2或更高版本&#xff09;代碼都可以在其中使用.W寬度說明符&…

facebook.com_如何降低電子商務的Facebook CPM

facebook.comWith the 2020 election looming, Facebook advertisers and e-commerce stores are going to continually see their ad costs go up as the date gets closer (if they haven’t already).隨著2020年選舉的臨近&#xff0c;隨著日期越來越近&#xff0c;Facebook…

Python中的If,Elif和Else語句

如果Elif Else聲明 (If Elif Else Statements) The if/elif/else structure is a common way to control the flow of a program, allowing you to execute specific blocks of code depending on the value of some data.if / elif / else結構是控制程序流程的常用方法&#x…

Hadoop安裝及配置

Hadoop的三種運行模式 單機模式&#xff08;Standalone,獨立或本地模式&#xff09;:安裝簡單,運行時只啟動單個進程,僅調試用途&#xff1b;偽分布模式&#xff08;Pseudo-Distributed&#xff09;:在單節點上同時啟動namenode、datanode、secondarynamenode、resourcemanage…

漏洞發布平臺-安百科技

一個不錯的漏洞發布平臺&#xff1a;https://vul.anbai.com/ 轉載于:https://blog.51cto.com/antivirusjo/2093758

Android 微信分享圖片

private String APP_ID "00000000000000000"; //微信 APPID private IWXAPI iwxapi; private void regToWx() {iwxapi WXAPIFactory.createWXAPI(context, APP_ID, true);//這里context記得初始化iwxapi.registerApp(APP_ID); } IMServer.getDiskBitmap(IMServer.u…

蒙蒂霍爾問題_常見的邏輯難題–騎士和刀,蒙蒂·霍爾和就餐哲學家的問題解釋...

蒙蒂霍爾問題While not strictly related to programming, logic puzzles are a good warm up to your next coding session. You may encounter a logic puzzle in your next technical interview as a way to judge your problem solving skills, so its worth being prepare…

西格爾零點猜想_我從埃里克·西格爾學到的東西

西格爾零點猜想I finished reading Eric Siegel’s Predictive Analytics. And I have to say it was an awesome read. How do I define an awesome or great book? A book that changes your attitude permanently. You must not be the same person that you were before y…

C/C++實現刪除字符串的首尾空格

StdStringTrimTest.cpp #include <iostream> int main() {std::string str(" 字符串 String ");std::cout << str << std::endl;std::cout << str.size() << std::endl;str.erase(str.find_first_of( ), str.find_first_not_of…

assign復制對象_JavaScript標準對象:assign,values,hasOwnProperty和getOwnPropertyNames方法介紹...

assign復制對象In JavaScript, the Object data type is used to store key value pairs, and like the Array data type, contain many useful methods. These are some useful methods youll use while working with objects.在JavaScript中&#xff0c; Object數據類型用于存…

HDFS 技術

HDFS定義 Hadoop Distributed File System&#xff0c;是一個使用 Java 實現的、分布式的、可橫向擴展的文件系 統&#xff0c;是 HADOOP 的核心組件 HDFS特點 處理超大文件流式地訪問數據運行于廉價的商用機器集群上&#xff1b; HDFS 不適合以下場合&#xff1a;低延遲數據…

深度學習算法和機器學習算法_啊哈! 4種流行的機器學習算法的片刻

深度學習算法和機器學習算法Most people are either in two camps:大多數人都在兩個營地中&#xff1a; I don’t understand these machine learning algorithms. 我不了解這些機器學習算法。 I understand how the algorithms work, but not why they work. 我理解的算法是如…