使用goframe框架进行websocket开发相当简单。我们以下通过实现一个简单的echo服务器来演示goframe框架的websocket的使用(客户端使用HTML5实现)。

HTML5客户端

先上H5客户端的代码

<!DOCTYPE html>
<html lang="zh">
<head>
    <title>gf websocket echo server</title>
 	<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
<div class="container">
    <div class="list-group" id="divShow"></div>
    <div>
        <div><input class="form-control" id="txtContent" autofocus placeholder="请输入发送内容"></div>
        <div><button class="btn btn-default" id="btnSend" style="margin-top:15px">发 送</button></div>
    </div>
</div>
</body>
</html>

<script type="application/javascript">
    // 显示提示信息
    function showInfo(content) {
        $("<div class=\"list-group-item list-group-item-info\">" + content + "</div>").appendTo("#divShow")
    }
    // 显示警告信息
    function showWaring(content) {
        $("<div class=\"list-group-item list-group-item-warning\">" + content + "</div>").appendTo("#divShow")
    }
    // 显示成功信息
    function showSuccess(content) {
        $("<div class=\"list-group-item list-group-item-success\">" + content + "</div>").appendTo("#divShow")
    }
    // 显示错误信息
    function showError(content) {
        $("<div class=\"list-group-item list-group-item-danger\">" + content + "</div>").appendTo("#divShow")
    }

    $(function () {
        const url = "ws://127.0.0.1:8199/ws";
        let ws  = new WebSocket(url);
        try {
            // ws连接成功
            ws.onopen = function () {
                showInfo("WebSocket Server [" + url +"] 连接成功!");
            };
            // ws连接关闭
            ws.onclose = function () {
                if (ws) {
                    ws.close();
                    ws = null;
                }
                showError("WebSocket Server [" + url +"] 连接关闭!");
            };
            // ws连接错误
            ws.onerror = function () {
                if (ws) {
                    ws.close();
                    ws = null;
                }
                showError("WebSocket Server [" + url +"] 连接关闭!");
            };
            // ws数据返回处理
            ws.onmessage = function (result) {
                showWaring(" > " + result.data);
            };
        } catch (e) {
            alert(e.message);
        }

        // 按钮点击发送数据
        $("#btnSend").on("click", function () {
            if (ws == null) {
                showError("WebSocket Server [" + url +"] 连接失败,请F5刷新页面!");
                return;
            }
            const content = $.trim($("#txtContent").val()).replace("/[\n]/g", "");
            if (content.length <= 0) {
                alert("请输入发送内容!");
                return;
            }
            $("#txtContent").val("")
            showSuccess(content);
            ws.send(content);
        });

        // 回车按钮触发发送点击事件
        $("#txtContent").on("keydown", function (event) {
            if (event.keyCode === 13) {
                $("#btnSend").trigger("click");
            }
        });
    })

</script>

注意我们这里的服务端连接地址为:ws://127.0.0.1:8199/ws

客户端的功能很简单,主要实现了这几个功能:

  • 与服务端websocket连接状态保持及信息展示;
  • 界面输入内容并发送信息到websocket服务端;
  • 接收到websocket的返回信息后回显在界面上;

WebSocket服务端

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/os/gctx"
	"github.com/gogf/gf/v2/os/gfile"
	"github.com/gogf/gf/v2/os/glog"
)

var ctx = gctx.New()

func main() {
	s := g.Server()
	s.BindHandler("/ws", func(r *ghttp.Request) {
		ws, err := r.WebSocket()
		if err != nil {
			glog.Error(ctx, err)
			r.Exit()
		}
		for {
			msgType, msg, err := ws.ReadMessage()
			if err != nil {
				return
			}
			if err = ws.WriteMessage(msgType, msg); err != nil {
				return
			}
		}
	})
	s.SetServerRoot(gfile.MainPkgPath())
	s.SetPort(8199)
	s.Run()
}

可以看到,服务端的代码相当简单,这里需要着重说明的是这几个地方:

  1. WebSocket方法 

websocket服务端的路由注册方式和普通的http回调函数注册方式一样,但是在接口处理中我们需要通过ghttp.Request.WebSocket方法(这里直接使用指针对象r.WebSocket())将请求转换为websocket操作,并返回一个WebSocket对象,该对象用于后续的websocket通信操作。当然,如果客户端请求并非为websocket操作时,转换将会失败,该方法会返回错误信息,使用时请注意判断方法的error返回值。

  1. ReadMessage & WriteMessage 

读取消息以及写入消息对应的是websocket的数据读取以及写入操作(ReadMessage & WriteMessage),需要注意的是这两个方法都有一个msgType的变量,表示请求读取及写入数据的类型,常见的两种数据类型为:字符串数据或者二进制数据。在使用过程中,由于接口双方都会约定统一的数据格式,因此读取和写入的msgType几乎都是一致的,所以在本示例中的返回消息时,数据类型参数直接使用的是读取到的msgType

HTTPS的WebSocket

