golang + vue + websocket 实现的简单在线消息转发例子

实现的功能是简单的websocket消息在线转发

应该算是学习产物吧,现在的版本应该没bug了(之前版本会挂)
之前的bug原因,close管道之后继续发送会panic
另外,在后期建立大量连接测试的时候,发送会产生broken pipe的错误,并导致发送服务阻塞
解决办法:1、如果其他goroutine中有对关闭管道的写操作,尽量不要用close
2、原因是关闭信号没有正确传达,设置超时时间,规定时间未接收到数据,关闭;发送超时,关闭

结果样例
  • 服务端只做数据转发,格式由客户端定义和解析

go代码
目录结构
gin 启动一个web服务,在对应路由处理函数里升级为 websocket。要在https的站点使用的话,TLS相关的配置是必须的(主要是下载证书文件,之后只能以 wss://域名/* 的方式使用)。如果实在没条件,把对应的中间件注释掉,RunTLS改为Run,但只能在http站点下使用,wss://变为ws://

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"github.com/unrolled/secure"
	"log"
	"net/http"
	"sync"
	"time"
)

// 自定义客户端
type Client struct {
	// 引入锁
	sync.Mutex
	// websocket连接
	conn *websocket.Conn
	// 读到的信息在这里
	readChan chan []byte
	// 需要发送的信息在这里
	writeChan chan []byte
	// 协程之间通信,连接是否已关闭
	closeChan chan struct{}
	// close closeChan 使用,避免panic
	closed bool
}

// 读取一条信息
// false 代表连接关闭失效/等待超时
func (c *Client) ReadMessage() ([]byte, bool) {
	select {
	// 超时,关闭连接
	// todo 心跳信息没有处理,在外部处理(自定义)
	case <-time.After(time.Minute*5 + time.Second*10):
		c.Close()
		return nil, false
	case data := <-c.readChan:
		return data, true
	case <-c.closeChan:
		return nil, false
	}
}

// 写入一条信息
// 不能判断成功/失败
func (c *Client) WriteMessage(data []byte) {
	select {
	case c.writeChan <- data:
	case <-c.closeChan:
	case <-time.After(time.Second * 3):
		// 为了防止阻塞,如果处理过慢,这里可以增加服务的稳定,不会导致一直阻塞影响其他接收者接收
		c.Close()
	}
}

// 关闭连接
func (c *Client) Close() {
	c.Lock()
	if !c.closed {
		close(c.closeChan)
		c.closed = true
		// ===
		// conn.Close 可以不加锁,多次使用,线程安全
		c.conn.Close()
		// ===
	}
	c.Unlock()
}

func (c *Client) readLoop() {
	for {
		_, msg, err := c.conn.ReadMessage()
		if err != nil {
			c.Close()
			return
		}
		select {
		case c.readChan <- msg:
		case <-c.closeChan:
			return
		}
	}
}

func (c *Client) writeLoop() {
	for {
		select {
		case msg := <-c.writeChan:
			// 因为缓冲区的存在所以,不会立即发送,加上异常关闭的连接也被视为存在
			// 可能会出现 broken pipe
			err := c.conn.WriteMessage(websocket.TextMessage, msg)
			if err != nil {
				c.Close()
				return
			}
		case <-c.closeChan:
			return
		}
	}
}

// 升级成websocket,开启读(生产者)、写(消费者)的协程,返回自定义*Client
func NewClient(c *gin.Context) (*Client, error) {
	//升级get请求为webSocket协议
	ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Println("NewClient err:", err)
		return nil, err
	}

	client := &Client{
		conn: ws,
		// 没必要设置太大,1也没问题,一般情况消费的足够快,除非是程序在发消息
		readChan:  make(chan []byte, 5),
		writeChan: make(chan []byte, 5),
		closeChan: make(chan struct{}),
	}

	go client.readLoop()
	go client.writeLoop()

	return client, nil
}

// 消息广播时使用
// 要广播的信息,源客户端信息和要发送的信息
type Message struct {
	client *Client
	msg    []byte
}

var (
	// key:client, value:struct{}{}
	clients sync.Map
	// 广播消息队列
	messages = make(chan Message, 100)

	// 广播消息发送 worker
	sendWorkers = make(chan struct{}, 100)

	// 测试时使用,客户端连接数
	//clientNum int32

	// 升级为 websocket
	upGrader = websocket.Upgrader{
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}
)

