`

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

阅读更多

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

一、效果如下

 
 
当用户登入(或登出)聊天室时,聊天界面显示一个欢迎的提示信息,同时刷新右边的在线用户列表。

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的组装方式

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

 

<%@ 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>
 

 

    服务器端:

    1.实现了ServerApplicationConfig的类

 

/**
 * 此类在服务器启动时,自动运行
 * @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;
	}
}
   2.@ServerEndpoint注解标注的类

 

 

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);
	}
}
   主要的代码就是以上的部分:(下面是几个用到的类)

 

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

 

/*** 消息的类型*/
private MsgTypeEnum msgTypeEnum;
/*** 返回给客户端的消息*/
private Object msg;
/*** 当前的在线用户 */
private Set<String> users;
   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());
}
 
 
 
  • 大小: 53.6 KB
1
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics