上文中,我们已经基本了解了webscoket的原理以及部分api的实现,接下来我们就使用websocket来实现一个简单的聊天室功能。
1. 需求分析
1.1 登陆聊天室:
1.2 登陆成功后与别人进行聊天
-
李四界面:
-
张三界面:
-
在李四页面的好友列表中,点击张三,与之聊天:
-
在张三好友列表页,点击李四,打开对话框,可以接收到李四的消息:
-
互相发送:
2. 实现流程:
(下图中蓝色的@onError应该为@onClose)
3. 消息格式:
4. 导入相关jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
5. 代码实现:
5.1 POJO类:
5.1.1 浏览器发送给服务器的webSocket数据:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String toName;
private String message;
private String fromName;
}
5.1.2 服务端给客户端发送的websocket数据:
@Data
@NoArgsConstructor
@AllArgsConstructor
//用户间传送的消息
public class ResultMessage {
private boolean isSystem;
private String fromName;
//private String toName;
private Object message;
}
5.1.3 用于登陆返回给浏览器的数据
@Data
@AllArgsConstructor
@NoArgsConstructor
//登录时用到的信息
public class Result {
private boolean flag;
private String message;
}
5.2 消息工具类:
//封装发送的消息内容
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,String fromName,Object message){
try{
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystem(isSystemMessage);
resultMessage.setMessage(message);
if(fromName != null){
resultMessage.setFromName(fromName);
}
// if(toName !=null ){
// resultMessage.setToName(toName);
// }
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(resultMessage);
}catch (JsonProcessingException e){
e.printStackTrace();
}
return null;
}
}
5.3 主要前端代码:
var toName;
var username;
//点击好友名称展示相关消息
function showChat(name){
toName = name;
//现在聊天框
$("#content").html("");
$("#content").css("visibility","visible");
// 显示正在和谁聊天
$("#Inchat").html("当前正与"+toName+"聊天");
// 从sessionStorage中获取历史信息
var chatData = sessionStorage.getItem(toName);
if (chatData != null){
// 将聊天记录渲染到聊天区
$("#content").html(chatData);
}
}
$(function () {
$.ajax({
url:"getUsername",
success:function (res) {
username = res;
},
async:false //同步请求,只有上面好了才会接着下面
});
//建立websocket连接
//获取host解决后端获取httpsession的空指针异常
var host = window.location.host;
var ws = new WebSocket("ws://"+host+"/chat");
ws.onopen = function (evt) {
$("#username").html("<h3 style=\"text-align: center;\">用户:"+ username +"<span>-在线</span></h3>");
}
//接受消息
ws.onmessage = function (evt) {
//获取服务端推送的消息
var dataStr = evt.data;
//将dataStr转换为json对象
var res = JSON.parse(dataStr);
//判断是否是系统消息
if(res.system){
//系统消息
//1.好友列表展示
//2.系统广播的展示
//此处声明的变量是调试时命名的,可以直接合并
var names = res.message;
var userlistStr = "";
var broadcastListStr = "";
var temp01 = "<a style=\"text-align: center; display: block;\" οnclick='showChat(\"";
var temp03 = "\")'>";
var temp04 = "</a></br>";
var temp = "";
for (var name of names){
if (name != username){
temp = temp01 + name + temp03 + name + temp04;
userlistStr = userlistStr + temp;
broadcastListStr += "<p style='text-align: center'>"+ name +"上线了</p>";
}
}
//渲染好友列表和系统广播
$("#hyList").html(userlistStr);
$("#xtList").html(broadcastListStr);
} else {
//不是系统消息
// 将服务器推送的消息进行展示
var str = "<span id='mes_left'>"+ res.message +"</span></br>";
if (toName === res.fromName) {
$("#content").append(str);
}
var chatData = sessionStorage.getItem(res.fromName);
if (chatData != null){
str = chatData + str;
}
//保存聊天消息
sessionStorage.setItem(res.fromName,str);
};
}
ws.onclose = function () {
$("#username").html("<h3 style=\"text-align: center;\">用户:"+ username +"<span>-离线</span></h3>");
}
// 给发送按钮绑定单机事件
//发送消息
$("#submit").click(function () {
//1.获取输入的内容
var data = $("#input_text").val();
//2.清空发送框
$("#input_text").val("");
var json = {"toName": toName ,"message": data};
//将自己发送的数据也展示在聊天区
var str = "<span id='mes_right'>"+ data +"</span></br>";
$("#content").append(str);
// 将聊天记录存放到sessionStorage中
var chatData = sessionStorage.getItem(toName);
if (chatData != null){
str = chatData + str;
}
// 存放键和值,和谁的聊天,聊天的内容是什么
sessionStorage.setItem(toName,str);
//3.发送数据
ws.send(JSON.stringify(json));
})
})
5.3 登陆及拦截器实现:
5.3.1 登陆接口:
@RestController
//模拟登录操作
@Slf4j
public class CertificationController {
@RequestMapping("/toLogin")
public Result toLogin(String user, String pwd, HttpSession httpSession){
Result result = new Result();
httpSession.setMaxInactiveInterval(30*60);
log.info(user+"登录验证中..");
if(httpSession.getAttribute("user") != null){
result.setFlag(false);
result.setMessage("不支持一个浏览器登录多个用户!");
return result;
}
if ("张三".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}else if ("李四".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}else if ("田七".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}
else if ("王五".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}else {
result.setFlag(false);
log.info(user+"验证失败");
result.setMessage("登录失败");
}
return result;
}
@RequestMapping("/getUsername")
public String getUsername(HttpSession httpSession){
String username = (String) httpSession.getAttribute("user");
return username;
}
}
5.3.2 拦截器:
//配置拦截路径
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor)
.addPathPatterns("/main");
}
}
@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
//没用数据库,暂且用集合来存储已登录等用户,进行拦截。
public static Map<String, String> onLineUsers = new ConcurrentHashMap<>();;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession httpSession = request.getSession();
String username = (String) httpSession.getAttribute("user");
log.info("进入拦截器"+"==="+"进入拦截器的用户是:"+username);
if(username != null && !onLineUsers.containsKey(username)){
onLineUsers.put(username,username);
log.info("已进入拦截器判断");
log.info("已存储的用户01"+onLineUsers);
return true;
}else {
log.info("已存储的用户02" + onLineUsers);
log.info("未进入判断,进行重定向");
httpSession.removeAttribute("user");
response.sendRedirect("/loginerror");
return false;
}
}
}
5.4 WebSocketConfig配置类:
作用:使使用了@ServerEndpoint注解的bean注册到spring中
@Configuration
//websocket要做的配置类
public class WebSocketConfig {
// 注入ServerEndpointExporter bean对象,可以自动注册使用了@ServerEndpoint注解的bean
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
5.5 从request中获取httpsession对象
//@Configuration
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
//此方法用来获取httpssion对象
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
if(httpSession != null) {
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
}
5.6 聊天室方法:
@Slf4j
// 声明资源路径chat, 让之前编写的插入HttpSession到config的配置文件生效
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
// 交给spring处理,但是还没完,spring并不会创建这个类的对象,需要加上spring对endpoint支持,
// 我们可以添加一个configuration的配置类,注入一个ServerEndpointExporter的bean对象,可以自动注册使用了@ServerEndpoint注解的bean
@Component
public class ChatEndPoint {
//用线程安全的map来保存每个对象对应的EndPoint对象
private static Map<String, ChatEndPoint> onLineUsers = new ConcurrentHashMap<>();
//声明一个session对象,通过该对象可以发送消息给指定用户,不能设置为静态,每个ChatEndPoint有一个session才能区分.(websocket的session)
private Session session;
//保存当前登录浏览器的用户
private HttpSession httpSession;
//建立连接时发送系统广播
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 将局部的session对象赋值给成员session
// 因为websocket涉及到多例,也就是多个线程调用此方法,所以使用this关键字来确保是同一个客户端在操作
this.session = session;
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
String username = (String) httpSession.getAttribute("user");
log.info("上线用户名称:" + username);
// 将当前成功登陆的用户存储到容器中
onLineUsers.put(username, this);
// 将当前在线用户的用户名推送给所有的客户端
// 1. 获取消息
String message = MessageUtils.getMessage(true, null, getNames());
// 2. 调用方法进行消息的推送
broadcastAllUsers(message);
}
//获取当前登录的用户
private Set<String> getNames() {
return onLineUsers.keySet();
}
//发送系统消息
private void broadcastAllUsers(String message) {
try {
// 需要将消息推送给所有在线用户
Set<String> names = onLineUsers.keySet();
for (String name : names) {
ChatEndPoint chatEndPoint = onLineUsers.get(name);
chatEndPoint.session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//用户之间的信息发送
@OnMessage
// 接收到客户端发送的数据时调用
public void onMessage(String message, Session session) {
try {
// 将string类型的message转换为message对象
ObjectMapper mapper = new ObjectMapper();
Message mess = mapper.readValue(message, Message.class);
// 找到服务器接收到的消息的客户端接收方(也就是找出消息是发送给谁的,消息接收方)
String toName = mess.getToName();
// 获取消息数据
String data = mess.getMessage();
// 获取当前当前登陆用户(消息发送方)
String username = (String) httpSession.getAttribute("user");
log.info(username + "向" + toName + "发送的消息:" + data);
// 格式化需要发送的消息和接收人
String resultMessage = MessageUtils.getMessage(false, username, data);
if (StringUtils.hasLength(toName)) {
// 发送数据
onLineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//用户断开连接的断后操作
@OnClose
public void onClose(Session session) {
String username = (String) httpSession.getAttribute("user");
log.info("离线用户:" + username);
if (username != null) {
onLineUsers.remove(username);
UserInterceptor.onLineUsers.remove(username);
}
httpSession.removeAttribute("user");
String message = MessageUtils.getMessage(true, null, getNames());
broadcastAllUsers(message);
}
}
至此,使用原生websocket实现一个聊天窗完成,接下来,将使用websocket+stomp实现群聊的功能