习惯了微信聊天,利用WebSocket手动实现个聊天功能怎么

1.背景

基于项目需求,最近需要实现一个简单的聊天功能。日常生活中,大家对于聊天也习以为常,微信、QQ等软件也经常用到,其实我们也可以引入一些第三方的sdk包等去实现,也可以利用WebSocket通信协议去手动实现简单的聊天。本文主要讲述下WebSocket实现的具体步骤及实现的效果图。

2.方案选型及优缺点介绍

  • 方案一 利用http接口手动实现三个接口:sengMsg(消息发送)、receiveMsg(消息接收)、getHistoryMsg(获取历史消息) ,然后前端发送消息时调用sendMsg接口,将数据写入数据库以便获取历史消息使用,接收消息时前端声明一个定时器,每一秒钟去刷新消息接收接口,来获取消息内容显示到聊天框中,最后,如果用户需要翻看历史消息,调用getHistoryMsg接口即可。
+ **优点** 后端实现简单,且能将聊天消息持久化到数据库永久保存,可以根据聊天室id随时获取消息内容
+ **缺点** 由于频繁调用接口,服务器和api接口压力比较大,高并发情况下服务器可能会宕机,而且不进行消息发送时,由于定时器的使用,前端频繁请求会造成空跑,显然不太合理
  • 方案二 利用已有的WebSocket服务实现聊天功能
+ **优点** 不用额外自己实现接口,直接按照WebSocket定义的规则直接套用即可
+ **缺点** 消息没有持久化,如果服务宕机,可能无法查看历史消息

