我是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 echogo 1.18
装置依赖
go get github.com/gorilla/websocket
拷贝echo代码
把gorilla/websocket
的官网demo拷贝过去即可,咱们缓缓剖析:
- https://github.com/gorilla/we...
只须要拷贝这一个文件,命名为server.go即可。
先尝试运行
go run server.go
而后浏览器关上 localhost:8080就能够了~
- 点击「Open」建设WebSocket连贯
- 编辑好文本,按Send发送一个音讯给服务器
- 服务器立马回复一个截然不同的音讯,这就是echo
- 点击「Close」敞开连贯,之后无奈Send
你的所有操作都会记录在页面上:
当然,也能够关上开发者工具,查看WebSocket连贯,就像你查看Http申请那样。这篇文章教了你怎么应用Chrome的开发者面板抓包:《遇到表格,手动翻页太麻烦?我教你写脚本,一页展现所有数据》。
代码解读
引入依赖
package mainimport ( "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 optionsfunc 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 噢~我有空了会分享做游戏的相干技术。