開發(fā)環(huán)境:Windows x64,Eclipse 4.6.2,Tomcat 7.0.79,Jdk 1.8。
介紹
現(xiàn)很多網(wǎng)站為了實現(xiàn)即時通訊,所用的技術(shù)都是輪詢(polling)。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務(wù)器發(fā)出HTTP request,然后由服務(wù)器返回最新的數(shù)據(jù)給客服端的瀏覽器。這種傳統(tǒng)的HTTP request 的模式帶來很明顯的缺點 – 瀏覽器需要不斷的向服務(wù)器發(fā)出請求,然而HTTP request 的header是非常長的,里面包含的數(shù)據(jù)可能只是一個很小的值,這樣會占用很多的帶寬。
而最比較新的技術(shù)去做輪詢的效果是Comet – 用了AJAX。但這種技術(shù)雖然可達到全雙工通信,但依然需要發(fā)出請求。
在 WebSocket API,瀏覽器和服務(wù)器只需要要做一個握手的動作,然后,瀏覽器和服務(wù)器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送。
依賴:
Tomcat 7 或者 J2EE7
我用的tomcat 7.0.79和Java jdk1.8.0(1.7Linux服務(wù)器架設(shè)有問題),我使用的時候,沒有需要自己手動引用別的jar包,tomcat默認jar包就可以運行。
遇到的問題:
開始用的eclipse4.3版本,不支持jdk1.8,后改用的4.6.2。
雖然tomcat7.0.47就已支持WebSocket,但是tomcat7.0.47版本只支持jdk1.6,所以改用tomcat7.0.79.
注意:早前業(yè)界沒有統(tǒng)一的標準,各服務(wù)器都有各自的實現(xiàn),現(xiàn)在J2EE7的JSR356已經(jīng)定義了統(tǒng)一的標準,請盡量使用支持最新通用標準的服務(wù)器。
詳見:
http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html
http://jinnianshilongnian.iteye.com/blog/1909962
用nginx做反向代理的需要注意啦,socket請求需要做特殊配置的,切記!
##服務(wù)端
服務(wù)端使用注解模式后,不需要在web.xml中做額外的配置,Tomcat啟動后就可以直接連接了(去掉注解,使用web.xml中配置后,啟動服務(wù)報錯cannot be cast to javax.servlet.Servlet)。
源碼:
package com.hdtytech.websocket;
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/websocket")?
public class MyWebSocket{
? ? //與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
? ? private Session session;
? ? /**
? ? * 連接建立成功調(diào)用的方法
? ? * @param session? 可選的參數(shù)。session為與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
? ? * @throws Exception
? ? */
? ? @OnOpen
? ? public void onOpen(Session session) throws Exception{
? ? ? ? this.session = session;
? ? ? ? WebSocketMapUtil.put(session.getQueryString(),this);
? ? }
? ? /**
? ? * 連接關(guān)閉調(diào)用的方法
? ? * @throws Exception
? ? */
? ? @OnClose
? ? public void onClose() throws Exception{
? ? //從map中刪除
? ? WebSocketMapUtil.remove(session.getQueryString());
? ? }
? ? /**
? ? * 收到客戶端消息后調(diào)用的方法
? ? * @param message 客戶端發(fā)送過來的消息
? ? * @param session 可選的參數(shù)
? ? * @throws IOException
? ? */
? ? @OnMessage
? ? public void onMessage(String message, Session session) throws IOException {
? ? ? ? try {
? ? ? ? MyWebSocket myWebSocket= ((MyWebSocket) WebSocketMapUtil.get(session.getQueryString().replace("service", "client")));
? ? ? ? if(myWebSocket != null){
? ? ? ? myWebSocket.sendMessage(message);
? ? ? ? }
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
? ? /**
? ? * 發(fā)生錯誤時調(diào)用
? ? * @param session
? ? * @param error
? ? */
? ? @OnError
? ? public void onError(Session session, Throwable error){
? ? ? ? error.printStackTrace();
? ? }
? ? /**
? ? * 發(fā)送消息方法。
? ? * @param message
? ? * @throws IOException
? ? */
? ? public void sendMessage(String message) throws IOException{
? ? ? ? this.session.getBasicRemote().sendText(message);
? ? }
? ? /**
? ? * 群發(fā)消息方法。
? ? * @param message
? ? * @throws IOException
? ? */
? ? public void sendMessageAll(String message) throws IOException{
? ? for(MyWebSocket myWebSocket : WebSocketMapUtil.getValues()){
? ? myWebSocket.sendMessage(message);
? ? }
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
##客戶端
源碼:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML>
<html>
? <head>
? ? <base href="<%=basePath%>">
? ? <title>My WebSocket</title>
? </head>
? <body>
? ? Welcome<br/>
? ? <input id="text" type="text" /><button onclick="send()">Send</button>? ? <button onclick="closeWebSocket()">Close</button>
? ? <div id="message">
? ? </div>
? </body>
? <script type="text/JavaScript">
? ? ? var websocket = null;
? ? ? //判斷當(dāng)前瀏覽器是否支持WebSocket
? ? ? if('WebSocket' in window){
? ? ? ? ? websocket = new WebSocket("ws://localhost:8080/MyWebSocket/websocket?client123");
? ? ? }
? ? ? else{
? ? ? ? ? alert('Not support websocket');
? ? ? }
? ? ? //連接發(fā)生錯誤的回調(diào)方法
? ? ? websocket.onerror = function(){
? ? ? ? ? setMessageInnerHTML("error");
? ? ? };
? ? ? //連接成功建立的回調(diào)方法
? ? ? websocket.onopen = function(event){
? ? ? ? ? setMessageInnerHTML("open");
? ? ? };
? ? ? //接收到消息的回調(diào)方法
? ? ? websocket.onmessage = function(event){
? ? ? ? ? setMessageInnerHTML(event.data);
? ? ? };
? ? ? //連接關(guān)閉的回調(diào)方法
? ? ? websocket.onclose = function(){
? ? ? ? ? setMessageInnerHTML("close");
? ? ? };
? ? ? //監(jiān)聽窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時,主動去關(guān)閉websocket連接,防止連接還沒斷開就關(guān)閉窗口,server端會拋異常。
? ? ? window.onbeforeunload = function(){
? ? ? ? ? websocket.close();
? ? ? };
? ? ? //將消息顯示在網(wǎng)頁上
? ? ? function setMessageInnerHTML(innerHTML){
? ? ? ? ? document.getElementById('message').innerHTML += innerHTML + '<br/>';
? ? ? }
? ? ? //關(guān)閉連接
? ? ? function closeWebSocket(){
? ? ? ? ? websocket.close();
? ? ? }
? ? ? //發(fā)送消息
? ? ? function send(){
? ? ? ? ? var message = document.getElementById('text').value;
? ? ? ? ? websocket.send(message);
? ? ? }
? </script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
##工具類ConcurrentMap
用來存儲唯一key和連接(ConcurrentMap是線性安全的Map)
這個是我業(yè)務(wù)的需要,因為我的業(yè)務(wù)是服務(wù)器需要主動點對點發(fā)送消息給客戶端,所以需要用Map來存儲客戶端連接。
遇到的問題:
本來想直接調(diào)用服務(wù)端的發(fā)送接口,從Map中取到需要發(fā)送的客戶端連接信息,但是只要在外部調(diào)用MapUtil,MapUtil就會重新實例化,Map為空,也就是把連接放在單獨靜態(tài)Map中不可行。
后來也考慮把Map放在session中,但是考慮到session容易丟失,就放棄了這個想法。
又考慮使用memcache,但是在往memcache放的時候,報需要序列化的錯,也放棄了這個想法。
解決辦法:
后來就模仿微信聊天一樣,把服務(wù)端做為一個中間轉(zhuǎn)發(fā)消息的中間件,在服務(wù)端定義一個客戶端類,在需要給客戶端發(fā)送消息時,調(diào)用客戶端類, 發(fā)送消息到中間件,通過中間件轉(zhuǎn)發(fā)消息到你想發(fā)送的客戶端,若需要取法消息,則可便利Map客戶端進行群發(fā)通知公告等。
源碼:
package com.hdtytech.websocket;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class WebSocketMapUtil {
public static ConcurrentMap<String, MyWebSocket> webSocketMap = new ConcurrentHashMap<>();
? ? public static void put(String key, MyWebSocket myWebSocket){
? ? webSocketMap.put(key, myWebSocket);
? ? }
? ? public static MyWebSocket get(String key){
? ? return webSocketMap.get(key);
? ? }
public static void remove(String key){
webSocketMap.remove(key);
}
public static Collection<MyWebSocket> getValues(){
return webSocketMap.values();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
##服務(wù)端用于主動發(fā)消息的客戶端類
源碼:
package com.hdtytech.websocket;
import java.io.IOException;
import java.net.URI;
import javax.websocket.ClientEndpoint;
import javax.websocket.ContainerProvider;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
@ClientEndpoint
public class MyClient {
private Session session;
@OnOpen
public void onOpen(Session session) throws IOException {
this.session = session;
}
@OnMessage
public void onMessage(String message) {
}
@OnError
public void onError(Throwable t) {
t.printStackTrace();
}
/**
? ? * 連接關(guān)閉調(diào)用的方法
? ? * @throws Exception
? ? */
? ? @OnClose
? ? public void onClose() throws Exception{
? ? }
? ? /**
? ? * 關(guān)閉鏈接方法
? ? * @param message
? ? * @throws IOException
? ? */
? ? public void closeSocket() throws IOException{
? ? ? ? this.session.close();
? ? }
? ? /**
? ? * 發(fā)送消息方法。
? ? * @param message
? ? * @throws IOException
? ? */
? ? public void sendMessage(String message) throws IOException{
? ? ? ? this.session.getBasicRemote().sendText(message);
? ? }
? ? //啟動客戶端并建立鏈接
? ? public void start(String uri) {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
try {
this.session = container.connectToServer(MyClient.class, URI.create(uri));
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
##測試main函數(shù)
說明:為了測試,寫了一個timer,每個兩秒,調(diào)用發(fā)送給客戶端發(fā)送一條消息;
源碼:
package com.hdtytech.websocket;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
public class MyClientApp {
public static void main(String[] args){
// TODO todo.generated by zoer?
? ? ? ? Timer timer = new Timer();?
? ? ? ? timer.schedule(new MyTask(), 1000, 2000);?
}
static int num = 0;
static class MyTask extends TimerTask {?
? ? @Override?
? ? public void run() {?
? ? MyClient client = new MyClient();
String uri = "ws://localhost:8080/MyWebSocket/websocket?service123";
client.start(uri);
try {
num++;
client.sendMessage("消息測試"+num);
client.closeSocket();
System.out.println(num);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
? ? }?
}
}
————————————————
版權(quán)聲明:本文為CSDN博主「君子丨莫笑」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/gaolele_92/article/details/76158087