脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Golang 从 TCP 升级为 WebSocket

Golang 从 TCP 升级为 WebSocket

2021-08-19 00:50马哥Linux运维 Golang

有一个服务器原来是 TCP 的私有协议,突然需求要支持 WebSocket,赶鸭子想在原来的端口上硬上 WebSocket。最后居然还比较简单地成功了,必须说 golang 很舒服。

Golang 从 TCP 升级为 WebSocket

今天推送的文章中作者记录了时间紧任务重且成功上线了 TCPWebSocket升级的操作。

有一个服务器原来是 TCP 的私有协议,突然需求要支持 WebSocket,赶鸭子想在原来的端口上硬上 WebSocket。最后居然还比较简单地成功了,必须说 golang 很舒服。

  1.  WebSocket 库
  2.  从 TCP 升级成 WebSocket
  3.  使用方法

WebSocket 库

websocket 库选了官方的 http://golang.org/x/net/websocket,可以从 https://github.com/golang/net.git 克隆(到 go/src/golang.org/x/net)。

官方的 websocket 库用起来还是挺简单的,接口文档可以参考:websocket · pkg.go.dev。

客户端 

  1. conn, err :websocket.Dial(url, subprotocol, origin)  
  2. websocket.Message.Send(conn, "") // 发送一个 string  
  3. var b []byte 
  4. websocket.Message.Receive(conn, &b) // 接收一个 []byte 

客户端用 websocket.Dial() 来创建连接 *websocket.Conn。其中:

  •  subprotocol 表示细分的协议格式(如多种不同的序列化方法),默认可为空
  •  origin 表示发起请求的网站(只需要 http://<host> 这样)

虽然 *websocket.Conn 有 Read/Write 等方法,但使用 websocket.Message 更方便,因为可以保证一个封包的完整性。

服务端 

  1. var recv func([]byte)  
  2. var err error  
  3. :func(conn *websocket.Conn) {  
  4.     for {  
  5.         var b []byte  
  6.         err = websocket.Message.Receive(conn, &b)  
  7.         if err != nil {  
  8.             return  
  9.         } else {  
  10.             recv(b)  
  11.         }  
  12.     }  
  13.  
  14. websocket.Handler(f).ServeHTTP(w, r) 

用 websocket.Handler 或者 websocket.Server 两个类来升级(Upgrade) HTTP 请求,在回调中会收到一个 *websocket.Conn 以供业务方使用。

  •  Handler 或 Server 均可注册到 net.http 中使用,但也可以自行调用 ServeHTTP 方法
  •  Handler 只有一个简单回调的函数接口,使用闭包可以使用更多上下文

连接收发

如前文所示,可以用 websocket.Message 来进行简单的二进制或者字符串的收发,并且一次是一个完整的封包。

websocket.Codec 还可以支持序列化与反序列化,直接收发 golang 对象,只需要定义两个函数就好了,一个序列化,一个反序列化。websocket.JSON 是预置的解码器。另外 websocket.Message 也是一个解码器。

当然 *websocket.Conn 本身也实现了 net.Conn,拥有 RemoteAddr、Read、Write 等方法。只是使用 Read/Write 会模糊 WebSocket 协议的封装,没有必要。

从 TCP 升级成 WebSocket

幸运的是,TCP 私有协议与 WebSocket 握手协议有完全不同的协议头。所以判断头三个字节是不是 GET,就可以区分要不要转 WebSocket。

服务端创建 *websocket.Conn 可以通过 Handler.ServeHTTP(),但 TCP 协议嘛,只有一个 *net.TCPConn,而且已经读取了一些内容了。现在需要把一个 []byte + *net.TCPConn 变成 http.ResponseWriter + *http.Request。

http.ResponseWriter

http.ResponseWriter 是一个接口,可以简单模拟,而且 WebSocket 会通过 Hijack 转走,所以可以暴力实现之:

  1. type wsFakeWriter struct {  
  2.  conn *net.TCPConn  
  3.  rw   *bufio.ReadWriter  
  4.  
  5. func makeHttpResponseWriter(conn *net.TCPConn) *wsFakeWriter {  
  6.  w :new(wsFakeWriter)  
  7.  w.conn = conn  
  8.  w.rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))  
  9.  return w  
  10.  
  11. func (w *wsFakeWriter) Header() http.Header {  
  12.  return nil  
  13.  
  14. func (w *wsFakeWriter) WriteHeader(int) {  
  15.  // 处理升级失败情况??  
  16.  
  17. func (w *wsFakeWriter) Write(b []byte) (int, error) {  
  18.  return 0, nil  
  19.  
  20. func (w *wsFakeWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {  
  21.  return w.conn, w.rw, nil  

*http.Request

对于 *http.Request,很幸运地有 http.ReadRequest() 可用:

  1. func ReadRequest(b *bufio.Reader) (*Request, error) 

只是需要把 []byte 和 *net.TCPConn 打包成 bufio.Reader:

  1. func joinBufferAndReader(buffer []byte, reader io.Reader) io.Reader {  
  2.  return io.MultiReader(bytes.NewReader(buffer), reader)  
  3.  
  4. func takeHttpRequest(buffer []byte, conn *net.TCPConn) (*http.Request, error) {  
  5.  r :joinBufferAndReader(buffer, conn) 
  6.   return http.ReadRequest(bufio.NewReader(r))  

托 golang 强大的接口自动认证的福,这个打包过程甚至不需要做太多,调用标准库就满足了。[]byte 可以变成 io.Reader,*net.TCPConn 自然就是 io.Reader,标准库还能串联 io.Reader,一切都完美。

使用方法 

  1. func WebsocketOnTCP(buffer []byte, conn *net.TCPConn, recv func(*Package)) error {  
  2.  req, err :takeHttpRequest(buffer, conn)  
  3.  if err != nil {  
  4.   return err  
  5.  }  
  6.  f :func(ws *websocket.Conn) {  
  7.   err = doWebSocket(ws, recv)  
  8.  }  
  9.  w :makeHttpResponseWriter(conn)  
  10.  websocket.Handler(f).ServeHTTP(w, req)  
  11.  return err  
  12.  
  13. func doWebSocket(conn *websocket.Conn, recv func(*Package)) error {  
  14.  remoteAddr :conn.RemoteAddr()  
  15.  reply :func(b []byte) error {  
  16.   websocket.Message.Send(conn, b) // 此处线程不安全  
  17.  }  
  18.  for {  
  19.   var b []byte  
  20.   err :websocket.Message.Receive(conn, &b)  
  21.   if err != nil {  
  22.    return err  
  23.   }  
  24.   pack :new(Package)  
  25.   pack.Addr = remoteAddr  
  26.   pack.Content = b  
  27.   pack.Reply = reply  
  28.   recv(pack)  
  29.  }  

以上只是粗略的使用方法,简单的收发可以成功。只是未验证过线程安全,Upgrade 失败等情况。

原文链接:https://mp.weixin.qq.com/s/zRAwYeLeGoucBjVqrGiNsA

延伸 · 阅读

精彩推荐
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02