1. Предпосылки
В соответствии с требованиями проекта недавно необходимо реализовать простую функцию чата. В повседневной жизни все привыкли общаться в чате, часто используется Wechat, QQ и др. На самом деле, мы также можем внедрить некоторые сторонние пакеты sdk для достижения этой цели, или мы можем использовать протокол связи WebSocket для ручной реализации простого чата. . В этой статье в основном описываются конкретные шаги реализации WebSocket и диаграмма результатов реализации.
2. Выбор схемы и знакомство с преимуществами и недостатками
-
Вариант первыйИспользуйте интерфейс http для ручной реализации трех интерфейсов: sengMsg (отправка сообщений), ReceiveMsg (получение сообщений), getHistoryMsg (получение исторических сообщений), а затем вызовите интерфейс sendMsg, когда внешний интерфейс отправляет сообщение, запишите данные в базу данных. для использования исторических сообщений и получения сообщений. Когда внешний интерфейс объявляет таймер, он обновляет интерфейс получения сообщений каждую секунду, чтобы получить содержимое сообщения и отобразить его в окне чата Наконец, если пользователю необходимо просмотреть исторические сообщения, просто вызовите интерфейс getHistoryMsg.
- преимуществоСерверная часть проста в реализации и может сохранять сообщения чата в базе данных для постоянного хранения, а содержимое сообщений может быть получено в любое время в соответствии с идентификатором комнаты чата.
- недостатокИз-за интерфейса частых вызовов нагрузка на сервер и API-интерфейсы относительно велика, сервер с высокой степенью параллелизма может быть отключен, но не при отправке сообщения, из-за использования таймера фронт вызовет часто запрашиваемый транспорт, явно не разумно
-
Вариант 2Используйте существующую службу WebSocket для реализации функции чата
- преимуществоВам не нужно реализовывать интерфейс самостоятельно, вы можете напрямую применять правила, определенные WebSocket.
- недостатокСообщения не сохраняются. Если служба не работает, исторические сообщения могут быть недоступны для просмотра.
3. Построение и внедрение услуг
- 3.1 Знакомство с зависимостями
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 3.2 Объявить класс конфигурации сокета
@Configuration
public class WebSocketConfig {
//注入一个ServerEndpointExporter
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 3.3 Объявление контроллера чата
/**
* 聊天控制器
* @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 Объявите класс сущности MyMessage в контроллере
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 Объявление класса инструмента StringUtil в контроллере
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-страница теста объявления фона
<!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>
Путь, соответствующий этому классу, выглядит следующим образом:
4. Запустите сервис и протестируйте
Введите ip+port на странице, чтобы установить соединение через веб-сокет и отправить сообщение, результат теста показан на рисунке:
Уведомление:
Уведомление
- 1. В обычных обстоятельствах вам нужно только ввести фактическое содержимое чата для отправки в поле ввода, например «Ты здесь, муж, срочно?», но для упрощения тестирования вводится склеенное тело сообщения json. на странице, а также идентификатор пользователя-получателя и тип сообщения.В фактической разработке формат данных может обрабатываться внешним интерфейсом, а внешний интерфейс может быть объединен в формат данных, показанный на входной блок-схеме. в соответствии с входным содержимым.
- 2. messageType используется для различения одиночного чата или группового чата, но групповой чат здесь — это все службы вебсокетов, которые устанавливают соединение, и нет понятия группировки.Пользователи, которые входят в комнату чата, помещаются в набор карт. чат отправляет сообщения, согласно разным идентификаторам комнаты, только сообщения группового чата могут быть отправлены пользователям в этой группе.
- 3. Кроме того, вы также можете использовать веб-сокет для онлайн-тестирования.Ссылка выглядит следующим образом:Websocket Онлайн-тестовая ссылка
Благополучие в конце статьи
Что ж, сегодняшняя публикация здесь. Если она была вам полезна, не забудьте оставить комментарий и поставить лайк редактору! Для высококачественного содержания комментариев вас ждут более изысканные подарки. Я выберу двух пользователей, чтобы дать случайный значок. Чего вы ждете? Приходите и участвуйте в комментариях, изысканных подарков не будет пропущенный!