func Chat(c *gin.Context) {
	// 获取封装好的自定义client
	conn, err := NewClient(c)
	if err != nil {
		return
	}

	// 存储至连接队列
	clients.Store(conn, struct{}{})

	//atomic.AddInt32(&clientNum, 1)
	//log.Println("now clients:", clientNum)

	go func() {
		// 收尾的函数
		defer func() {
			clients.Delete(conn)

			//atomic.AddInt32(&clientNum, -1)
			//log.Println("now clients:", clientNum)
		}()
		for {
			i, ok := conn.ReadMessage()
			if !ok {
				return
			}
			// 处理自定义心跳信息
			if string(i) != "{\"msg\":\"beat\"}" {
				tmp := Message{client: conn, msg: i}
				messages <- tmp
			}
		}
	}()
}

func sendWorker(c *Client, msg []byte) {
	sendWorkers <- struct{}{}
	go func() {
		c.WriteMessage(msg)
		<-sendWorkers
	}()
}

// 广播函数
func BroadCast() {
	// 取消息
	for msg := range messages {
		c := msg.client
		// 遍历客户端队列
		clients.Range(func(key, value interface{}) bool {
			// 不是本身就发送
			if key != c {
				// 因为 key 的类型确定,所以可以这么用
				sendWorker(key.(*Client), msg.msg)
			}
			// 继续迭代
			return true
		})
	}
}

// https 处理, 即变为 wss://*
func TlsHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		secureMiddleware := secure.New(secure.Options{
			SSLRedirect: true,
			SSLHost:     ":8849",
		})
		err := secureMiddleware.Process(c.Writer, c.Request)

		if err != nil {
			return
		}
	}
}

func main() {
	// goroutine 广播消息
	go BroadCast()

	// 发布模式
	gin.SetMode(gin.ReleaseMode)
	r := gin.Default()
	// 路由设定
	r.GET("/chat", Chat)
	// TLS 中间件设置
	r.Use(TlsHandler())
	// 启动服务
	// TLS 需要对应文件
	err := r.RunTLS(":8849", "*.pem", "*.key")
	if err != nil {
		panic(err)
	}
}

/*
没有条件升级https或不会升级的
注释掉
r.Use(TlsHandler())
改
err := r.RunTLS(":8849", "*.pem", "*.key")
为
err := r.Run(":8849")

使用时前端改wss:// 为 ws://
此时只能在极少部分的网站可以使用,现在大部分是https站点
*/

前端部分

  • 如果有需要,拿去用,自行修改
<!DOCTYPE html>
<html lang="ch">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <style>
        #chat-container {
            top: 100px;
            left: 100px;
            margin: 0 auto;
            border-radius: 8px;
            box-shadow: 0 0 0 -20px rgba(0, 0, 0, .2), 0 24px 38px 3px rgba(0, 0, 0, .14), 0 9px 46px 8px rgba(0, 0, 0, .12);
            position: fixed;
            background: rgba(255, 255, 255, 0.95);
            width: 150px;
            max-height: 100px;
            overflow: scroll;
            opacity: 40%;
            z-index: 999999999;
        }

        #chat-container div li p,
        #chat-container div input {
            font-weight: 100;
            padding: 0;
            margin: 0;
        }

        #chat-container div input {
            border-width: 0;
        }

        #chat-container div input:focus {
            outline: none;
        }

        #chat-container::-webkit-scrollbar {
            display: none;
        }

        #chat-container div li {
            list-style: none
        }

        .chat-right p {
            text-align: right;
        }

        .chat-left p {
            text-align: left;
        }

        [v-cloak] {
            display: none
        }
    </style>
</head>

<body>
<div id="chat-container" @mousedown="move" v-cloak v-show="show">
    <div>
        <li v-for="item in dataList" :class="{'chat-right':item.send,  'chat-left':!item.send}">
            <p>{{item.msg}}</p>
        </li>
    </div>
    <div style="text-align: center;">
        <input type="text" v-model="msg" :placeholder="tips" @keypress.enter="send"
               style="width: 100%;text-align: center;">
    </div>
