一,Comet-基於Ajax的長輪詢
定義:客戶端向服務器發送Ajax請求,服務器接到請求後hold住連接,直到有新消息才返回響應信息並關閉連接,客戶端處理完響應信息後再向服務器發送新的請求。
- 服務器端會阻塞請求直到有數據傳遞或超時才返回。
- 客戶端 JavaScript 響應處理函數會在處理完服務器返回的信息後,再次發出請求,重新建立連接。
- 當客戶端處理接收的數據、重新建立連接時,服務器端可能有新的數據到達;這些信息會被服務器端保存直到客戶端重新建立連接,客戶端會一次把當前服務器端所有的信息取回。
優點:在無消息的情況下不會頻繁的請求,耗費資源小。
缺點:服務器hold連接會消耗資源,返回數據順序無保證,難於管理維護。
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>My JSP 'index.jsp' starting page</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<link rel="stylesheet"
href="http://apps.bdimg.com/libs/jquerymobile/1.4.5/jquery.mobile-1.4.5.min.css">
<!-- 引入 jQuery 庫 -->
<script src="http://apps.bdimg.com/libs/jquery/1.10.2/jquery.min.js"></script>
<!-- 引入 jQuery Mobile 庫 -->
<script
src="http://apps.bdimg.com/libs/jquerymobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript">
$(function() {
getMsgNum();
});
function getMsgNum() {
$.ajax({
url : 'JsLongPollingMsgServlet',
type : 'post',
dataType : 'json',
data : {
"pageMsgNum" : $("#pageMsgNum").val()
},
timeout : 5000,
success : function(data, textStatus) {
if (data && data.msgNum) {
//請求成功,刷新數據
$("#msgNum").html(data.msgNum);
//這個是用來和後臺數據作對比判斷是否發生了改變
$("#pageMsgNum").val(data.msgNum);
}
if (textStatus == "success") {
//成功之後,再發送請求,遞歸調用
getMsgNum();
}
},
error : function(XMLHttpRequest, textStatus, errorThrown) {
if (textStatus == "timeout") {
//有效時間內沒有響應,請求超時,重新發請求
getMsgNum();
} else {
// 其他的錯誤,如網絡錯誤等
getMsgNum();
}
}
});
}
</script>
</head>
<body>
<div id="page1" data-role="page">
<div data-role="header">
<h1>AJAX長輪詢</h1>
</div>
<div data-role="content">
<input id="pageMsgNum" name="pageMsgNum" type="hidden" /> 您有<span
id="msgNum" style="color: red;">0</span>條消息!
</div>
<div data-role="footer">
<h1>CopyRight 2019</h1>
</div>
</div>
</body>
</html>
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class JsLongPollingMsgServlet
*/
@WebServlet("/JsLongPollingMsgServlet")
public class JsLongPollingMsgServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public JsLongPollingMsgServlet() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
ServletContext application = this.getServletContext();
List msglist= (List)application.getAttribute("msg");
request.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
String pageMsgNumStr = request.getParameter("pageMsgNum");
if(pageMsgNumStr==null || "".equals(pageMsgNumStr)){
pageMsgNumStr = "0";
}
int pageMsgNum = Integer.parseInt(pageMsgNumStr);
int num = 0;
StringBuffer json = null;
while(true){
num = msglist.size();
//數據發生改變 將數據響應客戶端
if(num != pageMsgNum){
json = new StringBuffer("{");
json.append("\"msgNum\":"+num);
json.append("}");
break;
}else{
//沒有新的數據 保持住連接
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
out.write(json.toString());
out.close();
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
二,Comet-基於 Iframe 及 htmlfile 的流(streaming)方式
- 在 iframe 方案的客戶端,iframe 服務器端並不返回直接顯示在頁面的數據,而是返回對客戶端 Javascript 函數的調用,如“<script type="text/javascript">js_func(“data from server ”)</script>”。服務器端將返回的數據作爲客戶端 JavaScript 函數的參數傳遞;客戶端瀏覽器的 Javascript 引擎在收到服務器返回的 JavaScript 調用時就會去執行代碼。
- 每次數據傳送不會關閉連接,連接只會在通信出現錯誤時,或是連接重建時關閉(一些防火牆常被設置爲丟棄過長的連接, 服務器端可以設置一個超時時間, 超時後通知客戶端重新建立連接,並關閉原來的連接)。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<ul id="content"></ul>
<form class="form">
<input type="text" placeholder="請輸入發送的消息" class="message" id="message"/>
<input type="button" value="發送" id="send" class="connect"/>
<input type="button" value="連接" id="connect" class="connect"/>
</form>
<script>
var oUl=document.getElementById('content');
var oConnect=document.getElementById('connect');
var oSend=document.getElementById('send');
var oInput=document.getElementById('message');
var ws=null;
oConnect.onclick=function(){
ws=new WebSocket('ws://localhost:3000');
ws.onopen=function(){
oUl.innerHTML+="<li>客戶端已連接</li>";
}
ws.onmessage=function(evt){
oUl.innerHTML+="<li>"+evt.data+"</li>";
}
ws.onclose=function(){
oUl.innerHTML+="<li>客戶端已斷開連接</li>";
};
ws.onerror=function(evt){
oUl.innerHTML+="<li>"+evt.data+"</li>";
};
};
oSend.onclick=function(){
if(ws){
ws.send(oInput.value);
}
}
</script>
</body>
</html>
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class SetMsg
*/
@WebServlet("/SetMsg")
public class SetMsg extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public SetMsg() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
ServletContext application = this.getServletContext();
List msglist= new ArrayList();
if(application.getAttribute("msg")!=null)
{
msglist=(List)application.getAttribute("msg");
}
msglist.add(request.getParameter("msgstr"));
application.setAttribute("msg", msglist);
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
三,SSH
- SSE 與 WebSocket 作用相似,都是建立瀏覽器與服務器之間的通信渠道,然後服務器向瀏覽器推送信息。
- 總體來說,WebSocket 更強大和靈活。因爲它是全雙工通道,可以雙向通信;SSE 是單向通道,只能服務器向瀏覽器發送,因爲流信息本質上就是下載。如果瀏覽器向服務器發送信息,就變成了另一次 HTTP 請求。
- SSE的優點
- SSE 使用 HTTP 協議,現有的服務器軟件都支持。WebSocket 是一個獨立協議。
- SSE 屬於輕量級,使用簡單;WebSocket 協議相對複雜。
- SSE 默認支持斷線重連,WebSocket 需要自己實現。
- SSE 一般只用來傳送文本,二進制數據需要編碼後傳送,WebSocket 默認支持傳送二進制數據。
- SSE 支持自定義發送的消息類型。
- SSE適用於更新頻繁、低延遲並且數據都是從服務端到客戶端。
通訊協議是基於純文本的簡單協議。
服務器向瀏覽器發送的 SSE 數據,必須是 UTF-8 編碼的文本,具有如下的 HTTP 頭信息。
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
第一行的Content-Type必須指定 MIME 類型爲event-steam。
每一次發送的信息,由若干個message組成,每個message之間用\n\n分隔。每個message內部由若干行組成,每一行都是如下格式。
[field]: value\n
類型爲 data,表示該行包含的是數據。以 data 開頭的行可以出現多次。所有這些行都是該事件的數據。
如果數據很長,可以分成多行,最後一行用\n\n結尾,前面行都用\n結尾。
類型爲 id,表示該行用來聲明事件的標識符,相當於每一條數據的編號。
類型爲 event,表示該行用來聲明事件的類型。瀏覽器在收到數據時,會產生對應類型的事件。默認是message事件。瀏覽器可以用addEventListener()監聽該事件。
類型爲 retry,表示該行用來聲明瀏覽器在連接斷開之後進行再次連接之前的等待時間。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script>
var eventSource;
function start() {
eventSource = new EventSource("HelloServlet");
eventSource.onmessage = function(event) {
document.getElementById("foo").innerHTML = event.data;
};
eventSource.addEventListener("ms",function(){})
}
function close(){
eventSource.close();
}
</script>
</head>
<body>
Time: <span id="foo"></span>
<br><br>
<button onclick="start()">Start</button>
<button onclick="close()">Close</button>
</body>
</html>
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public HelloServlet() {
super();
// TODO Auto-generated constructor stub
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
// ContentType 必須指定爲 text/event-stream
response.setContentType("text/event-stream");
// CharacterEncoding 必須指定爲 UTF-8
response.setCharacterEncoding("UTF-8");
PrintWriter pw = response.getWriter();
// 每次發送的消息必須以\n\n結束
pw.write("event:ms\n data: " + new Date() + " 這是第1次測試\n\n");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
pw.close();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
四,WebSocket
原理:
當客戶端連接服務端的時候,會向服務端發送一個類似下面的http報文:
可以看到,這是一個http get請求報文,注意該報文中有一個upgrade首部,它的作用是告訴服務端需要將通信協議切換到websocket,如果服務端支持websocket協議,那麼它就會將自己的通信協議切換到websocket,同時發給客戶端類似於以下的一個響應報文頭:
返回的狀態碼爲101,表示同意客戶端協議轉換請求,並將它轉換爲websocket協議。以上過程都是利用http通信完成的,稱之爲websocket協議握手(websocket Protocol handshake),進過這握手之後,客戶端和服務端就建立了websocket連接,以後的通信走的都是websocket協議了。所以總結爲websocket握手需要藉助於http協議,建立連接後通信過程使用websocket協議。同時需要了解的是,該websocket連接還是基於我們剛纔發起http連接的那個TCP連接。一旦建立連接之後,我們就可以進行數據傳輸了,websocket提供兩種數據傳輸:文本數據和二進制數據。
服務器端-javax.websocket
客戶端
<%@ 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>
<title>Java後端WebSocket的Tomcat實現</title>
<script type="text/javascript">
var websocket = null;
var host = document.location.host;
function connect() {
//判斷當前瀏覽器是否支持WebSocket
if ('WebSocket' in window) {
var value = document.getElementById("b").value;
websocket = new WebSocket("ws://" + host + "/exam5/websocket/" + value);
//連接發生錯誤的回調方法
websocket.onerror = function() {
setMessageInnerHTML("WebSocket連接發生錯誤");
};
//連接成功建立的回調方法
websocket.onopen = function() {
setMessageInnerHTML("WebSocket連接成功");
}
//接收到消息的回調方法
websocket.onmessage = function(event) {
setMessageInnerHTML(event.data);
}
//連接關閉的回調方法
websocket.onclose = function() {
setMessageInnerHTML("WebSocket連接關閉");
}
//監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
window.onbeforeunload = function() {
closeWebSocket();
}
} else {
alert('當前瀏覽器 Not support websocket')
}
}
//將消息顯示在網頁上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//關閉WebSocket連接
function closeWebSocket() {
websocket.close();
}
//發送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</head>
<body>
Welcome
<br />
<input id="text" type="text" />
<input type="button" onclick="send()" value="發送消息"/>
<br />
<input id="b" type="text" />
<!-- 這裏用於註冊不同的clientId, 多個webSocket客戶端只能同步收到相同clientId的消息 -->
<input type="button" onclick="connect()" value="連接"/>
<hr />
<input type="button" onclick="closeWebSocket()" value="關閉WebSocket連接"/>
<hr />
<div id="message"></div>
</body>
</html>
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ServerEndpoint 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端,
* 註解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
*/
@ServerEndpoint("/websocket/{clientId}")
public class WebSocket {
// 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
private static AtomicInteger onlineCount = new AtomicInteger(0);
// concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。
//若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以爲用戶標識
// private static CopyOnWriteArraySet<WebSocket> webSocketSet = new
// CopyOnWriteArraySet<WebSocket>();
// 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
//記錄每個客戶端的實例變量, 現在拿下面的全局map記錄
//private Session session;
private static Map<String, Session> webSocketMap = new ConcurrentHashMap<String, Session>();
/**
* 連接建立成功調用的方法
*
* @param session 可選的參數。session爲與某個客戶端的連接會話,需要通過它來給客戶端發送數據
*/
@OnOpen
public void onOpen(@PathParam("clientId") String clientId, Session session) {
// 用登錄用戶編號和sessionId的拼接來做webSocket通信的唯一標識
String key = getWebSocketMapKey(clientId, session);
webSocketMap.put(key, session);
addOnlineCount(); // 在線數加1
System.out.println("WebSocket有新連接加入!當前在線人數爲" + getOnlineCount());
}
/**
* 連接關閉調用的方法
*/
@OnClose
public void onClose(@PathParam("clientId") String clientId, Session session, CloseReason closeReason) {
String key = getWebSocketMapKey(clientId, session);
webSocketMap.remove(key, session);
subOnlineCount(); // 在線數減1
System.out.println("WebSocket有一連接關閉!當前在線人數爲" + getOnlineCount());
}
/**
* 收到客戶端消息後調用的方法
*
* @param message 客戶端發送過來的消息
* @param session 可選的參數
*/
@OnMessage
public void onMessage(@PathParam("clientId") String clientId, String message, Session session) {
System.out.println("WebSocket收到來自客戶端的消息:" + message);
sendMessageByClientId(clientId, message);
}
/**
* 獲取webSocketMap集合的Key
*
* @param clientId 用戶編號
* @param session webSocket的Session
* @return
*/
private String getWebSocketMapKey(String clientId, Session session) {
if (clientId==null) {
return session.getId();
} else {
return clientId + "_" + session.getId();
}
}
/**
* 發生錯誤時調用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("WebSocket發生錯誤");
}
// 羣發消息
public static void doSend(String message) {
if (webSocketMap.size() > 0) {
for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) {
try {
sendMessage(entry.getValue(), message);
} catch (IOException e) {
System.out.println("WebSocket doSend is error:");
continue;
}
}
}
}
public static void sendMessage(Session session, String message) throws IOException {
session.getBasicRemote().sendText(message);
}
public static int sendMessageByClientIdList(List<String> clientIdList, String message) {
int status = 0;
for (String clientId : clientIdList) {
status = sendMessageByClientId(clientId, message);
}
return status;
}
/**
* 通過用戶的編號來發送webSocket消息
*
* @param clientId
* @param message
*/
public static int sendMessageByClientId(String clientId, String message) {
int status = 0;
if (webSocketMap.size() > 0) {
for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) {
try {
String key = entry.getKey();
// 判斷webSocketMap中的clientId和發送的clientId是否相同
// 若相同則進行發送消息
String key1 = key.substring(0, key.lastIndexOf("_"));
if (key1.equals(clientId)) {
sendMessage(entry.getValue(), message);
status = 200;
}
} catch (IOException e) {
System.out.println("WebSocket doSend is error:");
continue;
}
}
}
return status;
}
public static void sendSpeechMessageByClientId(String clientId, String message) {
if (webSocketMap.size() > 0) {
for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) {
try {
String key = entry.getKey();
// 判斷webSocketMap中的clientId和發送的clientId是否相同
// 若相同則進行發送消息
String key1 = key.substring(0, key.lastIndexOf("_"));
if (key1.equals(clientId)) {
sendMessage(entry.getValue(), message);
}
} catch (IOException e) {
System.out.println("WebSocket doSend is error:");
continue;
}
}
}
}
public static synchronized AtomicInteger getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount.getAndIncrement();
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount.getAndDecrement();
}
}