当前位置:网站首页>[Go WebSocket] 你的第一个Go WebSocket服务: echo server

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

2022-08-10 18:34:00 51CTO

大家好,我是公众号「线下聚会游戏」作者,开发了​ ​《联机桌游合集》​​​,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏。其中的核心技术就是WebSocket,快来关注专栏​ ​《Go WebSocket》​​,跟我一起学习吧!

背景

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

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

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

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

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

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

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

技术选型

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

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

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

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

新建项目

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

[Go WebSocket] 你的第一个Go WebSocket服务: echo server_客户端_02

我们选用第一个即可。

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

      
      
module echo

go 1.18
  • 1.
  • 2.
  • 3.

安装依赖

      
      
go get github.com/gorilla/websocket
  • 1.

拷贝echo代码

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

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

先尝试运行

      
      
go run server.go
  • 1.

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

[Go WebSocket] 你的第一个Go WebSocket服务: echo server_客户端_03

  • 点击「Open」建立WebSocket连接
  • 编辑好文本,按Send发送一个消息给服务器
  • 服务器立马回复一个一模一样的消息,这就是echo
  • 点击「Close」关闭连接,之后无法Send

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

[Go WebSocket] 你的第一个Go WebSocket服务: echo server_后端_04

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

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

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

代码解读

引入依赖

      
      
package main

import (
"flag"
"html/template"
"log"
"net/http"

"github.com/gorilla/websocket"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

定义服务地址

      
      
var addr = flag . String( "addr", "localhost:8080", "http service address")
  • 1.

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

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

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

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

      
      
const addr = "localhost:8080"
  • 1.

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

      
      
log . Fatal( http . ListenAndServe( addr, nil))
  • 1.

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

主函数

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

      
      
func main() {
flag . Parse()
log . SetFlags( 0)
http . HandleFunc( "/echo", echo)
http . HandleFunc( "/", home)
log . Fatal( http . ListenAndServe( * addr, nil))
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

我们通过​​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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

当客户端使用​​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>
`))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.

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

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

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

写在最后

我是HullQin,独立开发了​ ​《联机桌游合集》​​​,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了​ ​《合成大西瓜重制版》​​​。还开发了​ ​《Dice Crush》​​​参加了某个游戏制作比赛。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:​ ​《教你做小游戏》​​​、​ ​《极致用户体验》​​。

本文正在参加​ ​技术专题18期-聊聊Go语言框架​​。

原网站

版权声明
本文为[51CTO]所创,转载请带上原文链接,感谢
https://blog.51cto.com/hullqin/5564889