WebSocket定义
- websocket 是HTML5提供的一种在单个tcp连接上进行全双工通讯的协议,在websocket API中客服端和服务端只需要完成一次握手就可以创建长连接,然后进行双向数据传递。
- websocket 不是一个全新的协议,而是基于http协议建立连接,创建过程:
- GET ws://localhost:3000/ws?param
- HOST: localhost
-Upgrade: websocket
-Connection: Upgrade
-Origin: http:localhost:3000
-Sec-WebSocket-Key: Client-random-string
-Sec-WebSocket-Version: 13
- 然后服务端接收到该请求,将http协议升级为WebSocket协议,建立tcp连接,服务端响应如下
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: sever-random-string
-
目前支持WebSocket协议的浏览器
- Chrome
- Firefox
- IE >= 10
- Sarafi >= 6
- Android >= 4.4
- IOS >= 8
-
目前网站实现推送技术的方式
- 拉模式(定时轮询访问接口获取数据)
- 数据更新频率低
- 在线用户数量多,则服务器查询负载很高
- 定时轮询不能满足实时要求
- 推模式(服务端向客服端进行数据的推送)
- 只有在数据更新时才有推送
- 需要维护大量的在线长连接
- 数据更新后可以立即推送
- 拉模式(定时轮询访问接口获取数据)
-
基于WebSocket 协议做推送
- 目前大多数浏览器都支持websocket协议
- 协议升级后底层继续服用Http协议的底层socket完成通信
- 以message进行数据传输,message会被切成多个frame帧进行传输
- 真正全双工通信,建立连接后客服端和服务端是相等的,客户端与服务端建立连接以后不再需要发送http header就可以交换数据
-
Golang 下websocket的下的选择
- 1.原生包 golang.org/x/net/websocket
-
-
github.com/gorilla/websocket
两者的比较区别
-
-
具体实现 基于github.com/gorilla/websocket
- connection.go
// WsMessage ...
type WsMessage struct {
messageType int
data []byte
}
// PushMessage 推送消息内容
type PushMessage struct {
ID int `json:"id"` // 编号
EmployeeNO string `json:"employee_no"` // 员工号
ReadFlag int `json:"read_flag"` // 已读标记
Extra string `json:"extra"` // 额外信息
MessageType int `json:"message_type"` // 消息类型
Content string `json:"content"` // 消息内容
Count int `json:"count"` // 总数
}
// WsConnection 连接对象
type WsConnection struct {
WsSocket *websocket.Conn
InChan chan *WsMessage // 读队列
OutChan chan *PushMessage // 写队列
Mutex sync.Mutex // 避免重复关闭通道
IsClosed bool // 是否关闭
CloseChan chan byte // 关闭通知
}
// WsReadLoop ...
func (wsConn *WsConnection) WsReadLoop() {
for {
msgType, data, err := wsConn.WsSocket.ReadMessage()
if err != nil {
goto error
}
req := &WsMessage{
msgType,
data,
}
fmt.Println(req)
// 放入请求队列
select {
case wsConn.InChan <- req:
case <-wsConn.CloseChan:
fmt.Println("wsReadLoop close websocket")
goto closed
}
}
error:
wsConn.wsClose()
closed:
}
// WsWriteLoop ...
func (wsConn *WsConnection) WsWriteLoop() {
for {
select {
//取一个应答
case msg := <-wsConn.OutChan:
if err := wsConn.WsSocket.WriteJSON(msg); err != nil {
goto error
}
case <-wsConn.CloseChan:
goto closed
}
}
error:
wsConn.wsClose()
closed:
}
// WsWrite ...
func (wsConn *WsConnection) WsWrite(message PushMessage) error {
select {
case wsConn.OutChan <- &message:
case <-wsConn.CloseChan:
return errors.New("websocket closed")
}
return nil
}
// WsRead ...
func (wsConn *WsConnection) WsRead() (*WsMessage, error) {
select {
case msg := <-wsConn.InChan:
return msg, nil
case <-wsConn.CloseChan:
}
return nil, errors.New("websocket closed")
}
// ProcLoop 心跳检测
func (wsConn *WsConnection) ProcLoop() {
// 启动一个goroutine 发送心跳
go func() {
for {
time.Sleep(50 * time.Second)
if err := wsConn.WsSocket.WriteMessage(websocket.PingMessage, []byte("heartbeat")); err != nil {
logrus.Errorf("heartbeat fail %v", err.Error())
wsConn.wsClose()
break
}
}
}()
}
func (wsConn *WsConnection) wsClose() {
wsConn.WsSocket.Close()
wsConn.Mutex.Lock()
defer wsConn.Mutex.Unlock()
if !wsConn.IsClosed {
wsConn.IsClosed = true
close(wsConn.CloseChan)
}
}
2.建立连接,实现对每个用户推送,采用map缓存用户和连接之间的关系
var upGrader = websocket.Upgrader{
// 允许跨域
CheckOrigin: func(r *http.Request) bool {
return true
},
}
//读写锁
var mutex sync.RWMutex
// Relation save websocket connection
var Relation = make(map[string]helpers.WsConnection)
// DoConnection websocket 协议
func DoConnection(ctx *gin.Context) {
employeeNo := ctx.GetString("employeeNo")
if employeeNo == "" {
ctx.JSON(http.StatusUnprocessableEntity, http.StatusText(http.StatusUnprocessableEntity))
return
}
// 升级为WebSocket协议
wsSocket, err := upGrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
logrus.Errorf("upgrade websocket fail")
return
}
wsConn := helpers.WsConnection{
WsSocket: wsSocket,
InChan: make(chan *helpers.WsMessage, 1000),
OutChan: make(chan *helpers.PushMessage, 1000),
CloseChan: make(chan byte),
IsClosed: false,
}
// 添加连接对应关系
handleConnect(employeeNo, wsConn)
}
func handleConnect(employeeNo string, wsConn helpers.WsConnection) {
mutex.Lock()
Relation[employeeNo] = wsConn
mutex.Unlock()
go wsConn.WsWriteLoop()
go wsConn.ProcLoop()
}
3.测试客服端代码 index.html
Click "Open" to create a connection to the server,
"Send" to send a message to the server and "Close" to close the connection.
You can change the message and send multiple times.
- 项目背景
- 该项目是针对客服系统接收到任务,对每条任务按照人推送相关消息,因为使用人员不是很多,连接数就直接在本地采用map 缓存
- 异步队列推送消息的时候 调用接口推送即可
- 项目如果使用到nginx做代理时,使用websocket协议必须配置header,不然会报404
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;