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

news2024/12/25 12:36:46

目录

  • 前言
  • 功能
  • 效果(一人分饰多角.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 + "." + imgType
		imgPath := 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 main

import (
	"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 + "." + imgType
		imgPath := 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 = conn
	goLive(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] = 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)
	privateMessageHandle(user)
}

// 用户上线,处理自己的私信消息
func privateMessageHandle(user User) {
	// 用户上线时,遍历消息列表,看是否有当前上线用户的未处理的私信
	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) // 处理完成后,将这条私信从消息列表中移除
		}
	}
}

// 公共消息
func publicMessage(msg Msg) {
	for _, user := range users {
		// 当 msg.IsSend 为true时,说明是发送消息,则必须判断 user.Name != msg.SendUser
		if 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 设置为true
			break
		}
	}
	if !msg.IsRece { // 只有接收人离线时,才将消息存到消息列表中
		msgs.PushBack(msg)
	}
}

// 用户下线
func offLine(user User) {
	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)
}

// 用户下线删除用户
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'); // 选择文件box
	        let img = document.getElementById('img'); // 头像img标签
	        let fileImg = document.getElementById('fileImg'); // 头像img标签box
	        let 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; // 转换为KB
                    if (fileSize > 1024) { // 限制为1MB
                        alert("文件大小超过限制!");
                        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); // 文件读取为url
			        fileReader.onload = function(e) {
				        imgBase64 = e.target.result; // 获取头像的base64
			        }
		        }
	        }
	        //获取文件地址
	        function getFileURL(file) {
		        var url = null ;
		        if (window.createObjectURL!=undefined) { // basic
			        url = window.createObjectURL(file) ;
		        } else if (window.URL!=undefined) { // mozilla(firefox)
			        url = window.URL.createObjectURL(file) ;
		        } else if (window.webkitURL!=undefined) { // webkit or chrome
			        url = 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;
            //连接WebSocket
            function connectWebSocket() {
                var sendUser = document.getElementById("sendUser").value;
                if (sendUser === "") {
                    alert("请输入用户名");
                    return;
                }
                //判断当前浏览器是否支持websocket
                if ('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(); // 空Map
                var 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转json
                websocket.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转换为json
            function  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.coloradmin.cn/o/972740.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用BAPI创建销售订单条件价格有多个

说明&#xff1a; 在用BAPI_SALESORDER_CREATEFROMDAT2创建销售订单的时候&#xff0c;业务配置的是Z001自动出来的&#xff0c;我在BAPI条件那块赋值更改标识也是U&#xff0c;但是创建出来的单据还是有两条件类型。 解决方法&#xff1a; LOGIC_SWITCH-PRICING G .即可

AP9193 升压恒流驱动芯片 美容护肤仪 美容灯 锂电池升压驱动IC

AP9193 是一款高效率、高精度的升 压型大功率 LED 灯恒流驱动控制芯片。 应用领域 LED 灯杯 电池供电的 LED 灯串 平板显示 LED 背光 恒流充电器控制 大功率 LED 照明 AP9193 内置高精度误差放大器&#xff0c;固 定关断时间控制电路&#xff0c;恒流驱动电路等&#xff…

签到系统怎么设计

背景 相信签到系统大家都有接触过&#xff0c;更多的是使用。但是有思考过这种系统是怎么设计的吗&#xff1f;比方说我统计一下每个月中每天的签到情况&#xff0c;怎么设计呢&#xff1f;今天一篇文章告诉你。 首先&#xff0c;我们熟悉的思维是&#xff1a;我设计一个数据…

电商平台-业务中台-SPU,SKU,SN概念简介

什么是SPU &#xff08;Standard Product Unit)? SPU标准属性是商品基本属性&#xff0c;基本属性中最核心两个属性是品牌和型号&#xff0c;电商平台一般采用 品牌和型号 来确定SPU&#xff08;Standard Product Unit&#xff09;标准化管理单元&#xff0c; 例如:小米 10 就…

Vue3 el-tooltip 根据内容控制宽度大小换行和并且内容太短不显示

el-tooltip 根据长度自适应换行以及显隐 环境 vue: "3.2.37" element-ui: "2.1.8"要求 tooltip 根据内容自动换行如果内容超出显示省略号显示&#xff0c;不超出不显示 tooltip 代码 组件 // ContentTip 组件 <template><el-tooltipv-bind&qu…

excel中的引用与查找函数篇1

1、COLUMN(reference)&#xff1a;返回与列号对应的数字 2、ROW(reference)&#xff1a;返回与行号对应的数字 参数reference表示引用/参考单元格&#xff0c;输入后引用单元格后colimn()和row()会返回这个单元格对应的列号和行号。若参数reference没有引用单元格&#xff0c;…

SOME/IP TTL 在各种Entry 中各是什么意思?有什么限制?

1 服务发现 SOME/IP SD 服务发现主要用于 定位服务实例检测服务实例状态是否在运行发布/订阅行为管理SOME/IP SD 也是 SOME/IP 消息,遵循 SOME/IP 消息格式,有固定的 Message ID、Request ID 以及 Message Type 等。并对 SOME/IP Payload 进行了详细的定义。 SOME/IP SD …

2023-9-4 快速幂求逆元

题目链接&#xff1a;快速幂求逆元 #include <iostream> #include <algorithm>using namespace std;typedef long long LL;LL qmi(int a, int k, int p) {LL res 1;while(k){if(k & 1) res (LL) res * a % p;k >> 1;a (LL) a * a % p;}return res; }i…

能力和结果之间的关系

大家好,这里是大话硬件。 今天这篇文章想和大家分享前段时间的一点工作体会,关于个人能力和工作结果之间的关系。 其实这些感悟是在上周三晚上下班,走在回家的路上,脑海中突然出现这样的体会,回到家里立马写了下来。因为是即时的灵感,完全是因为工作状态触发,立刻写下…

手写RPC框架--4.服务注册

RPC框架-Gitee代码(麻烦点个Starred, 支持一下吧) RPC框架-GitHub代码(麻烦点个Starred, 支持一下吧) 服务注册 服务注册a.添加服务节点和主机节点b.抽象注册中心c.本地服务列表 服务注册 a.添加服务节点和主机节点 主要完成服务注册和发现的功能&#xff0c;其具体流程如下&…

智能配电室运维云平台

智能配电室运维云平台依托电易云-智慧电力物联网&#xff0c;是通过物联网技术实现配电设备智能化管理和运维的云服务系统。该平台可以实时监测配电设备的运行状态、能耗情况、故障报警等信息&#xff0c;并通过云计算、大数据等技术进行分析和处理&#xff0c;提供精准的数据支…

数据结构前言

一、什么是数据结构&#xff1f; 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。 上面是百度百科的定义&#xff0c;通俗的来讲数据结构就是数据元素集合与数据元素集合或者数据元素与数据元素之间的组成形式。 举个…

MyBatisPlus 基础实现(一)

说明 创建一个最基本的MyBatisPlus项目&#xff0c;参考官网。 依赖 MyBatisPlus 依赖&#xff0c;最新版是&#xff1a;3.5.3.2 &#xff08;截止2023-9-4&#xff09;。 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-bo…

Xcode打包ipa文件,查看app包内文件

1、Xcode发布ipa文件前&#xff0c;在info中打开如下两个选项&#xff0c;即可在手机上查看app包名文件夹下的文件及数据。

基于QWebEngine实现无头浏览器

无头浏览器 无头浏览器&#xff08;Headless Browser&#xff09;是一种没有图形用户界面&#xff08;GUI&#xff09;的浏览器。它通过在内存中渲染页面&#xff0c;然后将结果发送回请求它的用户或程序来实现对网页的访问&#xff0c;而不会在屏幕上显示网页。这种方式使得无…

编译OpenWrt内核驱动

编译OpenWrt内核驱动可以参考OpenWrt内部其它驱动的编写例程&#xff0c;来修改成自己需要的驱动 一、OpenWrt源代码获取与编译 1.1、搭建环境 下载OpenWrt的官方源码&#xff1a; git clone https://github.com/openwrt/openwrt.git1.2、安装编译依赖项 sudo apt update -…

机器人中的数值优化(九)——拟牛顿方法(下)、BB方法

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考&#xff0c;主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等&#xff0c;本系列文章篇数较多&#xff0c;不定期更新&#xff0c;上半部分介绍无约束优化&#xff0c;…

CocosCreator3.8研究笔记(六)CocosCreator 脚本装饰器的理解

一、什么是装饰器&#xff1f; 装饰器是TypeScript脚本语言中的概念。 TypeScript的解释&#xff1a;在一些场景下&#xff0c;我们需要额外的特性来支持标注或修改类及其成员。装饰器&#xff08;Decorators&#xff09;为我们在类的声明及成员上通过元编程语法添加标注提供了…

Java23种设计模式之【单例模式】

目录 一.单例模式的起源&#xff0c;和应用场景 1.单例模式的前世今生&#xff01; 2.什么是单例模式&#xff1f; 2.1使用单例模式的注意事项 2.2如何理解单例模式&#xff1f; 2.3单例模式的优势以及不足&#xff01; 2.4使用场景 二.实现 1.实现思路 1.1创建一个 S…

华为OD机试-贪吃蛇

题目描述 【贪吃蛇】贪吃蛇是一个经典游戏&#xff0c;蛇的身体由若干方格连接而成&#xff0c;身体随蛇头移动。蛇头触碰到食物时&#xff0c;蛇的长度会增加一格。蛇头和身体的任一方格或者游戏版图边界碰撞时&#xff0c;游戏结束。 下面让我们来完成贪吃蛇游戏的模拟&…