写点什么

[Go WebSocket] 你的第一个 Go WebSocket 服务: echo server

作者:HullQin
  • 2022 年 9 月 02 日
    广东
  • 本文字数:3755 字

    阅读完需:约 12 分钟

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

背景

上篇文章:《为什么我选用 Go 重构 Python 版本的 WebSocket 服务?》,介绍了我的目标。


从这篇文章开始,我们进入实战,正式介绍 Go WebSocket 框架。

还没学过 Go,要先看什么?

建议你花 1 天时间,看一下 Go 的原理简介、基础语法。什么教程都可以,知名的教程就行。


至少要明白:各种数据类型,控制流(for、if 等)写法,弄懂 channel 和 goroutine,如何加锁。


一定要自己写写 goroutine 和 channel 试一下,了解一下基础语法。


此外,还要了解常用包的用法,包括 fmt、net/http。

技术选型

面对自己不熟悉的语言和不熟悉的框架,该怎么做技术选型呢?


我告诉你个小技巧,直接在 Github 上搜索,看 Star 最多的那个仓库,就可以啦~



看吧,我们搜到了gorilla/websocket,star 数以显著差异甩开了后面几名。这就没有什么好纠结的了,果断使用它。

新建项目

在使用 GoLand 时,新建 Go Project 会有 2 个选项:



我们选用第一个即可。


如果你没有 GoLand,也可以手动创建文件夹,在里面新建文件go.mod(我是使用的目前最新稳定版 1.18)


module echo
go 1.18
复制代码

安装依赖

go get github.com/gorilla/websocket
复制代码

拷贝 echo 代码

gorilla/websocket的官方 demo 拷贝过来即可,我们慢慢分析:


  • https://github.com/gorilla/websocket/blob/master/examples/echo/server.go


只需要拷贝这一个文件,命名为 server.go 即可。

先尝试运行

go run server.go
复制代码


然后浏览器打开 localhost:8080就可以了~



  • 点击「Open」建立 WebSocket 连接

  • 编辑好文本,按 Send 发送一个消息给服务器

  • 服务器立马回复一个一模一样的消息,这就是 echo

  • 点击「Close」关闭连接,之后无法 Send


你的所有操作都会记录在页面上:



当然,也可以打开开发者工具,查看 WebSocket 连接,就像你查看 Http 请求那样。这篇文章教了你怎样使用 Chrome 的开发者面板抓包:《遇到表格,手动翻页太麻烦?我教你写脚本,一页展示所有数据》。



代码解读

引入依赖

package main
import ( "flag" "html/template" "log" "net/http"
"github.com/gorilla/websocket")
复制代码

定义服务地址

var addr = flag.String("addr", "localhost:8080", "http service address")
复制代码


这是定义了服务器启动服务的地址,flag包用于处理命令行参数。意思是这个服务地址是可以通过命令行参数动态修改的。


比如你可以这样启动:go run server.go -addr="localhost:8888"


那么浏览器就应该打开localhost:8888来访问。


当然如果你不需要命令后参数传入 addr,完全可以删掉这行,改为:


const addr = "localhost:8080"
复制代码


同时,还要把 main 函数中,最后一行改成:(删掉了 addr 前面的星号)


log.Fatal(http.ListenAndServe(addr, nil))
复制代码


同时,把flag相关的行都删掉。(开头的 import 和 main 函数中的 Parse)

主函数

我们先介绍一下主函数(虽然主函数定义在后面)。但是主函数有一个路由的作用,分发了请求。我们先介绍一下,方便后续理解。


func main() {   flag.Parse()   log.SetFlags(0)   http.HandleFunc("/echo", echo)   http.HandleFunc("/", home)   log.Fatal(http.ListenAndServe(*addr, nil))}
复制代码


我们通过net/http提供的能力,使用ListenAndServe启动了 Http/WebSocket 服务。


其中,我们注册了 2 个处理函数,一个是针对 path 为/echo的,这是用 echo 函数处理。另一个是针对 path 为/的,这是用 home 函数处理。


当你用浏览器直接访问localhost:8080时,是用了home函数处理,一个 http 请求,获得一个 html 文件,在浏览器展示。


当你在 JS 中写new WebSocket('wss://localhost:8080/echo')时,是用了echo函数处理,一个 WebSocket 连接。


我们接下来介绍这 2 个函数。