</div>
</body>
<script>
    // f12 测试使用
    // setInterval(()=>{app.msg="test";app.send()}, 3000)
    /*
    自定义格式方法:
        1、修改显示消息的 p 标签内容,<p>{ {item.msg} }</p>
        2、修改 send 方法,修改发送的 data 对象
        * 自定义的字段可以写在 vue 的数据段 data 中,如昵称name等
     */

    document.addEventListener("keypress", function (ev) {
        switch (ev.key) {
            case "~":
                if (!window.top.chatapp)
                    window.top.chatapp = new Vue({
                        el: "#chat-container",
                        mounted: function () {
                            this.ws = new WebSocket("wss://*")
                            this.ws.onopen = () => {
                                this.tips = "连接成功,在此输入"
                            }
                            this.ws.onerror = () => {
                                this.tips = "发送失败"
                            }
                            this.ws.onmessage = (ev) => {
                                let item = JSON.parse(ev.data);
                                this.dataList.push(item)
                            }
                            // 10s检查一次ws状态
                            setInterval(() => {
                                if (this.ws.readyState > 1) {
                                    this.ws = new WebSocket("wss://*")
                                    this.ws.onopen = () => {
                                        this.tips = "连接成功,在此输入"
                                    }
                                    this.ws.onerror = () => {
                                        this.tips = "发送失败"
                                    }
                                    this.ws.onmessage = (ev) => {
                                        let item = JSON.parse(ev.data);
                                        this.dataList.push(item)
                                    }
                                }
                            }, 10000);
                            // 5min发送一次心跳信息
                            setInterval(() => {
                                let data = {
                                    msg: "beat",
                                }
                                this.ws.send(JSON.stringify(data))
                            }, 1000 * 60 * 5)
                        },
                        // 新消息滚动值最下方
                        updated: function () {
                            let showContent = document.getElementById("chat-container");
                            showContent.scrollTop = showContent.scrollHeight;
                        },
                        data: {
                            ws: null,
                            dataList: [],
                            msg: "",
                            positionX: 0,
                            positionY: 0,
                            tips: "",
                            show: true,
                        },
                        methods: {
                            send() {
                                if (this.msg.trim() && this.ws) {
                                    let data = {
                                        msg: this.msg.trim(),
                                    }
                                    this.ws.send(JSON.stringify(data))
                                    data['send'] = true
                                    this.dataList.push(data)
                                    this.msg = ""
                                }
                            },
                            move(e) {
                                let odiv = document.getElementById("chat-container");
                                let disX = e.clientX - odiv.offsetLeft;
                                let disY = e.clientY - odiv.offsetTop;
                                document.onmousemove = (e) => {
                                    let left = e.clientX - disX;
                                    let top = e.clientY - disY;
                                    this.positionX = top;
                                    this.positionY = left;
                                    odiv.style.left = left + 'px';
                                    odiv.style.top = top + 'px';
                                };
                                document.onmouseup = () => {
                                    document.onmousemove = null;
                                    document.onmouseup = null;
                                };
                            },
                        }
                    })
                else
                    window.top.chatapp.show = !window.top.chatapp.show
                break
        }
    })
</script>
<html>
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang中的Gin框架提供了一种简单而强大的方法来构建Web应用程序。与此同时,Golang标准库中的"net/http"包提供了构建WebSocket服务器和客户端的功能。 首先,我们来看一下如何使用Gin和WebSocket构建WebSocket服务器。首先,需要导入相关的包: ```go import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) ``` 接下来,在Gin中创建一个WebSocket处理函数: ```go func WebSocketHandler(c *gin.Context) { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } for { messageType, message, err := conn.ReadMessage() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err = conn.WriteMessage(messageType, message) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } } ``` 上面的代码创建了一个基本的WebSocket处理函数。它使用WebSocket标准库中的Upgrader结构来处理升级连接并创建一个WebSocket连接。 然后,我们需要在Gin中设置路由来处理WebSocket请求: ```go router := gin.Default() router.GET("/ws", WebSocketHandler) ``` 以上代码将在根路径下创建一个WebSocket处理函数。 接下来,我们来看一下如何使用Golang和Gin构建WebSocket客户端。首先,我们需要导入所需的包: ```go import ( "github.com/gorilla/websocket" "net/http" ) ``` 然后,我们可以使用以下代码来创建一个WebSocket客户端: ```go func main() { c, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil) if err != nil { log.Fatal("dial:", err) } defer c.Close() done := make(chan struct{}) go func() { defer close(done) for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-done: return case <-ticker.C: err := c.WriteMessage(websocket.TextMessage, []byte("Hello, Server!")) if err != nil { log.Println("write:", err) return } } } } ``` 上面的代码创建了一个WebSocket客户端,它使用WebSocket标准库中的`DefaultDialer`结构来建立WebSocket连接。 以上就是使用Golang Gin和WebSocket构建WebSocket客户端和服务器的简单示例。这些代码可以帮助我们使用Gin和Golang的标准库来构建强大的Web应用程序,并处理WebSocket通信。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值