Websocket

websocket是什么

websocket与http一样都是OSI模型中应用层的协议,都是基于TCP协议来传输数据的,我们把这些更高级的协议理解成对TCP的封装。

socket与websocket的关系

socket与websocket的关系:就是毫无关系。
socket并不是一种协议,而是一个抽象层,将物理层、数据链路层、网络层与传输层向用户抽象成一层,用户直接使用socket封装的API即可操作这四层所带来的功能。
websocket是一种完整的应用层协议,包含一套完整的API。WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。
实际上,许多语言、框架和服务器都提供了 WebSocket 支持。

有了http协议,为什么还要有websocket协议?

非常好的一篇文章:https://blog.csdn.net/java_beautiful/article/details/127283971

文章总结

问题的痛点

Q:怎么样才能在用户不做任何操作的情况下,使客户端的数据发生实时的变化?
如果只有http协议的话,我们需要使用http不断轮询,即前端不断定时发送http请求到服务器,服务器收到请求后响应消息。这是一种伪的实时更新,只是背着用户偷偷不断发请求,用户没有感觉到罢了。这种轮询的应用场景也有很多,比如扫码登录等需要扫二维码的场景,手机发送了请求,但是网页前端不知道用户扫没扫,就需要不断向后端询问,保证用户在扫码后能立即得到回应。
只用http轮询的缺点:

  1. 当你打开F12页面时,你会发现满屏的HTTP请求。虽然很小,但这其实也消耗带宽,同时也会增加下游服务器的负担。
  2. 用户会感到明显的卡顿
  3. 所以http接口来进行前后端交互,需要用在实时交互不是很密集的场景。由于http是短连接,一旦请求后会断开与服务器的连接,因此不适合一些长时间的实时交互。但是,优点是不受网络的限制,断开重连非常之快,一般非实时的请求都会使用HTTP。

而Websocket是长连接,缺点是会受到网络波动的影响,但优点也是明显的,就是所谓的全双工通信,服务器与客户端可以实时地交换数据,很明显游戏就一定是建立在websocket协议上的。

websocket原理

我们平时刷网页,一般都是在浏览器上刷的,一会刷刷图文,这时候用的是HTTP协议,一会打开网页游戏,这时候就得切换成我们新介绍的websocket协议。 为了兼容这些使用场景。浏览器在TCP三次握手建立连接之后,都统一使用HTTP协议先进行一次通信。
如果此时是普通的HTTP请求,那后续双方就还是老样子继续用普通HTTP协议进行交互,这点没啥疑问。
如果这时候是想建立websocket连接,就会在HTTP请求里带上一些特殊的header头。

Connection: Upgrade 
Upgrade: websocket 
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg
​
// 这些header头的意思是,浏览器想升级协议(Connection: Upgrade),并且想升级成websocket协议(Upgrade: websocket)。
// 同时带上一段随机生成的base64码(Sec-WebSocket-Key),发给服务器。

如果服务器正好支持升级成websocket协议。就会走websocket握手流程,同时根据客户端生成的base64码,用某个公开的算法变成另一段字符串,放在HTTP响应的 Sec-WebSocket-Accept 头里,同时带上101状态码(协议切换状态码),发回给浏览器。

HTTP/1.1 101 Switching Protocols
Sec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY
Upgrade: websocket
Connection: Upgrade 

TCP支持全双工通信的,即双方都可以同时发送数据。而HTTP在同一时间只有客户端或者服务端一方能主动发送数据,是半双工通信。为了弥补http无法满足实时互相大量发送数据的场景,所以websocket协议被设计出来了。

websocket借鸡生蛋

websocket并不是HTTP的新协议,因为websocket只有在建立连接时才用到了HTTP,升级完成了之后就跟HTTP没有任何区别。
websocket完美继承了TCP协议的全双工能力,并且还贴心的提供了解决粘包的方案。它适用于需要服务器和客户端(浏览器)频繁交互的大部分场景。比如网页/小程序游戏,网页聊天室,以及一些类似飞书这样的网页协同办公软件。

使用Go搭建简单的websocket

这里没有使用官方的websocket包

github.com/gorilla/websocket

http升级为websocket

服务端