如果需要支持HTTPSWebSocket服务,只需要依赖的WebServer支持HTTPS即可,访问的WebSocket地址需要使用 wss:// 协议访问。以上客户端HTML5页面中的WebSocket访问地址需要修改为:wss://127.0.0.1:8199/wss。服务端示例代码:

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/os/gctx"
	"github.com/gogf/gf/v2/os/gfile"
	"github.com/gogf/gf/v2/os/glog"
)

var ctx = gctx.New()

func main() {
	s := g.Server()
	s.BindHandler("/wss", func(r *ghttp.Request) {
		ws, err := r.WebSocket()
		if err != nil {
			glog.Error(ctx, err)
			r.Exit()
		}
		for {
			msgType, msg, err := ws.ReadMessage()
			if err != nil {
				return
			}
			if err = ws.WriteMessage(msgType, msg); err != nil {
				return
			}
		}
	})
	s.SetServerRoot(gfile.MainPkgPath())
	s.EnableHTTPS("../../https/server.crt", "../../https/server.key")
	s.SetPort(8199)
	s.Run()
}

示例结果展示

我们首先执行示例代码main.go,随后访问页面 http://127.0.0.1:8199/,随意输入请求内容并提交,随后在服务端关闭程序。可以看到,页面会回显提交的内容信息,并且即时展示websocket的连接状态的改变,当服务端关闭时,客户端也会即时地打印出关闭信息。

Websocket安全校验

GoFrame框架的websocket模块并不会做同源检查(origin),也就是说,这种条件下的websocket允许完全跨域。

安全的校验需要由业务层来处理,安全校验主要包含以下几个方面:

  1. origin的校验: 业务层在执行r.WebSocket()之前需要进行origin同源请求的校验;或者按照自定义的处理对请求进行校验(如果请求提交参数);如果未通过校验,那么调用r.Exit()终止请求。
  2. websocket通信数据校验: 数据通信往往都有一些自定义的数据结构,在这些通信数据中加上鉴权处理逻辑;

WebSocket Client 客户端

 package main

import (
	"crypto/tls"
	"fmt"
	"net/http"
	"time"

	"github.com/gogf/gf/v2/net/gclient"
	"github.com/gorilla/websocket"
)

func main() {
	client := gclient.NewWebSocket()
	client.HandshakeTimeout = time.Second    // 设置超时时间
	client.Proxy = http.ProxyFromEnvironment // 设置代理
	client.TLSClientConfig = &tls.Config{}   // 设置 tls 配置

	conn, _, err := client.Dial("ws://127.0.0.1:8199/ws", nil)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	err = conn.WriteMessage(websocket.TextMessage, []byte("hello word"))
	if err != nil {
		panic(err)
	}

	mt, data, err := conn.ReadMessage()
	if err != nil {
		panic(err)
	}
	fmt.Println(mt, string(data))
} 
Content Menu

  • No labels

