隨著互聯(lián)網(wǎng)的發(fā)展,傳統(tǒng)的HTTP協(xié)議已經(jīng)很難滿足Web應用日益復雜的需求了。近年來,隨著HTML5的誕生,WebSocket協(xié)議被提出,它實現(xiàn)了瀏覽器與服務器的全雙工通信,擴展了瀏覽器與服務端的通信功能,使服務端也能主動向客戶端發(fā)送數(shù)據(jù)。
我們知道,傳統(tǒng)的HTTP協(xié)議是無狀態(tài)的,每次請求(request)都要由客戶端(如瀏覽器)主動發(fā)起,服務端進行處理后返回response結果,而服務端很難主動向客戶端發(fā)送數(shù)據(jù);這種客戶端是主動方,服務端是被動方的傳統(tǒng)Web模式對于信息變化不頻繁的Web應用來說造成的麻煩較小,而對于涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通信、實時數(shù)據(jù)、訂閱推送等功能的應用。在WebSocket規(guī)范提出之前,開發(fā)人員若要實現(xiàn)這些實時性較強的功能,經(jīng)常會使用折衷的解決方法:輪詢(polling)和Comet技術。其實后者本質上也是一種輪詢,只不過有所改進。
輪詢是最原始的實現(xiàn)實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔周期性地向服務端發(fā)送請求,頻繁地查詢是否有新的數(shù)據(jù)改動。明顯地,這種方法會導致過多不必要的請求,浪費流量和服務器資源。
Comet技術又可以分為長輪詢和流技術。長輪詢改進了上述的輪詢技術,減小了無用的請求。它會為某些數(shù)據(jù)設定過期時間,當數(shù)據(jù)過期后才會向服務端發(fā)送請求;這種機制適合數(shù)據(jù)的改動不是特別頻繁的情況。流技術通常是指客戶端使用一個隱藏的窗口與服務端建立一個HTTP長連接,服務端會不斷更新連接狀態(tài)以保持HTTP長連接存活;這樣的話,服務端就可以通過這條長連接主動將數(shù)據(jù)發(fā)送給客戶端;流技術在大并發(fā)環(huán)境下,可能會考驗到服務端的性能。
這兩種技術都是基于請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了一定流量在相同的頭部信息上,并且開發(fā)復雜度也較大。
伴隨著HTML5推出的WebSocket,真正實現(xiàn)了Web的實時通信,使B/S模式具備了C/S模式的實時通信能力。WebSocket的工作流程是這樣的:瀏覽器通過JavaScript向服務端發(fā)出建立WebSocket連接的請求,在WebSocket連接建立成功后,客戶端和服務端就可以通過TCP連接傳輸數(shù)據(jù)。因為WebSocket連接本質上是TCP連接,不需要每次傳輸都帶上重復的頭部數(shù)據(jù),所以它的數(shù)據(jù)傳輸量比輪詢和Comet技術小了很多。本文不詳細地介紹WebSocket規(guī)范,主要介紹下WebSocket在Java Web中的實現(xiàn)。
JavaEE 7中出了JSR-356:Java API for WebSocket規(guī)范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat從7.0.27開始支持WebSocket,從7.0.47開始支持JSR-356,下面的Demo代碼也是需要部署在Tomcat7.0.47上才能運行。
對瀏覽器的支持情況:
新建一個dynamic web項目:
客戶端(Web主頁)代碼:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 | <%@pagelanguage= "java" import= "java.util.*" pageEncoding= "UTF-8" %> <% Stringpath=request.getContextPath(); StringbasePath=request.getScheme()+ "://" +request.getServerName()+ ":" +request.getServerPort()+path+ "/" ; %> <!DOCTYPEHTML> <html>
<head>
<basehref= "<%=basePath%>" >
<title>MyWebSocket</title>
</head>
<body>
Welcome<br/>
<inputid= "text" type= "text" /><buttononclick= "send()" >Send</button><buttononclick= "closeWebSocket()" >Close</button>
<divid= "message" >
</div>
</body>
<scripttype= "text/Javascript" >
var websocket= null ;
//判斷當前瀏覽器是否支持WebSocket
if ( 'WebSocket' in window){
websocket= new WebSocket( "ws://localhost:8080/MyWebSocket/websocket" );
}
else {
alert( 'Notsupportwebsocket' )
}
//連接發(fā)生錯誤的回調方法
websocket.onerror= function (){
setMessageInnerHTML( "error" );
};
//連接成功建立的回調方法
websocket.onopen= function (event){
setMessageInnerHTML( "open" );
}
//接收到消息的回調方法
websocket.onmessage= function (){
setMessageInnerHTML(event.data);
}
//連接關閉的回調方法
websocket.onclose= function (){
setMessageInnerHTML( "close" );
}
//監(jiān)聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
window.onbeforeunload= function (){
websocket.close();
}
//將消息顯示在網(wǎng)頁上
function setMessageInnerHTML(innerHTML){
document.getElementById( 'message' ).innerHTML+=innerHTML+ '<br/>' ;
}
//關閉連接
function closeWebSocket(){
websocket.close();
}
//發(fā)送消息
function send(){
var message=document.getElementById( 'text' ).value;
websocket.send(message);
}
</script> </html> |
Java Web后端代碼
創(chuàng)建一個注解為:@ServerEndpoint的webscoket的服務端.供前臺訪問.因為想實現(xiàn)點其它功能.所以在廣播給所有人這個方法里邊加上了type以備區(qū)分
注解說明圖:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 | package com.chen.websocket; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; 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; //該注解用來指定一個URI,客戶端可以通過這個URI來連接到WebSocket。類似Servlet的注解mapping。無需在web.xml中配置。 @ServerEndpoint ( "/websocket" ) public class MyWebSocket{
//靜態(tài)變量,用來記錄當前在線連接數(shù)。應該把它設計成線程安全的。
PRivate static int onlineCount= 0 ;
//concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現(xiàn)服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標識
private static CopyOnWriteArraySet<MyWebSocket>webSocketSet= new CopyOnWriteArraySet<MyWebSocket>();
//與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
private Sessionsession;
/**
*連接建立成功調用的方法
*@paramsession可選的參數(shù)。session為與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
*/
@OnOpen
public void onOpen(Sessionsession){
this .session=session;
webSocketSet.add( this ); //加入set中
addOnlineCount(); //在線數(shù)加1
System.out.println( "有新連接加入!當前在線人數(shù)為" +getOnlineCount());
}
/**
*連接關閉調用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove( this ); //從set中刪除
subOnlineCount(); //在線數(shù)減1
System.out.println( "有一連接關閉!當前在線人數(shù)為" +getOnlineCount());
}
/**
*收到客戶端消息后調用的方法
*@parammessage客戶端發(fā)送過來的消息
*@paramsession可選的參數(shù)
*/
@OnMessage
public void onMessage(Stringmessage,Sessionsession){
System.out.println( "來自客戶端的消息:" +message);
//群發(fā)消息
for (MyWebSocketitem:webSocketSet){
try {
item.sendMessage(message);
} catch (IOExceptione){
e.printStackTrace();
continue ;
}
}
}
/**
*發(fā)生錯誤時調用
*@paramsession
*@paramerror
*/
@OnError
public void onError(Sessionsession,Throwableerror){
System.out.println( "發(fā)生錯誤" );
error.printStackTrace();
}
/**
*這個方法與上面幾個方法不一樣。沒有用注解,是根據(jù)自己需要添加的方法。
*@parammessage
*@throwsIOException
*/
public void sendMessage(Stringmessage) throws IOException{
this .session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount(){
return onlineCount;
}
public static synchronized void addOnlineCount(){
MyWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount(){
MyWebSocket.onlineCount--;
} } |
該Demo在以下環(huán)境測試過:
1.Jdk7+Tomcat7.0.47
2.Jdk7+Tomcat7.0.52
3.Jdk7+Tomcat8.0.3
4.Jdk7+Glassfish4
注意事項:在Eclipse或者MyEclipse中,需要添加Tomcat的library。直接拷貝Tomcat里lib目錄下的jar包有時會出錯。
新聞熱點
疑難解答