龙空技术网

自己动手实现基于websocket的聊天web聊天功能高仿仿qq(服务端)

无极低码 591

前言:

此时姐妹们对“聊天窗口html”都比较关切,小伙伴们都想要了解一些“聊天窗口html”的相关文章。那么小编同时在网络上汇集了一些对于“聊天窗口html””的相关知识,希望同学们能喜欢,看官们一起来了解一下吧!

本篇主要讲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="" target="_blank">注册账号</a><a href=";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' //具体配置参考: ,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>

结合以上服务端代码和前端代码可以实现基本的聊天功能,本篇没涉及到登录接口和用户列表接口,下篇再做补充。需要的同学关注一下下篇。有问题欢迎留言指正

标签: #聊天窗口html