我是 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/we…
只须要拷贝这一个文件,命名为 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 噢~我有空了会分享做游戏的相干技术。