基于GO实现千万级WebSocket消息推送服务

在这里插入图片描述

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)
}

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,主要用于实时、双向数据交换,特别适合于像在线聊天、游戏和股票交易等需要频繁更新数据的应用场景。以下是WebSocket消息推送实现的通用步骤: 1. **客户端(Web浏览器)连接**:客户端创建一个WebSocket对象,并指定服务器的URL。然后调用`connect()`方法尝试建立连接。 ```javascript const socket = new WebSocket('wss://your-server.com'); ``` 2. **握手过程**:服务器收到请求后,会发送一个HTTP升级头响应,将HTTP升级为WebSocket协议。客户端收到这个响应后,会进入open状态,表示连接已建立。 3. **发送和接收消息**:一旦连接建立,客户端和服务器可以相互发送文本或二进制数据。在JavaScript中,可以使用`send()`方法发送数据,`onmessage`事件监听接收到的数据。 ```javascript socket.send('Hello Server'); socket.onmessage = function(event) { console.log('Received:', event.data); }; ``` 4. **持久化连接**:WebSocket会保持长连接,除非主动关闭。数据传输是持续的,直到一方关闭连接。 5. **错误处理**:客户端和服务器都需要设置错误处理机制,如`onerror`事件,来捕获和处理可能出现的问题。 6. **安全与认证**:通常,WebSocket连接会在服务器端进行验证,如使用HTTPS确保安全,或者通过鉴权机制验证客户端身份。 7. **断线重连**:为了提高用户体验,一些库会提供自动重连功能,当连接断开时尝试重新连接。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值