## 編寫一個 WebSocket 服務
> WebSocket 其實也是一個 Console 命令行,只是在 Command 中啟動了一個 gin 服務器,并將配置好的路由傳入服務器中執行,接下來將 HTTP 請求升級為 WebSocket 連接,然后對連接進行邏輯處理
WebSocket 是基于 http 協議完成握手的,因此我們編寫代碼時,也是和編寫 Web 項目是差不多的,差別就是請求過來后,我們需要使用一個 WebSocket 的升級器,將請求升級為 WebSocket 連接,接下來就是針對連接的邏輯處理,從這個部分開始就和傳統的 Socket 操作一致了。
首先我們使用 `mix` 命令創建一個 Web 項目骨架,因為 WebSocket 代碼包含在 Web 骨架中:
~~~
mix web --name=hello
~~~
由于 Command 啟動方式和 Web 一樣,這里就不重復描述,首先在 `routes/all.go` 文件中定義一個 WebSocket 的路由:
~~~
router.GET("websocket",
func(ctx *gin.Context) {
ws := controllers.WebSocketController{}
ws.Index(ctx)
},
)
~~~
然后創建一個 `controllers.WebSocketController` 結構體,文件路徑為 `controllers/ws.go`:
- 創建了一個 `upgrader` 的升級器,當請求過來時將會升級為 WebSocket 連接
- 定義了一個 `WebSocketSession` 的結構體負責管理連接的整個生命周期
- `session.Start()` 中啟動了兩個協程,分別處理消息的讀和寫
- 在消息讀取的協程中,啟動了 `WebSocketHandler` 結構體的 `Index` 方法來處理消息,在實際項目中我們可以根據不同的消息內容使用不同的結構體來處理,實現 Web 項目那種控制器的功能
~~~
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/mix-go/console"
"github.com/mix-go/web-skeleton/globals"
"net/http"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
type WebSocketController struct {
}
func (t *WebSocketController) Index(c *gin.Context) {
logger := globals.Logger()
if console.App.Debug {
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
session := WebSocketSession{
Conn: conn,
Header: c.Request.Header,
Send: make(chan []byte, 100),
}
session.Start()
srv := globals.Server
srv.RegisterOnShutdown(func() {
session.Stop()
})
logger.Infof("Upgrade: %s", c.Request.UserAgent())
}
type WebSocketSession struct {
Conn *websocket.Conn
Header http.Header
Send chan []byte
}
func (t *WebSocketSession) Start() {
go func() {
logger := globals.Logger()
for {
msgType, msg, err := t.Conn.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, 1001, 1006) {
logger.Error(err)
}
t.Stop()
return
}
if msgType != websocket.TextMessage {
continue
}
handler := WebSocketHandler{
Session: t,
}
handler.Index(msg)
}
}()
go func() {
logger := globals.Logger()
for {
msg, ok := <-t.Send
if !ok {
return
}
if err := t.Conn.WriteMessage(websocket.TextMessage, msg); err != nil {
logger.Error(err)
t.Stop()
return
}
}
}()
}
func (t *WebSocketSession) Stop() {
close(t.Send)
_ = t.Conn.Close()
}
type WebSocketHandler struct {
Session *WebSocketSession
}
func (t *WebSocketHandler) Index(msg []byte) {
t.Session.Send <- []byte("hello, world!")
}
~~~
## 編譯與測試
> 也可以在 Goland Run 里配置 Program arguments 直接編譯執行,[Goland 使用] 章節有詳細介紹
接下來我們編譯上面的程序:
~~~
// linux & macOS
go build -o bin/go_build_main_go main.go
// win
go build -o bin/go_build_main_go.exe main.go
~~~
首先在命令行啟動 `web` 服務器:
~~~
$ bin/go_build_main_go web
___
______ ___ _ /__ ___ _____ ______
/ __ `__ \/ /\ \/ /__ __ `/ __ \
/ / / / / / / /\ \/ _ /_/ // /_/ /
/_/ /_/ /_/_/ /_/\_\ \__, / \____/
/____/
Server Name: mix-web
Listen Addr: :8080
System Name: darwin
Go Version: 1.13.4
Framework Version: 1.0.9
time=2020-09-16 20:24:41.515 level=info msg=Server start file=web.go:58
~~~
瀏覽器測試
- 我們使用現成的工具測試:http://www.easyswoole.com/wstool.html
