目录
一、认识WebSocket
二、HTML实现聊天
三、微信小程序实现聊天
1.首先博主在初学Java时自我感觉走了很多弯路,因为以前见识短,在接触聊天功能时根本就没能想到有WebSocket这个聊天框架,就只能用底层的UDP或TCP实现聊天功能,及其繁琐。
1.在入门Java后的朋友学到网络编程会知道UDP和TCP两个知识点,没错WebSocket是一种在单个TCP连接上进行全双工通信的协议。基于TCP协议的一个框架,TCP知识点比较多,具体咱们就不多说了,直接实践怎么使用吧。
首先我先贴出完整代码,然后解释
1.html代码,这里我就不单独写js文件了(这个html实现的是一对一聊天,还有一对多,多对多群聊)
123
2.SpringBoot完整代码
(1)WebSocketConfig.java配置文件(关键文件)
package com.example.mengchuangyuan.common.chat.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/user","/agent","/topic"); // 定义消息代理,客户端订阅的地址前缀
config.setApplicationDestinationPrefixes("/app"); // 定义客户端发送消息的地址前缀
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS(); // 定义WebSocket端点,客户端连接的地址
}
}
(2)ChatController.java控制层
package com.example.mengchuangyuan.common.chat.controller;
import com.example.mengchuangyuan.common.chat.entry.ChatMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
@Slf4j
@Controller
public class ChatController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 发消息(群发)
* @param chatMessage 获取用户消息
* @return 返回要回复的消息
*/
//第一种方式
@MessageMapping("/chat/send") // 定义消息映射路径
@SendTo("/agent/public") // 发送消息到指定的代理路径
public ChatMessage sendMessage(ChatMessage chatMessage) {
log.info(chatMessage.toString());
return chatMessage;
}
//方法二
// @MessageMapping("/agent/send")
// public void getAgentInfo (@Payload ChatMessage chatMessage) {
// System.out.println("发送群发消息");
// // 使用api进行推送
// simpMessagingTemplate.convertAndSend("/agent/public2", chatMessage);
// }
/**
* 发送给自己?
* @param chatMessage
* @return
*/
@MessageMapping("/agent/send/user")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
@SendToUser("/agent/info")
public ChatMessage sendUserMessage(ChatMessage chatMessage) {
log.info(chatMessage.toString());
return chatMessage;
}
/**
* 发送给指定用户
* @param chatMessage
* @return
*/
@MessageMapping("/chat/send/to/user")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
public void sendToUserMessage(ChatMessage chatMessage) {
log.info("发送给指定用户:"+chatMessage.toString());
simpMessagingTemplate.convertAndSendToUser(chatMessage.getUnionId(),"/message",chatMessage);
}
}
package com.example.mengchuangyuan.common.chat.entry;
import lombok.Data;
@Data
public class ChatMessage {
private String unionId;
private String content;
private String sender;
}
以上就是完整代码
3.接下来我来简单解释一下,因为一对一聊天比其他相对绕一点,所以博主就解释它就好了,且看下面四段被截取的代码
function connect() {
var content = $("#message").val();//发送的消息内容
var sender = 'tbb';//接收的人
var socket = new SockJS('http://127.0.0.1:8080/chat');//连接后端socket的地址
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
//这里我写死了,接收来自tbb这个人的消息,'/user/tbb/message'可以改成'/user/'+sender+'/message'
stompClient.subscribe('/user/tbb/message', function (message) {
showMessage(JSON.parse(message.body));
});
});
}
上面的代码可以称之为连接服务器并且实时监听tbb给后端发来的消息
package com.example.mengchuangyuan.common.chat.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/user","/agent","/topic"); // 定义消息代理,客户端订阅的地址前缀
config.setApplicationDestinationPrefixes("/app"); // 定义客户端发送消息的地址前缀
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS(); // 定义WebSocket端点,客户端连接的地址
}
}
上面两段是对应的,在html的http://127.0.0.1:8080/chat中chat对应上面Java代码的chat,必须一致,要不然连不上,/user/tbb/message路由中user对应上面Java代码的user,必须一致,要不然发消息时对方收不到消息(博主以前踩过这个坑),而这个tbb则是接收发消息的人的消息
function sendMessage() { //发消息给对方
var content = $("#message").val();
var sender = $("#sender").val();
stompClient.send("/app/chat/send/to/user", {}, JSON.stringify({ //发消息的后端的路由
content: content,//消息内容
sender: sender,//消息姓名
unionId: sender,//消息id,这里我把消息姓名也作为id
}));
}
/**
* 发送给指定用户
* @param chatMessage
* @return
*/
@MessageMapping("/chat/send/to/user")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
public void sendToUserMessage(ChatMessage chatMessage) {
log.info("发送给指定用户:"+chatMessage.toString());
//chatMessage.getUnionId()是发送给某人的id,/message对应四段中第一段的/user/tbb/message中的message
simpMessagingTemplate.convertAndSendToUser(chatMessage.getUnionId(),"/message",chatMessage);
}
最后两段是对应的,用于给对方发消息。
首先,这是博主自己摸索了很久出来的一套小程序聊天体系。
1.聊天数据结构及框架
涉及到了redis缓存,因此需要下载redis的依赖包
聊天数据结构如下(自我感觉存在一定缺陷,懒得改进了):
整体储存结构:
聊天界面结构:{openid1+openid2:{linkType:[info1,info2]}},
如图聊天界面:
聊天列表结构:{linkType:[openid1+openid2,openid3+openid4]}
如图聊天列表:
info的结构:{mid:"",type:"",linkType:"",formUser:fromUser,toUser:toUser,message:"",date:"",nowDate:""}
fromUser和toUser的结构:{openid:"",phone:"",name:"",headImg:""}
openid1和openid2为fromUser和toUser的openid
linkType:属于哪个板块聊天(比如相亲聊天板块或者外卖或商城聊天板块)
type:作用于获取redis缓存的聊天记录和聊天心跳检测(备注:因为获取历史聊天记录和心跳检测是以聊天方式向后端发起请求,因此我用type的聊天要区分是用户发起聊天还是其他请求。由前端自动发动聊天请求,获取历史聊天记录,由前端发起聊天请求检测心跳,检测心跳的目的是为了确保聊天过程不掉线)
date:聊天的时间段(备注:可以设置5分钟显示一个时间段聊天的时间,比如微信隔5分钟后再发一条信息上面会显示时间,这里我设置的date就充当这个角色)
nowDate:每一句话的时间,主要用于计算当前时间是否与上一句聊天记录的时间是否间隔5分钟,如果间隔5分钟那么上面的date记录该时间,如果间隔不到5分钟,则date设置为空。
整体结构上:两个人的openid连接作为获取他们之间所有功能板块历史聊天记录的内容的键(key)。以linkType作为键(key)获取某个功能板块的所有历史聊天记录,这里聊天记录用集合来储存保证了聊天记录顺序。
2.Java SpringBoot代码
(1)MiniWebSocketConfig.java文件
package com.example.mengchuangyuan.common.chat.mini.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class MiniWebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(2)MiniWebSocketController.java文件
package com.example.mengchuangyuan.common.chat.mini.controller;
import com.example.mengchuangyuan.common.redis.tool.SufferVariable;
import com.example.mengchuangyuan.common.tool.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.*;
@Slf4j
@RestController
@RequestMapping("/mini/socket")
public class MiniWebSocketController {
@Value("${upload.messageChatUrl}")
private String messageChatUrl;
@Value("${upload.messageChatPath}")
private String messageChatPath;
private final List
(3)WebSocketEndPoint.java文件
package com.example.mengchuangyuan.common.chat.mini.mapper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.mengchuangyuan.common.redis.mapper.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
//对外公布的一个后端站点
//ws://localhost:8080/websocket/用户id
@ServerEndpoint(value = "/websocket/{userId}")
@Component
@Slf4j
public class WebSocketEndPoint {
//与某个客户端的连接会话,需要他来给客户端发送数据
private Session session;
@Autowired
private SessionPool sessionPool;
// @Autowired
// private RedisUtils redisUtils;
private static WebSocketEndPoint webSocketEndPoint;
//初始化 ②
@PostConstruct
public void init() {
webSocketEndPoint = this;
webSocketEndPoint.sessionPool = this.sessionPool;
}
//连接建立成功调用的方法
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
//把会话加入连接池中
//userId通过用户传入,session是系统自动产生
SessionPool.sessions.put(userId, session);
//TODO 可以添加日志操作
}
//关闭会话的时候
@OnClose
public void onClose(Session session) throws IOException {
webSocketEndPoint.sessionPool.close(session.getId());
session.close();
}
//接收客户端的消息后调用的方法,在这里可以进行各种业务逻辑的操作
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(message);
// log.info("redisUtils:"+redisUtils);
//心跳检测
if (message.equalsIgnoreCase("ping")) {
try {
Map params = new HashMap<>();
params.put("type", "pong");
session.getBasicRemote().sendText(JSON.toJSONString(params));
} catch (Exception e) {
e.printStackTrace();
}
return;
}
//将Json字符串转为键值对
// HashMap params = JSON.parseObject(message, HashMap.class);
JSONObject params = JSON.parseObject(message);
webSocketEndPoint.sessionPool.sendMessage(params);
//这里的业务逻辑仅仅是把收到的消息返回给前端
// SessionPool.sendMessage(message);
}
}
(4)SessionPool.java文件
package com.example.mengchuangyuan.common.chat.mini.mapper;
import com.alibaba.fastjson.JSON;
import com.example.mengchuangyuan.common.redis.mapper.RedisUtils;
import com.example.mengchuangyuan.common.redis.tool.SufferVariable;
import com.example.mengchuangyuan.common.tool.DateYMDms;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import javax.websocket.Session;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class SessionPool {
@Autowired
private RedisUtils redisUtils;
//key-value : userId - 会话(系统创建)
public static Map sessions = new ConcurrentHashMap<>();//避免多线程问题
public void close(String sessionId) {
//sessionId是在session中添加了一个标识,准确定位某条session
for (String userId : SessionPool.sessions.keySet()) {
Session session = SessionPool.sessions.get(userId);
if (session.getId().equals(sessionId)) {
sessions.remove(userId);
break;
}
}
}
public void sendMessage(String userId, String message) {
sessions.get(userId).getAsyncRemote().sendText(message);
}
//消息的群发,业务逻辑的群发
public void sendMessage(String message) {
// redisUtils.cacheValue("chatMessage","String.valueOf(SufferVariable.messageMap)");
for (String sessionId : SessionPool.sessions.keySet()) {
SessionPool.sessions.get(sessionId).getAsyncRemote().sendText(message);
}
}
//点对点的消息推送
public void sendMessage(Map params) {
log.info("消息内容:"+String.valueOf(params));
long mid = System.currentTimeMillis();
Map formUser = (Map) params.get("formUser");
Map toUser = (Map) params.get("toUser");
String userId = formUser.get("openid").toString();
String toUserId = toUser.get("openid").toString();
// String msg = params.get("message").toString();
String type = params.get("type").toString();
String linkType = params.get("linkType").toString();
//获取用户session
Session session = sessions.get(toUserId);
Map keyMap;
List setOpenid;
Map map;
List
以上小程序相关代码存在一些缺陷,并且未完整,
需要小程序及SpringBoot完整代码的朋友可以私信博主,
好了本次分享就到此结束。