Hello,我是普通Gopher,00后男孩,极致的共享主义者,想要成为一个终身学习者。专注于做最通俗易懂的计算机基础知识类公众号。每天推送Golang技术干货,内容起于K8S而不止于K8S,涉及Docker、微服务、DevOps、数据库、虚拟化等云计算内容及SRE经验总结
=======================
初次见面,我为你准备了100G学习大礼包:
1、《百余本最新计算机电子图书》
2、《30G Golang学习视频》
3、《20G Java学习视频》
4、《90G Liunx高级学习视频》
5、《10G 算法(含蓝桥杯真题)学习视频》
6、《英语四级,周杰伦歌曲免费送!》
路过麻烦动动小手,点个关注,持续更新技术文章与资料!
拉模式和推模式区别
1. 拉模式(定时轮询访问接口获取数据)
- 数据更新频率低,则大多数的数据请求时无效的
- 在线用户数量多,则服务端的查询负载很高
- 定时轮询拉取,无法满足时效性要求
2. 推模式(向客户端进行数据的推送)
- 仅在数据更新时,才有推送
- 需要维护大量的在线长连接
- 数据更新后,可以立即推送
- 基于WebSocket协议做推送
- 浏览器支持的socket编程,轻松维持服务端的长连接
- 基于TCP协议之上的高层协议,无需开发者关心通讯细节
- 提供了高度抽象的编程接口,业务开发成本较低
基于WebSocket推送
- 浏览器支持的socket编程,轻松维持服务端的长连接
- 基于TCP可靠传输之上的协议,无需开发者关心通讯细节
- 提供了高度抽象的编程接口,业务开发成本低
传输原理
- 协议升级后,继续复用HTTP的底层Socket完成后续通讯
- message底层被切分成多个frame帧传输
- 编程时只需操作message,无需关心frame
- 框架底层完成TCP网络I/O,webSocket协议解析,开发者无需关心
实现简单的HTTP服务端
server.go
package main
import "net/http"
func wsHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("hello"))
}
func main() {
http.HandleFunc("/ws", wsHandler)
_ = http.ListenAndServe(":7777", nil)
}
完成Websocket握手
server.go
package main
import (
"github.com/gorilla/websocket"
"net/http"
)
var (
upgrader = websocket.Upgrader{
// 支持跨域
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)
func wsHandler(w http.ResponseWriter, r *http.Request) {
var (
conn *websocket.Conn
err error
_ int
data []byte
)
// Upgrade: websocket
if conn, err = upgrader.Upgrade(w, r, nil); err != nil {
return
}
// websocket Conn
for {
// Text, Binary
if _, data, err = conn.ReadMessage(); err != nil {
goto ERR
}
if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
goto ERR
}
}
ERR:
conn.Close()
}
func main() {
http.HandleFunc("/ws", wsHandler)
_ = http.ListenAndServe(":7777", nil)
}
client.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function(evt) {
var output = document.getElementById("output");
var input = document.getElementById("input");
var ws;
var print = function(message) {
var d = document.createElement("div");
d.innerHTML = message;
output.appendChild(d);
};
document.getElementById("open").onclick = function(evt) {
if (ws) {
return false;
}
ws = new WebSocket("ws://localhost:7777/ws");
ws.onopen = function(evt) {
print("OPEN");
}
ws.onclose = function(evt) {
print("CLOSE");
ws = null;
}
ws.onmessage = function(evt) {
print("RESPONSE: " + evt.data);
}
ws.onerror = function(evt) {
print("ERROR: " + evt.data);
}
return false;
};
document.getElementById("send").onclick = function(evt) {
if (!ws) {
return false;
}
print("SEND: " + input.value);
ws.send(input.value);
return false;
};
document.getElementById("close").onclick = function(evt) {
if (!ws) {
return false;
}
ws.close();
return false;
};
});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>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.
</p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
封装WebSocket与添加安全锁机制
server..go
package main
import (
"errors"
"fmt"
"github.com/gorilla/websocket"
"net/http"
"sync"
"time"
)
// http升级websocket协议的配置
var wsUpgrader = websocket.Upgrader{
// 支持跨域
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// 客户端读写消息
type wsMessage struct {
messageType int
data []byte
}
// 客户端连接
type wsConnection struct {
wsSocket *websocket.Conn // 底层websocket
inChan chan *wsMessage // 读队列
outChan chan *wsMessage // 写队列
mutex sync.Mutex // 避免重复关闭管道
isClosed bool // 管道是否已经关闭
closeChan chan byte // 关闭通知
}
// 写入消息
func (wsConn *wsConnection) wsWrite(messageType int, data []byte) error {
select {
case wsConn.outChan <- &wsMessage{messageType, data}:
case <-wsConn.closeChan:
return errors.New("websocket closed")
}
return nil
}
// 读取消息
func (wsConn *wsConnection) wsRead() (*wsMessage, error) {
select {
case msg := <-wsConn.inChan:
return msg, nil
case <-wsConn.closeChan:
return nil, errors.New("websocket closed")
}
}
// 关闭websocket连接
func (wsConn *wsConnection) wsClose() {
wsConn.wsSocket.Close()
wsConn.mutex.Lock()
defer wsConn.mutex.Unlock()
if !wsConn.isClosed {
wsConn.isClosed = true
close(wsConn.closeChan)
}
}
// 循环读取
func (wsConn *wsConnection) wsReadLoop() {
for {
// 读一个message
msgType, data, err := wsConn.wsSocket.ReadMessage()
if err != nil {
goto error
}
req := &wsMessage{
messageType: msgType,
data: data,
}
// 请求放入队列
select {
case wsConn.inChan <- req:
case <-wsConn.closeChan:
goto closed
}
}
error:
wsConn.wsClose()
closed:
}
// 循环写入
func (wsConn *wsConnection) wsWriteLoop() {
for {
select {
// 取一个应答
case msg := <-wsConn.outChan:
// 写给websocket
if err := wsConn.wsSocket.WriteMessage(msg.messageType, msg.data); err != nil {
goto error
}
case <-wsConn.closeChan:
goto closed
}
}
error:
wsConn.wsClose()
closed:
}
// 发送存活心跳
func (wsConn *wsConnection) procLoop() {
// 启动一个gouroutine发送心跳
go func() {
for {
time.Sleep(2 * time.Second)
if err := wsConn.wsWrite(websocket.TextMessage, []byte("heartbeat from server")); err != nil {
fmt.Println("heartbeat fail")
wsConn.wsClose()
break
}
}
}()
// 这是一个同步处理模型(只是一个例子),如果希望并行处理可以每个请求一个gorutine,注意控制并发goroutine的数量!!!
for {
msg, err := wsConn.wsRead()
if err != nil {
fmt.Println("read fail")
break
}
fmt.Println(string(msg.data))
err = wsConn.wsWrite(msg.messageType, msg.data)
if err != nil {
fmt.Println("write fail")
break
}
}
}
func wsHandler(resp http.ResponseWriter, req *http.Request) {
// 应答客户端告知升级连接为websocket
wsSocket, err := wsUpgrader.Upgrade(resp, req, nil)
if err != nil {
return
}
// 初始化wsConn连接
wsConn := &wsConnection{
wsSocket: wsSocket,
inChan: make(chan *wsMessage, 1000),
outChan: make(chan *wsMessage, 1000),
closeChan: make(chan byte),
isClosed: false,
}
// 处理器
go wsConn.procLoop()
// 读协程
go wsConn.wsReadLoop()
// 写协程
go wsConn.wsWriteLoop()
}
func main() {
http.HandleFunc("/ws", wsHandler)
_ = http.ListenAndServe(":7777", nil)
}