天天看點

基于websocket實作的一個簡單的聊天室

本文是基于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方法中擷取到

                   [email protected] 标注方法(表示用戶端和伺服器端第一次建立連接配接時觸發)

                                  1.擷取到使用者名

                                  2.儲存session,此處的session為websocket的session ,使用此session可以向用戶端發送消息

                                  3.先用戶端發送一條歡迎消息,同時将所有的線上使用者發送給用戶端,将消息的類型也要發給用戶端

                                  4.因為上方說過,該類是一個多執行個體的,是以需要将使用者和該使用者的對應的session存入到一個靜态的map中。

                   [email protected]注解标注方法(表示用戶端發送消息過來時觸發)

                                  1.擷取用戶端發送過來的消息,并轉換成一個map (用戶端發送的消息為一個msg和toUser)  --> 私聊時,toUser中會有值,群聊時沒有。

                                  2.封裝用戶端發送過來的消息,此時不需要傳遞線上使用者清單(因為沒有新加入的使用者和離開的使用者),也需要給定消息的類型

                    [email protected]注解标注方法(表示用戶端關閉了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>
           

   [email protected]注解标注的類

<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());
}
           

繼續閱讀