本篇主要讲websocket的服务端实现,主要基于Tomcat9.0,关于登录,用户列表下篇再讲,
前端可以借鉴上篇文章,地址如下:
自己动手实现基于websocket的聊天web聊天功能高仿仿qq
基于Tomcat9的websocket服务实现
首先创建一个javaweb项目(此处以eclipse开发工具为例)

然后选中项目build path

假如tomcat的依赖jar包


websocket-api.jar
tomcat-websocket.jar
这样就可以了
下面是websocket的简单的实现:
此处“imchat”为访问路径,“{id}”为需传递的参数,是OnOpen时接收的参数这两处变量名需要写一样,如果是多参数可以后面继续追加如/imchat/{id}/{name}
@ServerEndpoint(value="/imchat/{id}")
public class ImSocket {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<ImSocket> webSocketSet = new CopyOnWriteArraySet<ImSocket>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接建立成功调用的方法,只在建立连接时调用
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(@PathParam("id") String id,Session session){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法,连接后所有交互数据都在此处理
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
//群发消息
for(ImSocket item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
ImSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
ImSocket.onlineCount--;
}
}
前端测试代码
<!DOCTYPE html>
<html>
<head>
<title>Java后端WebSocket的Tomcat实现</title>
</head>
<body>
Welcome<br/><input id="text" type="text"/>
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr/>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if (’WebSocket’ in window) {
websocket = new WebSocket("ws://localhost:8080/项目名/imchat/id123");
}
else {
alert(’当前浏览器 Not support websocket’)
}
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById(’message’).innerHTML += innerHTML + ’<br/>’;
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = document.getElementById(’text’).value;
websocket.send(message);
}
</script>
</html>
以上是简单的websocket后端与前端的连接与交互,以上测试通过后咱们写一下我们聊天的简单实现
基于上面的认识我们已经能够实现简单的前端与websocket服务端的交互,下面我们接着上篇的自己动手实现基于websocket的聊天web聊天功能高仿仿qq
首先我们看一下qq的逻辑,简单的讲就这 三步,登录---》获取好友列表---》发送消息给好友;
第一步登录和获取好友列表,这个我们有两种方式去实现,一种是http请求,一种是websocket去实现,考虑到这样请求websocket会使处理流程变的复杂,所以我们采用http的方式实现,这样我们得websocket主要用来处理消息转发 服务,
首先我们创建一个实体作为消息的承载体
public class ImMsgModel {
private boolean system;//消息类型
private String key;//事件类型// offline离线消息 online在线消息
private String avatar;
private String id;
private String sign;
private String status;
private String username;
private String name;
private String type;
private String content;
private long timestamp;
private String fromid;
public boolean isSystem() {
return system;
}
public void setSystem(boolean system) {
this.system = system;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public String getFromid() {
return fromid;
}
public void setFromid(String fromid) {
this.fromid = fromid;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getAvatar() {
return avatar;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getSign() {
return sign;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setType(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
第二步我们对websocket进行封装,此处消息实体根据layim前端封装,需要了解详情的请移步layui官网访问layim模块
@ServerEndpoint(value="/imchat/{id}")
public class WebsocketsListener {
private static final Set<WebsocketsListener> connections = new CopyOnWriteArraySet<WebsocketsListener>();
private Session session;
private String userid;
Logger log = null;
public WebsocketsListener() {
log = Logger.getGlobal();
}
@OnOpen
public void start(@PathParam(value="id") String id,Session session) {
// TODO Auto-generated method stub
//log.log(Level.INFO, "打开监听onOpen");
this.session=session;
this.userid=id;
connections.add(this);
// Redis.use().hmset("userid="+id, hash);
System.out.println(id+"用户session:"+session);
}
@OnMessage
public void incoming(String message) {
log.info("------------------"+message);
try {
ImMsgModel m = JSON.parseObject(message, ImMsgModel.class);
System.out.println(m.getFromid()+"发送给"+m.getId());
Session s = getSessionByID(m.getId());//接收者id
//消息接收方掉线
if(s==null){
Session s1 = getSessionByID(m.getFromid());
//发送方也不在线
if(s1==null) {
log.info( "用户已掉线线");
}else {
//发送消息给消息发送方提示消息接收方掉线
ImMsgModel msg = new ImMsgModel();
msg.setKey("offline");
msg.setSystem(true);
msg.setId(m.getId());
msg.setType("friend");
msg.setContent("对方已掉线");
log.info( "对方已掉线");
send2user(JSON.toJSONString(msg), s1);
}
}else{
//发送消息给消息接收方
m.setId(m.getFromid());
m.setKey("online");
send2user(JSON.toJSONString(m), s);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private Session getSessionByID(String id) {
System.out.println("连接用户数"+connections.size());
for (WebsocketsListener wb : connections) {
//接收对象id对应的session
if(id.equals(wb.userid)) {
return wb.session;
}
}
return null;
}
@OnClose
public void end() {
connections.remove(this);
}
@OnError
public void onError(Throwable t) throws Throwable {
log.info( "发生错误onError");
t.printStackTrace();
}
private void send2user(String msg,Session session){
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void sendAll(String string) {
for (WebsocketsListener wb : connections) {
try {
wb.session.getBasicRemote().sendText(string);
} catch (Exception e) {
e.printStackTrace();
connections.remove(wb);
try {
wb.session.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
以上完成了websocket的服务前端消息转发代码(数据持久化与 redis后面继续进行补充)
下面我贴一下上篇中讲的前端代码的完整版
前端代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>layui</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="js/css/modules/layui.css" media="all">
<!-- 注意:如果你直接复制所有代码到本地,上述css路径需要改成你本地的 -->
</head>
<body>
<script src="js/layui.js" charset="utf-8"></script>
<!-- 注意:如果你直接复制所有代码到本地,上述js路径需要改成你本地的 -->
<style>
/* img */
i{font-style:normal;}
.qq-login{width:430px;height:330px;margin:0 0 -165px -215px;bottom:50%;left:50%;position:fixed;z-index:9999;border-radius:3px;overflow:hidden;box-shadow:0 0 5px #333;background:#ebf2f9 url(js/images/bj/qq-login-bg.jpg) center top no-repeat;display:block;}
.login-menu{width:90px;height:30px;top:0;right:0;position:absolute;}
.login-menu span{float:left;width:30px;height:30px;background-image:url(js/images/bj/qq-login-bg.jpg);}
.login-menu span:hover{background-color:#3a95de;}
.login-menu span:nth-child(1){background-position:left center;}
.login-menu span:nth-child(2){background-position:-30px center;}
.login-menu span:nth-child(3){background-position:-90px center;}
.login-menu span:nth-child(3):hover{background-color:#ea4848;}
.login-ner{margin-top:182px;float:left;width:100%;height:148px;}
.login-left{float:left;width:133px;height:148px;}
.login-head{float:left;width:80px;height:80px;border-radius:50%;border:1px solid #ccc;overflow:hidden;margin:12px 11px 0 40px;}
.login-head img{width:80px;height:80px;}
.login-on{width:194px;height:148px;float:left;}
.login-txt{float:left;margin-top:12px;height:60px;width:100%;}
.login-txt input{border:1px solid #d1d1d1;float:left;height:30px;padding:0 7px;font-size:12px;width:100%;}
.login-txt input:nth-child(1){border-radius:4px 4px 0 0;}
.login-txt input:nth-child(2){border-radius:0 0 4px 4px;margin-top:-1px;}
.login-xuan{width:100%;float:left;height:14px;line-height:14px;margin-top:8px;}
.login-xuan input{width:14px;height:14px;float:left;}
.login-xuan i{float:left;padding-left:4px;}
.login-right{width:103px;height:60px;float:left;margin-top:12px;}
.login-right a{float:left;padding-left:10px;width:90%;color:#2786e4;line-height:30px;text-indent:10px;}
.login-but{width:100%;height:30px;margin:13px 0;float:left;background:#09a3dc;color:#fff;text-align:center;line-height:30px;border-radius:4px;font-size:14px; cursor:context-menu;}
.login-menu span {
float: left;
width: 30px;
height: 30px;
background-image: url(js/images/bj/wins.png);
}
.login-tips{line-height:40px;width:300px;padding:10px;color: white;top:0;left:0;position:absolute;}
</style>
<script id="login_html" type="text/html">
<div class="qq-login">
<div class="login-tips" id="login-tips"></div>
<div class="login-menu">
<span></span><span></span><span class="login-close"></span>
</div>
<div class="login-ner">
<div class="login-left">
<div class="login-head"><img src="js/css/modules/layim/skin/4.jpg"></div>
</div>
<div class="login-on">
<div class="login-txt"><input type="text" id="username" placeholder="QQ号码/手机/邮箱"><input id="password" type="password" placeholder="密码"></div>
<div class="login-xuan"><span class="fl"><input type="checkbox"><i>记住密码</i></span><span class="fr"><input type="checkbox"><i>自动登录</i></span></div>
<div class="login-but" id="login-but">安全登录</div>
</div>
<div class="login-right">
<a href="http://zc.qq.com/chs/index.html" target="_blank">注册账号</a><a href="https://aq.qq.com/cn2/findpsw/pc/pc_find_pwd_input_account?pw_type=0&aquin=" target="_blank">找回密码</a>
</div>
</div>
</div>
</script>
<script>
layui.use(’layim’, function(){
var layim = layui.layim,id;
$ = layui.jquery,
layer.open({
title:false
,type: 1
,offset: ’auto’ //具体配置参考:http://www.layui.com/doc/modules/layer.html#offset
,content: login_html.innerHTML
,btn: false
,shadeClose: false
,closeBtn: 0
,moveType: 0
,move: ’.login-head’
,btnAlign: ’r’ //按钮居中
,shade: 0 //不显示遮罩
});
//绑定登陆事件
$(document).on(’click’, ’#login-but’, function(data) {
login();
});
var tips= $(’#login-tips’);
function login(){
tips.html("正在登陆……");
var un= $("#username").val();
var ps= $("#password").val();
var d={"m":"login","username":un,"password":ps}
$.ajax({
type:"POST",
url:"../chat",
dataType:"json",
data:d,
success:function(data){
if(data.code==20000){
id=data.data.id
tips.html("登陆成功");
chartSetting();
connection();
}else{
tips.html("登陆失败请重试!"+data.msg);
}
},
error:function(jqXHR){
tips.html("发生错误"+jqXHR.status);
}
});
}
function chartSetting(){
//基础配置
layim.config({
//初始化接口
init: {
url: ’chat?m=list&id=’+id
,data: {}
}
//查看群员接口
,members: {
url: ’chat?m=getMembers&id=’+id
,data: {}
}
,uploadImage: {
url: ’uploadv2?filepath=’ //(返回的数据格式见下文)
,type: ’’ //默认post
}
,uploadFile: {
url: ’uploadv2?filepath=’ //(返回的数据格式见下文)
,type: ’’ //默认post
}
,isAudio: true //开启聊天工具栏音频
,isVideo: true //开启聊天工具栏视频
//扩展工具栏
,tool: [{
alias: ’code’
,title: ’代码’
,icon: ’’
}]
,brief: false //是否简约模式(若开启则不显示主面板)
,title: ’消息’ //自定义主面板最小化时的标题
,right: ’10px’ //主面板相对浏览器右侧距离
,minRight: ’90px’ //聊天面板最小化时相对浏览器右侧距离
,initSkin: ’3.jpg’ //1-5 设置初始背景
,skin: [’js/css/modules/layim/skin/6.jpg’,
’js/css/modules/layim/skin/1.jpg’] //新增皮肤
,isfriend: true //是否开启好友
,isgroup: true //是否开启群组
,min: false //是否始终最小化主面板,默认false
,notice: true //是否开启桌面消息提醒,默认false
,voice: true //声音提醒,默认开启,声音文件为:default.mp3
,msgbox: ’msgbox.html’ //消息盒子页面地址,若不开启,剔除该项即可
,find: ’find.html’ //发现页面地址,若不开启,剔除该项即可
,chatLog: ’chatlog.html’ //聊天记录页面地址,若不开启,剔除该项即可
});
}
//监听在线状态的切换事件
layim.on(’online’, function(status){
layer.msg(status);
});
//演示自动回复
var autoReplay = [
’您好,我现在有事不在,一会再和您联系。’,
’你没发错吧?face[微笑] ’,
’洗澡中,请勿打扰,*窥偷**请购票,个体四十,团体八折,订票电话:一般人我不告诉他!face[哈哈] ’,
’你好,我是主人的美女秘书,有什么事就跟我说吧,等他回来我会转告他的。face[心] face[心] face[心] ’,
’face[威武] face[威武] face[威武] face[威武] ’,
’<(@ ̄︶ ̄@)>’,
’你要和我说话?你真的要和我说话?你确定自己想说吗?你一定非说不可吗?那你说吧,这是自动回复。’,
’face[黑线] 你慢慢说,别急……’,
’(*^__^*) face[嘻嘻] ,是贤心吗?’
];
//监听在线状态的切换事件
layim.on(’online’, function(status){
layer.msg(status);
});
//监听签名修改
layim.on(’sign’, function(value){
layer.msg(value);
});
//监听自定义工具栏点击,以添加代码为例
layim.on(’tool(code)’, function(insert){
layer.prompt({
title: ’插入代码 - 工具栏扩展示例’
,formType: 2
,shade: 0
}, function(text, index){
layer.close(index);
insert(’[pre class=layui-code]’ + text + ’[/pre]’); //将内容插入到编辑器
});
});
//监听layim建立就绪
layim.on(’ready’, function(res){
//console.log(res.mine);
layim.msgbox(5); //模拟消息盒子有新消息,实际使用时,一般是动态获得
});
//监听发送消息
layim.on(’sendMessage’, function(data){
var To = data.to;
var Me = data.mine;
if(To.type === ’friend’){
layim.setChatStatus(’<span style="color:#FF5722;">对方正在输入。。。</span>’);
}
if(To.id==Me.id){
alert("无法和自己发起聊天");
return;
}else{
var data={
username: Me.username //消息来源用户名
,avatar: Me.avatar //消息来源用户头像
,id: To.id //消息的来源ID(如果是私聊,则是用户id,如果是群聊,则是群组id)
,type:To.type //聊天窗口来源类型,从发送消息传递的to里面获取
,content: Me.content //消息内容
,cid: 0 //消息id,可不传。除非你要对消息进行一些操作(如撤回)
,mine: false //是否我发送的消息,如果为true,则会显示在右方
,fromid:Me.id //消息的发送者id(比如群组中的某个消息发送者),可用于自动解决浏览器多窗口时的一些问题
,timestamp:new Date().getTime() //服务端时间戳毫秒数。注意:如果你返回的是标准的 unix 时间戳,记得要 *1000
};
//模拟系统消息
websocket.send(JSON.stringify(data));
layim.setChatStatus(’<span style="color:#FF5722;">在线</span>’);
}
});
//监听查看群员
layim.on(’members’, function(data){
//console.log(data);
});
//监听聊天窗口的切换
layim.on(’chatChange’, function(res){
var type = res.data.type;
console.log(res.data.id)
if(type === ’friend’){
//模拟标注好友状态
layim.setChatStatus(’<span style="color:#FF5722;">在线</span>’);
} else if(type === ’group’){
//模拟系统消息
layim.getMessage({
system: true
,id: res.data.id
,type: "group"
,content: ’模拟群员’+(Math.random()*100|0) + ’加入群聊’
});
}
});
function connection(){
tips.html("开始连接服务……");
if(’WebSocket’ in window){
websocket = new WebSocket("ws://"+sy()+"/imchat/"+id);
}else{
tips.html("不支持websocket");
}
//连接发生错误的回调方法
websocket.onerror = function(ev,data){
tips.html("连接发生错误的回调方法");
};
//连接成功建立的回调方法
websocket.onopen = function(e){
tips.html("");
};
//接收到消息的回调方法
websocket.onmessage = function(event){
// layim.getMessage(event.data);
var json=JSON.parse(event.data);
console.log("接收信息:");
console.log(event.data);
if(json.key=="offline"){
//用户离线
layim.setFriendStatus(json.id, ’offline’);
layim.setChatStatus(’<span style="color:gray;">离线</span>’);
layer.msg(json.content+"无法接收到消息", {
icon: 1
});
}else if(json.key=="online"){ //接收在线消息
//制造好友消息
layim.setFriendStatus(json.id, ’online’);
layim.setChatStatus(’<span style="color:#FF5722;">在线</span>’);
layim.getMessage(json);
}
};
//连接关闭的回调方法
websocket.onclose = function(event){
//alert(’连接关闭的回调方法’);
tips.html("连接已关闭,尝试重连……");
disConnect();
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
};
}
//检查链接,短线重连
var disConnect = function(){
setTimeout(function(){
connection();
},5000);
}
//关闭连接
function closeWebSocket(){
tips.html("关闭closeWebSocket");
websocket.close();
}
function sy(){
var curWwwPath = window.document.location.href;
var pathName = window.document.location.pathname;
var pos = curWwwPath.indexOf(pathName);
var localhostPaht = curWwwPath.substring(0,pos);
var projectName = pathName.substring(0,pathName.substr(1).indexOf(’/’)+1);
var ip=window.location.host;
var prot=window.location.port;
return (ip + projectName);
}
});
</script>
</body>
</html>
结合以上服务端代码和前端代码可以实现基本的聊天功能,本篇没涉及到登录接口和用户列表接口,下篇再做补充。需要的同学关注一下下篇。有问题欢迎留言指正