基于websocket实现的一个简单的聊天室

时间:2021-09-22 22:06:25

本文是基于websocket写的一个简单的聊天室的例子,可以实现简单的群聊和私聊。是基于websocket的注解方式编写的。(有一个小的缺陷,如果用户名是中文,会乱码,不知如何处理,如有人知道,请告知一下。在页面获取到的不会乱码,但是传递到websocket中,在@OnOpen注解标注的方法中获取就会乱码。用户名是在weboscket的url中以rest风格的参数传递过去的。

一、效果如下
基于websocket实现的一个简单的聊天室
 
 
当用户登入(或登出)聊天室时,聊天界面显示一个欢迎的提示信息,同时刷新右边的在线用户列表。

1.当不选中右边的在线用户列表时,发送的时群聊信息,所有人都可以看到,即 图片上化红线的部分。

2.当选中右边的在线用户时,自己发送的消息在右边显示,接受到消息的用户消息在左侧显示。即从上方画矩形的区域可以看出,私聊的消息只有自己和私聊的那个人可以看见。

二、开发环境:

window   tomcat7.0.63 jdk1.8  Chrome浏览器

因为websocket是html5的一个技术,有些浏览器并不支持,而且jdk貌似也要在1.7或1.7以上,tomcat的低版本是不支持websocket的。

三、开发步骤:

1.服务器端:

1.1先编写一个简单的登录servlet,完成登录的过程

1.2编写一个类实现ServerApplicationConfig接口,并实现getAnnotatedEndpointClasses(...)方法,该方法是基于注解的。

1.3 编写一个普通的java类,使用注解@ServerEndpoint标记,标明该类是一个websocket的服务类(该类是一个多例的

1.3.1 @ServerEndpoint("/chat/{username}") 标明可以端链接服务器的地址是:ws://localhost:端口号/项目名/chat/.... 使用rest风格的方式,后面的username即为用户名,需要在@OnOpen方法中获取到

1.3.2@OnOpen 标注方法(表示客户端和服务器端第一次建立连接时触发)

1.获取到用户名

2.保存session,此处的session为websocket的session ,使用此session可以向客户端发送消息

3.先客户端发送一条欢迎消息,同时将所有的在线用户发送给客户端,将消息的类型也要发给客户端

4.因为上方说过,该类是一个多实例的,因此需要将用户和该用户的对应的session存入到一个静态的map中。

1.3.2@OnMessage注解标注方法(表示客户端发送消息过来时触发)

1.获取客户端发送过来的消息,并转换成一个map (客户端发送的消息为一个msg和toUser)  --> 私聊时,toUser中会有值,群聊时没有。

2.封装客户端发送过来的消息,此时不需要传递在线用户列表(因为没有新加入的用户和离开的用户),也需要给定消息的类型

1.3.3@OnClose注解标注方法(表示客户端关闭了websocket连接)

1.将当前用户的移除

2.向客户端发送一条离开消息,需要传递在线用户列表和消息的类型

1.4广播消息

在该方法中需要判断,

当前是群聊还是私聊,如果是群聊需要将消息发送给所有的人。

当前是私聊聊,只发送给特定的人。

  2.客户端:(websocket的url是ws://开头的,如果是安全的则是wss://开头)

2.1编写一个简单的登录界面

2.2显示当前用户,以及和服务器端建立连接

2.2.1从request中获取到用户名,显示到页面中

2.2.2创建websocke对象,此处需要判断浏览器是否支持websocket

2.2.3创建websocket对象后,监听websocket的onopen,onmessage,onerror,onclose事件

2.3.3.1此处说一个onmessage方法,次方法当服务器发送数据过来时触发,改方法中有一个参数,假如叫r,r.data 即后台返回的数据

2.3.3.2获取到后台返回的数据,并将它转换成json对象(因为我在服务器端是以json的数据返回的),进行处理

-> 获取后台返回的数据的消息的类型

->欢迎信息或离开信息 。1.需要显示信息.2.需要刷新在线用户列表

->如果是聊天信息。  1.简单的封装一下,显示到界面上。

2.3.4给发送按钮绑定事件,

1.获取发送的数据,->获取右边选择的在线用户,如果没有就是空的->将消息发送到服务器端。如果是私聊,将自己发送的消息,放到右边显示。

四、代码实现:(需要注意一下我url的组装方式)

      客户端:(登录的界面代码就不贴出了,只贴聊天界面)

<strong><%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" isELIgnored="false"%>
<!DOCTYPE>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style type="text/css">
*{box-sizing:border-box;
-moz-box-sizing:border-box; /* Firefox */
-webkit-box-sizing:border-box}
.container{width: 400px;height: 300px;border: 1px solid lightblue;margin: 0 auto;}
.container .main{width: 70%;height:80%;float: left;border-right: 1px solid lightblue;overflow: scroll;}
.container .main .commonmsg{text-align: center;color: red;background-color: #f9f9f9;height: 50px;line-height: 50px;border-bottom: 1px solid lightblue;}
.container .main .smsg{text-align: right;padding: 5px;}
.container .onlineUsers{float: left;width: 29.8%;}
.container .msg{border-top: 1px solid lightblue;height: 20%;width: 100%;}
table[t_table]{width: 100%;border-collapse: collapse;}
table[t_table] thead{background-color: #eee;}
table[t_table] thead tr{background-color: #eee;}
table[t_table] thead tr th{padding: 2px;border: 1px solid #ccc;}
table[t_table] tbody tr td{border: 1px solid #ccc;padding: 2px;}
table[t_table] tbody tr{color:black;}
table[t_table] tbody tr:nth-child(odd){background-color:#fff;}
table[t_table] tbody tr:nth-child(even){background-color: #f9f9f9;}
table[t_table] tbody tr:hover{cursor: pointer;background-color: rgba(0,0,0,.05);color:red;}
</style>
<title>Insert title here</title>
<script type="text/javascript" src="${pageContext.request.contextPath }/js/jquery-2.1.1.js"></script>
</head>
<body>
<div class="container">
<h1 style="text-align: center;">当前用户:${username }</h1>
<div class="main">
</div>
<div class="onlineUsers">
<table t_table >
<thead>
<tr>
<th colspan="2" >在线用户列表</th>
</tr>
</thead>
<tbody id="onLineUsersTbody" style="overflow: scroll;"> </tbody>
</table>
</div>
<div style="clear: both;"></div>
<div class="msg">
<div contenteditable="true" style="width: 80%;float: left;">
<textarea style="width: 100%;height: 100%;" id="sendMsg"></textarea>
</div>
<div style="float: left;height: 100%;width: 20%;">
<input type="button" value="发送" style="display: block;width: 100%;height: 100%;" id="send"/>
</div>
<div style="clear: both;" id="send"></div>
</div>
</div>
<script type="text/javascript">
$(function(){
var username = '${requestScope.username}',
ws = null,
wsUrl = "ws://localhost:8080/study-websocket/chat/"+ username;
console.info(username);
var Chat = {
openConnection : function(){
if ('WebSocket' in window) {
ws = new WebSocket(wsUrl);
} else if ('MozWebSocket' in window) {
ws = new MozWebSocket(wsUrl);
} else {
alert('您的浏览器不支持websocket.');
return;
}
console.info("创建websocket对象成功.");
ws.onopen = function(){
console.info("websocket 连接打开.");
}
ws.onmessage = function(r){
console.info("后台返回的数据:");
console.info(r.data);
Chat.handleMsg(JSON.parse(r.data));
}
ws.onerror = function(e){
console.warn(e);
console.warn("websocket出现异常.");
}
ws.onclose = function(e){
console.info("websocket连接关闭.");
}
},
handleMsg : function(data){
var type = data.msgTypeEnum;
switch(type){
case "WELCOME":
case "LEAVE" :
Chat.handleWelcomeMsg(data);
break;
case "CHAT" :
Chat.handChatMsg(data);
break;
default :
console.info("后台返回未知的消息类型.");
break;
}
},
handChatMsg : function(data){
console.warn(data);
$('<div />').addClass("chatmsg").html(data.msg.date+" -- " + data.msg.fromUser + "<br / >" + data.msg.msg).appendTo(".main");
},
handleWelcomeMsg : function(data){
// 1.处理在线用户
var users = data.users;
var trs = "";
users.forEach(function(user,i){
trs += "<tr>".concat("<td>").concat("<input type='checkbox' value='"+user+"' />").concat("</td>")
.concat("<td>").concat(user).concat("</td>")
.concat("</tr>");
});
$('#onLineUsersTbody').html(trs); // 2.处理消息
$('<div />').addClass("commonmsg").html(data.msg).appendTo(".main");
},
sendMsg : function(){
if(ws){
$('#send').off('click').on('click',function(){
var msg = $('#sendMsg').val();
var toUser = [];
$('#onLineUsersTbody').find(":checked").each(function(i,ele){
toUser.push($(ele).val());
});
if(msg){
var jsonMsg = {
msg : msg,
toUser : toUser.join(",")
};
ws.send(JSON.stringify(jsonMsg));
$('#sendMsg').val('');
Chat.addPageMsg(toUser,msg);
}
});
}else{
alert('连接服务器的websocket通道已经关闭.')
}
},
addPageMsg : function(toUser,msg){
if(toUser.length){
$('<div />').addClass("smsg").html(username + ":" + msg).appendTo(".main");
}
}
}; Chat.openConnection();
Chat.sendMsg();
});
</script>
</body>
</html></strong>

    服务器端:

    1.实现了ServerApplicationConfig的类

<strong>/**
* 此类在服务器启动时,自动运行
* @author huan
*/
public class ChatConfig implements ServerApplicationConfig {
private Logger log = Logger.getLogger(ChatConfig.class);
@Override
/**
* @param classes 中的类是拥有@ServerEndpoint注解标注的类
*/
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> classes) {
for (Class<?> clazz : classes) {
log.info("加载websocket服务类:" + clazz.getName());
}
return classes;
}
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> arg0) {
return null;
}
}</strong>

   2.@ServerEndpoint注解标注的类

<strong>package com.huan.study.websocket.chat.endpoint;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import javax.websocket.CloseReason;
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.apache.log4j.Logger; import com.huan.study.websocket.chat.data.Msg;
import com.huan.study.websocket.chat.data.ResponseMsg;
import com.huan.study.websocket.chat.enums.MsgTypeEnum;
import com.huan.study.websocket.chat.util.JsonUtil; /**
* 该类表示websocket服务端,此类不需要别的配置
*
* @author huan
*
*/
@ServerEndpoint("/chat/{username}")
public class ChatEndpoint {
private static Logger log = Logger.getLogger(ChatEndpoint.class);
/** 保存的是用户名和该用户对应的session */
private static Map<String, ChatEndpoint> userSessionMap = new ConcurrentHashMap<String, ChatEndpoint>();
private Session session;
private String username;
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
log.info("【" + username + "】进入聊天室.");
this.session = session;
this.username = username;
userSessionMap.put(username, this);
Msg msg = new Msg();
msg.setMsg(String.format("欢迎【%s】进入聊天室", username));
msg.setMsgTypeEnum(MsgTypeEnum.WELCOME);
msg.setUsers(userSessionMap.keySet());
broadcast(JsonUtil.toJson(msg));
}
@OnMessage
public void onTextMessage(Session session, String msg) {
log.info(String.format("客户端发送消息:%s", msg));
Map<String, Object> msgMap = JsonUtil.toMap(msg);
String toUser = (String) msgMap.get("toUser");
Msg _msg = new Msg();
_msg.setMsg(new ResponseMsg(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), username, msgMap.get("msg")));
_msg.setMsgTypeEnum(MsgTypeEnum.CHAT);
broadcast(JsonUtil.toJson(_msg), toUser);
}
@OnClose
public void onClose(CloseReason closeReason) {
log.info("关闭: " + closeReason.getCloseCode());
log.info("关闭: " + closeReason.getReasonPhrase());
userSessionMap.remove(this.username);
Msg msg = new Msg();
msg.setMsg(String.format("欢迎【%s】离开聊天室", username));
msg.setMsgTypeEnum(MsgTypeEnum.LEAVE);
msg.setUsers(userSessionMap.keySet());
broadcast(JsonUtil.toJson(msg));
}
@OnError
public void OnError(Throwable t) {
t.printStackTrace();
}
private static void broadcast(String msg, String toUser) {
String[] arr = null;
if (null != toUser && !"".equals(toUser)) {
log.info("当前是单聊.");
arr = toUser.split(",");
} else {
log.info("当前是群聊.");
}
for (Map.Entry<String, ChatEndpoint> entry : userSessionMap.entrySet()) {
String username = entry.getKey();
if (null != arr) {
if (!Arrays.asList(arr).contains(username)) {
continue;
}
}
ChatEndpoint endpoint = entry.getValue();
synchronized (endpoint) {
try {
log.info(String.format("返回到客户端的消息:%s", msg));
endpoint.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
log.info("【" + username + "】离开了聊天室.");
userSessionMap.remove(username);
try {
endpoint.session.close();
} catch (IOException e1) {
e1.printStackTrace();
log.info(String.format("关闭用户【%s】的session失败", username));
}
Msg _msg = new Msg();
_msg.setMsg(String.format("【%s】 离开了聊天室.", username));
_msg.setMsgTypeEnum(MsgTypeEnum.LEAVE);
_msg.setUsers(userSessionMap.keySet());
_msg.getUsers().remove(username);
broadcast(JsonUtil.toJson(_msg));
}
}
}
}
/** 广播消息 */
private static void broadcast(String msg) {
broadcast(msg, null);
}
}
</strong>

   主要的代码就是以上的部分:(下面是几个用到的类)

Msg.java类,该类是返回给客户端的消息

<strong>/*** 消息的类型*/
private MsgTypeEnum msgTypeEnum;
/*** 返回给客户端的消息*/
private Object msg;
/*** 当前的在线用户 */
private Set<String> users;</strong>

   ResponseMsg.java类是私聊时或群聊时返回给客户端的消息类

private String date;
private String toUser;
private String fromUser;
private Object msg;

MsgTypeEnum.java 是一个枚举类,用于设定消息的类型(客户端根据消息的类型,以不同的方式处理消息)

WELCOME("进入聊天室"), LEAVE("离开聊天室"),CHAT("聊天类型的消息");

JsonUtil.java是一个json的序列化和反序列化类

public static String toJson(Object obj) {
return new Gson().toJson(obj);
}
public static Map<String, Object> toMap(Object obj) {
return new Gson().fromJson(obj.toString(), new TypeToken<Map<String, Object>>() {}.getType());
}