经过我上一篇的基本理解websocket的建立以及使用后,这篇就写一个简单的demo
实现im聊天
首先就是后端代码,详细解释我都放到了每一句的代码解析了,文章最后我会说怎么运行流程
放置后端代码
Go
package main
import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"gopkg.in/fatih/set.v0"
"log"
"net/http"
"strconv"
"sync"
)
type Message struct {
FormId string `json:"userID1"`
TargetId string `json:"userID2"`
Content string `json:"data"`
}
type Node struct {
Conn *websocket.Conn
DataQueue chan []byte //DataQueue 用于存储待处理的数据,从 WebSocket 连接接收到的消息数据。
//set.Interface 是一个集合类型的接口,可以用于实现不同类型的集合,例如包含不重复元素的集合,比如后续可以将多个用户分到同一个组
//然后可以对这个一个组内的消息进行遍历然后群发消息。可以理解为并发安全的集合。
GroupSets set.Interface
}
// 映射关系 通过一个map集合来存储多个用户和节点的映射关系,因为每个用户和服务端建立的websocket连接地址都不同
// 比如ws://localhost:8088/ws?userID1和ws://localhost:8088/ws?userID2等等。。。
var clientMap map[int64]*Node = make(map[int64]*Node, 0)
// 读写锁 多个用户在建立连接的时候都要存储到map中,所以这里用一个锁保证读写的安全
var rwLocker sync.RWMutex
func main() {
http.HandleFunc("/ws", Chat)
log.Fatal(http.ListenAndServe(":8088", nil))
}
// Chat 用来接收用户发起ws连接,以及后续用协程来持续读取消息,并推送给其他用户
func Chat(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
Id := query.Get("userID")
//log.Println("接收到的 userID:", Id)
userId, _ := strconv.ParseInt(Id, 10, 64)
isvalida := true
conn, err := (&websocket.Upgrader{ //将 HTTP 连接升级为 WebSocket 连接
//CheckOrigin 函数用于检查请求的来源是否合法,即允许跨域访问
CheckOrigin: func(r *http.Request) bool {
return isvalida
},
}).Upgrade(writer, request, nil) //request 参数包含了客户端发起连接时提供的 URL 信息ws://localhost:8088/userid
if err != nil {
log.Println("升级为WebSocket连接失败:", err)
return
}
//2.获取conn
node := &Node{
Conn: conn,
DataQueue: make(chan []byte, 50),
GroupSets: set.New(set.ThreadSafe),
}
defer conn.Close()
//4. userid 跟 node绑定 并加锁 用程序的读写锁抢占线程资源
rwLocker.Lock()
clientMap[userId] = node
rwLocker.Unlock()
//5.用协程完成发送消息推送逻辑
go sendProc(node)
//6.用协程完成发送消息逻辑
go recvProc(node)
//这里有点小问题其实,然后用一个select 来阻塞主handle,然后等待协程中的发送消息以及消息推送
select {}
}
// 5.完成发送逻辑 发送的是消息推送
func sendProc(node *Node) {
for {
select {
//当有人给自己发送了数据后,那么自己的node.DataQueue中就会有数据
//然后就可以通过webScoket把自己接收到数据的事情传递给前端。也就是消息推送
case data := <-node.DataQueue: //这个同样是阻塞式操作
//告诉前端有人私聊自己了,这个消息推送通过websocket发送出去
err := node.Conn.WriteMessage(websocket.TextMessage, data)
if err != nil {
fmt.Println(err)
return
}
}
}
}
// 6.完成发送消息逻辑
func recvProc(node *Node) {
for {
//发送操作是前端操作的,即前端将用户A发送的消息内容放置到的websocket中
//后端接收数据的data中就包含内容是谁给谁发送的信息。后续可以将这部分数据拆分出来后然后保存到数据库,
//使用的是Conn接收的数据,发送的请求,而不是http了
_, data, err := node.Conn.ReadMessage()
if err != nil {
fmt.Println("推送消息失败:", err)
break
}
//用户A发送的消息给解析展示一下
var myData Message
err = json.Unmarshal(data, &myData)
if err != nil {
log.Println("解析JSON失败:", err)
break
}
fmt.Println("[ws]发送消息:", myData)
//用户A发送给别人的消息,要将别人的node.DataQueue中放入消息,然后别人通过sendProc来发送给前端
broadMsg(data)
}
}
func broadMsg(data []byte) {
msg := Message{}
err := json.Unmarshal(data, &msg)
if err != nil {
fmt.Println(err)
return
}
str, err := strconv.ParseInt(msg.TargetId, 10, 64)
if err != nil {
fmt.Println(err)
return
}
//将用户A发送给别人(msg.TargetId)的消息放入到TargetId对应的node.DataQueue中
sendMsg(str, data)
//后续的扩展暂时不管群发,广播,广播
//switch msg.Type {
//case strconv.Itoa(1): //私信
// sendMsg(int64(msg.TargetId), data)
// case 2: //群发
// sendGroupMsg()
// case 3://广播
// sendAllMsg()
//case 4:
//
//}
}
func sendMsg(TargetId int64, msg []byte) {
rwLocker.RLock()
//通过接收消息的用户id来找到目标用户的node
node, ok := clientMap[TargetId] //因为这个方法是接收数据方法下沉过来的,所以接收对象的目的对象就是UserId 所以多个人建立多个对象,也就是都相符了。
rwLocker.RUnlock()
if ok {
//在这个目标用户的node的DataQueue中放入数据,这样如果对方是在线状态的话,它的sendProc函数中就会收到消息,然后推送给前端
node.DataQueue <- msg
}
}
放置前端代码
用户1 id是123
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="userID2">
<input type="text" id="data">
<button onclick="onclicksend()">发送</button >
<script>
id = 123
//与本地计算机通信
const ws = new WebSocket('ws://localhost:8088/ws?userID='+id)
//与其他计算机通信
//const ws = new WebSocket('ws://服务器地址:端口号')
function onclicksend(){
var userID2= document.getElementById("userID2").value;
var data= document.getElementById("data").value;
var sendData = {
userID1: ""+id,
userID2: userID2,
data: data
};
console.log(sendData);
var jsonStr = JSON.stringify(sendData);
ws.send(jsonStr);
}
ws.onopen = function () {
console.log('我们连接成功啦...')
// ws.send(11);
}
ws.onerror = function () {
console.log('连接失败了...')
}
ws.onmessage = function (e) {
console.log('服务端传来数据啦...' + e.data)
}
ws.onclose = function () {
console.log('连接关闭了...')
}
</script>
</body>
</html>
用户2 id是456
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="userID2">
<input type="text" id="data">
<button onclick="onclicksend()">发送</button >
<script>
id = 456
//与本地计算机通信
const ws = new WebSocket('ws://localhost:8088/ws?userID='+id)
//与其他计算机通信
//const ws = new WebSocket('ws://服务器地址:端口号')
function onclicksend(){
var userID2= document.getElementById("userID2").value;
var data= document.getElementById("data").value;
var sendData = {
userID1: ""+id,
userID2: userID2,
data: data
};
console.log(sendData);
var jsonStr = JSON.stringify(sendData);
ws.send(jsonStr);
}
ws.onopen = function () {
console.log('我们连接成功啦...')
// ws.send(11);
}
ws.onerror = function () {
console.log('连接失败了...')
}
ws.onmessage = function (e) {
console.log('服务端推送数据: ' + e.data)
}
ws.onclose = function () {
console.log('连接关闭了...')
}
</script>
</body>
</html>
运行的时候老样子,先运行后端服务器,然后再运行前端两个页面然后打开控制台看聊天
前端页面很简单就是两个text框,第一个是给目标用户id发信息,第二个是发送的信息内容
经过测试就会知道比如左边的页面:用户456给用户123发信息的时候左边的控制台也会输出内容
这样之后进行包装,将控制台打印的内容放到一个聊天页面就可以实现im聊天了。