37 Comments

  1. 可以给个服务端创建链接的例子么

    1. Tom

      可以拉最新 gf master 代码

      参照 https://goframe.org/pages/viewpage.action?pageId=1114298#WebSocket服务-WebSocketclient客户端 就可以创建服务端 websocket 客户端
      1. 他要的是服务端,文档中本来就有服务端。或者说他表达不对,其实是想要客户端。

      2. 感谢,已收到!

    2. 2022-4-23 11:35:34  文档没更新,v2 使用 gclient.NewWebSocket()

  2. 请教下 怎么广播给房间内所有人 

    1. 自己维护下scoketList  例如

      clientList  map[string]*ghttp.WebSocket
      
      
      /** 广播消息*/
      func (_this *Monitor) BroadcastMsg(aMsg string) {
         tJson := gjson.New(fmt.Sprintf(`{"msgType":0, "info":"%v"}`, aMsg))
         fmt.Println(tJson.MustToJsonString())
         for country := range _this.clientList {
            _this.clientList[country].WriteMessage(1, []byte(tJson.MustToJsonString()))
         }
      }
      
      /** 发送消息*/
      func (_this *Monitor) SendMsg(aIp string, aMsg string) {
         tJson := gjson.New(fmt.Sprintf(`{"msgType":1, "info":"%v"}`, aMsg))
         _this.clientList[aIp].WriteMessage(1, []byte(tJson.MustToJsonString()))
      }
      
      /** 群发*/
      func (_this *Monitor) MassMailing(aIp map[string]bool, aMsg string) {
         tJson := gjson.New(fmt.Sprintf(`{"msgType":0, "info":"%v"}`, aMsg))
         for index := range aIp {
            _this.clientList[index].WriteMessage(1, []byte(tJson.MustToJsonString()))
         }
      }
      1. 我测试过这个写法不行,_this.clientList[aIp]  这个写法用下标来存储websoket推送不到指定的客户端
        我改这个写法是才可以



  3. websocket有没有绑定到controller和action的例子

    1. 服务端示例

       s.BindHandler("/wss", func(r *ghttp.Request) {

      这里就是吧



      1. 这明显不是。gf对用php的来说很亲切。我之前用的swoole,有个例子不错

        /**
         * Class WebSocketParser
         *
         * 此类是自定义的 websocket 消息解析器
         * 此处使用的设计是使用 json string 作为消息格式
         * 当客户端消息到达服务端时,会调用 decode 方法进行消息解析
         * 会将 websocket 消息 转成具体的 Class -> Action 调用 并且将参数注入
         *
         * @package App\WebSocket
         */
        class WebSocketParser implements ParserInterface
        {
            /**
             * decode
             * @param  string         $raw    客户端原始消息
             * @param  WebSocket      $client WebSocket Client 对象
             * @return Caller         Socket  调用对象
             */
            public function decode($raw, $client) : ? Caller
            {
                // 解析 客户端原始消息
        //        var_dump($raw);
                $data = json_decode($raw, true);
                if (!is_array($data)) {
                    echo "decode message error! \n";
                    return null;
                }
        
                // new 调用者对象
                $caller =  new Caller();
                /**
                 * 设置被调用的类 这里会将ws消息中的 class 参数解析为具体想访问的控制器
                 * 如果更喜欢 event 方式 可以自定义 event 和具体的类的 map 即可
                 */
                $class = '\\App\\WebSocket\\'. ucfirst($data['class'] ?? 'Index');
                $caller->setControllerClass($class);
        
                // 提供一个事件风格的写法
        //         $eventMap = [
        //             'index' => Index::class
        //         ];
        //         $caller->setControllerClass($eventMap[$data['class']] ?? Index::class);
        
                // 设置被调用的方法
                $caller->setAction($data['action'] ?? 'index');
                // 检查是否存在args
                if (isset($data['content']) && is_array($data['content'])) {
                    $args = $data['content'];
                }
        
                // 设置被调用的Args
                $caller->setArgs($args ?? []);
                return $caller;
            }
        
            /**
             * encode
             * @param  Response     $response Socket Response 对象
             * @param  WebSocket    $client   WebSocket Client 对象
             * @return string             发送给客户端的消息
             */
            public function encode(Response $response, $client) : ? string
            {
                /**
                 * 这里返回响应给客户端的信息
                 * 这里应当只做统一的encode操作 具体的状态等应当由 Controller处理
                 */
                return $response->getMessage();
            }
        }
        1. 目前对golang还不熟练。不知道怎么写了。

            1. 看了,写的不错的。主要还是自身golang不够熟练。想捡个轮子。

              1. 把service下的websocket整个提取出来就可以直接用

                1. 十分感谢。

  4. concurrent write to websocket connection

    并发比较大的时候 遇到了这个问题~

      1. https://github.com/gogf/gf-demos

        这个demo里面的群发消息

        users.RlockFunc 改为 users.LockFunc 就解决问题了

        1. 长连业务,并发数据下发,最好不要用锁,用channel通过队列下发。可以参考nano游戏框架源码

  5. V2版本websocket 应用demo

    实现功能

    全局广播
    单个房间推送
    单个用户推送
    单个客户端推送

    加入房间
    退出房间
    用户登录

    https://github.com/SmallRuralDog/gf-websocket

    1. 大佬可以写点例子吗?萌新感觉看懂了又没完全看懂

    2. 大佬,结合上面的HTML5 客户端,写点例子、

    3. 大佬, 给个加入房间的demo

    4. 这个demo太好了,拿来主义,直接用上,省事

    5. 大佬写的代码太好了,非常有质量,不过我发现有一个地方写错了

      文件client_manager.go里面,NewClientManager初始化的时候少了 ClientBroadcast,导致推送单个客户端不成功

    6. 真的太优秀了!

      NewClientManager这里需要添加以下代码,不然就无法实现登录和单个客户推送

      Login:            make(chan *Login, 1000),
      ClientBroadcast: make(chan *model.ClientWResponse, 1000),

       

  6. WebSocket client 客户端 代码用不了,

    ghttp.NewWebSocketClient()

    v2里没有了,需要怎么弄,请会的指点一下

  7. webSocket 如何向客户端发送一个html文件,让客户端显示啊?能不能提供一个示例

    1. 把内容当做字符发就可以(发送body部分让前端动态load).但是不建议这么做.

  8. ws.WriteMessage()

    这个方法发的消息,前端都是在onMessage回调里面收到的,有什么方法能让前端在onOpen的时候就收到消息呢?我看了js,说onOpen的时候是可以收到一条消息的

  9. 遇到一个问题。使用tsl单项认证后,使用wss连接,控制台会一直输出tsl

  10. webscocket 和 http 不可以在同一个应用程序启动吗? 

  11. 基于gf v2.7的一个WebSocket实现,包含服务端和客户端。

    已实现

    • 仿http路由与路由组写法
    • 路由支持链式调用与路由组层级嵌套
    • 路由/组与中间件实现
    • 通过中间件可实现消息限流()
    • Action绑定到控制器
    • 群聊与私聊,上下线通知
    • 支持单点登录被迫下线()
    • 异常处理,隐式ping,客户端自动重连等

    源码:WebSocket Demo