欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Boot + WebSocket实现聊天

程序员文章站 2022-05-17 09:28:40
...

Spring Boot + WebSocket 实现聊天

采用全注解方式实现websocket服务端聊天

环境

  • spring-boot-starter-parent-2.0.8.RELEASE
  • Java 1.8
  • maven 3.5.+

可根据具体实际情况进行版本的替换选择

依赖

websocket的依赖


<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.8.RELEASE</version>
</parent>

<dependencies>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

	<!-- spring boot WebSocket相关包 -->
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-websocket</artifactId>
	</dependency>


	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
	</dependency>

	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.2.73</version>
	</dependency>


</dependencies>

<!-- spring boot 插件 -->
<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>

实现websocket服务端

package com.tianya.springboot.websocket.server;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.tianya.springboot.websocket.handle.AppIdMsgHandle;
import lombok.extern.slf4j.Slf4j;

/**
 * @description
 *	WebSocket 服务端
 * 每种应用下,对应的会话连接
 * <pre>key: appId 不同应用,
 * 如:
 * 	msg对应消息推送,
 * 	log对应日志推送等,
 * 	chat对应聊天信息推送</pre>
 * @author TianwYam
 * @date 2020年9月17日下午1:47:22
 */
@Slf4j
@Service
@ServerEndpoint(value = "/ws/{appId}")
public class WebSocketServer {
	
	// 自动注入消息处理的实现
	// @Autowired 在这儿自动注入会报空指针异常,取不到
	private static Set<AppIdMsgHandle> appIdMsgHandles ;

	/*** 每种应用下,对应的会话连接 */
	private static final ConcurrentHashMap<String, LinkedHashSet<Session>> APPID_CONNECT_SESSION_MAP = new ConcurrentHashMap<>();
	
	
	/**
	 * @description
	 *	session 连接 开始
	 * @author TianwYam
	 * @date 2020年9月17日下午2:27:12
	 * @param session 会话连接
	 * @param appId 应用ID
	 */
	@OnOpen
	public void open(Session session, @PathParam(value = "appId") String appId) {
		
		// 获取此应用下所有连接
		LinkedHashSet<Session> sessions = getSessions(appId);
		if (sessions == null) {
			sessions = new LinkedHashSet<>();
			APPID_CONNECT_SESSION_MAP.put(appId, sessions);
		}
		
		// 新会话session 连接成功
		sessions.add(session);
		log.info("新连接进入,目前[{}]总人数:{}", appId, sessions.size());
	}
	
	/**
	 * @description
	 *	session 连接关闭
	 * @author TianwYam
	 * @date 2020年9月17日下午3:32:45
	 * @param session
	 * @param appId
	 */
	@OnClose
	public void close(Session session, @PathParam(value = "appId") String appId) {
		
		// 获取此应用下所有连接
		LinkedHashSet<Session> sessions = getSessions(appId);
		if (sessions != null) {
			// 会话session 连接断开
			sessions.remove(session);
			log.info("连接断开,目前[{}]总人数:{}", appId, sessions.size());
		}
		
	}
	
	/**
	 * @description
	 *	接受消息 
	 * @author TianwYam
	 * @date 2020年9月17日下午3:39:22
	 * @param message 消息内容
	 * @param session 会话
	 * @param appId 应用
	 */
	@OnMessage
	public void onMessage(String message, Session session, @PathParam(value = "appId") String appId) {
		
		// 消息处理
		for (AppIdMsgHandle appIdMsgHandle : appIdMsgHandles) {
			if (Objects.equals(appIdMsgHandle.getAppId(), appId)) {
				appIdMsgHandle.handleMsg(message, session);
			}
		}
	}
	
	/**
	 * @description
	 *	连接 报错
	 * @author TianwYam
	 * @date 2020年9月17日下午3:50:12
	 * @param session
	 * @param error
	 */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("websocket发生错误", error);
    }

	/**
	 * @description
	 *	根据应用获取所有在线人员
	 * @author TianwYam
	 * @date 2020年9月17日下午3:17:04
	 * @param appId 应用ID
	 * @return
	 */
	public static LinkedHashSet<Session> getSessions(String appId){
		return APPID_CONNECT_SESSION_MAP.get(appId);
	}
	
	/**
	 * @description
	 *	发送文本消息
	 * @author TianwYam
	 * @date 2020年9月17日下午3:52:52
	 * @param session
	 * @param message
	 */
	public static void sendTxtMsg(Session session , String message) {
		try {
			session.getBasicRemote().sendText(message);
		} catch (IOException e) {
			log.error("websocket发生消息失败", e);
		}
	}



	/**
	 * @description
	 * <pre>
	 *	WebSocket是线程安全的,有用户连接时就会创建一个新的端点实例,
	 *		一个端点只能保证一个线程调用。总结就是,WebSocket是多例的。
	 *	Autowired 是程序启动时就注入进去
	 *	用静态变量来保存消息处理实现类,避免出现空指针异常
	 *	另一种方式就是 使用 ApplicationContext.getBean()的方式也可以获取
	 *	</pre>
	 * @author TianwYam
	 * @date 2020年9月17日下午7:43:13
	 * @param appIdMsgHandles
	 */
	@Autowired
	public void setAppIdMsgHandles(Set<AppIdMsgHandle> appIdMsgHandles) {
		WebSocketServer.appIdMsgHandles = appIdMsgHandles;
	}
}

具体消息处理

用于在不同场景下,实现不同的消息处理机制
消息处理接口

