效果展示:
实现步骤
- 通过idea新建一个springboot的项目
创建项目等待完成
- maven依赖整合,以及配置文件编写
除了选中的创建项目是选中4个依赖以外,附加了这三个,mybatis-plus作为跟数据库之间的orm,druid用于做数据库连接池,之后做扩展准备,fastjson用于将字符串跟Object对象之间做转换。
依赖文件我是用:resource/application.yml 没有的自行创建或者使用properties也可以
3. 在src目录下创建基础的项目结构
4. 数据库表的创建(这是基础结构,之后会考虑扩展)
CREATE TABLE `t_message` (
`id` int NOT NULL AUTO_INCREMENT, // 消息id
`from_id` int DEFAULT NULL, // 发起消息的用户ID
`to_id` int DEFAULT NULL, // 接收消息的用户ID
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, // 消息主体内容
`is_read` int DEFAULT '0', // 是否已读
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
- WebSocketConfig配置
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
- CorsConfig配置,解决前端js跨域的问题
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600L);
}
}
- 创建消息实体类
Message.java
// 这里用到了lombok,如果不懂的可以留言或者百度,问的多的话我抽空做一期
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class Message {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer fromId;
private Integer toId;
private String content;
private int isRead=0;
}
- 创建message的dao层
// 这里用到了mybatis-plus,如果不懂的可以留言或者百度,问的多的话我抽空做一期
@Mapper
public interface IMessageMapper extends BaseMapper<Message> {
}
- 创建message的service以及他的实现
public interface MessageService {
void insertMessage(Message message);
}
-----------------------------------------------------------------
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
IMessageMapper messageMapper;
@Override
public void insertMessage(Message message) {
messageMapper.insert(message);
}
}
- 下面是核心的部分创建WebSocketServer以及工具类
public class WebSocketUtils {
private final static MessageService messageService = MessageApplication.getBean(MessageServiceImpl.class); // 获取到messageService
public static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0); // 存放到当前在线的连接数
public static final Map<Integer, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>(); // 存放当前在线用户的session
/**
* 推送消息接口
* @param session 目标session
* @param message 消息内容
*/
public static void sendMessage(Session session, String message) {
if (session == null) { return; }
RemoteEndpoint.Async async = session.getAsyncRemote(); 发起消息
if (async == null) { return; }
async.sendText(message);
}
/**
* 指定推送给某人
* @param message 消息对象
*/
public static void sendMessageSingle(Message message) {
messageService.insertMessage(message); // 将message插入数据库中
sendMessage(ONLINE_USER_SESSIONS.get(message.getToId()), JSON.toJSONString(message)); // 获取到制定目标的session,发出消息,消息内容是json对象
}
/**
* 全用户推送
* @param message 消息主体
*/
public static void sendMessageAll(String message) {
ONLINE_USER_SESSIONS.forEach((sessionId, session) -> { // 循环对所有的session进行发送消息
sendMessage(session, message);
});
}
}
@Component
@ServerEndpoint("/v1/websocket/{id}") // 此处是核心注入,创建一个websocket的控制器传入对象是用户id
public class WebSocketServer{
@OnOpen // 标注的打开websocket链接的方法
public void onOpen(@PathParam("id") int id,Session session){
WebSocketUtils.ONLINE_COUNT.incrementAndGet(); // 链接数加一
WebSocketUtils.ONLINE_USER_SESSIONS.put(id,session); // 将session添加到session池中
}
@OnMessage // 标注的监听websocket消息的方法
public void onMessage(@PathParam("id") int id,String msg){
Message message = JSON.parseObject(msg,Message.class); // 解析发送的消息,将json字符串转换为message对象
message.setFromId(id); // 设置fromId=id
System.out.println(message); // 打印一下消息对象
WebSocketUtils.sendMessageSingle(message); // 发起消息
}
@OnError // 标注的websocket异常的方法
public void onError(@PathParam("id") int id,Session session,Throwable error) throws IOException {
error.printStackTrace(); // 打印异常堆栈
WebSocketUtils.ONLINE_COUNT.decrementAndGet(); // 链接数减一
WebSocketUtils.ONLINE_USER_SESSIONS.remove(id); // 移除异常的session
session.close(); // 关闭session
}
@OnClose // 标注的websocket关闭的方法
public void onClose(Session session,@PathParam("id") int id) throws IOException {
WebSocketUtils.ONLINE_COUNT.decrementAndGet(); // 链接数减一
WebSocketUtils.ONLINE_USER_SESSIONS.remove(id); // 移除session
session.close(); // 关闭session
}
}
这里会存在的问题: 问题:在写到插入数据库的操作的时候,本来是用的是@Autowired的方式注入的,但是出现了messageService为空的情况
原因:是因为Spring默认的是单例模式,而WebSocket是多实例,每个session对应一个实例
解决方案:目前为了简单,我通过入口类实现了一个接口完成的
- 通过实现ApplicationContextAware接口将所有的bean都能获取出来
@SpringBootApplication
public class MessageApplication implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static void main(String[] args) {
SpringApplication.run(MessageApplication.class, args);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
MessageApplication.applicationContext = applicationContext;
}
public ApplicationContext getApplicationContext(){
return applicationContext;
}
public static <T> T getBean(Class<T> type) { // 根据对象类型获取实例
try {
return applicationContext.getBean(type);
} catch (NoUniqueBeanDefinitionException e) { //出现多个,选第一个
String beanName = applicationContext.getBeanNamesForType(type)[0];
return applicationContext.getBean(beanName, type);
}
}
public static <T> T getBean(String beanName, Class<T> type) {
return applicationContext.getBean(beanName, type);
}
}
并且在
WebSocketUtils.java中通过下面的方式获取messageService
private final static MessageService messageService = MessageApplication.getBean(MessageServiceImpl.class); // 获取到messageService
到这里就完全实现了一个可以传递json消息传输的websocket服务器
测试可以通过网上任意的测试工具进行测试,效果在顶部
总结
万里长征第一步,websocket搭建实际上教程很多,这也算是我个人对于实践的一些总结,本来的目的也是为了实现一个支持查看未读消息的一个服务。之后我应该会对这个服务继续扩展,争取实现成一个可独立应用的消息模块。
如果有什么疑问或者想要探讨的,可以随时跟我联系。