利用websocket和Java完成的消息推送功能,服務器用的是tomcat7.0,一些東西是自己琢磨的,也不知道恰不恰當,不恰當處,還請各位見諒,並指出。
程序簡單來說,就是客戶A可以發送消息給客戶B,但有很多可以擴展的地方,
比如
1.如果加入數據庫後,A發消息時客戶B未上線,服務端將會把消息存在數據庫中,等客戶B上線後,在將消息取出發送給客戶B
2.服務端也可發送消息到任意客戶端上。
程序的運行效果截圖如下(在Chrome,搜狗,Firefox下測試通過):代碼將在最後給出
首先我們打開一個浏覽器,顯示輸入您的名字,這裡我輸入soar
在打開第二個浏覽器,這裡我輸入bill
這是如果我發送hello bill i am soar給bill,點擊send
在另一個浏覽器上就可以看到
Websocket
1.websocket是什麼?
WebSocket是為解決客戶端與服務端實時通信而產生的技術。其本質是先通過HTTP/HTTPS協議進行握手後創建一個用於交換數據的TCP連接,
此後服務端與客戶端通過此TCP連接進行實時通信。
2.websocket的優點
以前我們實現推送技術,用的都是輪詢,在特點的時間間隔有浏覽器自動發出請求,將服務器的消息主動的拉回來,在這種情況下,我們需要不斷的向服務器 發送請求,然而HTTP request 的header是非常長的,裡面包含的數據可能只是一個很小的值,這樣會占用很多的帶寬和服務器資源。會占用大量的帶寬和服務器資源。
WebSocket API最偉大之處在於服務器和客戶端可以在給定的時間范圍內的任意時刻,相互推送信息。在建立連接之後,服務器可以主動傳送數據給客戶端。
此外,服務器與客戶端之間交換的標頭信息很小。
WebSocket並不限於以Ajax(或XHR)方式通信,因為AJax技術需要客戶端發起請求,而WebSocket服務器和客戶端可以彼此相互推送信息;
關於AJax,comet,websocket的詳細介紹,和websocket報文的介紹,大家可以參看http://www.shaoqun.com/a/54588.ASPx 網頁設計]AJax、Comet與Websocket,
我如果以後有時間,也會寫出來的
3.如何使用websocket
客戶端
在支持WebSocket的浏覽器中,在創建socket之後。可以通過onopen,onmessage,onclose即onerror四個事件實現對socket進行響應
一個簡單是示例
var ws = new WebSocket(“ws://localhost:8080”); ws.onopen = function() { console.log(“open”); ws.send(“hello”); }; ws.onmessage = function(evt) { console.log(evt.data) }; ws.onclose = function(evt) { console.log(“WebSocketClosed!”); }; ws.onerror = function(evt) { console.log(“WebSocketError!”); };
1.var ws = new WebSocket(“ws://localhost:8080”);
申請一個WebSocket對象,參數是需要連接的服務器端的地址,同http協議使用http://開頭一樣,WebSocket協議的URL使用ws://開頭,另外安全的WebSocket協議使用wss://開頭。
ws.send(“hello”);
用於叫消息發送到服務端
2.ws.onopen = function() { console.log(“open”)};
當websocket創建成功時,即會觸發onopen事件
3.ws.onmessage = function(evt) { console.log(evt.data) };
當客戶端收到服務端發來的消息時,會觸發onmessage事件,參數evt.data中包含server傳輸過來的數據
4.ws.onclose = function(evt) { console.log(“WebSocketClosed!”); };
當客戶端收到服務端發送的關閉連接的請求時,觸發onclose事件
5.ws.onerror = function(evt) { console.log(“WebSocketError!”); };
如果出現連接,處理,接收,發送數據失敗的時候就會觸發onerror事件
我們可以看出所有的操作都是采用事件的方式觸發的,這樣就不會阻塞UI,使得UI有更快的響應時間,得到更好的用戶體驗。
服務端
現在有很多的服務器軟件支持websocket,比如node.JS,jetty,tomcat等
這裡我使用的是tomat 7.0和eclipse4.2
在tomcat下使用websocket首先需要導入相關的jar
tomcat7提供的與WebSocket相關的類均位於包org.apache.catalina.websocket之中(包org.apache.catalina.websocket的實現包含於文件catalina.jar之中
這裡我們把tomcat的全部導入就行了
在build path->configure build path->librarise->add library->server runtime->apache tomcat v7.0
image
同時需要import以下包
import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.catalina.websocket.WebSocketServlet;
我們需要兩個類
第一個用於處理websocket請求
第二個用於處理每一次具體的WebSocket任務
第一個類
public class SocketServer extends WebSocketServlet {
private static final long serialVersionUID = 1L;
//……
@Override
protected StreamInbound createWebSocketInbound(String arg0,
HttpServletRequest arg1) {
// TODO Auto-generated method stub
return new ChatWebSocket(users);
}
}
這個Servlet繼承自WebSocketServlet,實現createWebSocketInbound方法。該方法返回第二個類的實例。
第二個類
public class ChatWebSocket extends MessageInbound {
@Override
protected void onTextMessage(CharBuffer message) throws IOException {
}
@Override
protected void onOpen(WsOutbound outbound) {
}
@Override
protected void onClose(int status) {
}
@Override
protected void onBinaryMessage(ByteBuffer arg0) throws IOException {
}
//其余略
}
protected void onTextMessage(CharBuffer message) throws IOException { }
文本消息響應
protected void onBinaryMessage(ByteBuffer arg0) throws IOException { }
二進制消息響應
protected void onOpen(WsOutbound outbound) { }
建立連接的觸發的事件
protected void onClose(int status) { }
關閉連接時觸發的事件
4.程序代碼
Html部分
<!DOCTYPE Html>
<Html>
<head>
<meta charset="utf-8">
<script type="text/Javascript" src="js/jquery.JS"></script>
<script type="text/Javascript" src="js/socket.JS"></script>
<title>無標題文檔</title>
</head>
<script language="Javascript">
</script>
<body>
<table>
<tr>
<td>Message</td>
<td><input type="text" id="message"></td>
</tr>
<tr>
<td>Name</td>
<td><input type="text" id="othername"></td>
</tr>
<tr>
<td><input id="sendbutton" type="button" value="send" onClick="click" disabled="true">
</input></td>
</tr>
</table>
<script>
</script>
</body>
</Html>
JS部分(關於jquery部分不進行講解)
var username = window.prompt("輸入你的名字:");
document.write("Welcome<p id=\"username\">"+username+"</p>");
if (!window.WebSocket && window.MozWebSocket)
window.WebSocket=window.MozWebSocket;
if (!window.WebSocket)
alert("No Support ");
var ws;
$(document).ready(function(){
$("#sendbutton").attr("disabled", false);
$("#sendbutton").click(sendMessage);
startWebSocket();
})
function sendMessage()
{
var othername=$("#othername").val();
var msg="MSG\t"+username+"_"+othername+"_"+$("#message").val();
send(msg);
}
function send(data)
{
console.log("Send:"+data);
ws.send(data);
}
function startWebSocket()
{
ws = new WebSocket("ws://" + location.host + "/WebSocket/SocketServer");
ws.onopen = function(){
console.log("success open");
$("#sendbutton").attr("disabled", false);
};
ws.onmessage = function(event)
{
console.log("RECEIVE:"+event.data);
handleData(event.data);
};
ws.onclose = function(event) {
console.log("Client notifIEd socket has closed",event);
};
}
function handleData(data)
{
var vals=data.split("\t");
var msgType=vals[0];
switch(msgType)
{
case "NAME":
var msg=vals[1];
var mes="NAME"+"\t"+msg+"_"+ username;
send(mes);
break;
case "MSG":
var val2s=vals[1].split("_");
var from=val2s[0];
var message=val2s[2];
alert(from+":"+message);
break;
default:
break;
}
}
Java部分
import Java.io.IOException;
import Java.nio.ByteBuffer;
import Java.nio.CharBuffer;
import Javax.servlet.http.HttpServletRequest;
import Java.util.Set;
import Java.util.concurrent.CopyOnWriteArraySet;
import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.catalina.websocket.WebSocketServlet;
public class SocketServer extends WebSocketServlet {
private static final long serialVersionUID = 1L;
public final Set<ChatWebSocket> users = new CopyOnWriteArraySet<ChatWebSocket>();
public static int USERNUMBER = 1;
@Override
protected StreamInbound createWebSocketInbound(String arg0,
HttpServletRequest arg1) {
// TODO Auto-generated method stub
return new ChatWebSocket(users);
}
public class ChatWebSocket extends MessageInbound {
private String username;
private Set<ChatWebSocket> users = new CopyOnWriteArraySet<ChatWebSocket>();;
public ChatWebSocket() {
}
public ChatWebSocket(Set<ChatWebSocket> users) {
this.users = users;
}
@Override
protected void onTextMessage(CharBuffer message) throws IOException {
// 這裡處理的是文本數據
}
public void onMessage(String data) {
String[] val1 = data.split("\\t");
if(val1[0].equals("NAME"))
{
String[] val2=val1[1].split("_");
for(ChatWebSocket user:users){
if (user.username.equals(val2[0])){
user.username=val2[1];
}
}
}
else if(val1[0].equals("MSG"))
{
String[] val2=val1[1].split("_");
for(ChatWebSocket user:users){
if (user.username.equals(val2[1])){
try {
CharBuffer temp=CharBuffer.wrap(data);
user.getWsOutbound().writeTextMessage(temp);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
else
{
System.out.println("ERROR");
}
}
@Override
protected void onOpen(WsOutbound outbound) {
// this.connection=connection;
this.username = "#" + String.valueOf(USERNUMBER);
USERNUMBER++;
try {
String message = "NAME" + "\t" + this.username;
CharBuffer buffer = CharBuffer.wrap(message);
this.getWsOutbound().writeTextMessage(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
users.add(this);
}
@Override
protected void onClose(int status) {
users.remove(this);
}
@Override
protected void onBinaryMessage(ByteBuffer arg0) throws IOException {
}
}
}
解釋
這裡我的想法是
1 每個用戶在訪問的時候首先需要輸入自己的名字,接著向服務端發送連接請求
2 服務端在接受到客戶端的連接請求後,會new ChatWebSocket(users);用於處理這個請求,並把它加入在線的用戶列表中,由於這個時候,服務端尚不知道客戶的名字。它會給這個用戶假定一個名字,#1,然後服務端會發送"NAME" + "\t" +“#1”給客戶端,你叫什麼?
3 客戶端收到這個消息會知道,服務器在問自己叫什麼名字,於是客戶端會發送"NAME"+"\t"+“#1”+"_"+ 自己的名字到服務端,(我叫xxx)
4 服務端收到這個消息後根據#1在當前在線的用戶列表中進行查找,將#1替換為客戶的名字,這樣服務端就知道了這個客戶的名字了
5 當客戶離開時,服務端會觸發onClose事件,服務端會把當前用戶從在線列表中移除
用圖畫出來類似這樣(畫的不好,—_—!!)
代碼
JS
ws = new WebSocket("ws://" + location.host + "/WebSocket/SocketServer");
連接服務端
Java
protected StreamInbound createWebSocketInbound(String arg0,
HttpServletRequest arg1) {
// TODO Auto-generated method stub
return new ChatWebSocket(users);
}
創建一個chatwebsocket用於處理這個請求,觸發該chatwebsocket對象的onOpen事件
@Override
protected void onOpen(WsOutbound outbound) {
// this.connection=connection;
this.username = "#" + String.valueOf(USERNUMBER);
USERNUMBER++;
try {
String message = "NAME" + "\t" + this.username;
CharBuffer buffer = CharBuffer.wrap(message);
this.getWsOutbound().writeTextMessage(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
users.add(this);
}
為這個客戶假定一個姓名,並發送NAME+“\t”+假定的姓名 給該客戶端,同時將該客戶端加入當前連接的客戶列表中
JS
function handleData(data)
{
var vals=data.split("\t");
var msgType=vals[0];
switch(msgType)
{
case "NAME":
var msg=vals[1];
var mes="NAME"+"\t"+msg+"_"+ username;
send(mes);
break;
//………
}
}
接受並處理服務端發來到的消息,發現是服務端問自己叫什麼名字,於是發送”NAME"+"\t"+假定的名字+"_"+ 真正的名字 給服務端
Java
public void onMessage(String data) {
String[] val1 = data.split("\\t");
if(val1[0].equals("NAME"))
{
String[] val2=val1[1].split("_");
for(ChatWebSocket user:users){
if (user.username.equals(val2[0])){
user.username=val2[1];
}
}
}
//………
}
處理並接受客戶端發來的消息,發現是客戶端回復自己叫什麼名字,於是在根據先前假定的名字在當前連接的客戶列表中進行查找,將假名變成真名
JS
function sendMessage()
{
var othername=$("#othername").val();
var msg="MSG\t"+username+"_"+othername+"_"+$("#message").val();
send(msg);
}
客戶對另一個人發起對話,消息格式為:“MSG”+自己的名字+_+對方的名字+_+消息
Java
public void onMessage(String data) {
///…………
else if(val1[0].equals("MSG"))
{
String[] val2=val1[1].split("_");
for(ChatWebSocket user:users){
if (user.username.equals(val2[1])){
try {
CharBuffer temp=CharBuffer.wrap(data);
user.getWsOutbound().writeTextMessage(temp);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
///…………
}
發現是客戶發送的消息,根據對方的姓名,在當前連接的客戶列表中查找,並將消息發給他
JS
function handleData(data)
{
var vals=data.split("\t");
var msgType=vals[0];
switch(msgType)
{
///…
case "MSG":
var val2s=vals[1].split("_");
var from=val2s[0];
var message=val2s[2];
alert(from+":"+message);
break;
default:
break;
}
}
發現是另一個客戶發來的消息,通過alert顯示出來
Java
@Override
protected void onClose(int status) {
users.remove(this);
}
發現客戶離開了,將客戶從連接的客戶列表中移除
可以改進的地方
1.若客戶端A發送消息給B時,B不在線,可將消息存入數據庫中,當發現B上線時,從數據庫中取出,發送給B
2 服務端發送你叫什麼時,可加入超時機制,若客戶端一定時間內沒有回復自己叫什麼,則可將該客戶從在線列表中刪掉