Go WebSocket 200 行代码开发一个简易聊天室

2023-01-30 18:11:48 +08:00
 Nazz

lib

github.com/lxzan/gws

效果图

服务端 main.go

package main

import (
	_ "embed"
	"encoding/json"
	"github.com/lxzan/gws"
	"log"
	"net/http"
	"time"
)

const PingInterval = 15 * time.Second // 客户端心跳间隔

//go:embed index.html
var html []byte

func main() {
	var handler = NewWebSocket()
	var upgrader = gws.NewUpgrader(func(c *gws.Upgrader) {
		c.CompressEnabled = true
		c.EventHandler = handler

		// 在 querystring 里面传入用户名
		// 把 Sec-WebSocket-Key 作为连接的 key
		// 刷新页面的时候, 会触发上一个连接的 OnClose/OnError 事件, 这时候需要对比 key 并删除 map 里存储的连接
		c.CheckOrigin = func(r *gws.Request) bool {
			var name = r.URL.Query().Get("name")
			if name == "" {
				return false
			}
			r.SessionStorage.Store("name", name)
			r.SessionStorage.Store("key", r.Header.Get("Sec-WebSocket-Key"))
			return true
		}
	})

	http.HandleFunc("/connect", func(writer http.ResponseWriter, request *http.Request) {
		socket, err := upgrader.Accept(writer, request)
		if err != nil {
			log.Printf("Accept: " + err.Error())
			return
		}
		socket.Listen()
	})

	http.HandleFunc("/index.html", func(writer http.ResponseWriter, request *http.Request) {
		_, _ = writer.Write(html)
	})

	if err := http.ListenAndServe(":3000", nil); err != nil {
		log.Fatalf("%+v", err)
	}
}

func NewWebSocket() *WebSocket {
	return &WebSocket{sessions: gws.NewConcurrentMap(16)}
}

type WebSocket struct {
	sessions *gws.ConcurrentMap // 使用内置的 ConcurrentMap 存储连接, 可以减少锁冲突
}

func (c *WebSocket) getName(socket *gws.Conn) string {
	name, _ := socket.SessionStorage.Load("name")
	return name.(string)
}

func (c *WebSocket) getKey(socket *gws.Conn) string {
	name, _ := socket.SessionStorage.Load("key")
	return name.(string)
}

// 根据用户名获取 WebSocket 连接
func (c *WebSocket) GetSocket(name string) (*gws.Conn, bool) {
	if v0, ok0 := c.sessions.Load(name); ok0 {
		if v1, ok1 := v0.(*gws.Conn); ok1 {
			return v1, true
		}
	}
	return nil, false
}

// RemoveSocket 移除 WebSocket 连接
func (c *WebSocket) RemoveSocket(socket *gws.Conn) {
	name := c.getName(socket)
	key := c.getKey(socket)
	if mSocket, ok := c.GetSocket(name); ok {
		if mKey := c.getKey(mSocket); mKey == key {
			c.sessions.Delete(name)
		}
	}
}

func (c *WebSocket) OnOpen(socket *gws.Conn) {
	name := c.getName(socket)
	if v, ok := c.sessions.Load(name); ok {
		var conn = v.(*gws.Conn)
		conn.Close(1000, []byte("connection replaced"))
	}
	socket.SetDeadline(time.Now().Add(3 * PingInterval))
	c.sessions.Store(name, socket)
	log.Printf("%s connected\n", name)
}

func (c *WebSocket) OnError(socket *gws.Conn, err error) {
	name := c.getName(socket)
	c.RemoveSocket(socket)
	log.Printf("onerror, name=%s, msg=%s\n", name, err.Error())
}

func (c *WebSocket) OnClose(socket *gws.Conn, code uint16, reason []byte) {
	name := c.getName(socket)
	c.RemoveSocket(socket)
	log.Printf("onclose, name=%s, code=%d, msg=%s\n", name, code, string(reason))
}

func (c *WebSocket) OnPing(socket *gws.Conn, payload []byte) {}

func (c *WebSocket) OnPong(socket *gws.Conn, payload []byte) {}

type Input struct {
	To   string `json:"to"`
	Text string `json:"text"`
}

func (c *WebSocket) OnMessage(socket *gws.Conn, message *gws.Message) {
	defer message.Close()

	// chrome websocket 不支持 ping 方法, 所以在 text frame 里面模拟 ping
	if b := message.Bytes(); len(b) == 4 && string(b) == "ping" {
		socket.WriteMessage(gws.OpcodeText, []byte("pong"))
		socket.SetDeadline(time.Now().Add(3 * PingInterval))
		return
	}

	var input = &Input{}
	_ = json.Unmarshal(message.Bytes(), input)
	if v, ok := c.sessions.Load(input.To); ok {
		v.(*gws.Conn).WriteMessage(gws.OpcodeText, message.Bytes())
	}
}

客户端 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ChatRoom</title>
    <style>
        #app {
            width: 400px;
            margin: 50px auto 0;
        }

        .form {
            margin: 10px auto;
        }

        #app input {
            width: 300px;
            height: 20px;
            float: right;
        }

        #app span {
            height: 26px;
            line-height: 26px;
        }

        textarea {
            width: 400px;
        }
    </style>
</head>
<body>

<div id="app">
    <div class="form"><span>From</span> <input type="text" id="from"></div>
    <div class="form"><span>To</span> <input type="text" id="to"></div>
    <div><textarea id="text" cols="30" rows="10"></textarea></div>
    <button onclick="connect()">Connect</button>
    <button onclick="send()">Send</button>
</div>

<script>

    function connect() {
        let from = document.getElementById("from").value;
        window.ws = new WebSocket(`ws://127.0.0.1:3000/connect?name=${from}`);
        window.ws.onclose = function (event) {
            console.log(event);
        }
        if (window.interval !== undefined) {
            clearInterval(window.interval)
        }
        window.interval = setInterval(function () {
            window.ws.send("ping");
        }, 15 * 1000)
    }

    function send() {
        let to = document.getElementById("to").value;
        let text = document.getElementById("text").value;
        ws.send(JSON.stringify({to, text}));
    }
</script>

</body>
</html>

2268 次点击
所在节点    程序员
11 条回复
puzzle9
2023-01-30 20:51:51 +08:00
那啥 我不知道这个应该怎么形容 祝你新年快乐 start 多多
Nazz
2023-01-30 21:07:10 +08:00
@puzzle9 新年快乐,祝你升职涨薪:)
maocat
2023-01-31 00:29:55 +08:00
那啥,让我先学下 gws 这个包
Ranying
2023-01-31 03:03:06 +08:00
客户端 ws 断开连接没有明显提示信息,没有看到 ws 的 onmessage 方法。
Nazz
2023-01-31 07:19:41 +08:00
@Ranying 断开连接触发的是 onclose/onerror
Nazz
2023-01-31 07:21:30 +08:00
@maocat 重点是学习长连接生命周期管理,API 很简单
lizhenda
2023-01-31 09:22:15 +08:00
API 清晰,不过 ping pong 是不是反过来了?
linauror
2023-01-31 09:46:50 +08:00
@lizhenda 客户端发 ping ,服务端回 pong ,应该没问题啊
Nazz
2023-01-31 09:57:38 +08:00
@lizhenda 主动方发送 ping, 被动方回复 pong
quicksand
2023-01-31 11:13:48 +08:00
最近正好在看 go ,学习一下
Nazz
2023-01-31 11:17:05 +08:00
@quicksand 学习使我快乐

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/911733

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX