html5+go+websocket不到150行代码,实现一个在线实时聊天的功能

阮一峰websocket
相关参考

websocket

什么是websocket

在了解什么是websocket之前,我们下说一说http,因为HTTP我们太熟了。我们知道,HTTP是一种基于应用层的网络协议,往往都是一个请求,一个相应。websocket呢,也是一种基于应用层的网络协议,但是它不仅可以实现请求-相应这种模式,还可以实现主动推送,即你不请求,我也可以给你发消息通知。它实现了浏览器与服务器之间的双工通信。浏览器和服务器只需要完成一次握手,两者就可以创建一个持续的链接。

为什么出现websocket

  • http满足了我们大部分的需求,比如浏览网页 图片,视频,音频等等,我想要什么内容,我就给服务器发什么请求。
  • 但是随着互联网的发展,我们有了网络聊天的功能,在最早时候,要看对方有没有给你发送消息,得需要ajax轮询来实现,还有一些其他需要实时推送消息的场景,比如股票价格,体育解说,弹幕,直播这种,都需要服务器来主动推送消息到客户端。http不再能满足我们的需求。所以,在2008年,websocket诞生了。2011年成为了国际标准。

websocket特点

  • websocket最大的特点就是实现了浏览器和服务端之间的双工通信,更好的支持实时通信。

  • websocket头部信息很少,一般只有2bytes左右,节省了网络IO。

  • 因为websocket大部分的使用场景也是在浏览器中使用,HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的,因此其连接和断开,都要遵循 TCP 协议中的三次握手和四次挥手 ,所以websocket复用了http的握手机制,所以很好的兼容了http,默认端口也是80和443,复用 HTTP 的 Upgrade 机制,完成升级协议的协商过程,能通过各种http代理服务器。

  • 可以发送二进制数据

  • 没有同源策略,客户端可以和任意服务器通信。

  • 协议标识符是ws或者wss(加密),比如:ws://example.com:80/some/path

  • WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。 实际上,许多语言、框架和服务器都提供了 WebSocket 支持,例如 Nginx Apache C C++ Node Python等

websocket的工作原理
  • 我们先来启动一个简单的websocket服务,代码非常简单,大家理解完代码之后,再来下一步分析
    服务端代码
package main

import (
    "fmt"
    "github.com/gorilla/websocket"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "time"
)
//显示页面
func rootHandler(w http.ResponseWriter, r *http.Request) {
    file, err := ioutil.ReadFile("othor-project/websocket-demo/index.html")
    if err != nil {
        fmt.Println(os.Getwd())
        fmt.Println(err)
    }
    _, err = fmt.Fprintf(w, "%s", file)
    if err != nil {
        fmt.Println(err)
    }
}

//ws连接
func wsHandler(w http.ResponseWriter, r *http.Request) {
    //声明协议是websocket
    var upgrader = websocket.Upgrader{} //定义一个websocket
    ws, err := upgrader.Upgrade(w, r, nil)//初始化
    fmt.Println("i got a websocket")
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    ws.PongHandler()
    //主动发送消息给浏览器
    for i:=0;i<3;i++ {
        time.Sleep(time.Second*2)
        str:=fmt.Sprintf("hello i am %d",i)
        fmt.Println(str)
        err := ws.WriteMessage(websocket.TextMessage, []byte(str))
        if err != nil {
            fmt.Println("send message err")
            fmt.Println(err)
        }
    }

    //监听消息
    for  {
        _, i, err := ws.ReadMessage()
        if err != nil {
            fmt.Println("read msg error")
            fmt.Println(err)
        }
        fmt.Println(string(i))

    }
}

func main() {
    fmt.Println("server start")
    //展示页面
    http.HandleFunc("/", rootHandler)
    //ws连接
    http.HandleFunc("/ws", wsHandler)
    //先开启端口监听
    err := http.ListenAndServe(":30000", nil)
    if err != nil {
        fmt.Println(err)
    }
}

我们简单了解一下h5怎么操作websocket
[图片上传失败...(image-5bfaa8-1678190324648)]
我们使用var ws = new WebSocket("ws://hostname/path", ["protocol1", "protocol2"])来建立一个连接。第一个参数是服务端websocket地址,如果是https+websocket,那么前缀写成wss。第二个参数并不是必须的,它约定了双方通讯使用的自定义子协议,会被放到这个Header中: Sec-WebSocket-Protocol。子协议在某些场合是很必要的,例如服务端要与多个客户端版本兼容,那么若干个版本之后,服务端设定支持子协议 v1.5, v2.0, 而客户端发送的却是 v1.0,那么他们就可以在握手阶段失败,不会继续通信下去导致奇奇怪怪的错误。

WebSocket构造函数只有两个变量,不能提供通过设置自定义Header的方式来携带其它信息,那么我们如何对ws的连接认证呢?可以通过以下方式实现认证:

  • 通过ws地址填写形如 ws://username:password@hostname/path, 即构造出了 Authorization Header
  • 通过ws地址填写形如 ws://:password@hostname/path ,即构造出了 Bearer Token Header
  • 通过在Cookie中加入值,也能够携带额外的信息
  • 在websocket连接建立后,再通过自定义的认证协议,走websocket进行认证。

客户端代码

<script type="text/javascript">
    var mess = document.getElementById("mess");
    // 1 创建一个ws连接
    var ws = new WebSocket("ws://" + location.host + "/ws")
    //监听消息
    ws.onmessage = function (event) {
        var chat = event.data;
        var res = chat.toString();
        console.log(res);
    }

    ws.onopen = function()
    {
        // Web Socket 已连接上,使用 send() 方法发送数据
        ws.send("i am client")
    };

</script>

启动后的效果,这是服务端往客户端推送的消息
[图片上传失败...(image-2cc882-1678190324648)]
这是客户端往服务端推送的消息
[图片上传失败...(image-fbc0c1-1678190324648)]

  • 建立连接
    具体连接过程如下图(ws表示协议头,101表示协议切换,upgrade表示要升级协议,upgrade:websocket 表示要升级到websocket协议,Sec-WebSocket-Key表示),websocket-key和websocket-accept之间的加密方式是base64(hsa1(sec-websocket-key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)),如果这个Sec-WebSocket-Accept计算错误浏览器会提示Sec-WebSocket-Accept dismatch,如果返回成功,Websocket就会回调onopen事件。注意,每个ws的的key都是唯一的,所以如果你刷新了页面,ws的key也会刷新,服务端就会认为是新连接。
    [图片上传失败...(image-3f2939-1678190324648)]

  • 交换数据
    具体的数据格式是怎么样的呢?WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。

  • 保持连接
    websocket使用心跳机制(ping和pong)来保持正常通信。定时发送一个数据包,让对方知道自己在线且正常工作。如果对方无法相应,则可以弃用旧连接,开启新连接。

群聊服务

  • 基于上面的知识,我们写一个简单的群聊系统
    服务端代码
package main

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/websocket"
    "io/ioutil"
    "log"
    "net/http"
    "os"
)

//定义一个聊天的结构体,姓名和信息
type Msg struct {
    Uid string `json:"uid"`
    Msg string `json:"msg"`
}

//定义一个全局的channel用来接收消息
var msgChan = make(chan *Msg, 10)
var upgrader = websocket.Upgrader{} // use default options
//全局变量用来存储用户连接 用于发送消息
var clients = make(map[*websocket.Conn]int)

//用来存放用户连接,读取完毕开启一个协程处理客户端发来的请求,用于读取消息
var clientsMsg = make(map[*websocket.Conn]int)

//将channel中的消息推送给客户端
func sendMsg() {
    for {
        //从channel获取值
        msg := <-msgChan
        if msg != nil {
            fmt.Printf("我接收到了信息,我开始发送信息了\r\n")
            fmt.Println(msg)
            msgStr, _ := json.Marshal(msg)
            //给每一个websocket发送消息
            for client, v := range clients {
                fmt.Printf("我在给第%d个用户发信息\n\r", v)
                err := client.WriteMessage(websocket.TextMessage, msgStr)
                defer client.Close()
                if err != nil {
                    fmt.Printf("给第%d个用户发信息失败了\r\n", v)
                    fmt.Println(err)
                }
            }

        }

    }
}

//接收客户端发来的msg,并写入chan
func getMsg() {
    for {
        //遍历所有的链接
        for client := range clientsMsg {
            defer client.Close()
            if client == nil {
                delete(clientsMsg, client)
                break
            } else {
                go listenMsg(client)
                delete(clientsMsg, client)
            }

        }
    }
}

