和 HTTP 协议一样,WebSocket 协议也建立在 TCP/IP 协议基础上,但不一样的是 HTTP 协议 为单向协议,即只能客户端向服务器请求资源,服务器才能将数据传送给浏览器。
而 WebSocket 协议 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/UA 都能主动的向对方发送或接收数据。
WebSocket 可以认为是与 HTTP 属于同一个层级的兄弟协议,但实际上它又有些依赖 HTTP 协议,因为 WebSocket 的初始连接是通过 HTTP 协议的三次握手实现,完成连接之后再向服务器请求升级成为 websocket 协议;

简单版的 serv.go
- package main
-
- // go get -u -v github.com/gorilla/websocket 安装第三方包
- import (
- "encoding/json"
- "fmt"
- "github.com/gorilla/websocket"
- "net/http"
- )
-
- func main() {
- // http请求,跳转到一个简单的前端页面
- http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
- http.ServeFile(writer, request, "home.html")
- })
- // http请求,连接成功后申请升级为websocket服务
- http.HandleFunc("/ws", indexHandler)
-
- err := http.ListenAndServe("0.0.0.0:8888", nil)
- if err != nil {
- return
- }
- }
-
- // 消息体
- type replyMsg struct {
- Type string `json:"type,omitempty"`
- Uid string `json:"uid,omitempty"`
- Msg string `json:"msg,omitempty"`
- }
-
- var (
- upgrader = websocket.Upgrader{
- // 允许所有CORS跨域请求
- CheckOrigin: func(r *http.Request) bool {
- return true
- },
- }
- // 用户列表
- userMaps = make(map[string]*websocket.Conn)
- )
-
- func indexHandler(w http.ResponseWriter, r *http.Request) {
- var (
- replyMsg replyMsg
- conn *websocket.Conn
- err error
- data []byte
- )
- // 主动升级成websocket( Upgrade:websocket)
- if conn, err = upgrader.Upgrade(w, r, nil); err != nil {
- return
- }
-
- for {
- // 监听新消息请求
- if _, data, err = conn.ReadMessage(); err != nil {
- goto Err
- }
- // json -> 结构体
- err := json.Unmarshal([]byte(data), &replyMsg)
- if err != nil {
- goto Err
- }
- fmt.Println(replyMsg.Type)
- switch {
- // 监听加入群聊的请求
- case replyMsg.Type == "register" && replyMsg.Uid != "":
- // 注册用用户集合中
- userMaps[replyMsg.Uid] = conn
- // 监听用户关闭页面
- case replyMsg.Type == "logout" && replyMsg.Uid != "":
- // 剔除群聊
- delete(userMaps, replyMsg.Uid)
- case replyMsg.Type == "message":
-
- }
- // 循环推送消息给每个用户
- for _, v := range userMaps {
- err = v.WriteMessage(websocket.TextMessage, data)
- if err != nil {
- goto Err
- }
- }
-
- }
- Err:
- conn.Close()
- }
并发即时聊天室的实现思路
升级版的 serv.go
- package main
-
- // go get -u -v github.com/gorilla/websocket --安装官方的websocket包
- import (
- "github.com/gorilla/websocket"
- "log"
- "net/http"
- )
-
- var (
- // 升级成 WebSocket 协议
- upgrader = websocket.Upgrader{
- // 允许CORS跨域请求
- CheckOrigin: func(r *http.Request) bool {
- return true
- },
- }
- conn *websocket.Conn
- err error
- )
-
- // CenterHandler 处理中心,关联着每个 Client 的注册、注销、广播通道,相当于每个用户的中心通讯的中介。
- type CenterHandler struct {
- // 广播通道,有数据则循环每个用户广播出去
- broadcast chan []byte
- // 注册通道,有用户进来 则推到用户集合map中
- register chan *Client
- // 注销通道,有用户关闭连接 则将该用户剔出集合map中
- unregister chan *Client
- // 用户集合,每个用户本身也在跑两个协程,监听用户的读、写的状态
- clients map[*Client]bool
- }
-
- // 处理中心的一个接口,监控状态
- func (ch *CenterHandler) monitoring() {
- for {
- select {
- // 注册,新用户连接过来会推进注册通道,这里接收推进来的用户指针
- case client := <-ch.register:
- ch.clients[client] = true
- // 注销,关闭连接或连接异常会将用户推出群聊
- case client := <-ch.unregister:
- delete(ch.clients, client)
- // 消息,监听到有新消息到来
- case message := <-ch.broadcast:
- println("消息来了,message:" + string(message))
- // 推送给每个用户的通道,每个用户都有跑协程起了writePump的监听
- for client := range ch.clients {
- client.send <- message
- }
- }
- }
- }
-
- // Client 抽象出来的 Client,里面有这个 websocket 连接的 读 和 写 操作
- type Client struct {
- handler *CenterHandler
- conn *websocket.Conn
- // 每个用户自己的循环跑起来的状态监控
- send chan []byte
- }
-
- // 写,主动推送消息给客户端
- func (c *Client) writePump() {
- defer func() {
- c.handler.unregister <- c
- c.conn.Close()
- }()
- for {
- // 广播推过来的新消息,马上通过websocket推给自己
- message, _ := <-c.send
- if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
- return
- }
- }
- }
-
- // 读,监听客户端是否有推送内容过来服务端
- func (c *Client) readPump() {
- defer func() {
- c.handler.unregister <- c
- c.conn.Close()
- }()
- for {
- // 循环监听是否该用户是否要发言
- _, message, err := c.conn.ReadMessage()
- if err != nil {
- // 异常关闭的处理
- if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
- log.Printf("error: %v", err)
- }
- break
- }
- // 要的话,推给广播中心,广播中心再推给每个用户
- c.handler.broadcast <- message
- }
- }
-
- func main() {
- // 应用一运行,就初始化 CenterHandler 处理中心对象
- handler := CenterHandler{
- broadcast: make(chan []byte),
- register: make(chan *Client),
- unregister: make(chan *Client),
- clients: make(map[*Client]bool),
- }
- // 起个协程跑起来,监听注册、注销、消息 3 个 channel
- go handler.monitoring()
-
- // http请求,跳转到一个简单的前端页面
- http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
- http.ServeFile(writer, request, "home.html")
- })
- // websocket 请求,建立双工通讯连接
- http.HandleFunc("/ws", func(writer http.ResponseWriter, request *http.Request) {
- // 由 http 升级成为 websocket 服务
- if conn, err = upgrader.Upgrade(writer, request, nil); err != nil {
- log.Println(err)
- return
- }
- // 为每个连接创建一个 Client 实例,(实际上这里应该还有绑定用户真实信息的操作)
- client := &Client{&handler, conn, make(chan []byte, 256)}
- // 推给监控中心注册到用户集合中
- handler.register <- client
- // 每个 client 都挂起 2 个新的协程,监控读、写状态
- go client.writePump()
- go client.readPump()
- })
-
- if err := http.ListenAndServe(":8888", nil); err != nil {
- log.Fatal("ListenAndServe:", err)
- }
- }
最后,附上一个简单的前端文件(home.html)及效果截图
- <!DOCTYPE html>
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <title>聊天室-demo</title>
- <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
- </head>
- <body>
- <h1>聊天室-demo</h1>
- <div class="show-data"></div>
- <input class="content">
- <button class="send-btn">发送</button>
- <script>
- // 测试用户
- var username = "用户_" + Math.floor(Math.random() * 10000);
- var ws_url = "ws://localhost:8888/ws";
- var ws = new WebSocket(ws_url);
- // 新连接
- ws.onopen = function (ev) {
- ws.send('{"type":"register","uid":"' + username + '","msg":" 上线了"}');
- };
- // 发送内容
- $(".send-btn").click(function () {
- var content = $(".content").val();
- content = '{"type":"msg","uid":"' + username + '","msg":"' + content + '"}'
- ws.send(content);
- $(".content").val("");
- });
- // 监听接收新消息
- ws.onmessage = function (ent) {
- var r = JSON.parse(ent.data);
- console.log(r)
- if(r.uid == username){
- r.uid = username + "(你)"
- }
- $(".show-data").append(r.uid + ":" + r.msg + "<br>");
- }
- // 退出页面
- ws.onclose = function (ent) {
- ws.send('{"type":"logout","uid":"' + username + '","msg":" 下线了"}');
- }
- </script>
-
- </body>
- </html>
开两个窗户的即时聊天,体验比前端轮询好的多~

>>>对比 && 总结 GatewayWorker 开发实时聊天功能的基本思路
>>>对比 && 总结 Swoole 快速起步:创建 WebSocket 服务器(聊天室)