网站投注建设,买空间做网站,专题学习网站开发流程,电子商务网站建设与管理的有关论文目录
为什么需要websocket
使用场景
在线教育
视频弹幕
Web端即时通信方式
什么是web端即时通讯技术#xff1f;
轮询
长轮询
长连接 SSE
websocket
通信方式总结
Websocket介绍
协议升级
连接确认
数据帧
socket和websocket
常见状态码
gorilla/websocket实…目录
为什么需要websocket
使用场景
在线教育
视频弹幕
Web端即时通信方式
什么是web端即时通讯技术
轮询
长轮询
长连接 SSE
websocket
通信方式总结
Websocket介绍
协议升级
连接确认
数据帧
socket和websocket
常见状态码
gorilla/websocket实战和底层代码分析
简单使用
Upgrader
Conn
服务端示例
客户端示例
源码走读
Upgrade 协议升级
ReadMessage 读消息
WriteMessage 写消息
advanceFrame 解析数据帧
heartbeat 心跳
总结 为什么需要websocket 初次接触 websocket 的人可能都会有这样的疑问我们已经有了 http 协议为什么还需要websocket协议它带来了什么好处 原因是http每次请求只能由客户发起而websocket最大特点就是服务器可以主动向客户端推送信息客户端也可以主动向服务器发送信 使用场景
在线教育 老师进行一对多的在线授课在客户端内编写的笔记、大纲等信息需要实时推送至多个学生的客户端需要通过WebSocket协议来完成。 视频弹幕 终端用户A在自己的手机端发送了一条弹幕信息但是您也需要在客户A的手机端上将其他N个客户端发送的弹幕信息一并展示。需要通过WebSocket协议将其他客户端发送的弹幕信息从服务端全部推送至客户A的手机端从而使客户A可以同时看到自己发送的弹幕和其他用户发送的弹幕。 当然还有体育实况更新、视频会议和聊天等等这里都不一一列举了 Web端即时通信方式
什么是web端即时通讯技术 可以理解为实现这样一种功能服务器端可以即时地将数据的更新或变化反应到客户端例如消息推送等功能都是通过这种技术实现的。 但是在Web中由于浏览器的限制实现即时通讯需要借助一些方法。这种限制出现的主要原因是一般的Web通信都是浏览器先发送请求到服务器服务器再进行响应完成数据的现实更新。 Web端实现即时通讯主要有四种方式 轮询、长轮询(comet)、长连接(SSE)、WebSocket。 它们大体可以分为两类一种是在HTTP基础上实现的包括短轮询、长轮询(comet)、长连接(SSE)另一种不是在HTTP基础上实现是即WebSocket。下面分别介绍一下这四种轮询方式。 轮询 基本思路就是客户端每隔一段时间向服务器发送http请求服务器端在收到请求后不管是否有所需数据返回都直接进行响应。 这种方式本质上还是客户端不断发送请求才形成客户端能实时接收服务端数数据变化的假象。 实现比较简单缺点是需要不断建立http连接浪费资源而且在客户端数量级很大的情况下会导致服务器压力陡增显然不是好选择
长轮询 长轮询方式是服务器收到客户端发来的请求后想挂起请求服务器端不会直接进行响应在超时时间内比如20S,接收请求和处理请求进行响应。 有两种情况长轮询会响应
达到http请求超时时间服务器正常处理请求返回响应结果 长轮询和短轮询比起来明显减少了很多不必要的http请求次数但是连接挂起也会导致资源的浪费
长连接 SSE 长连接是指在一个连接上可以连续发送多个数据包在连接保持期间如果没有数据包发送需要双方发链路检测包。 SSE是HTML5新增的功能全称为Server-Sent Events它可以允许服务器推送数据到客户端。 SSE在本质上就与之前的长轮询、轮询不同虽然都是基于http协议的但是轮询需要客户端先发送请求服务端才能响应。而SSE最大的特点就是不需要持续客户端发送请求可以实现只要服务器端数据有更新就可以马上发送到客户端。 长链接流程连接-传输数据-保持连接 - 传输数据- ....-直到一方关闭连接客户端关闭连接 SSE的优势在于它不需要建立或保持大量的客户端发往服务器端的请求节约了很多资源提升应用性能但是可以关闭一些长时间不读写操作的连接这样可以避免一些恶意连接导致server端压力。
websocket WebSocket协议是基于TCP的一种新的网络协议它实现了客户端与服务器全双工full-duplex通信同一时间里双方都可以主动向对方发送数据。 在WebSocket中客户端和服务器只需要完成一次握手两者之间就直接可以创建持久性的连接并进行双向数据传输。 通信方式总结 ✏️兼容性角度短轮询长轮询长连接SSEWebSocket ✏️性能方面WebSocket长连接SSE长轮询短轮询 Websocket介绍 我们已经知道了WebSocket 是一种网络传输协议可在单个 TCP 连接上进行全双工通信位于 OSI 模型的应用层。 而通过WebSocket使得客户端和服务器之间的数据交换变得更加简单允许服务端主动向客户端推送数据只需要完成一次握手两者之间就直接可以创建持久性的连接。
协议升级 出于兼容性的考虑websocket 的握手使用 HTTP 来实现客户端的握手消息就是一个「普通的带有 Upgrade 头的HTTP Request 消息」。 想建立websoket连接就需要在http请求上带一些特殊的header头才行 我们看下WebSocket协议客户端请求和服务端响应示例关于http这里就不多介绍了这里自行回想下Http请求的request和reposone部分 header头的意思是浏览器想升级http协议并且想升级成websocket协议
客户端请求
//以下是WebSocket请求头中的一些字段Upgrade: websocket // 1
Connection: Upgrade // 2
Sec-WebSocket-Key: xx // 3
Origin: http: // 4
Sec-WebSocket-Protocol: chat, superchat // 5
Sec-WebSocket-Version: 13 // 6
上述字段说明如下
Upgrade字段必须设置 websocket表示希望升级到 WebSocket 协议Connection须设置 Upgrade表示客户端希望连接升级Sec-WebSocket-Key是随机的字符串服务器端会用这些数据来构造出一个 SHA-1 的信息摘要Origin字段是可选的只包含了协议和主机名称Sec-WebSocket-Extensions用于协商本次连接要使用的 WebSocket 扩展Sec-WebSocket-Version表示支持的 WebSocket 版本RFC6455 要求使用的版本是 13
服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake // 1
Connection: Upgrade // 2
Upgrade: websocket // 3
Sec-WebSocket-Accept: 2mQFj9iUA/Nz8E6OA4c2/MboVUk //4
上述字段说明如下
101 响应码确认升级到 WebSocket 协议Connection值为 “Upgrade” 来指示这是一个升级请求Upgrade表示升级为 WebSocket 协议Sec-WebSocket-Accept签名的键值验证协议支持 1ws 协议默认使用 80 端口wss 协议默认使用 443 端口和 http 一样 2WebSocket 没有使用 TCP 的“IP 地址 端口号”开头的协议名不是“http”引入的是两个新的名字“ws”和“wss”分别表示明文和加密的 WebSocket 协议 连接确认 发建立连接是前提但是只有当请求头参数Sec-WebSocket-Key字段的值经过固定算法加密后的数据和响应头里的Sec-WebSocket-Accept的值保持一致该连接才会被认可建立。 如下图从浏览器截图的两个关键参数 服务端返回的响应头字段 Sec-WebSocket-Accept 是根据客户端请求 Header 中的Sec-WebSocket-Key计算出来。那么时如何进行参数加密验证和比对确认的呢如下图。 具体流程如下
客户端握手中的 Sec-WebSocket-Key 头字段的值是16字节随机数并经过base64编码服务端需将该值和固定的 GUID 字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接后使用 SHA-1 进行哈希并采用 base64 编码后服务端将编码后的值作为响应作为的Sec-WebSocket-Accept 值返回。客户端也必须按照服务端生成 Sec-WebSocket-Accept 的方式一样生成字符串与服务端回传的进行对比相同就是协议升级成功不同就是失败 在协议升级完成后websokcet就建立完成了接下来就是客户端和服务端使用websocket进行数据传输通信了!
数据帧 一旦升级成功 WebSocket 连接建立后后续数据都以帧序列的形式传输 协议规定了数据帧的格式服务端要想给客户端推送数据必须将要推送的数据组装成一个数据帧这样客户端才能接收到正确的数据同样服务端接收到客户端发送的数据时必须按照帧的格式来解包才能真确获取客户端发来的数据 我们来看下对帧的格式定义吧 看看数据帧字段代表的含义吧
FIN 1个bit位用来标记当前数据帧是不是最后一个数据帧RSV1, RSV2, RSV3 这三个各占用一个bit位用做扩展用途没有这个需求的话设置位0Opcode 的值定义的是数据帧的数据类型。值为1 表示当前数据帧内容是文本值为2 表示当前数据帧内容是二进制值为8表示请求关闭连接MASK 表示数据有没有使用掩码
服务端发送给客户端的数据帧不能使用掩码客户端发送给服务端的数据帧必须使用掩码
Payload len 数据的长度Payload data的长度占7bits716bits764bitsMasking-key 数据掩码 (设置位0则该部分可以省略如果设置位1则用来解码客户端发送给服务端的数据帧)Payload data 帧真正要发送的数据可以是任意长度 上面我们说到Payload len三种长度最开始的7bit的值来标记数据长度这里具体看下是哪三种 情况1值设置在0-125 那么这个有效载荷长度Payload len就是对应的数据的值 情况2值设置为126 如果设置为 126可表示payload的长度范围在 126~65535 之间那么接下来的 2 个字节扩展用16bit Payload长度会包含Payload真实数据长度 情况3值设置为127 可表示payload的长度范围在 65535 那么接下来的 8 个字节扩展用16bit 32bit 16bit Payload长度会包含Payload真实数据长度这种情况能表示的数据就很大了完全够用 socket和websocket 这两者名字上差距不大虽然都有带个socket但是完全是两个不同的东西 大家千万别被名字给带的傻傻分不清楚了 我们来看下之间的区别 socket是在应用层和传输层之间的一个中间软件抽象层是一组接口它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。 websocket是基于TCP的一种新的网络协议和http协议一样属于应用层协议。 下图中分别表示了socket和websocket在网络中的位置 常见状态码 下面显示了从服务器到客户端的通信的 WebSocket 状态码和错误提示WebSocket 状态码遵循 RFC 正常关闭连接标准
1000 CLOSE_NORMAL 连接正常关闭1001 CLOSE_GOING_AWAY 终端离开 例如服务器错误或者浏览器已经离开此页面1002 CLOSE_PROTOCOL_ERROR 因为协议错误而中断连接1003 CLOSE_UNSUPPORTED 端点因为受到不能接受的数据类型而中断连接1004 保留1005 CLOSE_NO_STATUS 保留, 用于提示应用未收到连接关闭的状态码1006 CLOSE_ABNORMAL 期望收到状态码时连接非正常关闭 (也就是说, 没有发送关闭帧)1007 Unsupported Data 收到的数据帧类型不一致而导致连接关闭1008 Policy Violation 收到不符合约定的数据而断开连接1009 CLOSE_TOO_LARGE 收到的消息数据太大而关闭连接1010 Missing Extension 客户端因为服务器未协商扩展而关闭1011 Internal Error 服务器因为遭遇异常而关闭连接1012 Service Restart 服务器由于重启而断开连接1013 Try Again Later 服务器由于临时原因断开连接, 如服务器过载因此断开一部分客户端连接1015 TLS握手失败关闭连接 gorilla/websocket实战和底层代码分析 相信很多使用Golang的小伙伴都知道Gorilla这个工具包长久以来gorilla/websocket 都是比官方包更好的websocket包。 gorilla/websocket 框架开源地址为: https://github.com/gorilla/websocket 简单使用 安装Gorilla Websocket Go软件包只需要使用即可go get
go get github.com/gorilla/websocket 在正式使用之前我们先简单了解下两个数据结构 Upgrader 和 Conn
Upgrader Upgrader指定用于将 HTTP 连接升级到 WebSocket 连接
type Upgrader struct {HandshakeTimeout time.DurationReadBufferSize, WriteBufferSize intWriteBufferPool BufferPoolSubprotocols []stringError func(w http.ResponseWriter, r *http.Request, status int, reason error)CheckOrigin func(r *http.Request) boolEnableCompression bool
}
HandshakeTimeout 握手完成的持续时间ReadBufferSize和WriteBufferSize以字节为单位指定I/O缓冲区大小。如果缓冲区大小为零则使用HTTP服务器分配的缓冲区CheckOrigin 函数应仔细验证请求来源 防止跨站点请求伪造 这里一般会设置下CheckOrigin来解决跨域问题 Conn Conn类型表示WebSocket连接这个结构体的组成包括两部分写入字段Write fields和 读取字段Read fields
type Conn struct {conn net.ConnisServer bool...// Write fieldswriteBuf []byte writePool BufferPoolwriteBufSize intwriter io.WriteCloser isWriting bool ...// Read fieldsreadRemaining int64readFinal bool readLength int64 messageReader *messageReader ...
}
isServer 字段来区分我们是否用Conn作为客户端还是服务端也就是说说gorilla/websocket中同时编写客户端程序和服务器程序但是一般是Web应用程序使用单独的前端作为客户端程序。 部分字段说明如下图 服务端示例 出于说明的目的我们将在Go中同时编写客户端程序和服务端程序其实因为本人不会前端。 当然我们在开发程序的时候基本都是单独的前端通常使用Javascriptvue等实现websocket客户端这里为了让大家有比较直观的感受用【gorilla/websocket】分别写了服务端和客户端示例。
var upGrader websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return true},
}func main() {http.HandleFunc(/ws, wsUpGrader)err : http.ListenAndServe(localhost:8080, nil)if err ! nil {log.Println(server start err, err)}
}func wsUpGrader(w http.ResponseWriter, r *http.Request) {//转换为升级为websocketconn, err : upGrader.Upgrade(w, r, nil)if err ! nil {log.Println(err)return}//释放连接defer conn.Close()for {//接收消息messageType, message, err : conn.ReadMessage()if err ! nil {log.Println(err)return}log.Println(server receive messageType, messageType, message, string(message))//发送消息err conn.WriteMessage(messageType, []byte(pong))if err ! nil {log.Println(err)return}}
} 我们知道websocket协议是基于http协议进行upgrade升级的 这里使用 net/http提供原始的http连接。 http.HandleFunc接受两个参数第一个参数是字符串表示的 url 路径第二个参数是该 url 实际的处理对象 http.ListenAndServe 监听在某个端口启动服务准备接受客户端的请求 HandleFunc的作用通过类型转换让我们可以将普通的函数作为HTTP处理器使用 服务端代码流程
Gorilla在使用websocket之前是先将http装为websocket用的是初始化的upGrader结构体变量调用Upgrade方法进行请求协议升级升级后返回 *Conn此时isServer true后续使用它来处理websocket连接服务端消息读写分别用 ReadMessage()、WriteMessage() 客户端示例
import (fmtgithub.com/gorilla/websocketlogtime
)func main() {//服务器地址 websocket 统一使用 ws://url : ws://localhost:8080/ws //使用默认拨号器向服务器发送连接请求ws, _, err : websocket.DefaultDialer.Dial(url, nil)if err ! nil {log.Fatal(err)}//关闭连接defer ws.Close()//发送消息go func() {for {err : ws.WriteMessage(websocket.BinaryMessage, []byte(ping))if err ! nil {log.Fatal(err)}//休眠两秒time.Sleep(time.Second * 2)}}()//接收消息for {_, data, err : ws.ReadMessage()if err ! nil {log.Fatal(err)}fmt.Println(client receive message: , string(data))}
} 客户端的实现看起来也是简单先使用默认拨号器向服务器地址发送连接请求拨号成功时也返回一个*Conn开启一个协程每隔两秒向服务端发送消息同样都是使用ReadMessage和W riteMessage读写消息。 示例代码运行结果如下 源码走读 看完上面基本的客户端和服务端案例之后我们对整个消息发送和接收的使用已经熟悉了实际开发中要做的就是如何结合业务去定义消息类型和发送场景了我们接着走读下底层的实现逻辑
Upgrade 协议升级 Upgrade顾名思义【升级】在进行协议升级之前是需要对协议进行校验的之前我们知道待升级的http请求是有固定请求头的这里列举几个 ✏️ Upgrade进行校验的目的是看该请求是否符合协议升级的规定 Upgrade的部分校验代码如下return处进行了省略
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {if !tokenListContainsValue(r.Header, Connection, upgrade) {return ...}if !tokenListContainsValue(r.Header, Upgrade, websocket) {return ...}//必须是get请求方法if r.Method ! http.MethodGet {return ...}if !tokenListContainsValue(r.Header, Sec-Websocket-Version, 13) {return ...}if _, ok : responseHeader[Sec-Websocket-Extensions]; ok {return ...}...c : newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)...
} tokenListContainsValue的目的是校验请求的Header中是否有upgrade需要的特定参数比如我们上图列举的一些。 newConn就是初始化部分Conn结构体的方法中的第二个参数为true代表这是服务端 computeAcceptKey 计算接受密钥 这个函数重点说下在上一期中在websocket【连接确认】这一章节中知道websocket协议升级时需要满足如下条件 ✏️只有当请求头参数Sec-WebSocket-Key字段的值经过固定算法加密后的数据和响应头里的Sec-WebSocket-Accept的值保持一致该连接才会被认可建立。 var keyGUID []byte(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)func computeAcceptKey(challengeKey string) string {h : sha1.New() h.Write([]byte(challengeKey))h.Write(keyGUID)return base64.StdEncoding.EncodeToString(h.Sum(nil))
} 上面 computeAcceptKey 函数的实现验证了之前说的关于 Sec-WebSocket-Accept的生成 服务端需将Sec-WebSocket-Key和固定的 GUID 字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接后使用 SHA-1 进行哈希并采用 base64 编码后返回 ReadMessage 读消息 ReadMessage方法内部使用NextReader获取读取器并从该读取器读取到缓冲区如果是一条消息由多个数据帧则会拼接成完整的消息返回给业务层。
func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {var r io.ReadermessageType, r, err c.NextReader()if err ! nil {return messageType, nil, err}//ReadAll从r读取,直到出现错误或EOF并返回读取的数据p, err io.ReadAll(r)return messageType, p, err
} 该方法返回三个参数分别是消息类型、内容、error messageType是int型值可能是 BinaryMessage二进制消息 或 TextMessage文本消息 NextReader该方法得到一个消息类型 messageTypeio.Readererr
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {...for c.readErr nil {//解析数据帧方法advanceFrame// frameType : 帧类型frameType, err : c.advanceFrame()if err ! nil {c.readErr hideTempErr(err)break}//数据类型是 文本或二进制类型if frameType TextMessage || frameType BinaryMessage {c.messageReader messageReader{c}c.reader c.messageReaderif c.readDecompress {c.reader c.newDecompressionReader(c.reader)}return frameType, c.reader, nil}}...
} c.advanceFrame() 是核心代码主要是实现解析这条消息这里在最后章节会讲。 这里有个 c.messageReader 当前的低级读取器赋值给c.reader为什么要这样呢 c.messageReader 是更低级读取器而 c.reader 的作用是当前读取器返回到应用程序。简单就是messageReader 是实现了 c.reader 接口的结构体 从而也实现了 io.Reader接口 图上加一个 bufio.Read方法Read读取数据写入p。本方法返回写入p的字节数。本方法一次调用最多会调用下层Reader接口一次Read方法因此返回值n可能小于len(p)。读取到达结尾时返回值n将为0而err将为io.EOF
messageReader的 Read方法 我们看下Read的具体实现Read方法主要是读取数据帧内容直到出现并返回io.EOF或者其他错误为止而实际调用它的正是 io.ReadAll。
func (r *messageReader) Read(b []byte) (int, error) {...for c.readErr nil {//当前帧中剩余的字节if c.readRemaining 0 {if int64(len(b)) c.readRemaining {b b[:c.readRemaining]}//读取到切片b中n, err : c.br.Read(b)c.readErr hideTempErr(err)//当Conn是服务端if c.isServer {c.readMaskPos maskBytes(c.readMaskKey, c.readMaskPos, b[:n])}//readRemaining字节数转int64rem : c.readRemainingrem - int64(n)//跟踪连接上剩余的字节数if err : c.setReadRemaining(rem); err ! nil {return 0, err}if c.readRemaining 0 c.readErr io.EOF {c.readErr errUnexpectedEOF}//返回读后字节数return n, c.readErr}//标记是否最后一个数据帧if c.readFinal {// messageRader 置为nilc.messageReader nilreturn 0, io.EOF}//获取数据帧类型frameType, err : c.advanceFrame()switch {case err ! nil:c.readErr hideTempErr(err)case frameType TextMessage || frameType BinaryMessage:c.readErr errors.New(websocket: internal error, unexpected text or binary in Reader)}}err : c.readErrif err io.EOF c.messageReader r {err errUnexpectedEOF}return 0, err
}
io.ReadAll ReadAll从r读取这里是实现如果一条消息由多个数据帧会一直读直到最后一帧的关键。
func ReadAll(r Reader) ([]byte, error) {b : make([]byte, 0, 512)for {if len(b) cap(b) {// 给[]byte添加更多容量b append(b, 0)[:len(b)]}n, err : r.Read(b[len(b):cap(b)])b b[:len(b)n]if err ! nil {if err EOF {err nil}return b, err}}
} 可以看出在for 循环中一直读取直至读取到最后一帧直到返回io.EOF或网络原因错误为止否则一直进行阻塞读这些 error 可以从上面讲到的messageReader的 Read方法可以看出来。 总结下整个流程如下 WriteMessage 写消息 既然读消息是对数据帧进行解析那么写消息就自然会联想到将数据按照数据帧的规范组装写入到一个writebuf中然后写入到网络中。 我们继续看WriteMessage是如何实现的
func (c *Conn) WriteMessage(messageType int, data []byte) error {...//w 是一个io.WriteCloserw, err : c.NextWriter(messageType)if err ! nil {return err}//将data写入writeBuf中if _, err w.Write(data); err ! nil {return err}return w.Close()
} WriteMessage方法接收一个消息类型和数据主要逻辑是先调用Conn的NextWriter方法得到一个io.WriteCloser然后写消息到这个Conn的writeBuf写完消息后close它。
NextWriter实现如下
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {var mw messageWriterif err : c.beginMessage(mw, messageType); err ! nil {return nil, err}c.writer mw...return c.writer, nil
} 注意看这里有个messageWriter赋值给了Conn的writer也就是说messageWriter实现了io.WriterCloser接口。 这里的实现跟读消息中的NextReader方法中的messageReader很像也是通过实现io.Reader接口然后赋值给了Conn的Reader这里可以做个小联动找到读写消息实际的实现者 messageReader、messageWriter。 messageWriter的Write实现 前置知识如果没有设置Conn中writeBufferSize, 默认情况下会设置为 4096个字节另外加上14字节的数据帧头部大小【这些在newConn中初始化的时候有代码说明】 func (w *messageWriter) Write(p []byte) (int, error) {...//如果字节长度大于初始化的writeBuf空间大小if len(p) 2*len(w.c.writeBuf) w.c.isServer {//写入方法err : w.flushFrame(false, p)...}//字节长度不大于初始化的writeBuf空间大小nn : len(p)for len(p) 0 {//内部也是调用的flushFramen, err : w.ncopy(len(p))...}return nn, nil
} messageWriter中的Write方法主要的目的是将数据写入到writeBuf中它主要存储结构化的数据帧内容所谓结构化就是按照数据帧的格式用Go实现写入的。 总结下整个流程如下 而flushFrame方法将缓冲数据和额外数据作为帧写入网络这个final参数表示这是消息中的最后一帧。 至于flushFrame内部是如何实现写入网络中的你可以看看 net.Conn 是怎么Write的因为最终就是调这个写入网络的这里就不再深究了有兴趣的可以自己挖一挖 advanceFrame 解析数据帧 解析数据帧放在最后前面的代码走读主要是为了方便能把整体流程搞清楚而数据帧的解析是更加需要对websocket基础有了解特别是数据帧的组成因为解析就是按照协定用Go代码实现的一种方式而已。 根据上图回顾下数据帧各部分代表的意思 FIN 1个bit位用来标记当前数据帧是不是最后一个数据帧 RSV1, RSV2, RSV3 这三个各占用一个bit位用做扩展用途没有这个需求的话设置位0 Opcode 该值定义的是数据帧的数据类型 1 表示文本 2 表示二进制 MASK 表示数据有没有使用掩码 Payload length 数据的长度Payload data的长度占7bits716bits764bits Masking-key 数据掩码 (设置位0则该部分可以省略如果设置位1则用来解码客户端发送给服务端的数据帧) Payload data 帧真正要发送的数据可以是任意长度 advanceFrame 解析方法 实现代码会比较长如果直接贴代码会看不下去该方法返回数据类型和error, 这里我们只会截取其中一部分
func (c *Conn) advanceFrame() (int, error) {...//读取前两个字节p, err : c.read(2)if err ! nil {return noFrame, err}//数据帧类型frameType : int(p[0] 0xf)// FIN 标记位final : p[0]finalBit ! 0//三个扩展用rsv1 : p[0]rsv1Bit ! 0rsv2 : p[0]rsv2Bit ! 0rsv3 : p[0]rsv3Bit ! 0//mask 是否使用掩码mask : p[1]maskBit ! 0...switch c.readRemaining {case 126:p, err : c.read(2)if err ! nil {return noFrame, err}if err : c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err ! nil {return noFrame, err}case 127:p, err : c.read(8)if err ! nil {return noFrame, err}if err : c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err ! nil {return noFrame, err}}..
}
整个流程分为了 7 个部分
跳过前一帧的剩余部分毕竟这是之前帧的数据读取并解析帧头的前两个字节从上面图中可以看出只读取到 Payload len根据读取和解析帧长度根据 Payload length的值来获取Payload data的长度处理数据帧的mask掩码如果是文本和二进制消息强制执行读取限制并返回 (结束)读取控制帧有效载荷 即 play data设置setReadRemaining以安全地更新此值并防止溢出过程控制帧有效载荷如果是ping/pong/close消息类型返回 -1 noFrame 结束 advanceFrame方法的主要目的就是解析数据帧获取数据帧的消息类型而对于数据帧的解析都是按照上图帧格式来的 heartbeat 心跳 WebSocket 为了确保客户端、服务端之间的 TCP 通道连接没有断开使用心跳机制来判断连接状态。如果超时时间内没有收到应答则认为连接断开关闭连接释放资源。流程如下
发送方 - 接收方ping接收方 - 发送方pong
ping、pong 消息它们对应的是 WebSocket 的两个控制帧opcode分别是0x9、0xA对应的消息类型分别是PingMessage, PongMessage前提是应用程序需要先读取连接中的消息才能处理从对等方发送的 close、ping 和 pong 消息。 总结 本文主要了解 什么是Websocket以及gorilla/websocket 框架的使用和部分底层实现原理代码走读。 不过流行的开源 Go 语言 Web 工具包 Gorilla 宣布已正式归档目前已进入只读模式。“它发出的信号是这些库在未来将不会有任何发展。也就是说 gorilla/websocket 这个被广泛使用的 websocket 库也会停止更新了真是个令人悲伤的消息 正如作者所说的那样“没有一个项目需要永远存在。这可能不会让每个人都开心但生活就是这样。”