WebSocket
以及在SSM
中的简单应用
WebSocket
是为了解决由于http
协议只能由客户端发起,服务端无法直接进行推送,导致服务端有持续的变化客户端想要获知就比较麻烦的问题。
WebSocket
协议是基于TCP的一种网络协议,它实现了浏览器与服务器全双工通信,客户端和服务端都可以主动的推送消息,可以是文本也可以是二进制数据,而且没有同源策略的限制,不存在跨域问题。
这个过程可以描述为,在webscoket
协议中, client
利用http
来建立TCP连接, 建立TCP连接之后, client
与server
就可以基于TCP连接来通信。
WebSocket
的应用有很多,比如消息推送,实况更新,即时通信等。这里实现一个简单的网页版即时通信功能。
以下配置和一些简单的接口是在之前的工程上添加,这里仅展示SSM
结合WebSocket
实现即时通信的功能的增加的部分。
添加依赖
<!-- 一下是Websocket需要配置的所有依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</versi
</dependency>
web.xml
中对SpringMVC
的配置
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置springMVC需要加载的配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<!-- 匹配所有请求,此处也可以配置成 / 形式 -->
<url-pattern>*.do</url-pattern>
</servlet-mapping>
创建spring-websocket.xml
用于spring
对WebSocket
的配置,这里是直接扫描注解来实现(WebSocket的三个Java文件均需置于这里配置的包下),当然也可以配置bean来实现,注释部分即为配置bean的样例
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!-- websocket处理类-->
<!--<bean id="myHandler" class="com.cloneZjrt.webSocket.MyWebSocketHander"/>-->
<!--握手接口/拦截器-->
<!--<bean id="myInterceptor" class="com.cloneZjrt.webSocket.MyHandShakeInterceptor"/>-->
<!--<websocket:handlers>-->
<!--<websocket:mapping path="/websocket" handler="myHandler"/>-->
<!--<websocket:handshake-interceptors >-->
<!--<ref bean="myInterceptor"/>-->
<!--</websocket:handshake-interceptors>-->
<!--</websocket:handlers>-->
<!-- 扫描webSocket包下所有使用注解的类型 -->
<context:component-scan base-package="com.cloneZjrt.webSocket"/>
<!-- 配置jsp 显示ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
在ApplicationContext.xml中注入spring-websocket.xml,使web.xml可以扫描到spring-websocket.xml
<import resource="classpath*:/spring/spring-websocket.xml"/>
WebSocket
拦截器
/**
* websocket拦截器
* 拦截握手前,握手后的两个切面
* 目前暂做简单的拦截
*/
@Component
public class MyHandShakeInterceptor implements HandshakeInterceptor {
/**
* 握手之前,若返回false,则不建立链接
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse,
WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
System.out.println("=====!!!!");
if(serverHttpRequest instanceof ServletServerHttpRequest){
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) serverHttpRequest;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if(null != session.getAttribute("user")){
UserEntity user = (UserEntity) session.getAttribute("user");
map.put("ws_user", user);
System.out.println(user);
}else{
System.out.println("握手=======失败");
return false;
}
}
System.out.println("--------------握手开始...");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("--------------握手成功啦...");
}
}
spring中对WebSocket
的注解配置(这里的注释对应xml文件中的bean配置)
@Component
@EnableWebSocket
//@Configuration
public class MyWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Autowired
private MyWebSocketHander myWebSocketHander;
// websocket 处理类
// @Bean
// public MyWebSocketHander myWebSocketHandler(){
// return new MyWebSocketHander();
// }
private static final String LINK_URI = "websocket.do";
//添加websocket处理器,添加握手拦截器 拦截器先执行 然后到处理器
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(myWebSocketHander,LINK_URI).addInterceptors(new MyHandShakeInterceptor());
}
}
创建MyWebSocketHander
类继承WebSocketHandler
类,用来处理消息的接收和发送
@Component
public class MyWebSocketHander implements WebSocketHandler {
//在线用户的SOCKETsession(存储了所有的通信通道)
public static final Map<UserEntity, WebSocketSession> USER_SOCKETSESSION_MAP;
//存储所有的在线用户
static {
USER_SOCKETSESSION_MAP = new HashMap<UserEntity, WebSocketSession>();
}
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
//这里的"ws_user"对应HandshakeInterceptor中配置的
UserEntity user = (UserEntity) webSocketSession.getAttributes().get("ws_user");
USER_SOCKETSESSION_MAP.put(user,webSocketSession);
//这里传到前端的应该是JSON格式
String messageFormat = "{onlineNum:\"%d\",userName:\"%s\" , msgTyp " + ":\"%s\"}";
String msg = String.format(messageFormat, USER_SOCKETSESSION_MAP.size(), USER_SOCKETSESSION_MAP.keySet().toString(), "notice");
TextMessage testMsg = new TextMessage(msg + "");
sendMessageToAll(testMsg);
}
/**
* 群发信息:给所有在线用户发送消息
* @param message
*/
private void sendMessageToAll(final TextMessage message){
//对用户发送的消息内容进行转义
//获取到所有在线用户的SocketSession对象
Set<Map.Entry<UserEntity, WebSocketSession>> entrySet = USER_SOCKETSESSION_MAP.entrySet();
for (Map.Entry<UserEntity, WebSocketSession> entry : entrySet) {
//某用户的WebSocketSession
final WebSocketSession webSocketSession = entry.getValue();
//判断连接是否仍然打开的
if(webSocketSession.isOpen()){
//开启多线程发送消息(效率高)
new Thread(new Runnable() {
public void run() {
try {
if (webSocketSession.isOpen()) {
webSocketSession.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
/**
*
* 说明:给某个人发信息
* @param UserEntity
* @param message
*/
private void sendMessageToUser(UserEntity UserEntity, TextMessage message) throws IOException{
//获取到要接收消息的用户的session
WebSocketSession webSocketSession = USER_SOCKETSESSION_MAP.get(UserEntity);
if (webSocketSession != null && webSocketSession.isOpen()) {
//发送消息
webSocketSession.sendMessage(message);
}
}
/**
* 客户端发送服务器的消息时的处理函数,在这里收到消息之后可以分发消息
*/
@Autowired
private MessageService messageService;
@Autowired
private UserService userService;
@Override
public void handleMessage(WebSocketSession webSocketSession,
WebSocketMessage<?> webSocketMessage) throws Exception {
String messageFormat = null;
FileOutputStream output;
System.out.println(webSocketSession.getAttributes().get("ws_user"));
//发送消息的时间
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String sendMsgDate = dateFormat.format(new Date());
UserEntity user = (UserEntity) webSocketSession.getAttributes().get("ws_user");
String msgContent = webSocketMessage.getPayload() + "";
System.out.println("前端传到后台的数据 " + msgContent);
JSONObject chat = JSON.parseObject(msgContent);
//消息的内容
String msgJSON = chat.get("message").toString();
//消息接收者名字
String receiveUserName = chat.get("receive").toString();
//消息的样式
String msgJSONType = chat.get("type").toString();
String exit = "exit";
String MessageEntity = "MessageEntity";
String img = "img";
System.out.println("JSON验证" + chat);
System.out.println(chat.get("type").toString());
if (msgJSONType.equals(exit)) {
messageFormat = "{onlineNum:\"%d\",userName:\"%s\" ,userNameList:\"%s\", msgTyp:\"%s\"}";
//从用户列表中移除已退出的用户
USER_SOCKETSESSION_MAP.remove(user);
String msg = String.format(messageFormat, USER_SOCKETSESSION_MAP.size()-1 ,msgJSON ,USER_SOCKETSESSION_MAP.keySet().toString(),"exit");
TextMessage testMsg = new TextMessage(msg + "");
sendMessageToAll(testMsg);
} else if (msgJSONType.equals(MessageEntity)) {
MessageEntity messageEntity = new MessageEntity(user.getUserId(), sendMsgDate, msgContent, user.getUserId(), "msg");
messageService.addMessage(messageEntity);
messageFormat = "{UserEntity:\"%s\",sendDate:\"%s\" ," + "sendContent:\"%s\" , msgTyp :\"%s\"}";
if(!receiveUserName.equals("") || receiveUserName!=null){
String message = String.format(messageFormat, user.getUserName(), sendMsgDate, msgJSON , "msg");
TextMessage testMsg = new TextMessage(message + "");
sendMessageToUser(userService.getUserByName(receiveUserName),testMsg);
if(!receiveUserName.equals(user.getUserName())){
sendMessageToUser(user,testMsg);
}
}else {
String message = String.format(messageFormat, user.getUserName(), sendMsgDate, msgJSON , "msg");
TextMessage testMsg = new TextMessage(message + "");
//确保每个用户信息都能同步到
sendMessageToAll(testMsg);
}
}else if(msgJSONType.equals(img)){
System.out.println("send pic");
//设置图片保存路径
output = new FileOutputStream(new File("D:\\images\\"+chat.get("filename").toString().split(":")[0]));
System.out.println("图片路径"+"D:\\images\\"+chat.get("filename").toString().split(":")[0]);
output.close();
}
}
@Override
public void handleTransportError(WebSocketSession webSocketSession,
Throwable throwable) throws Exception {
// 记录日志,准备关闭连接
System.out.println("Websocket异常断开:" + webSocketSession.getId() + "已经关闭");
//一旦发生异常,强制用户下线,关闭session
if (webSocketSession.isOpen()) {
webSocketSession.close();
}
UserEntity user = (UserEntity) webSocketSession.getAttributes().get("ws_user");
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String sendMsgDate = dateFormat.format(new Date());
String messageFormat = "{UserEntity:\"%s\",sendDate:\"%s\" ," + "sendContent:\"%s\" , msgTyp :\"%s\"}";
String message = String.format(messageFormat, user.getUserName(), sendMsgDate, "万众瞩目的【"+user.getUserName()+"】退出了群聊!" , "msg");
System.out.println(message);
TextMessage testMsg = new TextMessage(message + "");
//确保每个用户信息都能同步到
sendMessageToAll(testMsg);
Collection<WebSocketSession> values = USER_SOCKETSESSION_MAP.values();
values.remove(webSocketSession);
}
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession,
CloseStatus closeStatus) throws Exception {
// 记录日志,准备关闭连接
System.out.println("Websocket正常断开:" + webSocketSession.getId() + "已经关闭");
UserEntity userRemove = (UserEntity) webSocketSession.getAttributes().get("ws_user");
USER_SOCKETSESSION_MAP.remove(userRemove);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
之后是操作层
@Controller
@RequestMapping("/user")
@ResponseBody
public class UserController {
@Resource
private UserService userService;
@RequestMapping("getUser")
public @ResponseBody UserEntity getUser(Long userId) {
return userService.getUserById(userId);
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "login";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ModelAndView loginUser(String userName, HttpSession session) {
System.out.println(userName);
if(userService.getUserByName(userName) == null){
try{
userService.register(new UserEntity(userName));
}catch (Exception e){
System.out.println(e.getMessage());
}
}
UserEntity user = userService.getUserByName(userName);
session.setAttribute("user",user);
return new ModelAndView("chat");
}
}
JSP页面
login.jsp
用于用户登陆
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%String path = request.getContextPath();%>
<html>
<link rel="stylesheet" href="static/css/bootstrap.min.css" type="text/css">
<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.min.js" type="text/javascript"></script>
<body>
<div class="container">
<form action="user/login.do" method="post" name="loginForm" onsubmit="return validateForm()">
<div class="form-group col-sm-8">
<label for="user">用户名</label>
<input type="text" class="form-control" name="userName" maxlength="10" id="user"><br/>
<p id="uNClass"></p>
</div>
<div id="buttonBox">
<input type="submit" value="登录" class="btn btn-info btn-lg col-sm-2" style="margin-right: 3%;">
</div>
</form>
</div>
</body>
<script type="text/javascript">
function validateForm() {
var name = document.forms["loginForm"]["userName"].value;
if (name == null || name == "") {
text = "输入不能为空";
document.getElementById("uNClass").innerHTML = text;
return false;
}
}
</script>
</html>
chat.jsp聊天界面
<%--
Created by IntelliJ IDEA.
User: 98031
Date: 2018/10/1
Time: 12:24
To change this template use File | Settings | File Templates.
--%>
<!--
* 客户端连接服务端websocket
* 并且订阅一系列频道,用来接收不同种类的消息
* /app/chat/participants :当前在线人数的消息,只会接收一次
* /topic/login : 新登录用户的消息
* /topic/chat/message : 聊天内容消息
* /topic/logout : 用户离线的消息
* 服务器发回json实例 {"userName":"chris","sendDate":1494664021793,"content":"hello"}
-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% String path = request.getContextPath();
String socketPath = request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<html>
<head>
<title>Start chatting now</title>
</head>
<link rel="stylesheet" href="<%=path%>/static/css/bootstrap.min.css"
type="text/css">
<link rel="stylesheet" href="<%=path%>/static/css/chat.css" type="text/css">
<script src="<%=path%>/static/js/jquery.min.js"></script>
<script src="<%=path%>/static/js/bootstrap.min.js"
type="text/javascript"></script>
<body>
<div id="container" style="width:500px">
<div id="header">
<h1 id="title">chat-room</h1></div>
<div class="middle">
<div id="menu">
<!-- <b id="pOnline">在线人数<span>0</span></b> -->
<p id="tou">欢迎来到聊天室</p>
<br>在线用户人数<br>
<p id="onlineNum">0</p>
<p>当前在线用户</p>
<div id="onlineUser"></div>
</div>
<div class="chatter" id="chatter">
<p id="msg"></p>
</div>
</div>
<div id="content">
<textarea class="form-control" rows="3" placeholder="我想说....."
id="msgContent">
</textarea>
</div>
<div style="background-color: #F8F8F8;">
<div id="buttons">
<button id="butSent" type="button" class="btn btn-default"
onclick="getConnection()">连接
</button>
<button type="button" class="btn btn-default"
onclick="sendMsgClose()">断开
</button>
<button type="button" class="btn btn-default" onclick="sendMsg()">
发送
</button>
<button type="button" class="btn btn-default" onclick="clearMsg()">
清屏
</button>
<button type="button" class="btn btn-default" data-toggle="modal"
data-target="#imgModal">
上传图片
</button>
<input type="text" name="receiveUserName" id="receiveUserName" value=""><br/>
<input type="file" name="userImage" id="userImage1"
onchange="check()" placeholder="请选择要上传的图片">
<button type="button" class="btn btn-default"
onclick="sendImg()">
发送图片
</button>
<button type="button" class="btn btn-default"
onclick="location.href='http://localhost:8080/chatroom/chatHistory.jsp'">
消息记录
</button>
</div>
</div>
<div id="footer">
Designed by Annie
</div>
</div>
</div>
</body>
<script>
var wsServer = null;
wsServer = "ws://" + location.host + "${pageContext.request.contextPath}" + "/websocket.do";
var websocket = null;
//从后台接受聊天消息,并展示到前端
function showChat(evnt) {
var message = eval("(" + evnt.data + ")");
var msg = $("#msg");
//msg.html是之前的聊天内容,空一行
msg.html(msg.html() + "<br/>" + "用户: " + message.user + " 发送时间:" + message.sendDate + "<br/>" + message.sendContent);
}
//打开链接
function getConnection() {
if (websocket == null) {
websocket = new WebSocket(wsServer);
websocket.onopen = function (evnt) {
alert("链接服务器成功!");
};
websocket.onmessage = function (evnt) {
var onlineUser = $("#onlineUser");
var message = eval("(" + evnt.data + ")");
//显示在线人数及在线用户
if (message.msgTyp === "notice") {
var htmlOnline;
$("#onlineNum").text(message.onlineNum);
htmlOnline = "<p> " + message.userName + " </p>";
//实时更新在线用户
onlineUser.html("");
$(onlineUser).append(htmlOnline);
} else if (message.msgTyp === "msg") {
showChat(evnt);
} else if (message.msgTyp === "exit") {
$("#onlineNum").text(message.onlineNum);
var msg = $("#msg");
//msg.html是之前的聊天内容,空一行
msg.html(msg.html() + "<br/>" + "用户: " + message.userName + "退出聊天");
}
};
websocket.onerror = function (evnt) {
alert("发生错误,与服务器断开了链接!")
};
websocket.onclose = function (evnt) {
alert("与服务器断开了链接!")
};
$('#send').bind('click', function () {
send();
});
} else {
alert("连接已存在!")
}
}
function closeConnection() {
if (websocket != null) {
websocket.close();
websocket = null;
alert("已经关闭连接")
} else {
alert("未开启连接")
}
}
function sendMsgClose() {
var closeMsg = "${user.userName}";
websocket.send(JSON.stringify({
message: closeMsg,
type: "exit"
}));
websocket.onmessage = function (evnt) {
var onlineUser = $("#onlineUser");
var message = eval("(" + evnt.data + ")");
//显示在线人数及在线用户
if (message.msgTyp === "exit") {
$("#onlineNum").text(message.onlineNum);
var msg = $("#msg");
//msg.html是之前的聊天内容,空一行
msg.html(msg.html() + "<br/>" + "用户: " + message.userName + "退出聊天");
htmlOnline = "<p> " + message.userNameList + " </p>";
onlineUser.html("");
$(onlineUser).append(htmlOnline);
closeConnection();
}
}
}
function sendImg() {
var inputElement = document.getElementById("userImage1");
var fileList = inputElement.files;
var file = fileList[0];
if (!file) return;
var reader = new FileReader();
//以二进制形式读取文件
reader.readAsArrayBuffer(file);
//文件读取完毕后该函数响应
reader.onload = function loaded() {
var blob = document.getElementById("userImage1").files[0];
//发送二进制表示的文件
websocket.send(JSON.stringify({
message: blob,
filename: file.name,
type: "img"
}));
inputElement.outerHTML = inputElement.outerHTML; //清空<input type="file">的值
}
}
function sendMsg() {
var msg = $("#msgContent");
var receiveUserName = document.getElementById("receiveUserName").value;
if (websocket == null) {
alert("连接未开启!");
return;
}
var message = msg.val();
//输入完成后,清空输入区
msg.val("");
if (message == null || message === "") {
alert("输入不能为空的哦");
return;
}
//向后台MyWebSocketHandler中的handlemessage发送信息
websocket.send(JSON.stringify({
message: message,
type: "chatMsg",
receive: receiveUserName,
}));
}
//清屏函数
function clearMsg() {
$("#chatter").html("");
}
</script>
</html>
这样,大致的聊天室就完成了
代码参考自博文:SSM+WebSocket实现一个简易网页版通讯工具
博客中提供源码,可下载尝试运行