java SSM项目中用WebSocket 实现聊天功能
前言
当我们开发的javaweb项目中需要实现聊天功能时,可以用HTML5的WebSocket协议来实现。在实现功能之前,先来看一下WebSocket的基本知识,加深对它的认识后面才能更好的理解。
(一) WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。(相比于其他推送技术,WebSocket 协议能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。)
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,附加信息如图所示(看看就行):
(二) 连接过程
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当获取 Web Socket 的连接后,就可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
连接语法:var Socket = new WebSocket(url, [protocol] );
url是必须的,指定连接的url。 protocol是可选的,代表可接受的子协议。
(三) WebSocket的属性、事件、方法
1、WebSocket的属性
属性 | 描述 |
---|---|
Socket.readyState | 只读属性 readyState 表示连接状态,可以是以下值: |
0 - 表示连接尚未建立。 | |
1 - 表示连接已建立,可以进行通信。 | |
2 - 表示连接正在进行关闭。 | |
3 - 表示连接已经关闭或者连接不能打开。 |
2、WebSocket的事件(这里的Socket.事件名的Socket是js中new WebSocket(url)创建的对象)
事件 | 事件处理程序(写在js中) | 描述 |
---|---|---|
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
3、WebSocket的方法
方法 | 描述 |
---|---|
Socket.send() | 使用连接发送数据 |
Socket.close() | 关闭连接 |
正文
好了,看到这里,对WebSocket也有了一定的了解。接下来,开始用WebSocket实现聊天。
开发环境:Eclipse、java version “10.0.2”
开发语言:java、javascript、html
框架:ssm框架 maven本地仓库
开发目标:实现用户之间的及时通讯(不包含数据库)
开发步骤
1、首先引入WebSocket的库文件
在maven的pom文件中引入依赖(如果没用maven,则需要自己去网上下载这个jar文件):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
2、设计思想
用户通过账号密码登录,登录之后通过WebSocket的onopen事件自动连接(相当于进入聊天室)。后台通过Map来记录用户连接情况。
进入聊天室之后,选择聊天对象,在input标签中输入文字,然后点击发送按钮,js响应按钮点击事件,把有关的各种JSON信息转换成字符串通过WebSocket的send()方法发送到后台服务器。
服务器接收到用户发送来的信息,解析之后转发给另一个用户,另一个用户通过WebSocket的onmessage事件接收消息。
至此,聊天功能实现。
3、后台代码(本人第一次编写WebSocket代码,所以直接写在Controller层了)
package cn.sx.handler;
/*
*作者:Ji Pengjie
*日期:2021年5月3日
*时间:下午4:10:05
**/
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.json.Json;
import javax.json.JsonObject;
import javax.security.auth.message.callback.PrivateKeyCallback.Request;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.socket.server.standard.SpringConfigurator;
import com.alibaba.fastjson.JSONObject;
//websocket连接URL地址和可被调用配置 @ServerEndpoint这个注解用来标记一个类是 WebSocket 的处理器。
@ServerEndpoint(value="/websocketDemo/{fromId}",configurator = SpringConfigurator.class)
public class WebsocketDemo {
//日志记录
private Logger logger = LoggerFactory.getLogger(WebsocketDemo.class);
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//记录每个用户下多个终端的连接
public static Map<String, Set<WebsocketDemo>> userSocket = new HashMap<>();
//需要session来对用户发送数据, 获取连接特征fromId
private Session session;
private String fromId;
private String toId;
/**
* @Title: onOpen
* @Description: websocekt连接建立时的操作
* @param @param fromId 用户id
* @param @param session websocket连接的session属性
* @param @throws IOException
*/
@OnOpen
public void onOpen(@PathParam("fromId") String fromId, Session session) throws IOException{
this.session = session;
this.fromId = fromId;
onlineCount++;
//根据该用户当前是否已经在别的终端登录进行添加操作
if (userSocket.containsKey(this.fromId)) {
logger.debug("当前用户id:{}已有其他终端登录",this.fromId);
userSocket.get(this.fromId).add(this); //增加该用户set中的连接实例
}else {
logger.debug("当前用户id:{}第一个终端登录",this.fromId);
Set<WebsocketDemo> addUserSet = new HashSet<>();
addUserSet.add(this);
userSocket.put(this.fromId, addUserSet);
}
logger.debug("用户{}登录的终端个数是为{}",fromId,userSocket.get(this.fromId).size());
logger.debug("当前在线用户数为:{},所有终端个数为:{}",userSocket.size(),onlineCount);
}
/**
* @Title: onClose
* @Description: 连接关闭的操作
*/
@OnClose
public void onClose(){
//移除当前用户终端登录的websocket信息,如果该用户的所有终端都下线了,则删除该用户的记录
if (userSocket.get(this.fromId).size() == 0) {
userSocket.remove(this.fromId);
}else{
userSocket.get(this.fromId).remove(this);
}
logger.debug("用户{}退出连接!",this.fromId);
logger.debug("用户{}登录的终端个数是为{}",this.fromId,userSocket.get(this.fromId).size());
userSocket.remove(this.fromId);
logger.debug("当前在线用户数为:{},所有终端个数为:{}",userSocket.size(),--onlineCount);
}
/**
* @Title: onMessage
* @Description: 收到消息后的操作
* @param @param message 收到的消息
* @param @param session 该连接的session属性
*/
@OnMessage
public void onMessage(String message, Session session) {
logger.debug("收到来自用户id为:{}的消息:{}",this.fromId,message);
JSONObject jsonObject = JSONObject.parseObject(message);//将前台转来的字符串转为json对象
this.toId = jsonObject.getString("toId");
if(userSocket.containsKey(toId)) {
sendMessageToUser(toId, message);
}else {
logger.debug("用户{}不在线!", toId);
}
if(session ==null) logger.debug("session null");
}
/**
* @Title: onError
* @Description: 连接发生错误时候的操作
* @param @param session 该连接的session
* @param @param error 发生的错误
*/
@OnError
public void onError(Session session, Throwable error){
logger.debug("用户id为:{}的连接发送错误",this.fromId);
error.printStackTrace();
}
/**
* @Title: sendMessageToUser
* @Description: 发送消息给用户下的所有终端
* @param @param fromId 用户id
* @param @param message 发送的消息
* @param @return 发送成功返回true,反则返回false
*/
public Boolean sendMessageToUser(String toId,String message){
if (userSocket.containsKey(toId)) {
logger.debug(" 给用户id为:{}的所有终端发送消息:{}",toId,message);
for (WebsocketDemo WS : userSocket.get(toId)) {
logger.debug("sessionId为:{}",WS.session.getId());
try {
WS.session.getBasicRemote().sendText(message);//将消息发送给对方 getBasicRemote().sendText(message) 同步发送
} catch (IOException e) {
e.printStackTrace();
logger.debug(" 给用户id为:{}发送消息失败",toId);
return false;
}
}
return true;
}
logger.debug("发送错误:当前连接不包含id为:{}的用户",toId);
return false;
}
}
解释:
声明websocket地址类似Spring MVC中的@controller注解类似,websocket使用@ServerEndpoint来进行声明接口:@ServerEndpoint(value="/websocket/{paraName}") ; 其中 “ { } ”用来表示带参数的连接,如果需要获取{}中的参数,在接收参数时使用注解:@PathParam(“paraName”)
4、前端连接代码
<div id="body1" >
<div class="page-header" id="tou">
webSocket多终端聊天测试
</div>
<div class="well" id="msg"></div>
<div class="col-lg">
<div class="input-group">
<textarea class="form-control" placeholder="发送信息..." id="message"></textarea>
<!-- <input type="text" class="form-control" placeholder="发送信息..." id="message"> -->
</div>
<div class="form-control1">
<span class="float_r">
<button class="span_b" type="button" id="send">发送</button>
<button class="span_b" type="button" id="close">关闭</button>
</span>
</div>
</div>
</div>
<script type="text/javascript">
var tid;//全局变量
var status = 0;//接收消息提醒标志,初始化为0,当收到消息并查看时设置为1
//jquery代码当页面DOM树加载完毕之后触发
$(function() {
var websocket;//创建对象,用户连接WebSocket
if('WebSocket' in window) {
console.log("此浏览器支持websocket");
websocket = new WebSocket("ws://127.0.0.1:3311/recruitment/websocketDemo/${session.username}");
} else if('MozWebSocket' in window) {
alert("此浏览器只支持MozWebSocket");
} else {
alert("此浏览器只支持SockJS");
}
//连接之后,WebSocket的事件
websocket.onopen = function(evnt) {
$("#msg_a").css("pointer-events","none"); //阻止点击产生效果
/* var str = location.href;返回当前显示的文档的完整 URL。
if(str.split('?')[1]){
tid = str.split('?')[1].split('=')[1];//分割字符串,返回字符串数组
$("#tou").html("链接通道成功!开始与用户" + tid + "聊天!")
} */
};
//接收消息的WebSocket事件
websocket.onmessage = function(evnt) {
var message = JSON.parse(evnt.data);//将数据解析成JSON形式
var fromId = message.fromId;//对方用户的账号
var toId = message.toId;//自己的账号
var time = new Date(message.date);//对方发送信息时间
var timeStr = time.toLocaleTimeString();
if(status == 0){//这个可以不用实现也行
alert("收到来自用户 " + fromId + "的消息")
var msgg = document.getElementById("img_a");
msgg.src = "./imgs/msg2.png";
var msg1 = document.getElementById("msg_a");
msg1.href = "#?id=" + fromId;
$("#msg_a").css("pointer-events","auto"); //阻止a标签点击产生效果
}
var text = message.text;//获取对方发送来的文本消息
$("#msg").html($("#msg").html() + "<br/>" + timeStr + " "+ fromId + " " + "<br/>" + text);//将信息显示在div页面
scroll();
};
//发生错误时,WebSocket的处理事件
websocket.onerror = function(evnt) {};
//关闭连接时,WebSocket的处理事件
websocket.onclose = function(evnt) {
$("#tou").html("与服务器断开了链接!")
};
$(".imga").click(function (){
status = 1;
});
//点击发送按钮,响应。
$('#send').bind('click', function() {
var str = location.href;返回当前显示的文档的完整 URL,里面包含着要发送给谁(对方)的id
if(str.split('?')[1]){
tid = str.split('?')[1].split('=')[1];//分割字符串,返回字符串数组
//把自己的username写进去,然后调用send自定义js函数
send('${session.username}');
$("#tou").html("链接通道成功!开始与用户" + tid + "聊天!")
}
});
function send(id) {
if(websocket != null) {
//json字符串
var msg = {
fromId: id,//自己的id
text: document.getElementById("message").value,//自己要发送的消息
toId: tid,//对方的id
date: Date.now()
};
/* var message = document.getElementById('message').value; */
//调用WebSocket的send()方法,发送数据给服务器
websocket.send(JSON.stringify(msg));//把json数组转换成字符串传到后台
var div = document.getElementById('msg');
//把自己发送的信息显示在div上
div.innerHTML = div.innerHTML + "<br/>" +new Date(msg.date).toLocaleTimeString() + " " + msg.fromId + " " + "<br/>" + msg.text;
document.getElementById('message').value = "";
scroll();
} else {
alert('未与服务器链接.');
}
}
$("#close").click(function (){
status = 0;
var msg = document.getElementById("img_a");
msg.src = "./imgs/msg1.png";
location.href = location.href.split('#')[0];//设置当前页面的url href地址
$("#msg_a").css("pointer-events","none"); //阻止点击产生效果
});
//点击退出按钮,退出当前账号,并关闭连接
$("#exit").click(function (){
close();
});
function close() {
websocket.close();
}
});
</script>
解释:
websocket = new WebSocket(“ws://127.0.0.1:3311/recruitment/websocketDemo/${session.username}”);
ws://本机地址:端口号/项目名/Websocket的处理器/{参数}
最后两个参数 要和WebSocket处理器注解@ServerEndpoint中的value值一样,参数直接传值就行,多个参数可以用/分割开
5、实现界面
首先登录
登录之后日志
开始发送消息
另一边收到消息通知
参考:
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications#closing_the_connection
https://www.jianshu.com/p/3398d0230e5f
https://www.runoob.com/html/html5-websocket.html