本文是基于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方法中擷取到
[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());
}