【Java转Go】Go中使用WebSocket实现聊天室(私聊+群聊)

目录

  • 前言
  • 功能
  • 效果(一人分饰多角.jpg😎)
    • 用户上线、群聊
    • 私聊和留言
    • 下线
  • 实现
    • 思路
    • 代码
      • 服务端 chat.go 完整代码
      • 客户端 html 完整代码
  • 最后

前言

之前在Java中,用 springboot+websocket 实现了一个聊天室:springboot+websocket聊天室(私聊+群聊)

有这几个功能:上线、下线、消息发送到公共频道(群聊)、消息发送给指定用户(私聊)。

这两天Go学了网络编程,也学了 websocket ,所以打算也用 Go + websocket 实现一个聊天室。然后这次用Go实现,还比Java多了个 离线留言上传头像 的功能。

这次用Go实现的聊天室,客户端同样用的html+js实现,就是在以前写的那个页面的基础上,改了亿点,然后还把公共频道和私聊频道分开来了。功能更加完善,可玩性更高了。

功能

  • 上线
  • 下线
  • 上传头像
  • 消息发送到公共频道(群聊)
  • 消息发送给指定用户(私聊)
  • 离线留言

效果(一人分饰多角.jpg😎)

废话不多说,我们直接先来看实现效果。

在这里插入图片描述

这是新样式,我后面又把消息显示的样式改了下,但是下面那些图又懒得重新截了。

在这里插入图片描述

如果没有上传头像,则默认用名称的前两个字作为头像。

在这里插入图片描述

用户上线、群聊

不指定接收人,消息发送到公共频道

在这里插入图片描述

私聊和留言

指定接收人为私聊,显示在私聊频道。

并且当接收人不在线时,则私聊信息为留言,当接收人上线了,处理私信,将私信内容显示出来。

私聊对象不在线:

在这里插入图片描述

私聊对象在线:

在这里插入图片描述

不在线的私聊对象上线了:

在这里插入图片描述

下线

关闭连接

在这里插入图片描述

实现

思路

我们通过用户名来区分用户,一个用户一个连接,每个用户可以发送消息,然后一个消息对象需要多个字段来存发送人、接收人等这些信息。

  • 所以我们要先定义两个结构体,一个是用户结构体,一个是消息结构体
  • 有了这两个结构体后,我们还需要两个列表,一个存储在线用户的列表,一个存储消息的列表。
  • 用户列表中,用户下线时,将用户从这个列表中删除。
  • 消息列表中,是为了实现离线留言的功能:指定给某个人发送消息,而这个人不在线时,将消息存储起来;等这个人上线了,再把消息发过去。如果是公共频道或者接收人在线的消息,则不进行存储。
  • 用户上线,需要判断上线的用户名是否已存在,存在,则不可以重复上线;不存在,则上线成功,将该用户添加到用户列表,并往公共频道发送一条消息。
  • 同时需要在消息列表中,找到有没有接收人为这个用户的消息,有就发送给该用户的私聊频道。
  • 用户上线之后,处理消息,是群聊还是私聊。
  • 用户下线,关闭连接。

代码

1、定义用户和消息结构体,同时给消息结构体绑定两个方法:一个是解析客户端发来的消息;一个是将消息编码,发给客户端。

然后再定义两个列表

  • 用户列表因为是用户名唯一,所以用的map,用户名作为key。
  • 消息列表,本来一开始用的切片,但是删除元素不好删,百度了一下,决定使用 list 。