//给连接设置一个监听消息
func listenMsg(client *websocket.Conn) {
    var jsonMsg Msg
    for {
        err := client.ReadJSON(&jsonMsg)
        if err != nil {
            fmt.Println("get msg error")
            fmt.Println(err)
        }
        fmt.Println(jsonMsg)
        if err != nil {
            fmt.Println("json error")
        }
        msgChan <- &jsonMsg
    }
}

//显示网页
func rootHandler(w http.ResponseWriter, r *http.Request) {
    file, err := ioutil.ReadFile("othor-project/websocket/index.html")
    if err != nil {
        fmt.Println(os.Getwd())
        fmt.Println(err)
    }
    _, err = fmt.Fprintf(w, "%s", file)
    if err != nil {
        fmt.Println(err)
    }
}

//建立ws连接
func wsHandler(w http.ResponseWriter, r *http.Request) {
    //声明协议是websocket
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    ws.PongHandler()
    clength := len(clients) + 1
    clients[ws] = clength //用于发送消息 把所有的socket连接都放到一个map,因为我收到一个消息,要发送到每一个连接的的socket
    //这里为了方便 我们就用第几个来代表用户id,项目中需要谨慎定义
    clientsMsg[ws] = clength //用于读取消息
    fmt.Printf("新用户id:%d", clength)
    clientLen := fmt.Sprintf("%d", clength)
    err = ws.WriteMessage(websocket.TextMessage, []byte(clientLen))
    if err != nil {
        fmt.Println("send id error")
    }
}

/**
 * Notes:利用websocket快速实现一个聊天室
 */
func main() {
    fmt.Println("server start")
    //页面
    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/ws", wsHandler)
    // 监听所有ws发送的消息
    go getMsg()
    //发送消息给所有的ws
    go sendMsg()
    //监听端口
    err := http.ListenAndServe(":30000", nil)
    if err != nil {
        fmt.Println(err)
    }
}

客户端代码

<!doctype html>
<html lang="en">
<meta charset="UTF-8">
<body>
<style>
    .chat-group {
        background-color: rgb(73, 73, 73);
    }

    .chat {
        margin-top: 10px;
        background-color: #dbdede;
    }
</style>
<h1>聊天系统:</h1>
<div class="chat-group" id="chat-group">

</div>

<p>uid:<span id="uid"></span></p>
<p>信息:<input type="text" name="msg" id="msg"></p>
<button id="btn">发送</button>
<script src="https://m.acurd.com/template/home/default/js/vendor/jquery-1.12.4.min.js"></script>
<script type="text/javascript">
    // 1.建立连接
    var ws = new WebSocket("ws://" + location.host + "/ws")
    //接收消息
    ws.onmessage = function (event) {
        var data = event.data;
        //如果是消息 返回的是json
        if (!isJSON(data)) {
            $("#uid").text(data)
            return
        }
        addMsg(data)
    }
    ws.onopen = function (event) {
        console.log("连接上了");
    }
    //发送消息
    $("#btn").click(function () {
        var msg = $("#msg").val();
        var uid = $("#uid").text();
        var obj = {msg: msg, uid: uid}
        var json = JSON.stringify(obj)
        //发送json字符串
        ws.send(json)
    })
    function addMsg(data) {
        data = $.parseJSON(data)
        $("#chat-group").append("<div class=\"chat\">\n" +
            "        id: <span>" +
            data.uid +
            "</span>\n" +
            "        信息<span>" +
            data.msg +
            "</span>\n" +
            "    </div>")
        console.log(data.uid);
    }

    function isJSON(str) {
        if (typeof str == 'string') {
            try {
                var obj = JSON.parse(str);
                if (typeof obj == 'object' && obj) {
                    return obj;
                } else {
                    return false;
                }

            } catch (e) {
                console.log('error:' + str + '!!!' + e);
                return false;
            }
        }
        console.log('It is not a string!')
    }
</script>
</body>
</html>
  • 我模拟了3个用户,我们来看一下效果吧
    [图片上传失败...(image-3844c1-1678190415553)]
    看一下服务端的信息打印
    [图片上传失败...(image-3b8d9d-1678190415553)]
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容