package com.tianya.springboot.websocket.handle;

import javax.websocket.Session;
import org.springframework.stereotype.Service;

/**
 * @description
 *	应用消息处理
 * @author TianwYam
 * @date 2020年9月17日下午4:07:03
 */
@Service
public interface AppIdMsgHandle {

	/**
	 * @description
	 *	消息处理机制
	 * @author TianwYam
	 * @date 2020年9月17日下午4:45:36
	 * @param message
	 * @param session
	 */
	void handleMsg(String message, Session session);
	
	/**
	 * @description
	 *	获取应用ID
	 * @author TianwYam
	 * @date 2020年9月17日下午4:45:48
	 * @return
	 */
	String getAppId() ;
}

用于聊天的:聊天消息处理实现类

package com.tianya.springboot.websocket.handle.impl;

import java.util.LinkedHashSet;
import javax.websocket.Session;
import org.springframework.stereotype.Service;
import com.tianya.springboot.websocket.handle.AppIdMsgHandle;
import com.tianya.springboot.websocket.server.WebSocketServer;

@Service
public class ChatAppIdMsgHandleImpl implements AppIdMsgHandle{
	
	private static final String APP_ID = "chat" ;

	@Override
	public void handleMsg(String message, Session session) {
		
		LinkedHashSet<Session> sessions = WebSocketServer.getSessions(APP_ID);
		if (sessions != null) {
			for (Session otherSession : sessions) {
				// 排除自己
//				if (!Objects.equals(otherSession, session)) {
					WebSocketServer.sendTxtMsg(otherSession, message);
//				}
			}
		}
	}

	@Override
	public String getAppId() {
		return APP_ID;
	}

}

启动类

添加websocket的扫描 自动注解

package com.tianya.springboot.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

@SpringBootApplication
// 添加websocket的扫描 自动注解类
@EnableWebSocket
public class WebSocketApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(WebSocketApplication.class, args);
	}

}

额外websocket配置

若是使用spring boot内置 web容器 tomcat ,则需要添加此类,否则会报错
若是使用外置web容器,则可以不用添加

package com.tianya.springboot.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
	
	/**
	 * @description
	 *	若是使用spring boot内置 web容器 tomcat ,则需要添加此类,否则会报错
	 *	若是使用外置web容器,则可以不用添加
	 * @author TianwYam
	 * @date 2020年9月17日上午11:30:03
	 * @return
	 */
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		return new ServerEndpointExporter();
	}
}

客户端

界面JS

$(function() {

    var websocket;

    // 首先判断是否 支持 WebSocket
    if('WebSocket' in window) {
        websocket = new WebSocket("ws://localhost:8080/ws/chat");
    } else if('MozWebSocket' in window) {
        websocket = new MozWebSocket("ws://localhost:8080/ws/chat");
    } else {
        websocket = new SockJS("http://localhost:8080/ws/chat");
    }
    
    // 打开时
    websocket.onopen = function(event) {
        console.log("  websocket.onopen  ");
        console.log(event);
        $("#msg").append("<p>(<font color='blue'>欢迎加入聊天群</font>)</p>");
    };
    
    // 处理消息时
    websocket.onmessage = function(event) {
    	console.log(event);
        $("#msg").append("<p>(<font color='red'>" + event.data + "</font>)</p>");
        console.log("  websocket.onmessage   ");
    };


    websocket.onerror = function(event) {
        console.log("  websocket.onerror  ");
    };

    websocket.onclose = function(event) {
        console.log("  websocket.onclose  ");
    };


    // 点击了发送消息按钮的响应事件
    $("#TXBTN").click(function(){
    
        // 获取消息内容
        var text = $("#tx").val();
        // 判断
        if(text == null || text == ""){
            alert(" content  can not empty!!");
            return false;
        }

        // 发送消息
        websocket.send(text);
        $("#tx").val('');
    });
});

界面HTML

<!DOCTYPE HTML>
<html>
    <head>
        <title>WebSocket聊天</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="renderer" content="webkit">
        <!-- 引入 JQuery  -->
        <script type="text/javascript" src="js/jquery.min.js"></script>
        <!-- 引入 sockJS  -->
        <script type="text/javascript" src="js/sockjs.min.js" ></script>
        <!-- 自定义JS文件 -->
        <script type="text/javascript" src="js/index.js"></script>
    </head>

    <body>
        <!-- 最外边框 -->
        <div style="margin: 20px auto; border: 1px solid blue; width: 300px; height: 500px;">
            <!-- 消息展示框 -->
            <div id="msg" style="width: 100%; height: 70%; border: 1px solid yellow;overflow: auto;"></div>
            <!-- 消息编辑框 -->
            <textarea id="tx" style="width: 98%; height: 20%;"></textarea>
            <!-- 消息发送按钮 -->
            <button id="TXBTN" style="width: 100%; height: 8%;">发送数据</button>
        </div>
    </body>
</html>

项目结构

Spring Boot + WebSocket实现聊天

聊天效果

用户A:
Spring Boot + WebSocket实现聊天
用户B:
Spring Boot + WebSocket实现聊天

原先也写了一篇关于: Spring+SpringMVC+WebSocket实现聊天
可以访问:Spring+SpringMVC+WebSocket

希望有所帮助到各位同仁,希望能给个star,多谢!

本文项目地址:spring-boot-learn下面的spring-boot-learn-websocket项目
此项目工程是基于maven多模块工程来记录学习springboot的知识的一个过程