• WebSocket原理简介


    go语言webSocket框架——gorilla_go websocket_尚墨1111的博客-CSDN博客

    慢聊Go之GoLang中使用Gorilla Websocket|Go主题月 - 掘金 (juejin.cn)

    【Go项目】24. WebSocket 基本原理_哔哩哔哩_bilibili

    1.http和socket的区别

    1)

    http要先给服务器发请求,然后才会得到响应,基本是一问一答式。

    而socket建立的是一条双工通道,双方都可以发送和接收信息。

    2)

    socket效率更高,因为http里包含很多东西:get/post,header,cookie之类很多

    socket有什么发什么,没有要求,所以没有解析过程。

    但是维持连接需要内存,大量连接会消耗内存。

    2.websocket

    因为浏览器只会发http请求,所以为了实现发送socket,websocket是结合了socket和http的特点。

    要实现websocket,要先发送请求告诉服务器用websocket通信,收到回复之后就会建立socket通道。

    服务器响应:

    go语言webSocket框架——gorilla_go websocket_尚墨1111的博客-CSDN博客

    3.具体信息

    3.1 是什么

    WebSocket是一种在单个TCP连接上进行全双工通信的协议,长连接,双向传输
    需要安装第三方包:go get -u -v github.com/gorilla/websocket
    WebSocket 协议实现起来相对简单。 HTTP 协议初始握手建立连接,WebSocket 实质上使用原始 TCP 读取 / 写入数据
    http有良好的兼容性,ws和http的默认端口都是81,wss和https的默认端口都是443

    3.2 webSocket握手协议

    3.2.1 客户端请求 Request Header

    1. GET /chat HTTP/1.1
    2. Host: server.example.com
    3. Upgrade: websocket // 指明使用WebSocket协议
    4. Connection: Upgrade // 指明使用WebSocket协议
    5. Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== // Bse64 encode的值,是浏览器随机生成的
    6. Sec-WebSocket-Protocol: chat, superchat
    7. Sec-WebSocket-Version: 13 //指定Websocket协议版本
    8. Origin: http://example.com

    服务端收到Sec-WebSocket-Key后拼接上一个固定的GUID,进行一次SHA-1摘要,再转成Base64编码,得到Sec-WebSocket-Accept返回给客户端。客户端对本地的Sec-WebSocket-Key执行同样的操作跟服务端返回的结果进行对比,如果不一致会返回错误关闭连接。如此操作是为了把websocket header 跟http header区分开
     

    3.2.2 服务器响应 Response Header

    1. HTTP/1.1 111 Switching Protocols
    2. Upgrade: websocket
    3. Connection: Upgrade
    4. Sec-WebSocket-Accept: HSmrc1sMlYUkAGmm5OPpG2HaGWk=
    5. Sec-WebSocket-Protocol: chat

    3.2.3 websocket发送的消息类型

    5种:TextMessag、BinaryMessage、CloseMessag、PingMessage、PongMessage

    TextMessag和BinaryMessage分别表示发送文本消息和二进制消息

    CloseMessage关闭帧,接收方收到这个消息就关闭连接

    PingMessage和PongMessage是保持心跳的帧,服务器发ping给浏览器,浏览器返回pong消息

    3.2.4 控制类消息

    Websocket协议定义了三种控制消息:Close、Ping和Pong。通过调用Conn的WriteControl、WriteMessage或NextWriter方法向对端发送控制消息。

    Conn收到了Close消息之后,调用由SetCloseHandler方法设置的handler函数,然后从NextReader 、ReadMessage或消息的Read 方法返回一个*CloseError。缺省的close handler会发送一个Close消息到对端。

    Conn收到了Ping消息之后,调用由SetPingHandler 方法设置的handler函数。缺省的ping handler会发送一个Pong消息到对象。

    Conn收到了Pong消息之后,调用由SetPongHandler 设置的handler函数。缺省的pong handler什么也不做。

    控制消息的handler函数是从NextReader、ReadMessage和消息的Read方法中调用的。缺省的close handler和ping handler向对端写数据时可能会短暂阻塞这些方法。

    应用程序必须读取Conn,使得对端发送的close、ping、和pong消息能够得到处理。即使应用程序不关心对端发送的消息,也应该启动一个goroutine来读取对端的消息并丢弃。
     

    4.gorilla/websocket

    websocket由http升级而来,首先发送附带Upgrade请求头的Http请求,所以我们需要在处理Http请求时拦截请求并判断其是否为websocket升级请求,如果是则调用gorilla/websocket库相应函数处理升级请求

    4.1 Upgrader

    Upgrader发送附带Upgrade请求头的Http请求,把 http 请求升级为长连接的 WebSocket,结构如下

    1. //升级器结构
    2. type Upgrader struct {
    3. // 升级 websocket 握手完成的超时时间
    4. HandshakeTimeout time.Duration
    5. // io 操作的缓存大小,如果不指定就会自动分配。
    6. ReadBufferSize, WriteBufferSize int
    7. // 写数据操作的缓存池,如果没有设置值,write buffers 将会分配到链接生命周期里。
    8. WriteBufferPool BufferPool
    9. //按顺序指定服务支持的协议,如值存在,则服务会从第一个开始匹配客户端的协议。
    10. Subprotocols []string
    11. // http 的错误响应函数,如果没有设置 Error 则,会生成 http.Error 的错误响应。
    12. Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
    13. // 如果请求Origin标头可以接受,CheckOrigin将返回true。 如果CheckOrigin为nil,则使用安全默认值:如果Origin请求头存在且原始主机不等于请求主机头,则返回false
    14. // 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造。如果不检查,就设置一个返回值为true的函数
    15. CheckOrigin func(r *http.Request) bool
    16. // EnableCompression 指定服务器是否应尝试协商每个邮件压缩(RFC 7692)。 将此值设置为true并不能保证将支持压缩。 目前仅支持“无上下文接管”模式
    17. EnableCompression bool
    18. }

    4.1.1 创建Upgrader实例

    该实例用于升级请求

    checkOrigin用于拦截域外请求

    r.Method判断方法

     r.URL.Path判断路由

    1. var upgrader = websocket.Upgrader{
    2. ReadBufferSize: 1124, //指定读缓存大小
    3. WriteBufferSize: 1124, //指定写缓存大小
    4. CheckOrigin: checkOrigin,
    5. }
    6. // 检测请求来源
    7. func checkOrigin(r *http.Request) bool {
    8. if r.Method != "GET" {
    9. fmt.Println("method is not GET")
    10. return false
    11. }
    12. if r.URL.Path != "/ws" {
    13. fmt.Println("path error")
    14. return false
    15. }
    16. return true
    17. }

    其中CheckOringin是一个函数,该函数用于拦截或放行跨域请求。函数返回值为bool类型,即true放行,false拦截。如果请求不是跨域请求可以不赋值

    4.1.2 升级协议

    服务器收到请求之后回传一个响应resp告诉客户端是否成功,升级成功则返回一个连接conn,代表这个客户端和服务器的连接,后续客户端要给服务器发什么都要通过这个conn进行

    1. // responseHeader包含在对客户端升级请求的响应中。
    2. // 使用responseHeader指定cookie(Set-Cookie)和
    3. //应用程序协商的子协议(Sec-WebSocket-Protocol)。
    4. // 如果升级失败,则升级将使用HTTP错误响应回复客户端
    5. // 返回一个 Conn 指针,使用 Conn 读写数据与客户端通信。
    6. func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request,
    7. responseHeader http.Header) (*Conn, error)

    升级为websocket连接并获得一个conn实例,之后的发送接收操作皆有conn,其类型为websocket.Conn。

    服务器端请求升级的过程:

    判断是否为ws升级->发升级请求->获取conn->用conn写消息/协程读消息

    1. //Http入口
    2. func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    3. //判断请求是否为websocket升级请求。
    4. if websocket.IsWebSocketUpgrade(r) {
    5. // 收到 http 请求后升级协议
    6. conn, err := upgrader.Upgrade(w, r, w.Header())
    7. // 向客户端发送消息使用 WriteMessage(messageType int, data []byte),参数1为消息类型,参数2消息内容
    8. conn.WriteMessage(websocket.TextMessage, []byte("升级成功"))
    9. // 接受客户端消息使用 ReadMessage(),该操作阻塞线程所以建议运行在其他协程上。
    10. //返回值(接收消息类型、接收消息内容、发生的错误)当然正常执行时错误为 nil。一旦连接关闭返回值类型为-1可用来终止读操作。
    11. go func() {
    12. for {
    13. t, c, _ := conn.ReadMessage()
    14. fmt.Println(t, string(c))
    15. if t == -1 {
    16. return
    17. }
    18. }
    19. }()
    20. } else {
    21. //处理普通请求
    22. c := newContext(w, r)
    23. e.router.handle(c)
    24. }
    25. }

    4.1.3 客户端设置关闭连接的监听

    这个的意思就是监听有没有接收到关闭请求,如果接收到,就执行断开连接行动。

    函数为SetCloseHandler(h func(code int, text string) error)函数接收一个函数为参数,参数为nil时有一个默认实现,其底层源码为:

    1. func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
    2. if h == nil {
    3. h = func(code int, text string) error {
    4. message := FormatCloseMessage(code, "")
    5. c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
    6. return nil
    7. }
    8. }
    9. c.handleClose = h
    10. }

    这里作为参数的函数的参数为int和string类型对应前端js设置的一个关闭按钮,点击关闭后传送一个int和string给这个函数func(code int, text string) error所使用。

    函数使用:

    1. // 设置关闭连接监听
    2. conn.SetCloseHandler(func(code int, text string) error {
    3. fmt.Println(code, text) // 断开连接时将打印code和text
    4. return nil
    5. })

    4.1.4 总览 

    这里是服务器代码

    1. type WsServer struct {
    2. ......
    3. // 定义一个 upgrade 类型用于升级 http 为 websocket
    4. upgrade *websocket.Upgrader
    5. }
    6. //升级方法
    7. func NewWsServer() *WsServer {
    8. ws.upgrade = &websocket.Upgrader{
    9. ReadBufferSize: 4196,//指定读缓存区大小
    10. WriteBufferSize: 1124,// 指定写缓存区大小
    11. // 检测请求来源
    12. CheckOrigin: func(r *http.Request) bool {
    13. if r.Method != "GET" {
    14. fmt.Println("method is not GET")
    15. return false
    16. }
    17. if r.URL.Path != "/ws" {
    18. fmt.Println("path error")
    19. return false
    20. }
    21. return true
    22. },upgrade
    23. }
    24. return ws
    25. }
    26. //客户端请求升级
    27. func (self *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    28. ......
    29. // 收到 http 请求后 升级 协议
    30. conn, err := self.upgrade.Upgrade(w, r, nil)
    31. if err != nil {
    32. fmt.Println("websocket error:", err)
    33. return
    34. }
    35. fmt.Println("client connect :", conn.RemoteAddr())
    36. go self.connHandle(conn)
    37. }

    5.简单代码

    服务端

    升级->读取请求/写响应->读失败就是关闭连接了->退出

    1. package main
    2. import (
    3. "github.com/gorilla/websocket"
    4. "log"
    5. "net/http"
    6. )
    7. //升级器设置
    8. var upgrade = websocket.Upgrader{
    9. ReadBufferSize: 1124,
    10. WriteBufferSize: 1124,
    11. CheckOrigin: func(r *http.Request) bool {
    12. return true
    13. },
    14. }
    15. //升级和信息读写函数
    16. func HelloHTTP(w http.ResponseWriter, r *http.Request) {
    17. //1.升级协议,并返回升级后的长连接
    18. conn, err := upgrade.Upgrade(w, r, nil)
    19. if err != nil {
    20. log.Println("Error during connection upgrade:", err)
    21. return
    22. }
    23. defer conn.Close()
    24. for {
    25. // 2.读取客户端的请求信息
    26. messageType, message, err := conn.ReadMessage()
    27. if err != nil {
    28. log.Println("Error during message writing:", err)
    29. return
    30. }
    31. log.Printf("Recive message:%s", message)
    32. // 3.返回给客户端信息
    33. err = conn.WriteMessage(messageType, message)
    34. if err != nil {
    35. log.Println("Error during message writing:", err)
    36. return
    37. }
    38. }
    39. }
    40. //注册路由,监听接口
    41. func main() {
    42. http.HandleFunc("/socket", HelloHTTP)
    43. http.ListenAndServe(":8181", nil)
    44. }

    客户端

    确定服务器地址->拨号请求连接升级->获取conn->持续给服务器发数据->同时持续接收数据

    1. package main
    2. import (
    3. "github.com/gorilla/websocket"
    4. "log"
    5. "time"
    6. )
    7. //一直读取服务器发过来的数据
    8. func ReceiveHandler(con *websocket.Conn) {
    9. for {
    10. _, message, err := con.ReadMessage()
    11. if err != nil {
    12. log.Println("Error during Receive:", err)
    13. return
    14. }
    15. log.Printf("Receive:%s\n", message)
    16. }
    17. }
    18. func main() {
    19. //目标服务器地址
    20. socketUrl := "ws://localhost:8181" + "/socket"
    21. //对该ip发送升级连接请求
    22. // 使用 net.Dialer Dialer.Dial 函数建立 TCP 连接,建立成功后,取得了 net.Conn 对象,
    23. conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
    24. if err != nil {
    25. log.Fatal("Error connecting to websocket Server:", err)
    26. }
    27. defer conn.Close()
    28. //定时器,每秒执行一次
    29. ticker := time.Tick(time.Second)
    30. for range ticker {
    31. //每秒给服务器写一次数据
    32. err = conn.WriteMessage(websocket.TextMessage, []byte("Hello World!"))
    33. if err != nil {
    34. log.Println("Error during writing to websocket:", err)
    35. return
    36. }
    37. //开协程读消息
    38. // 接受客户端消息使用ReadMessage()该操作会阻塞线程所以建议运行在其他协程上
    39. go ReceiveHandler(conn)
    40. }
    41. }

    6.加强版

     服务端

    这个相比于上面就是加了一个关闭连接的监听器,等待前端点击关闭按钮,就会收到参数,结束函数

    1. package main
    2. import (
    3. "fmt"
    4. "github.com/gorilla/websocket"
    5. "log"
    6. "net/http"
    7. )
    8. var upgrader = websocket.Upgrader{
    9. ReadBufferSize: 4196,
    10. WriteBufferSize: 1124,
    11. CheckOrigin: func(r *http.Request) bool {
    12. //if r.Method != "GET" {
    13. // fmt.Println("method is not GET")
    14. // return false
    15. //}
    16. //if r.URL.Path != "/ws" {
    17. // fmt.Println("path error")
    18. // return false
    19. //}
    20. return true
    21. },
    22. }
    23. // ServerHTTP 用于升级协议
    24. func ServerHTTP(w http.ResponseWriter, r *http.Request) {
    25. // 收到http请求之后升级协议
    26. conn, err := upgrader.Upgrade(w, r, nil)
    27. if err != nil {
    28. log.Println("Error during connection upgrade:", err)
    29. return
    30. }
    31. defer conn.Close()
    32. for {
    33. // 服务端读取客户端请求
    34. messageType, message, err := conn.ReadMessage()
    35. if err != nil {
    36. log.Println("Error during message reading:", err)
    37. break
    38. }
    39. log.Printf("Received:%s", message)
    40. // 开启关闭连接监听
    41. conn.SetCloseHandler(func(code int, text string) error {
    42. fmt.Println(code, text) // 断开连接时将打印code和text
    43. return nil
    44. })
    45. //服务端给客户端返回请求
    46. err = conn.WriteMessage(messageType, message)
    47. if err != nil {
    48. log.Println("Error during message writing:", err)
    49. return
    50. }
    51. }
    52. }
    53. func home(w http.ResponseWriter, r *http.Request) {
    54. fmt.Fprintf(w, "Index Page")
    55. }
    56. func main() {
    57. http.HandleFunc("/socket", ServerHTTP)
    58. http.HandleFunc("/", home)
    59. log.Fatal(http.ListenAndServe("localhost:8181", nil))
    60. }

     服务端

            这里加了两个管道传递中断信息来控制程序运行。

            interrupt专门用来捕获系统中断, 这句signal.Notify(interrupt, os.Interrupt),当系统中断的时候interrupt就会关闭。

            在循环发消息和开启读消息线程的同时(select是并发随机操作的),如果接收到系统中断,interrupt被关闭,第一个select会检测到,然后就给服务器发送关闭连接消息。

            连接关闭后,协程里的读消息就会出错,协程就知道连接关闭了,就会返回。返回之前关闭done管道,这样第二个select就会发现done出问题了,然后知道读消息器关闭了,就会关闭程序。

    1. // client.go
    2. package main
    3. import (
    4. "github.com/gorilla/websocket"
    5. "log"
    6. "os"
    7. "os/signal"
    8. "time"
    9. )
    10. //两种管道
    11. var done chan interface{}
    12. var interrupt chan os.Signal
    13. //持续读消息
    14. func receiveHandler(connection *websocket.Conn) {
    15. //如果接收器关闭,那么也把done关闭,这样select就会检测到done的关闭就会执行程序关闭
    16. defer close(done)
    17. for {
    18. _, msg, err := connection.ReadMessage()
    19. if err != nil {
    20. log.Println("Error in receive:", err)
    21. return
    22. }
    23. log.Printf("Received: %s\n", msg)
    24. }
    25. }
    26. func main() {
    27. //1号管道存储消息接收器的结束状态
    28. done = make(chan interface{}) // Channel to indicate that the receiverHandler is done
    29. //2号管道监听中断信号来结束
    30. interrupt = make(chan os.Signal) // Channel to listen for interrupt signal to terminate gracefully
    31. //告知中断管道 SIGINT中断信号
    32. signal.Notify(interrupt, os.Interrupt) // Notify the interrupt channel for SIGINT
    33. //服务器地址
    34. socketUrl := "ws://localhost:8181" + "/socket"
    35. //拨号连接ws
    36. conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
    37. if err != nil {
    38. log.Fatal("Error connecting to Websocket Server:", err)
    39. }
    40. defer conn.Close()
    41. //读消息
    42. go receiveHandler(conn)
    43. // 无限循环使用select来通过通道监听事件
    44. for {
    45. select {
    46. case <-time.After(time.Duration(1) * time.Millisecond * 1111):
    47. //conn.WriteMessage()每秒钟写一条消息
    48. err := conn.WriteMessage(websocket.TextMessage, []byte("Hello from GolangDocs!"))
    49. if err != nil {
    50. log.Println("Error during writing to websocket:", err)
    51. return
    52. }
    53. //如果激活了中断信号,则所有未决的连接都将关闭
    54. case <-interrupt:
    55. // We received a SIGINT (Ctrl + C). Terminate gracefully...
    56. log.Println("Received SIGINT interrupt signal. Closing all pending connections")
    57. // Close our websocket connection
    58. err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
    59. if err != nil {
    60. log.Println("Error during closing websocket:", err)
    61. return
    62. }
    63. select {
    64. // 如果receiveHandler通道退出,则通道'done'将关闭
    65. case <-done:
    66. log.Println("Receiver Channel Closed! Exiting....")
    67. //如果'done'通道未关闭,则在1秒钟后会有超时,因此程序将在1秒钟超时后退出
    68. case <-time.After(time.Duration(1) * time.Second):
    69. log.Println("Timeout in closing receiving channel. Exiting....")
    70. }
    71. return
    72. }
    73. }
    74. }

    总结:客户端给指定ip服务器发请求,然后持续读写消息,等待某些信号就关闭连接。

    服务器如果接收到升级请求,就给他升级,然后读写消息,等待收到连接关闭消息后结束程序。

  • 相关阅读:
    LED灯实验
    L1-094 剪切粘贴
    Linux扩展篇之Shell编程二(运算符和条件判断)
    更改linux centos 7系统语言
    LeetCode:买卖股票的最佳时机 系列Ⅰ、Ⅱ、Ⅲ、Ⅳ、含冷冻期(C++)
    LangChain: 大语言模型的新篇章
    WebGl-Blender:建模 / 想象成形 / 初识 Blender
    慢SQL,压垮团队的最后一根稻草!
    网络规划设计
    Linux-sed
  • 原文地址:https://blog.csdn.net/m0_50973548/article/details/132683394