乐趣区

关于后端:Go-WebSocket-你的第一个Go-WebSocket服务-echo-server

我是 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 噢~我有空了会分享做游戏的相干技术。

退出移动版