// 定义一个用户结构体
type User struct {Name  string          // 用户名Pic   string          // 头像图片地址IsImg bool            // 头像是否是图片Conn  *websocket.Conn // 用户连接
}// 解析base64图片
func (user *User) EncodingBase64() error {if user.IsImg {splits := strings.Split(user.Pic, ",")// 截取文件后缀imgType := splits[0][strings.LastIndex(splits[0], "/")+1 : strings.LastIndex(splits[0], ";")]imgType = strings.Replace(imgType, "e", "", -1) // jpeg 去掉 e,改成jpg格式// 解码base64图片数据imageBytes, err := base64.StdEncoding.DecodeString(strings.Replace(user.Pic, splits[0]+",", "", 1))if err != nil {fmt.Println(err)return err}dirPath := "img"// 创建目录err = os.MkdirAll(dirPath, os.ModePerm)if err != nil {fmt.Println(err)return err}// 拼接图片路径//savePath := "聊天室/main/img/" + user.Name + "." + imgTypeimgPath := dirPath + "/" + user.Name + "." + imgType // 相对路径// 保存图片到服务器err = os.WriteFile(imgPath, imageBytes, 0644)if err != nil {fmt.Println(err)return err}user.Pic = imgPath}return nil
}// 定义一个消息结构体
type Msg struct {SendUser string // 发送人ReceUser string // 接收人SendTime string // 发送时间Msg      string // 消息内容IsPublic bool   // 消息类型是否是公开的 true 公开 false 私信IsRece   bool   // 接收人是否接收成功 true 接收成功 false 离线还未接收(当接收人离线时,设置为false,当对方上线时,将消息发过去,改为true)IsSend   bool   // 是否是发送消息,用于区分发送消息和上线下线消息(true 发送消息 false 上线/下线消息)IsImg    bool   // 头像是否是图片Pic      string // 头像图片地址
}// 解析消息的方法(将客户端返回的消息解析)
func (msg *Msg) ParseMessage(message []byte) error {fmt.Println(string(message))err := json.Unmarshal(message, msg)if err != nil {fmt.Println(err)}return nil
}// 编码消息(将服务端消息发送给客户端)
func (msg *Msg) EncodeMessage() []byte {b, _ := json.Marshal(msg) // 直接将对象返回过去return b
}var users = make(map[string]User) // 用户列表,用户名作为key
var msgs = list.New()             // 消息列表(用于存储私信消息)

2、定义WebSocket连接

// 定义WebSocket连接的升级器。升级器是一个http.HandlerFunc,它将HTTP连接升级为WebSocket连接
var upgrader = websocket.Upgrader{ReadBufferSize:  1024,WriteBufferSize: 1024,CheckOrigin: func(r *http.Request) bool {return true},
}func main() {http.HandleFunc("/web-socket", func(w http.ResponseWriter, r *http.Request) {// 在这里处理连接})log.Fatal(http.ListenAndServe(":7070", nil))
}// 在这里面处理连接
func handleWebSocket(w http.ResponseWriter, r *http.Request) {}

3、开始连接,拿到客户端传过来的用户名,然后校验该用户名是否已在线。

conn, err := upgrader.Upgrade(w, r, nil)
fmt.Println(conn.RemoteAddr().String())
if err != nil {log.Println("err====>>>", err)return
}
defer conn.Close()
user := User{}
data := r.FormValue("data") // 获取连接的数据
err := json.Unmarshal([]byte(data), &user)
if err != nil {conn.WriteMessage(websocket.TextMessage, []byte("连接发生错误"))return
}
_, ok := users[user.Name]
if ok { // 当用户已经在线时,不允许重复连接conn.WriteMessage(websocket.TextMessage, []byte("该用户已连接,不允许重复连接"))return
}
err = user.EncodingBase64() // 解码用户头像
if err != nil {conn.WriteMessage(websocket.TextMessage, []byte("连接发生错误"))return
}

4、连接没有出错,并且校验通过了,把用户添加到用户列表,然后发送一条 该用户上线 的消息给公共频道。同时处理属于该用户的私信信息。

// 用户上线
user := User{ // 添加用户到用户列表Name: sendUser,Conn: conn,
}
users[user.Name] = user
str := fmt.Sprintf("%s 加入聊天室,当前聊天室人数为 %d。", user.Name, len(users))
fmt.Println(str)// 发送上线消息给其他用户
msg := Msg{SendUser: user.Name,SendTime: time.Now().Format("2006-01-02 15:04:05"), // 日期格式化为 yyyy-MM-dd HH:mm:ss 格式Msg:      str,IsPublic: true,IsRece:   true,IsSend:   false,IsImg:    user.IsImg,Pic:      user.Pic,
}
publicMessage(msg) // 公共消息// 用户上线时,遍历消息列表,看是否有当前上线用户的未处理的私信
var next *list.Element
for el := msgs.Front(); el != nil; el = next {next = el.Next()v := el.Value.(Msg) // 用户上线处理这个用户的私信消息if v.ReceUser == user.Name && !v.IsRece {err := user.Conn.WriteMessage(websocket.TextMessage, v.EncodeMessage())if err != nil {log.Println(err)}msgs.Remove(el) // 处理完成后,将这条私信从消息列表中移除}
}

5、循环监听连接,读取客户端发过来的消息,进行处理。

// 处理消息
for {_, message, err := conn.ReadMessage()if err != nil {conn.WriteMessage(websocket.TextMessage, []byte("连接已关闭"))log.Println(conn.RemoteAddr().String(), "关闭连接", err)break}// 解析消息msg := Msg{}err = msg.ParseMessage(message)if err != nil {log.Println(err)break}if msg.IsPublic {// 群聊消息publicMessage(msg)} else {// 私聊消息privateMessage(msg)}
}

6、当监听到客户端关闭了连接时,用户列表里删除下线的用户,并发送一条 该用户下线 的消息给公共频道。

// 用户下线
name := user.Name
removeUser(user) // 删除用户
str := fmt.Sprintf("%s 离开了聊天室,当前聊天室人数为 %d。", name, len(users))
fmt.Println(str)
// 发送下线消息给其他用户
msg1 := Msg{SendUser: name,SendTime: time.Now().Format("2006-01-02 15:04:05"),Msg:      str,IsPublic: true,IsRece:   true,IsSend:   false,IsImg:    user.IsImg,Pic:      user.Pic,
}
publicMessage(msg1)

服务端 chat.go 完整代码

package mainimport ("container/list""encoding/base64""encoding/json""fmt""github.com/gorilla/websocket""log""net/http""os""strings""time"
)/*
聊天室:上线:输入用户名登录下线:离线群聊:在公共频道发送消息,全部人可见私聊:指定给某个人发送消息,仅那一个人可见留言:用户下线后,其他人给这个人发送消息,是留言
*/// 定义一个用户结构体
type User struct {Name  string          // 用户名Pic   string          // 头像图片地址IsImg bool            // 头像是否是图片Conn  *websocket.Conn // 用户连接
}// 解析base64图片
func (user *User) EncodingBase64() error {if user.IsImg {splits := strings.Split(user.Pic, ",")// 截取文件后缀imgType := splits[0][strings.LastIndex(splits[0], "/")+1 : strings.LastIndex(splits[0], ";")]imgType = strings.Replace(imgType, "e", "", -1) // jpeg 去掉 e,改成jpg格式// 解码base64图片数据imageBytes, err := base64.StdEncoding.DecodeString(strings.Replace(user.Pic, splits[0]+",", "", 1))if err != nil {fmt.Println(err)return err}dirPath := "img"// 创建目录err = os.MkdirAll(dirPath, os.ModePerm)if err != nil {fmt.Println(err)return err}// 拼接图片路径//savePath := "聊天室/main/img/" + user.Name + "." + imgTypeimgPath := dirPath + "/" + user.Name + "." + imgType // 相对路径// 保存图片到服务器err = os.WriteFile(imgPath, imageBytes, 0644)if err != nil {fmt.Println(err)return err}user.Pic = imgPath}return nil
}// 定义一个消息结构体
type Msg struct {SendUser string // 发送人ReceUser string // 接收人SendTime string // 发送时间Msg      string // 消息内容IsPublic bool   // 消息类型是否是公开的 true 公开 false 私信IsRece   bool   // 接收人是否接收成功 true 接收成功 false 离线还未接收(当接收人离线时,设置为false,当对方上线时,将消息发过去,改为true)IsSend   bool   // 是否是发送消息,用于区分发送消息和上线下线消息(true 发送消息 false 上线/下线消息)IsImg    bool   // 头像是否是图片Pic      string // 头像图片地址
}// 解析消息的方法(将客户端返回的消息解析)
func (msg *Msg) ParseMessage(message []byte) error {fmt.Println(string(message))err := json.Unmarshal(message, msg)if err != nil {fmt.Println(err)}return nil
}// 编码消息(将服务端消息发送给客户端)
func (msg *Msg) EncodeMessage() []byte {b, _ := json.Marshal(msg) // 直接将对象返回过去return b
}var users = make(map[string]User) // 用户列表,用户名作为key
var msgs = list.New()             // 消息列表(用于存储私信消息)// 定义WebSocket连接的升级器。升级器是一个http.HandlerFunc,它将HTTP连接升级为WebSocket连接
var upgrader = websocket.Upgrader{ReadBufferSize:  1024,WriteBufferSize: 1024,CheckOrigin: func(r *http.Request) bool {return true},
}func main() {http.HandleFunc("/web-socket", func(w http.ResponseWriter, r *http.Request) {conn, err := upgrader.Upgrade(w, r, nil)if err != nil {log.Println("err====>>>", err)return}go handleConnection(conn, r)})log.Fatal(http.ListenAndServe(":7070", nil))
}func handleConnection(conn *websocket.Conn, r *http.Request) {defer conn.Close()user := User{}data := r.FormValue("data") // 获取连接的数据err := json.Unmarshal([]byte(data), &user)if err != nil {conn.WriteMessage(websocket.TextMessage, []byte("连接发生错误"))return}_, ok := users[user.Name]if ok { // 当用户已经在线时,不允许重复连接conn.WriteMessage(websocket.TextMessage, []byte("该用户已连接,不允许重复连接"))return}err = user.EncodingBase64() // 解码用户头像if err != nil {conn.WriteMessage(websocket.TextMessage, []byte("连接发生错误"))return}// 用户上线user.Conn = conngoLive(user)// 处理消息for {_, message, err := conn.ReadMessage()if err != nil {conn.WriteMessage(websocket.TextMessage, []byte("连接已关闭"))log.Println(conn.RemoteAddr().String(), "关闭连接", err)break}// 解析消息msg := Msg{}err = msg.ParseMessage(message)if err != nil {log.Println(err)break}if msg.IsPublic {// 群聊消息publicMessage(msg)} else {// 私聊消息privateMessage(msg)}}offLine(user)
}// 用户上线
func goLive(user User) {users[user.Name] = userstr := fmt.Sprintf("%s 加入聊天室,当前聊天室人数为 %d。", user.Name, len(users))fmt.Println(str)// 发送上线消息给其他用户msg := Msg{SendUser: user.Name,SendTime: time.Now().Format("2006-01-02 15:04:05"), // 日期格式化为 yyyy-MM-dd HH:mm:ss 格式Msg:      str,IsPublic: true,IsRece:   true,IsSend:   false,IsImg:    user.IsImg,Pic:      user.Pic,}publicMessage(msg)privateMessageHandle(user)
}// 用户上线,处理自己的私信消息
func privateMessageHandle(user User) {// 用户上线时,遍历消息列表,看是否有当前上线用户的未处理的私信var next *list.Elementfor el := msgs.Front(); el != nil; el = next {next = el.Next()v := el.Value.(Msg) // 用户上线处理这个用户的私信消息if v.ReceUser == user.Name && !v.IsRece {err := user.Conn.WriteMessage(websocket.TextMessage, v.EncodeMessage())if err != nil {log.Println(err)}msgs.Remove(el) // 处理完成后,将这条私信从消息列表中移除}}
}// 公共消息
func publicMessage(msg Msg) {for _, user := range users {// 当 msg.IsSend 为true时,说明是发送消息,则必须判断 user.Name != msg.SendUserif user.Conn != nil && ((msg.IsSend && user.Name != msg.SendUser) || !msg.IsSend) {err := user.Conn.WriteMessage(websocket.TextMessage, msg.EncodeMessage())if err != nil {log.Println(err)}}}
}// 发送私聊消息给指定用户
func privateMessage(msg Msg) {for _, user := range users {if user.Name == msg.ReceUser && user.Conn != nil { // 当接收人在线时// 发送私聊消息err := user.Conn.WriteMessage(websocket.TextMessage, msg.EncodeMessage())if err != nil {log.Println(err)}msg.IsRece = true // 将 IsRece 设置为truebreak}}if !msg.IsRece { // 只有接收人离线时,才将消息存到消息列表中msgs.PushBack(msg)}
}// 用户下线
func offLine(user User) {name := user.NameremoveUser(user) // 删除用户str := fmt.Sprintf("%s 离开了聊天室,当前聊天室人数为 %d。", name, len(users))fmt.Println(str)// 发送下线消息给其他用户msg1 := Msg{SendUser: name,SendTime: time.Now().Format("2006-01-02 15:04:05"),Msg:      str,IsPublic: true,IsRece:   true,IsSend:   false,IsImg:    user.IsImg,Pic:      user.Pic,}publicMessage(msg1)
}// 用户下线删除用户
func removeUser(user User) {for _, v := range users {if v.Name == user.Name {os.Remove(user.Pic) // 删除头像文件delete(users, v.Name)break}}
}

客户端 html 完整代码

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>聊天室</title><script src="https://code.jquery.com/jquery-3.3.1.min.js"></script><style>/* 设置滚动条的样式 */::-webkit-scrollbar {width:5px;}/* 滚动槽 */::-webkit-scrollbar-track {-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.3);border-radius:10px;}/* 滚动条滑块 */::-webkit-scrollbar-thumb {border-radius:10px;background:rgba(0,0,0,0.1);-webkit-box-shadow:inset 0 0 6px rgba(72, 192, 164,0.5);}::-webkit-scrollbar-thumb:window-inactive {background:rgba(72, 192, 164,0.4);}input{width: 15%;height: 30px;line-height: 25px;padding: 5px 10px;border-radius: 5px;border: 1px solid #48c0a4;font-size: 16px;outline: none;}#头像box{display: inline-block;width: 50px;height: 50px;line-height: 50px;vertical-align: middle;text-align: center;cursor: pointer;font-size: 14px;}#fileImg{display: none;vertical-align: middle;}#fileBox{display: inline-block;width: 50px;height: 42px;line-height: 44px;border: 1px solid #48c0a4;border-radius: 5px;}.file{position: absolute;opacity: 0;width: 50px;height: 42px;cursor: pointer;padding: 0;margin-left: -12px;}#msg{width: 30%;}button{width: 6%;height: 44px;/*padding: 5px 10px;*/border-radius: 5px;border: 1px solid #48c0a4;outline: none;cursor: pointer;}.msgBox{width: 40%;float: left;margin-left: 20px;margin-right: 20px;}#publicMsg{width: 100%;height: 620px;border: 1px solid #48c0a4;overflow: auto;border-radius: 15px;}#privateMsg{width: 100%;height: 620px;border: 1px solid #48c0a4;overflow: auto;border-radius: 15px;}.msg{margin: 15px 5px 10px 5px;}.msg .left{float: left;width:50px;height:50px;line-height:50px;text-align: center;}.msg .right{margin-left: 58px;}.msg .msg-name{color: #6d9eeb;font-size: 12px;margin-bottom: 1px;}.msg .msg-name1{font-weight: bold;color: #f75c2f;}.msg .msg-name2{font-weight: bold;}.msg .msg-time{color: grey;font-size: 12px;}.msg .msg-msginfo{font-size: 14px;color: #1e88a8;display: block;margin-top: 3px;word-break: break-all;}.msg .msg-pic{color: dodgerblue;display: block;font-weight: bold;width:50px;vertical-align: middle;}</style></head><body><h2 style="margin-left: 10px;">聊天室</h2><input type="text" id="sendUser" placeholder="自己的用户名"><div id="头像box"><div id="fileImg"><img src="" id="img" width="50"></div><div id="fileBox"><input type="file" id="file" class="file" multiple="multiple" accept="image/jpeg,image/png,image/jpg"><span style="cursor: pointer;">头像</span></div></div><button id="上线" onclick="connectWebSocket()">上线</button><button id="下线" onclick="closeWebSocket()">下线</button><br/><br><input type="text" id="receUser" placeholder="接收人的用户名"/><input type="text" id="msg" placeholder="要发送的信息"/><button onclick="send()">发送</button><br><br><hr><div><div class="msgBox"><h3>公共频道</h3><div id="publicMsg"></div></div><div class="msgBox"><h3>私聊频道</h3><div id="privateMsg"></div></div></div><script>let file = document.getElementById('file'); // 选择文件let fileBox = document.getElementById('fileBox'); // 选择文件boxlet img = document.getElementById('img'); // 头像img标签let fileImg = document.getElementById('fileImg'); // 头像img标签boxlet isImg = false; // 标识当前用户是否上传了头像let imgBase64 = "",fileSuffix = "";file.onchange = function (e){if (e.target.files.length>0){var selectedImage = e.target.files[0];var fileSize = selectedImage.size / 1024; // 转换为KBif (fileSize > 1024) { // 限制为1MBalert("文件大小超过限制!");return;}var name = selectedImage.name;fileSuffix = name.slice(name.lastIndexOf(".")).replace("e",""); // 获取文件后缀img.src = getFileURL(selectedImage);fileImg.style.display = "inline-block";fileBox.style.display = "none";isImg = true;var fileReader = new FileReader();fileReader.readAsDataURL(selectedImage); // 文件读取为urlfileReader.onload = function(e) {imgBase64 = e.target.result; // 获取头像的base64}}}//获取文件地址function getFileURL(file) {var url = null ;if (window.createObjectURL!=undefined) { // basicurl = window.createObjectURL(file) ;} else if (window.URL!=undefined) { // mozilla(firefox)url = window.URL.createObjectURL(file) ;} else if (window.webkitURL!=undefined) { // webkit or chromeurl = window.webkitURL.createObjectURL(file) ;}return url;}// 定义两个模板字符串,使用占位符 ${} 来表示待填充的位置// 公共频道的模板const publicTemplate = `<div class="msg"><div class="left">{{ImgHtmlSnippet}}</div><div class="right"><span class="msg-name">${"{{SendUser}}"}</span><span class="msg-time">${"{{SendTime}}"}</span><span class="msg-msginfo">${"{{Msg}}"}</span></div></div>`;// 私聊频道的模板const privateTemplate = `<div class="msg"><div class="left">{{ImgHtmlSnippet}}</div><div class="right"><span class="msg-name msg-name2">${"{{SendUser}}"}</span><span class="msg-time">发给</span><span class="msg-name msg-name1">${"{{ReceUser}}"}</span><span class="msg-time">${"{{SendTime}}"}</span><span class="msg-msginfo">${"{{Msg}}"}</span></div></div>`;// 填充模板function filledTemplate(obj){var isPublic = obj["IsPublic"];// 头像代码段const imgHtmlSnippet = obj["IsImg"] ? `<img src='${obj["Pic"]}' width="50" class="msg-pic">` : `<span class="msg-pic">${obj["Pic"]}</span>`;var filledTemplate;if (isPublic){msgList = document.getElementById("publicMsg");// 替换模板中的占位符filledTemplate = publicTemplate.replace("{{ImgHtmlSnippet}}", imgHtmlSnippet).replace(/\{\{(\w+)\}\}/g, (match, key) => {if (key == "SendUser" && !obj["IsSend"]){return "";}else if (key == "SendUser" && obj["IsSend"]){return "&nbsp;"+obj[key];}return obj[key] || "";});}else {msgList = document.getElementById("privateMsg");var sendUser = document.getElementById("sendUser").value; // 当前用户// 替换模板中的占位符filledTemplate = privateTemplate.replace("{{ImgHtmlSnippet}}", imgHtmlSnippet).replace(/\{\{(\w+)\}\}/g, (match, key) => {if (key == "ReceUser" && obj[key] == sendUser){ // 当前用户是接收人时return "我";}return obj[key] || "";});}// 将生成的 HTML 插入到页面中msgList.innerHTML += filledTemplate;}</script><script type="text/javascript">var websocket = null;//连接WebSocketfunction connectWebSocket() {var sendUser = document.getElementById("sendUser").value;if (sendUser === "") {alert("请输入用户名");return;}//判断当前浏览器是否支持websocketif ('WebSocket' in window) {var val = document.getElementById("sendUser").value;// websocket = new WebSocket("ws://localhost:7070/web-socket/"+val);// websocket = new WebSocket("ws://localhost:7070/web-socket?username="+val);var pic = isImg ? imgBase64 : val.slice(0,2);var jsonData = { // 准备要发送的JSON数据Name: val,Pic: pic,IsImg: isImg,};var jsonStr = JSON.stringify(jsonData); // 将JSON数据转换为字符串websocket = new WebSocket(`ws://localhost:7070/web-socket?data=${encodeURIComponent(jsonStr)}`);} else {alert('当前浏览器不支持 websocket')}//连接发生错误的回调方法websocket.onerror = function () {alert("连接发生错误");};//连接成功建立的回调方法websocket.onopen = function () {// 连接成功后,将连接的用户输入框和上线按钮禁用var sendUser = document.getElementById("sendUser");var 上线 = document.getElementById("上线");sendUser.readOnly = true;sendUser.style.backgroundColor='#9c9c9c';file.style.display = "none";fileBox.style.backgroundColor = "#9c9c9c";上线.removeAttribute("onclick");上线.style.backgroundColor='#9c9c9c';}//接收到消息的回调方法websocket.onmessage = function (event) {try {// 将服务器发过来的消息,转为json格式,捕获异常var obj = JSON.parse(event.data);filledTemplate(obj); // 填充模板} catch (error) {// 如果不是json格式则直接用 alert 提示alert(event.data);}}//连接关闭的回调方法websocket.onclose = function () {alert("连接已关闭");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function () {closewebsocket();}}//关闭连接function closeWebSocket() {websocket.close();}//发送消息function send() {var m = new Map(); // 空Mapvar sendUser = document.getElementById("sendUser");  //发送者var msg = document.getElementById("msg").value;  //发送消息if (msg === "") {alert("请输入消息");return;}var receUser = document.getElementById("receUser").value; //接收者var currentTime = getCurrentTime();m.set("SendUser",sendUser.value);m.set("SendTime",currentTime);m.set("Msg",msg);m.set("IsSend",true);// 接收者为空时,为群聊,否则为私聊if (receUser === "") {m.set("IsPublic",true);}else{m.set("ReceUser",receUser);m.set("IsPublic",false);}m.set("IsImg",isImg);m.set("Pic",isImg ? "img/"+sendUser.value + fileSuffix : sendUser.value.slice(0,2));var json = mapToJson(m); // map转jsonwebsocket.send(JSON.stringify(json)); // 先将json转json字符串,再发送json["SendUser"] = "我";filledTemplate(json);document.getElementById("msg").value = ""; // 清空消息框}// 获取当前时间function getCurrentTime(){//可以使用字符串操作方法来将日期时间格式化为特定格式的字符串。例如:const date = new Date();const year = date.getFullYear().toString().padStart(4, '0');const month = (date.getMonth() + 1).toString().padStart(2, '0');const day = date.getDate().toString().padStart(2, '0');const hour = date.getHours().toString().padStart(2, '0');const minute = date.getMinutes().toString().padStart(2, '0');const second = date.getSeconds().toString().padStart(2, '0');return `${year}-${month}-${day} ${hour}:${minute}:${second}`; // 2023-02-16 08:25:05}//map转换为jsonfunction  mapToJson(map) {var obj= Object.create(null);for (var[k,v] of map) {obj[k] = v;}return obj;}</script></body>
</html>

最后

html的代码,消息显示样式有一点变化,就是本文的前三张图片,更好看一点了,但是后面的图懒得重新截了。


不知道我这种代码实现有没有问题?我百度查看别人实现的都是有考虑高并发,用管道和协程来处理读写数据(接收客户端消息、给客户端发送消息),还有需要服务端不停地向客户端发送消息保持心跳?但是我都没有这样子实现😢,顶多也就给每个连接开了个协程去处理,也不确定这种实现方式有没有啥问题🤣如果有的话,可以在评论区说一下😘


ok,以上就是本篇文章的全部内容了,如果你觉得文章对你有帮助或者写得还不错的话,不要吝啬你的大拇指,给博主点个赞吧~😎😘

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/121147.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【广州华锐互动】利用AR远程指导系统进行机械故障排查,实现远程虚拟信息互动

随着工业自动化和智能化的不断发展&#xff0c;机械故障诊断已经成为了工业生产中的重要环节。为了提高故障诊断的准确性和效率&#xff0c;近年来&#xff0c;AR&#xff08;增强现实&#xff09;远程协助技术逐渐应用于机械故障诊断领域。本文将探讨AR远程协助技术在机械故障…

华为数通方向HCIP-DataCom H12-821题库(单选题:201-220)

第201题 BGP 协议用​​ beer default-route-advertise​​ 命令来给邻居发布缺省路由,那么以下关于本地 BGP 路由表变化的描述&#xff0c;正确的是哪一项? A、在本地 BGP 路由表中生成一条活跃的缺省路由并下发给路由表 B、在本地 BGP 路由表中生成一条不活跃的缺省路由&…

应用于伺服电机控制、 编码器仿真、 电动助力转向、发电机、 汽车运动检测与控制的旋变数字转换器MS5905P

MS5905P 是一款 12bit 分辨率的旋变数字转换器。 片上集成正弦波激励电路&#xff0c;正弦和余弦允许输入峰峰值 幅度为 2.3V 到 4.0V &#xff0c;可编程激励频率为 10kHz 、 12kHz 、 15kHz 、 20kHz 。 转换器可并行或串行输出角度 和速度对应的数字量。 MS5905…

动态贴纸、美颜SDK与AR:创造独特的互动体验

目前&#xff0c;动态贴纸、美颜SDK、增强现实&#xff08;AR&#xff09;等技术是比较热门的话题&#xff0c;它们所结合的新兴玩法更是收到大家推崇&#xff0c;正潜移默化的改变我们与数字世界互动的方式。 一、动态贴纸&#xff1a;个性化互动的开始 动态贴纸&#xff0c…

JVM-CMS

when 堆大小要求为4-8G 原理 初始标记&#xff1a;执行CMS线程->STW&#xff0c;标记GC Root直接关联的对象->低延迟 并发标记&#xff1a;执行CMS线程和业务线程&#xff0c;从GC Root直接关联的对象开始遍历整个对象图 重新标记&#xff1a;执行CMS线程->STW&a…

大数据时代下的数据安全防护

随着大数据时代的来临&#xff0c;数据安全防护成为了一个重要的问题。在大数据时代&#xff0c;数据的规模和价值都得到了极大的提升&#xff0c;因此数据安全的重要性也变得越来越突出。本文将从数据加密、访问控制、网络安全和人员管理四个方面来介绍大数据时代下的数据安全…

CVE-2023-23752:Joomla未授权访问漏洞复现

CVE-2023-23752&#xff1a;Joomla未授权访问漏洞复现 前言 本次测试仅供学习使用&#xff0c;如若非法他用&#xff0c;与本文作者无关&#xff0c;需自行负责&#xff01;&#xff01;&#xff01; 一.Openfire简介 Joomla是一个免费的开源内容管理系统&#xff08;CMS&a…

Unity中Shader的UV扭曲效果的实现

文章目录 前言一、实现的思路1、在属性面板暴露一个 扭曲贴图的属性2、在片元结构体中&#xff0c;新增一个float2类型的变量&#xff0c;用于独立存储将用于扭曲的纹理的信息3、在顶点着色器中&#xff0c;根据需要使用TRANSFORM_TEX对Tilling 和 Offset 插值&#xff1b;以及…

群晖NAS:DS Video、Jellyfin等视频电影电视剧海报、背景墙搜刮器

群晖NAS&#xff1a;DS Video、Jellyfin等视频电影电视剧海报、背景墙搜刮器 本文只使用豆瓣插件方式&#xff0c;系统默认的 The Movie Database 好注册&#xff0c;但是授权码输入后域名不通过&#xff0c;很麻烦。 1、插件地址&#xff1a; https://www.aliyundrive.com/s…

WebRTC音视频通话-WebRTC推拉流过程中日志log输出

WebRTC音视频通话-WebRTC推拉流过程中日志log输出 之前实现iOS端调用ossrs服务实现推拉流流程。 推流&#xff1a;https://blog.csdn.net/gloryFlow/article/details/132262724 拉流&#xff1a;https://blog.csdn.net/gloryFlow/article/details/132417602 在推拉流过程中的…

【java】解决sprintboot项目开发遇到的问题

目录 一、java: 程序包org.junit.jupiter.api不存在 二、Cannot resolve com.mysql:mysql-connector-j:unknown 三、Unsatisfied dependency expressed through bean property sqlSessionFactory; nested exception is org.springframework 四、org.apache.ibatis.binding…

算法笔记:点四叉树

点四叉树是一种用于主要是针对空间点存储与索引的树形数据结构在点四叉树中&#xff0c;空间被分割成四个矩形&#xff0c;四个不同的多边形对应于SW、NW、SE、NE四个象限 1 基本操作 1.1 初始化 创建一个根节点&#xff0c;该节点代表整个二维空间区域 1.2 插入点 当一个新…

基于SSM的新能源汽车在线租赁系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

ELK高级搜索(三)

文章目录 11&#xff0e;索引Index入门11.1 索引管理11.2 定制分词器11.3 type底层结构11.4 定制dynamic mapping11.5 零停机重建索引 12&#xff0e;中文分词器 IK分词器12.1 Ik分词器安装使用12.2 ik配置文件12.3 使用mysql热更新 13&#xff0e;java api 实现索引管理14&…

kafka-- kafka集群环境搭建

kafka集群环境搭建 # 准备zookeeper环境 (zookeeper-3.4.6) # 下载kafka安装包 https://archive.apache.org/dist/kafka/2.1.0/kafka_2.12-2.1.0.tgz # 上传 : 172.16.144.133 cd /usr/local/softwaretar -zxvf /usr/local/software/kafka_2.12-2.1.0.tgz -C /usr/local…

Kubernetes_概念篇

Kubernetes_概念篇 一、架构二、概念1&#xff0c;Label&#xff08;对象标签&#xff09;2&#xff0c;Namespace3&#xff0c;Deployment4&#xff0c;Service 三、资源对象Master组件1&#xff0c;kube-apiserver2&#xff0c;kube-controller-manager3&#xff0c;kube-sch…

go语言基础语法

1、注释 package mainimport "fmt"/* *多行注释*/ func main() {//单行注释fmt.Println("hello world") }2、变量 2.1、变量定义 标准格式 var 变量名 变量类型 如 var age int Go语言的基本类型有&#xff1a; boolstringint、int8、int16、int32、…

什么是安全运营中心(SOC),应该了解什么

安全运营中心&#xff08;SOC&#xff09; 是一种企业监视和警报设施&#xff0c;可帮助组织检测安全威胁、监视安全事件和分析性能数据以改进公司运营。 什么是安全运营中心&#xff08;SOC&#xff09; 安全运营中心&#xff08;SOC&#xff09;是一个中央监视和监视中心&a…

Flowable7 设计器

1、flowable7 已经在主版本上移除了Flowable UI相关的包&#xff0c;包含bpm-json相关的所有包和流程设计器相关前端文件。 2、flowable7 版本目前只保留了xml运行相关的包&#xff0c;ui modeler已经移除 3、目前官方给的回复是只能在 flowable 云产品上使用设计器&#xff…

Servlet属性、监听者和会话

没有servlet能单独存在。在当前的现代Web应用中&#xff0c;许多组件都是在一起协作共同完成一个目标。怎么让这些组件共享信息&#xff1f;如何隐藏信息&#xff1f;怎样让信息做到线程安全&#xff1f; 1 属性和监听者 1.1 初始化 容器初始化一个servlet时&#xff0c;会为…