package main
​
import (
    "fmt"
    "github.com/gorilla/websocket"
    "log"
    "net/http"
)
​
var UP = websocket.Upgrader{
    // HandshakeTimeout:  0,     // 握手时间0为不限制
    ReadBufferSize:  1024, // 以字节为单位的IO缓冲区,如果缓冲区大小为0,则使用HTTP服务器分配的缓冲区(不为0就是读写做限制)
    WriteBufferSize: 1024, // 以字节为单位的IO缓冲区,如果缓冲区大小为0,则使用HTTP服务器分配的缓冲区(不为0就是读写做限制)
    // WriteBufferPool:   nil,   // WriteBufferPool是用于写操作的缓冲池
    // Error:             nil,   // 指定用于生成HTTP错误响应的函数
    // CheckOrigin:       nil,   // 对过来的请求做校验用的
    // EnableCompression: false, // 指定服务器是否应尝试根据进行协商消息压缩
}
​
func handler(w http.ResponseWriter, r *http.Request) { // http 原生写法
    conn, err := UP.Upgrade(w, r, nil) // 响应,请求,响应头可以不写
    if err != nil {
        log.Println(err)
        return
    }
​
    for {
        messageType, content, err := conn.ReadMessage()
        if err != nil {
            break
        }
        fmt.Println(messageType, string(content))
    }
    defer conn.Close()
    log.Println("服务关闭...")
}
​
func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8888", nil) // 端口号前必须加上冒号
}

客户端

package main
​
import (
    "bufio"
    "fmt"
    "github.com/gorilla/websocket"
    "log"
    "os"
)
​
func main() {
    dl := websocket.Dialer{
        // NetDial:           nil,
        // NetDialContext:    nil,
        // NetDialTLSContext: nil,
        // Proxy:             nil,
        // TLSClientConfig:   nil,
        // HandshakeTimeout:  0,
        // ReadBufferSize:    0, // 重要
        // WriteBufferSize:   0, // 重要
        // WriteBufferPool:   nil,
        // Subprotocols:      nil,
        // EnableCompression: false,
        // Jar:               nil,
    }
​
    conn, _, err := dl.Dial("ws://127.0.0.1:8888", nil) // 返回连接,http响应,错误
    if err != nil {
        log.Println(err)
        return
    }
​
    go send(conn)
    for {
        messageType, content, err := conn.ReadMessage()
        if err != nil {
            break
        }
        fmt.Println(messageType, string(content))
    }
​
}
​
func send(conn *websocket.Conn) {
    for {
        reader := bufio.NewReader(os.Stdin)
        l, _, _ := reader.ReadLine()
        conn.WriteMessage(websocket.TextMessage, l)
    }
}

多个客户端之间进行通讯,在服务端进行处理:

package main
​
import (
    "github.com/gorilla/websocket"
    "log"
    "net/http"
)
​
var UP = websocket.Upgrader{
    // HandshakeTimeout:  0,     // 握手时间0为不限制
    ReadBufferSize:  1024, // 以字节为单位的IO缓冲区,如果缓冲区大小为0,则使用HTTP服务器分配的缓冲区(不为0就是读写做限制)
    WriteBufferSize: 1024, // 以字节为单位的IO缓冲区,如果缓冲区大小为0,则使用HTTP服务器分配的缓冲区(不为0就是读写做限制)
    // WriteBufferPool:   nil,   // WriteBufferPool是用于写操作的缓冲池
    // Error:             nil,   // 指定用于生成HTTP错误响应的函数
    // CheckOrigin:       nil,   // 对过来的请求做校验用的
    // EnableCompression: false, // 指定服务器是否应尝试根据进行协商消息压缩
}
​
// 客户端每一个conn都是独立的,因此要完成客户端之间的通信,我们要把所有注册进来的conn都获取
var conns []*websocket.Conn
​
func handler(w http.ResponseWriter, r *http.Request) { // http 原生写法
    conn, err := UP.Upgrade(w, r, nil) // 响应,请求,响应头可以不写
    if err != nil {
        log.Println(err)
        return
    }
    conns = append(conns, conn)
    for {
        _, content, err := conn.ReadMessage()
        if err != nil {
            break
        }
        // 每一个客户端发一个请求,我们就让所有用户都能获取信息
        for i := range conns {
            conns[i].WriteMessage(websocket.TextMessage, []byte("你说的是:"+string(content)+"吗?"))
        }
    }
    defer conn.Close()
    log.Println("服务关闭...")
}
​
func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8888", nil) // 端口号前必须加上冒号
}

作者:陈双寅

posted @   中亿丰数字科技  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示