3.服务搭建及实现

  • 3.1 引入依赖
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  • 3.2 声明socket配置类
1
2
3
4
5
6
7
8
9
typescript复制代码@Configuration
public class WebSocketConfig {

//注入一个ServerEndpointExporter
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
  • 3.3 声明聊天Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
typescript复制代码/**
* 聊天控制器
* @ServerEndpoint("/chat/{userId}")中的userId是前端创建会话窗口时当前用户的id,即消息发送者的id
*/
@ServerEndpoint("/chat/{userId}")
@Component
public class ChatWebSocketController {

private final Logger logger = Logger.getLogger(ChatWebSocketController.class);

//onlineCount:在线连接数
private static AtomicInteger onlineCount = new AtomicInteger(0);

//webSocketSet:用来存放每个客户端对应的MyWebSocket对象。
public static List<ChatWebSocketController> webSocketSet = new ArrayList<>();

//存放所有连接人信息
public static List<String> userList = new ArrayList<>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

//用户ID
public String userId = "";

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
this.userList.add(userId) ;
//加入set中
webSocketSet.add(this);
//在线数加1
onlineCount.incrementAndGet();
logger.info("有新连接加入!" + userId + "当前在线用户数为" + onlineCount.get());
JSONObject msg = new JSONObject();
try {
msg.put("msg", "连接成功");
msg.put("status", "SUCCESS");
msg.put("userId", userId);
sendMessage(JSON.toJSONString(msg));
} catch (Exception e) {
logger.debug("IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("userId") String userId ) {
//从set中删除
webSocketSet.remove(this);
onlineCount.decrementAndGet(); // 在线数减1
logger.info("用户"+ userId +"退出聊天!当前在线用户数为" + onlineCount.get());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId ) {
//客户端输入的消息message要经过处理后封装成新的message,后端拿到新的消息后进行数据解析,然后判断是群发还是单发,并调用对应的方法
logger.info("来自客户端" + userId + "的消息:" + message);
try {
MyMessage myMessage = JSON.parseObject(message, MyMessage.class);
String messageContent = myMessage.getMessage();//messageContent:真正的消息内容
String messageType = myMessage.getMessageType();
if("1".equals(messageType)){ //单聊
String recUser = myMessage.getUserId();//recUser:消息接收者
sendInfo(messageContent,recUser,userId);//messageContent:输入框实际内容 recUser:消息接收者 userId 消息发送者
}else{ //群聊
sendGroupInfo(messageContent,userId);//messageContent:输入框实际内容 userId 消息发送者
}
} catch (Exception e) {
logger.error("解析失败:{}", e);
}
}

/**
* 发生错误时调用的方法
*
* @OnError
**/
@OnError
public void onError(Throwable error) {
logger.debug("Websocket 发生错误");
error.printStackTrace();
}

public synchronized void sendMessage(String message) {
this.session.getAsyncRemote().sendText(message);
}

/**
* 单聊
* message : 消息内容,输入的实际内容,不是拼接后的内容
* recUser : 消息接收者
* sendUser : 消息发送者
*/
public void sendInfo( String message , String recUser,String sendUser) {
JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息
for (ChatWebSocketController item : webSocketSet) {
if (StringUtil.equals(item.userId, recUser)) {
logger.info("给用户" + recUser + "传递消息:" + message);
//拼接返回的消息,除了输入的实际内容,还要包含发送者信息
msgObject.put("message",message);
msgObject.put("sendUser",sendUser);
item.sendMessage(JSON.toJSONString(msgObject));
}
}
}

/**
* 群聊
* message : 消息内容,输入的实际内容,不是拼接后的内容
* sendUser : 消息发送者
*/
public void sendGroupInfo(String message,String sendUser) {
JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息
if (StringUtil.isNotEmpty(webSocketSet)) {
for (ChatWebSocketController item : webSocketSet) {
if(!StringUtil.equals(item.userId, sendUser)) { //排除给发送者自身回送消息,如果不是自己就回送
logger.info("回送消息:" + message);
//拼接返回的消息,除了输入的实际内容,还要包含发送者信息
msgObject.put("message",message);
msgObject.put("sendUser",sendUser);
item.sendMessage(JSON.toJSONString(msgObject));
}
}
}
}

/**
* Map/Set的key为自定义对象时,必须重写hashCode和equals。
* 关于hashCode和equals的处理,遵循如下规则:
* 1)只要重写equals,就必须重写hashCode。
* 2)因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
* 3)如果自定义对象做为Map的键,那么必须重写hashCode和equals。
*
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ChatWebSocketController that = (ChatWebSocketController) o;
return Objects.equals(session, that.session);
}

@Override
public int hashCode() {
return Objects.hash(session);
}
}
  • 3.4 声明Controller中的MyMessage实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typescript复制代码public class MyMessage implements Serializable {

private static final long serialVersionUID = 1L;

private String userId;
private String message;//消息内容
private String messageType;//消息类型 1 代表单聊 2 代表群聊

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public String getMessageType() {
return messageType;
}

public void setMessageType(String messageType) {
this.messageType = messageType;
}
}
  • 3.5 声明Controller中的StringUtil工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
ini复制代码public final class StringUtil {

/**
* 对象为空
*
* @param object
* @return
*/
public static boolean isEmpty(Object object) {
if (object == null) {
return true;
}
if (object instanceof String && "".equals(((String) object).trim())) {
return true;
}
if (object instanceof List && ((List) object).size() == 0) {
return true;
}
if (object instanceof Map && ((Map) object).isEmpty()) {
return true;
}
if (object instanceof CharSequence && ((CharSequence) object).length() == 0) {
return true;
}
if (object instanceof Arrays && (Array.getLength(object) == 0)) {
return true;
}
return false;
}

/**
* 对象不为空
*
* @param object
* @return
*/
public static boolean isNotEmpty(Object object) {
return !isEmpty(object);
}

/**
* 查询字符串中某个字符首次出现的位置 从1计数
*
* @param string 字符串
* @param c
* @return
*/
public static int strFirstIndex(String c, String string) {
Matcher matcher = Pattern.compile(c).matcher(string);
if (matcher.find()) {
return matcher.start() + 1;
} else {
return -1;
}
}

/**
* 两个对象是否相等
*
* @param obj1
* @param obj2
* @return
*/
public static boolean equals(Object obj1, Object obj2) {
if (obj1 instanceof String && obj2 instanceof String) {
obj1 = ((String) obj1).replace("\\*", "");
obj2 = ((String) obj2).replaceAll("\\*", "");
if (obj1.equals(obj2) || obj1 == obj2) {
return true;
}
}
if (obj1.equals(obj2) || obj1 == obj2) {
return true;
}
return false;
}

/**
* 根据字节截取内容
*
* @param bytes 自定义字节数组
* @param content 需要截取的内容
* @return
*/
public static String[] separatorByBytes(double[] bytes, String content) {
String[] contentArray = new String[bytes.length];
double[] array = new double[bytes.length + 1];
array[0] = 0;
//复制数组
System.arraycopy(bytes, 0, array, 1, bytes.length);
for (int i = 0; i < bytes.length; i++) {
content = content.substring((int) (array[i] * 2));
contentArray[i] = content;
}
String[] strings = new String[bytes.length];
for (int i = 0; i < contentArray.length; i++) {
strings[i] = contentArray[i].substring(0, (int) (bytes[i] * 2));
}
return strings;
}

/**
* 获取指定字符串出现的次数
*
* @param srcText 源字符串
* @param findText 要查找的字符串
* @return
*/
public static int appearNumber(String srcText, String findText) {
int count = 0;
Pattern p = Pattern.compile(findText);
Matcher m = p.matcher(srcText);
while (m.find()) {
count++;
}
return count;
}


/**
* 将字符串str每隔2个分割存入数组
*
* @param str
* @return
*/
public static String[] setStr(String str) {
int m = str.length() / 2;
if (m * 2 < str.length()) {
m++;
}
String[] strings = new String[m];
int j = 0;
for (int i = 0; i < str.length(); i++) {
if (i % 2 == 0) {
//每隔两个
strings[j] = "" + str.charAt(i);
} else {
strings[j] = strings[j] + str.charAt(i);
j++;
}
}
return strings;
}


/**
* 定义一个StringBuffer,利用StringBuffer类中的reverse()方法直接倒序输出
* 倒叙字符串
*
* @param s
*/
public static String reverseString2(String s) {
if (s.length() > 0) {
StringBuffer buffer = new StringBuffer(s);
return buffer.reverse().toString();
} else {
return "";
}
}

/**
* 截取字符串中的所有日期时间
*
* @param str
* @return
*/
public static List<String> dateTimeSubAll(String str) {
try {
List<String> dateTimeStrList = new ArrayList<>();
String regex = "[0-9]{4}[-][0-9]{1,2}[-][0-9]{1,2}[ ][0-9]{1,2}[:][0-9]{1,2}[:][0-9]{1,2}";
Pattern pattern = compile(regex);
Matcher matcher = pattern.matcher(str);
while (matcher.find()) {
String group = matcher.group();
dateTimeStrList.add(group);
}
return dateTimeStrList;
} catch (Exception e) {
e.getMessage();
return null;
}
}


/**
* 截取字符串中的所有日期
*
* @param str
* @return
*/
public static List<String> dateSubAll(String str) {
try {
List<String> dateStrList = new ArrayList<>();
Pattern pattern = compile("[0-9]{4}[-][0-9]{1,2}[-][0-9]{1,2}");
Matcher matcher = pattern.matcher(str);
while (matcher.find()) {
String group = matcher.group();
dateStrList.add(group);
}
return dateStrList;
} catch (Exception e) {
e.getMessage();
return null;
}
}

/**
* 获取随机字符串
*
* @param length
* @return
*/
public static String getRandomString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
  • 3.6 后台声明测试的html页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
xml复制代码<!DOCTYPE HTML>
<html>
<head>
<title>WebSocket Chat Demo</title>
</head>

<body>
<input id="inputContent" type="text" style="width:600px;"/>
<button onclick="send()">Send</button>
<button onclick="closeConnection()">Close</button>
<div id="msg"></div>
</body>

<script type="text/javascript">

var websocket = null;

//声明自己搭建的websocket服务
if ('WebSocket' in window) {
var random = parseInt(Math.random() * 1000000) + "";
websocket = new WebSocket("ws://localhost:8005/chat/"+ random);
} else {
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("error");
};

//连接成功建立的回调方法
websocket.onopen = function(event) {
//setMessageInnerHTML("open");
}

//接收到消息的回调方法
websocket.onmessage = function(event) {
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("close");
}

//监听窗口关闭事件,当窗口关闭时关闭对应websocket连接
window.onbeforeunload = function() {
websocket.close();
}

//将消息回显在页面上
function setMessageInnerHTML(innerHTML) {
document.getElementById('msg').innerHTML += innerHTML + '<br/>';
}

//关闭连接
function closeConnection() {
websocket.close();
}

//发送消息
function send() {
var msg = document.getElementById('inputContent').value;
websocket.send(msg);
}
</script>
</html>

该类对应的路径如下:

微信图片_20210902214029.jpg

4.启动服务并测试

页面输入ip+端口建立websocket连接并发送一条消息,测试结果如图:

1.png

2.jpg

3.png
注意

4.jpg

5.png

6.png

7.png

8.jpg

注意

  • 1.正常情况下,输入框中只输入要发送的实际聊天内容即可,比如“在吗老公,急事”,但是为了更容易测试,页面中输入的是拼接后的json消息体,接收者用户id,以及消息类型,实际开发中数据格式让前端处理即可,前端根据输入的内容拼接成如输入框图所示的数据格式即可
  • 2.messageType来区分单聊还是群聊,但是此处的群聊是建立连接的所有websocket服务,没有区分组概念,如果区分的话,后台接口请求路径中要添加上roomId参数,然后建立连接时将进入该聊天室的用户放入一个map集合中,群聊发送消息时,根据不同的roomId,只给该组的用户推送群聊消息即可
  • 3.此外,测试时也可以使用websocket在线测试 链接如下:websocket在线测试链接

文末福利

好了,今天的分享就到这里,如果对你有所帮助的话,记得给小编评论和点赞哦!对于优质评论内容,更有掘金精美礼品等你来领,我会抽取两名用户赠送随机徽章一份,还在等什么吗,赶快参与评论吧,精美礼品不容错过!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%