引入websocket并基于redis实现消息发送
websocket详细解析
WebSocket协议是一种在Web应用程序中实现双向通信的技术。它使得服务器和客户端可以通过一个固定的持久连接进行实时数据传输和交互,而不像HTTP请求那样需要每次都建立新的连接。
WebSocket的工作原理很简单。客户端通过HTTP协议发起一次特殊的请求,请求头中标明要升级为WebSocket协议。服务器接收到请求后进行升级,双方之间的连接就建立好了。之后服务器和客户端可以通过这个长连接进行实时的双向通信。
简单的使用Go语言实现WebSocket服务器
数据类型websocket.Conn
websocket.Conn
是golang
中websocket
连接的数据类型,它表示一个websocket
连接,用于在客户端和服务器之间进行双向数据交换。
websocket.Conn
可以通过websocket.Upgrade
函数从HTTP连接升级而来,例如:
func handler(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
// 使用conn进行websocket通信
}
在上面的代码中,upgrader.Upgrade
将HTTP连接升级为websocket
连接,返回一个websocket.Conn
对象,用于后续的websocket
通信。
websocket.Conn
提供了一系列方法来进行websocket
通信,其中常用的包括:
WriteMessage
:向websocket
连接中写入数据,包括文本
、二进制
和Ping/Pong
消息。ReadMessage
:从websocket
连接中读取数据,返回文本
、二进制
和Ping/Pong
消息。Close
:关闭websocket
连接。SetReadDeadline
:设置websocket
连接的读取过期时间。SetWriteDeadline
:设置websocket
连接的写入过期时间。
除了上述方法,websocket.Conn
还包含一些其他的方法用于管理websocket
连接。
在这个例子中
package main
import (
"fmt"
"net/http"
"golang.org/x/net/websocket"
)
func Echo(ws *websocket.Conn) {
fmt.Println("WebSocket connected")
for {
var msg string
err := websocket.Message.Receive(ws, &msg)
if err != nil {
fmt.Println("Error receiving message: ", err.Error())
break
}
fmt.Println("Received message: ", msg)
err = websocket.Message.Send(ws, "Received: "+msg)
if err != nil {
fmt.Println("Error sending message: ", err.Error())
break
}
}
}
func main() {
fmt.Println("Starting WebSocket server...")
http.Handle("/", websocket.Handler(Echo))
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server: ", err.Error())
}
}
,我们创建了一个Echo函数来处理WebSocket连接。在一个无限循环中,它通过websocket.Message.Receive()方法接收客户端发送的消息,并通过websocket.Message.Send()方法将接收到的消息返回给客户端。
我们通过http.Handle()将Echo函数作为WebSocket处理程序暴露在默认的HTTP路由根目录"/"下。最后通过http.ListenAndServe()启动WebSocket服务器并监听8080端口。
当客户端连接到服务器时,WebSocket服务器会打印出"WebSocket connected"。当客户端发送消息时,服务器会打印出"Received message: "和消息内容,并将"Received: "和消息内容返回给客户端。
我们可以使用浏览器的WebSocket API(如WebSocket对象)来与这个WebSocket服务器进行通信。在浏览器中执行以下代码:
var ws = new WebSocket("ws://localhost:8080");
ws.onmessage = function(event) {
console.log(event.data);
};
ws.send("Hello, world!");
这将创建一个WebSocket对象并连接到我们的WebSocket服务器。当WebSocket对象接收到服务器发送的消息时,它会打印消息内容到浏览器的控制台。最后,它发送一条"Hello, world!"消息给服务器。
通过这种方式,WebSocket实现了高效的双向通信,可以在实时性要求较高的Web应用程序中大显身手。
自己动手写一个简单的收发消息连接
导入websocket包
go get github.com/gorilla/websocket
写一个路由接口并调用
//发送消息
r.POST("/user/sendMsg", service.SendMsg)
实现函数前的前置条件
链接结点
type Node struct {
Conn *websocket.Conn
DataQueue chan []byte
GroupSets set.Interface
}
这是一个Go语言的结构体类型Node,其中包含以下三个字段:
1. Conn是一个指向websocket.Conn的指针,表示该节点持有的WebSocket连接。
2. DataQueue是一个chan []byte类型的管道,用于该节点的数据传输队列。其他节点可以将数据写入该管道,节点自身会从中读取数据并进行处理。
3. GroupSets是一个set.Interface类型的接口,表示该节点所属的组(group)集合。组是一种将多个WebSocket会话连接在一起的逻辑单位,可用于群组聊天、广播等场景。set.Interface是一个集合的通用接口类型,包含常见的集合操作方法。
防止跨站点请求伪造
跨站点请求伪造(CSRF)是一种攻击方式,攻击者可以通过伪造用户的请求来执行非法操作,例如修改用户信息、发送垃圾邮件等。为了防止CSRF攻击,网站可以采取以下措施:
-
验证请求来源:服务器必须验证每个请求的来源,只接受来自该站点的合法请求。
-
添加令牌(Token):在表单或URL中添加一个随机的令牌,用于验证请求的合法性。攻击者伪造请求时,由于缺少令牌,请求将被拒绝。
-
限制HTTP方法:只允许使用POST方法提交敏感信息,禁止使用GET方法。
-
在cookie中添加安全标识:将一个随机的安全标识添加到cookie中,用于验证请求的合法性。
-
使用验证码:在执行重要操作之前,要求用户输入验证码,以防止自动化攻击。
以上措施可以综合使用,来提高网站的安全性。
代码实现
创建一个 websocket.Upgrader
实例,并设置了一个函数 CheckOrigin
,该函数返回 true
。Upgrader
是用于升级 HTTP 连接为 WebSocket 连接的工具。CheckOrigin
函数是用于检查请求来源的函数,当请求源符合设定的要求时,返回 true
,允许升级为 WebSocket 连接。在这里,将 CheckOrigin
设为恒返回 true
,表示允许来自任何来源的 WebSocket 连接。
(后续再补充)
// 防止跨域站点的伪造请求
var upGrede = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
实现收发消息函数
主题框架
HTTP请求的响应接口——http.ResponseWriter
http.ResponseWriter是Go语言中的一个接口类型,用于HTTP请求的响应输出。它定义了一个方法集合,用于向客户端发送HTTP响应的内容,包括HTTP状态码、响应头、响应实体等。通过实现该接口,开发者可以控制Web应用程序对HTTP请求的响应。
下面是http.ResponseWriter接口中常用的方法:
- WriteHeader(statusCode int):设置HTTP状态码。
- Header() http.Header:获取响应头。
- Write([]byte) (int, error):输出HTTP响应实体。
- WriteString(string) (int, error):输出字符串类型的HTTP响应实体。
在使用http.ResponseWriter时,我们可以通过其Header()方法获取响应头,然后通过Set()方法设置响应头的属性值。此外,我们也可以通过其Write()方法直接输出HTTP响应实体,例如:
func handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, world!"))
}
在上面的例子中,我们通过WriteHeader()方法设置HTTP状态码为200,然后使用Write()方法输出了"Hello, world!"字符串作为HTTP响应实体。
创建一个HTTP请求头——Header() http.Header
Header()
是Go标准库中的一个函数,用于创建一个HTTP请求头(HTTP headers)。它的参数是一个http.Header
类型,它代表一个HTTP请求或响应中的一组header键值对(header fields)。header包含了有关HTTP请求或响应的元数据信息。
http.Header
是一个键-值对集合类型,其中每个键和值都是一个字符串。它是一个字符串到字符串切片的映射(map)。这意味着它可以包含多个相同键名的键-值对。
示例代码:
package main
import (
"fmt"
"net/http"
)
func main() {
// 创建一个 Header
header := http.Header{}
header.Set("Content-Type", "application/json")
header.Set("Authorization", "Bearer abcdefg")
// 打印 Header
fmt.Println(header)
}
输出结果:
map[Authorization:[Bearer abcdefg] Content-Type:[application/json]]
可以看到,我们创建了一个Header,包含了两个键值对。Content-Type
是application/json
,Authorization
是Bearer abcdefg
。输出结果是一个 map
类型,其中每个键都是一个header字段名,每个值都是一个字符串切片,因为同一个header字段名可能出现多次。
客户端向服务器发送请求的接口——http.Request
HTTP请求(HTTP Request)是客户端向服务器发送请求的方式,它包含了客户端想要获取的资源信息。在Go语言中,通过http包中的Request结构体表示HTTP请求,它包含了以下几个重要的属性:
- Method:表示请求的HTTP方法,如GET、POST等。
- URL:表示请求的URL地址。
- Proto:表示HTTP协议的版本,如HTTP/1.1。
- Header:表示请求的头部信息,它是一个map类型。
- Body:表示请求的消息体,它可以是空的,也可以是一个有内容的IO流,在POST、PUT等请求中,它通常用来传递数据。
除此之外,Request结构体还包含了一些其他的属性和方法,用于处理HTTP请求。通常,我们使用http包中的函数来处理HTTP请求,如http.NewRequest()用于创建一个新的请求,http.ReadRequest()用于读取一个请求。
获取URL的参数——request.URL.Query()
在Golang的net/http包中,request.URL.Query()是一个函数,用于返回HTTP请求的URL的查询参数,以一个url.Values类型的值表示。
查询参数是URL中的一部分,通常由一系列key-value键值对组成,以“&”符号分隔。例如,一个包含查询参数的URL可能如下所示:
http://example.com/search?q=golang&limit=10&page=2
当使用request.URL.Query()函数时,它会将查询参数解析为一个url.Values类型的值,其中每个键名和键值都是一个字符串。在上面的示例中,查询结果可能是:
url.Values{
"q": {"golang"},
"limit": {"10"},
"page": {"2"},
}
要访问特定的查询参数,可以使用url.Values类型的Get()方法,例如:
q := request.URL.Query().Get("q")
limit := request.URL.Query().Get("limit")
page := request.URL.Query().Get("page")
处理消息框架代码
func Chat(writer http.ResponseWriter, request *http.Request) {
//1、获取参数并校验
query := request.URL.Query()
Id := query.Get("userID")
userId, _ := strconv.ParseInt(Id, 10, 64)
msgType := query.Get("type")
targetId := query.Get("targetId")
context := query.Get("context")
isvalida := true //checkToke() 待做
conn, err := (&websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return isValida
},
}).Upgrade(writer, request.nil)
if err != nil {
fmt.Println(err)
return
}
//2、获取连接
node := &Node{
Conn: conn,
DataQueue: make(chan []byte, 50),
GroupSets: set.New(set.ThreadSafe),
}
//3、用户关系
//4、userid 跟node绑定并加锁
rwLocaker.Lock()
clientMap[userId] = node
rwLocaker.Unlock()
//5、发送消息
go sendProc(node)
//6、接受消息
go recvProc(node)
}
该函数为一个处理 WebSocket 连接的函数,具体过程如下:
-
从 URL 参数中获取
userID
、type
、targetId
和context
等参数,并对userID
进行转换成 int64 类型的处理。 -
创建一个
Node
实例,其中包含一个 WebSocket 连接Conn
、一个消息缓冲队列DataQueue
和一个群组集合GroupSets
,表示该连接所属的群组。 -
建立用户关系,具体实现未给出。
-
使用读写锁来保证操作
clientMap
的线程安全,将userId
与node
绑定,存入clientMap
中。 -
启动一个
sendProc
协程,负责从DataQueue
中获取消息,并通过 WebSocket 连接发送给客户端。 -
启动一个
recvProc
协程,负责从 WebSocket 连接中读取客户端发送过来的消息,并将其转发给所有的群组成员。
其中,使用 websocket.Upgrader
对 HTTP 连接进行升级,将其转换成 WebSocket 连接。使用 isValida
来判断连接来源是否合法。
HTTP请求和响应的相关信息——gin.Context
gin.Context
是Gin
框架中的一个上下文(Context)对象,其中包含了HTTP请求和响应的相关信息,包括请求路径、请求方式、请求头、请求体、响应状态码、响应头等。其常用的方法包括:
- c.Param(key):获取路径中的参数值。
- c.Query(key):获取URL中的查询参数值。
- c.PostForm(key):获取POST请求中的表单参数值。
- c.Request.Body:获取请求体中的原始数据。
- c.GetHeader(key):获取请求头中指定key的值。
- c.SetHeader(key, value):设置响应头中指定key的值。
- c.JSON(statusCode, obj):将响应数据序列化为JSON格式返回,同时设置响应状态码为statusCode。
- c.String(statusCode, format, a…):将响应数据格式化为字符串返回,同时设置响应状态码为statusCode。
通过gin.Context
对象的相关方法,我们可以很方便地获取和操作HTTP请求和响应的相关信息,可以在中间件或处理函数中使用。
发送消息代码
在websocket中写数据——sendProc
// 发送消息
func sendProc(node *Node) {
for {
select {
case data := <-node.DataQueue:
err := node.Conn.WriteMessage(websocket.TextMessage, data)
if err != nil {
fmt.Println(err)
return
}
}
}
}
这段代码实现了一个发送消息的协程,它不断地从节点的数据队列中取出数据,并通过 WebSocket 连接发送出去。
具体来说,它使用了一个无限循环,不断地从数据队列中取出数据。这里使用了 select 语句,其 case 子句监听
了一个数据队列的通道。一旦有数据进入队列,就会触发该 case 子句,从而执行对应的代码。
在该 case 子句中,它首先使用 Conn 对象的 WriteMessage 方法将数据以 TextMessage 的形式发送出去。如果发送过程中出现了错误,比如连接断开,它会输出错误信息并退出 sendProc 协程。
需要注意的是,该协程中只处理了一个 case 子句,也就是监听数据队列的通道。如果要在该协程中处理多个 case 子句,需要使用 select 语句的多个 case 子句。此外,由于该协程没有终止条件,因此需要在其他地方手动停止它。
从websocket中读数据——recvProc
func recvProc(node *Node) {
for {
_, data, err := node.Conn.ReadMessage()
if err != nil {
fmt.Println(err)
return
}
broadMsg(data)
fmt.Println("[ws]<<<<<<<<<<<", data)
}
}
这是一个无限循环的函数,使用 WebSocket 从连接的节点 (Node) 中读取
消息。在每次循环中,它尝试从节点的连接中读取数据。如果读取时发生错误,将会打印这个错误并结束函数。如果成功读取到数据,则将数据作为参数传递到 broadMsg 函数中,并打印一条消息,表示数据已经被成功接收。在这个函数中,我们可以看到数据的接收和广播的处理方式。这个函数的目的是在 WebSocket 连接中处理接收到的消息。
var udpsendChan chan []byte = make(chan []byte, 1024)
func broadMsg(data []byte) {
udpsendChan <- data
}
这段代码定义了一个"udpsendChan"变量,它是一个带有1,024个缓冲区的通道类型(chan []byte)。
函数"broadMsg"将数据([]byte类型)传递给该通道。这意味着在调用该函数时,它将把传递的数据添加到udpsendChan通道的缓冲区中。然后,该缓冲区中的数据将等待从通道中读取。
这种方法通常用于实现生产者-消费者模型。生产者(例如,broadMsg函数)将数据添加到通道中,而消费者从通道中读取数据并进行处理。由于通道是线程安全的,因此可以在多个goroutine之间安全地共享数据。
总结
总的来说做消息通信功能一共三步
1、建立路由接口
2、初始化
信息
3、循环监听
通道读取
消息和
循环读取
通道发送
消息
另一个简单化的例子
func SendMsg(c *gin.Context) {
ws, err := upGrede.Upgrade(c.Writer, c.Request, nil)
if err != nil {
fmt.Println(err)
return
}
defer func(ws *websocket.Conn) {
err = ws.Close()
if err != nil {
fmt.Println(err)
}
}(ws)
MsgHandler(ws, c)
}
注释
代码是WebSocket
的HTTP处理器函数,在gin框架中用于处理WebSocke
t连接请求。它的作用是通过Upgrade
函数将HTTP连接升级为WebSocket
连接,然后调用MsgHandler
函数处理收到的消息,并在函数结尾处关闭WebSocket
连接。
Upgrade
协议升级(Upgrade)是WebSocket协议的一个关键特性之一。在HTTP协议中,客户端发送请求,服务器响应请求,并完成一系列事务后,请求和响应就结束了。然而,在WebSocket中,客户端和服务器在完成初始握手之后,可以通过Upgrade请求将连接升级为WebSocket连接,从而建立一条双向通信的长连接。
在HTTP协议中,客户端和服务器之间的通信是“请求-响应”式的,也就是说,客户端发出请求,服务器响应请求,然后连接就断开了。这种通信模型不适合实时通信,因为每次通信都需要建立一个新的连接,对服务器的压力很大,同时也会影响通信的实时性。
而WebSocket连接使用Upgrade请求协议升级,可以实现长连接,即在连接建立后,客户端和服务器可以随时发送数据,而不需要像HTTP那样每次都需要重新建立连接,这样可以大大降低服务器的负载,同时也提高了通信的实时性。
因此,Upgrade协议在WebSocket中扮演着至关重要的角色,实现了WebSocket的实时性和高效性。
MsgHandler
func MsgHandler(ws *websocket.Conn, c *gin.Context) {
msg, err := utils.Subscribe(c, utils.PubilshKey)
if err != nil {
fmt.Println(err)
}
tm := time.Now().Format("2006-01-02 15:04:05")
m := fmt.Sprintf("[ws][%s]:%s", tm, msg)
err = ws.WriteMessage(1, []byte(m))
if err != nil {
fmt.Println(err)
}
}
代码定义了一个函数MsgHandler
,它有两个参数,一个是websocket.Conn
类型的ws
,另一个是gin.Context
类型的c
。
函数主要实现了一个消息处理器的功能,它首先调用utils包中的Subscribe函数,订阅一个指定的key(utils.PublishKey),获取最新的消息内容。
然后,将当前时间和消息内容通过字符串格式化的方式,组成一个新的消息字符串m,并调用ws.WriteMessage
方法将此消息通过WebSocket发送给客户端。
如果发送消息时发生了错误,函数将打印出错误信息,但实际上并没有进行任何处理。
utils.Subscribe
// Subscribe 订阅消息到Redis
func Subscribe(ctx context.Context, channel string) (string, error) {
sub := Red.Subscribe(ctx, channel)
msg, err := sub.ReceiveMessage(ctx)
if err != nil {
fmt.Println(err)
}
fmt.Println("Publish...", msg.Payload)
return msg.Payload, err
}
这是一个订阅 Redis 消息的函数。具体解释如下:
- 接收两个参数:一个上下文 context 和一个字符串 channel,表示要订阅的消息通道。
- 调用 Redis 实例的 Subscribe 方法,开始订阅指定通道上的消息,并返回一个 Subscription 对象。
- 调用 Subscription 的 ReceiveMessage 方法等待接收消息。该方法会一直阻塞,直到有消息到来,或者上下文被取消。
- 如果接收到消息,会将消息内容打印出来,并返回消息内容和一个空的错误值。如果遇到错误,会打印错误信息并返回错误值。
- 注意,在实际的生产环境中,应该将消息通道作为函数参数传入,而不是硬编码在函数中。
Subscribe
Redis的Subscribe方法是用于订阅消息的方法,可以订阅一个或多个频道,以便在这些频道上接收发布的消息。
该方法的语法是:
SUBSCRIBE channel [channel ...]
其中,channel是要订阅的频道名称,可以订阅一个或多个频道。
调用Subscribe方法后,Redis会开始监听指定的频道,一旦有消息发布到这些频道中,就会将消息推送给当前客户端。客户端可以通过类似于下面的代码来接收消息:
while True:
message = redis_client.parse_response()
# 处理消息
此外,还可以通过Unsubscribe方法来取消对某个频道的订阅,例如:
UNSUBSCRIBE channel
当订阅的频道中没有消息时,Subscribe方法会一直阻塞等待,直到有消息发布或者取消订阅。因此,在使用该方法时需要注意控制好订阅频道的个数,以免导致客户端长时间阻塞,影响系统的性能和稳定性。