定义 echo 服务(WebSocket 协议)

var upgrader = websocket.Upgrader{} // use default options
func echo(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Print("upgrade:", err) return } defer c.Close() for { mt, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) break } log.Printf("recv: %s", message) err = c.WriteMessage(mt, message) if err != nil { log.Println("write:", err) break } }}
复制代码


当客户端使用new WebSocket('ws://localhost:8080/echo')建立时,就会开启一个 goroutine,执行类似go echo(w, r)的操作。只要这个 WebSocket 没有关闭,那么这个 goroutine 就会一直存在。


如果客户端关闭了 WebSocket,或者服务端的这个 goroutine 执行结束了(因为有defer c.Close()),都会导致 WebSocket 断掉。这是合理且正确的,不这么写会有问题。


这段echo函数很简单,不断循环,读取消息c.ReadMessage(),如果没消息,那么就会暂停执行,直到有了消息。有消息后,通过log打印收到的消息,并且通过c.WriteMessage(mt, message)输出消息给客户端。


这里mt是消息类型 Message Type,有 2 种:二进制消息、文本消息。


当服务器输出完毕后,又在等待客户端的输入了。


可以看到,目前是一个有序的线性服务:收一个、发一个、收一个、发一个。如果客户端同时发了 100 个,那么服务端也会按照这 100 个消息的顺序读取,并且按原先的顺序 echo 回去。处理完一个、才会去接收下一个。好处是保证了收发的顺序性(服务端发的顺序一定跟收的顺序一致),坏处是无法并发的读,性能有影响,如果每个处理收到消息要处理很久,后面的消息就阻塞、积压在内存中了。


下一篇我们会介绍chat server,避免了这种问题。 敬请期待,可以先关注专栏、关注我噢~。

Html 文本服务(Http 协议)

func home(w http.ResponseWriter, r *http.Request) {   homeTemplate.Execute(w, "ws://"+r.Host+"/echo")}
var homeTemplate = template.Must(template.New("").Parse(`<!DOCTYPE html><html><head><meta charset="utf-8"><script> window.addEventListener("load", function(evt) { var output = document.getElementById("output"); var input = document.getElementById("input"); var ws; var print = function(message) { var d = document.createElement("div"); d.textContent = message; output.appendChild(d); output.scroll(0, output.scrollHeight); }; document.getElementById("open").onclick = function(evt) { if (ws) { return false; } ws = new WebSocket("{{.}}"); ws.onopen = function(evt) { print("OPEN"); } ws.onclose = function(evt) { print("CLOSE"); ws = null; } ws.onmessage = function(evt) { print("RESPONSE: " + evt.data); } ws.onerror = function(evt) { print("ERROR: " + evt.data); } return false; }; document.getElementById("send").onclick = function(evt) { if (!ws) { return false; } print("SEND: " + input.value); ws.send(input.value); return false; }; document.getElementById("close").onclick = function(evt) { if (!ws) { return false; } ws.close(); return false; };});</script></head><body><table><tr><td valign="top" width="50%"><p>Click "Open" to create a connection to the server, "Send" to send a message to the server and "Close" to close the connection. You can change the message and send multiple times.<p><form><button id="open">Open</button><button id="close">Close</button><p><input id="input" type="text" value="Hello world!"><button id="send">Send</button></form></td><td valign="top" width="50%"><div id="output" style="max-height: 70vh;overflow-y: scroll;"></div></td></tr></table></body></html>`))
复制代码


这个服务比较简单,就是 Html 模板渲染。


注意有个模板变量:"ws://"+r.Host+"/echo",其实这个模板变量是不需要的。


HTML 中可以直接这么写:把ws = new WebSocket("{{.}}");改为ws = new WebSocket('ws://' + window.location.host + '/echo');

写在最后

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

发布于: 刚刚阅读数: 4
用户头像

HullQin

关注

公众号【线下聚会游戏】 2020.10.07 加入

game.hullqin.cn 我做了一些联机桌游网页:支持2-10人联机的UNO、2-4人联机的斗地主、2人联机的五子棋。无需下载,点开即玩!叫上朋友,即刻开局!不看广告,不做任务,享受「纯粹」的游戏!

评论

发布
暂无评论
[Go WebSocket] 你的第一个Go WebSocket服务: echo server_Go_HullQin_InfoQ写作社区