一般客戶端和服務端交互是由客戶端發起一個請求,服務端回答響應。但有時候服務端需要主動的推送數據,比如視頻、彈幕、新聞實時刷新等,這時候就用到了服務器推送技術。
1.Ajax短輪詢
Ajax短輪詢就是前端通過ajax不斷向服務端發送請求,這種方式最簡單但是性能最低,尤其在服務端未使用netty等高性能框架下。
客戶端代碼樣例:
function showTime(){
…//發送請求
setInterval(showTime, 1000);
2.Ajax長輪詢
長輪詢是短輪詢的變種,服務器使用我在之前的文章裏面介紹的DeferredResult,服務器實現異步處理請求,增大服務器響應併發。有興趣的可以看下之前的文章。
3.SSE
SSE(Server-Sent Events)全稱是服務器發送事件。我們知道HTTP協議無法做到服務器主動推送信息,但是SSE協議中由服務器向客戶端聲明接下來要發送的是流信息,即數據會不斷地發送過來。這時客戶端不會關閉連接,會一直等着服務器發過來的新的數據流。
SSE基於HTTP協議,目前瀏覽器基本都支持(除了IE/Edge)。
SSE協議規範:
服務器向瀏覽器發送的數據,必須是UTF-8編碼並且具有如下的HTTP頭信息:
- Content-Type: text/event-stream
- Cache-Control: no-cache
- Connection: keep-alive
每次響應的信息由若干個message組成,每個message之間用\n\n分隔。每個message內部由若干行組成,每一行都是如下格式:
[field]: value\n。
field有四種類型的值:
- data:數據內容
- event:信息類型
- id:數據標識符,瀏覽器用lastEventId屬性讀取這個值。一旦連接斷線,瀏覽器會發送一個 HTTP 頭,裏面包含一個Last-Event-ID頭信息,將這個值發送回來,用來幫助服務器端重建連接。
- retry:最大間隔時間
此外,有冒號開頭的行表示註釋。通常服務器每隔一段時間就會向瀏覽器發送一個註釋,保持連接不中斷。
一個JSON數據的示例:
data: {\n
data: "context": "aaa",\n
data: }\n\n
SSE相對WebSocket優勢:
- SSE基於HTTP協議,現有的服務器軟件都支持。WebSocket是一個獨立協議。
- SSE使用簡單,WebSocket協議相對複雜。
- SSE默認支持斷線重連,WebSocket需要自己實現。
- SSE支持自定義發送的消息類型。
缺點:
WebSocket更強大和靈活。因爲它是全雙工通道,可以雙向通信;SSE是單向通道,只能服務器向瀏覽器發送,因爲流信息本質上就是下載。
前端核心代碼:
if(!!window.EventSource){//判斷瀏覽器支持度
//拿到sse的對象
var source = new EventSource('needPrice');
//接收到服務器的消息
source.onmessage=function (e) {
var dataObj=e.data;
....//業務處理
};
source.onopen=function (e) {
};
source.onerror=function () {
};
}else{
$("#hint").html("您的瀏覽器不支持SSE!");
}
服務端核心代碼:
public void push(HttpServletResponse response){
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");
Random r = new Random();
int sendCount = 0;
try {
PrintWriter pw = response.getWriter();
while(true){
if(pw.checkError()){
return;
}
Thread.sleep(1000);
//字符串拼接
StringBuilder sb = new StringBuilder("");
sb//.append("retry:2000\n")
.append("data:")
.append((r.nextInt(1000)+50)+",")
.append((r.nextInt(800)+100)+",")
.append((r.nextInt(2000)+150)+",")
.append((r.nextInt(1500)+100)+",")
.append("\n\n");
pw.write(sb.toString());
pw.flush();
sendCount++;
if(sendCount>=100){
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
4.WebSocket
WebSocket可以讓客戶端和服務器進行雙向通信。WebSocket只需要建立一次連接,就可以一直保持連接狀態。相比於輪詢方式的不停建立連接顯然效率要大大提高。使用WebSockets需要客戶端和服務器都支持WebSockets協議,但不是所有瀏覽器都支持。
4.1 通信規範:
客戶端的請求:
- Connection必須設置Upgrade,表示連接升級。
- Upgrade字段必須設置Websocket,表示升級到Websocket協議。
- Sec-WebSocket-Key是隨機的字符串,服務器端會用這些數據來構造出一個SHA-1的信息摘要。 Sec-WebSocket-Key加上一個特殊字符串計算SHA-1摘要,再進行BASE-64編碼,將結果做爲Sec-WebSocket-Accept 頭的值返回給客戶端。如此操作,可以儘量避免普通 HTTP 請求被誤認爲 Websocket 協議。
- Sec-WebSocket-Version表示支持的Websocket版本。
服務器端:
Upgrade:websocket
Connection:Upgrade
告訴客戶端即將升級的是Websocket協議。Sec-WebSocket-Accept表示是經過服務器確認,並且加密過後的Sec-WebSocket-Key。
Sec-WebSocket-Protocol表示最終使用的協議。
4.2 代碼示例:
(1)STOMP
WebSocket是個規範,在實際的實現中有HTML5規範中的WebSocket API、WebSocket的子協議STOMP。
STOMP(Simple Text Oriented Messaging Protocol):
- 簡單(流)文本定向消息協議
- STOMP協議專爲消息中間件設計,屬於消息隊列的一種協議, 和AMQP, JMS平級。STOMP協議很多MQ都已支持, 比如RabbitMq, ActiveMq。
- 生產者(發送消息)、消息代理、消費者(訂閱然後收到消息)
- STOMP是基於幀的協議
客戶端代碼:
var stompClient = null;
$(function(){
//連接SockJS的endpoint名稱爲"endpointMark"
var socket = new SockJS('/endpointMark');
stompClient = Stomp.over(socket);//使用STMOP子協議的WebSocket客戶端
stompClient.connect({},function(frame){//連接WebSocket服務端
console.log('Connected:' + frame);
//廣播接收信息
stompTopic();
});
})
//關閉瀏覽器時關閉連接
window.onunload = function() {
if(stompClient != null) {
stompClient.disconnect();
}
}
//一對多,發起訂閱
function stompTopic(){
//通過stompClient.subscribe訂閱目標(destination)發送的消息(廣播接收信息)
stompClient.subscribe('/mass/getResponse',function(response){
var message=JSON.parse(response.body);
...//業務處理
});
}
//羣發消息
function sendMassMessage(){
var postValue={};
var chatValue=$("#sendChatValue");
var userName=$("#selectName").val();
postValue.name=userName;
postValue.chatValue=chatValue.val();
stompClient.send("/massRequest",{},JSON.stringify(postValue));
chatValue.val("");
}
//單獨發消息
function sendAloneMessage(){
var postValue={};
var chatValue=$("#sendChatValue2");
var userName=$("#selectName").val();
var sendToId=$("#selectName2").val();
var response = $("#alone_div");
postValue.name=userName;//發送者姓名
postValue.chatValue=chatValue.val();//聊天內容
postValue.userId=sendToId;//發送給誰
stompClient.send("/aloneRequest",{},JSON.stringify(postValue));
response.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+chatValue.val()+"</span>" +
" <i class='triangle-user'></i>" +
" </div>" +userName+
" </div>");
chatValue.val("");
}
//一對一,發起訂閱
function stompQueue(){
var userId=$("#selectName").val();
alert("監聽:"+userId)
//通過stompClient.subscribe訂閱目標(destination)發送的消息(隊列接收信息)
stompClient.subscribe('/queue/' + userId + '/alone',
function(response){
var message=JSON.parse(response.body);
...//業務處理
});
}
前端頁面核心代碼:
<body>
<div>
<div style="float:left;width:47%">
<p>請選擇你是誰:
<select id="selectName" onchange="stompQueue();">
<option value="1">請選擇</option>
<option value="Mark">Mark</option>
<option value="James">James</option>
<option value="Lison">Lison</option>
<option value="Peter">Peter</option>
<option value="King">King</option>
</select>
</p>
<div class="chatWindow">
<p style="color:darkgrey">羣聊:</p>
<section id="chatRecord1" class="chatRecord">
<div id="mass_div" class="mobile-page">
</div>
</section>
<section class="sendWindow">
<textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea>
<input type="button" name="sendMessage" id="sendMassMessage" class="sendMessage" onclick="sendMassMessage()" value="發送">
</section>
</div>
</div>
<div style="float:right; width:47%">
<p>請選擇你要發給誰:
<select id="selectName2">
<option value="1">請選擇</option>
<option value="A">A</option>
<option value="B">B</option>
</select>
</p>
<div class="chatWindow">
<p style="color:darkgrey">單聊:</p>
<section id="chatRecord2" class="chatRecord">
<div id="alone_div" class="mobile-page">
</div>
</section>
<section class="sendWindow">
<textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea>
<input type="button" name="sendMessage" id="sendAloneMessage" class="sendMessage" onclick="sendAloneMessage()" value="發送">
</section>
</div>
</div>
</div>
<!-- 獨立JS -->
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script th:src="@{wechat_room.js}"></script>
</body>
服務端核心代碼:
@Configuration
/*開啓使用Stomp協議來傳輸基於消息broker的消息
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*註冊STOMP協議的節點(endpoint),並映射指定的url,
* 添加一個訪問端點“/endpointMark”,客戶端打開雙通道時需要的url,
* 允許所有的域名跨域訪問,指定使用SockJS協議。*/
registry.addEndpoint("/endpointMark")
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/*配置一個消息代理
* mass 負責羣聊
* queue 單聊*/
registry.enableSimpleBroker(
"/mass","/queue");
//一對一的用戶,請求發到/queue
registry.setUserDestinationPrefix("/queue");
}
}
@Controller
public class StompController {
@Autowired
private SimpMessagingTemplate template;/*Spring實現的一個發送模板類*/
/*消息羣發,接受發送至自massRequest的請求*/
@MessageMapping("/massRequest")
@SendTo("/mass/getResponse")
//SendTo 發送至 Broker 下的指定訂閱路徑mass ,
// Broker再根據getResponse發送消息到訂閱了/mass/getResponse的用戶處
public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
return response;
}
/*單獨聊天,接受發送至自aloneRequest的請求*/
@MessageMapping("/aloneRequest")
//@SendToUser
public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
//會發送到訂閱了 /user/{用戶的id}/alone 的用戶處
this.template.convertAndSendToUser(chatRoomRequest.getUserId()
+"","/alone",response);
return response;
}
}
(2)websocket
前端核心代碼:
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("遺憾:您的瀏覽器不支持WebSocket");
} else {
console.log("恭喜:您的瀏覽器支持WebSocket");
//實現化WebSocket對象
//指定要連接的服務器地址與端口建立連接
//ws對應http、wss對應https。
socket = new WebSocket("ws://localhost:8080/ws/asset");
//連接打開事件
socket.onopen = function() {
socket.send("消息內容");
};
//收到消息事件
socket.onmessage = function(msg) {
};
//連接關閉事件
socket.onclose = function() {
};
//發生了錯誤事件
socket.onerror = function() {
}
//窗口關閉時,關閉連接
window.unload=function() {
socket.close();
};
}
後端核心代碼:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@ServerEndpoint(value = "/ws/asset")
@Component
public class WebSocketServer {
private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
// concurrent包的線程安全Set,用來存放每個客戶端對應的Session對象。
private static CopyOnWriteArraySet<Session> SessionSet
= new CopyOnWriteArraySet<Session>();
/**
* 連接建立成功調用的方法
*/
@OnOpen
public void onOpen(Session session) {
SessionSet.add(session);
int cnt = OnlineCount.incrementAndGet(); // 在線數加1
SendMessage(session, "連接成功");
}
/**
* 連接關閉調用的方法
*/
@OnClose
public void onClose(Session session) {
SessionSet.remove(session);
int cnt = OnlineCount.decrementAndGet();
}
/**
* 收到客戶端消息後調用的方法
*
* @param message
* 客戶端發送過來的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
SendMessage(session, "收到消息,消息內容:"+message);
}
/**
* 出現錯誤
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 發送消息,實踐表明,每次瀏覽器刷新,session會發生變化。
* @param session
* @param message
*/
public static void SendMessage(Session session, String message) {
try {
session.getBasicRemote()
.sendText(String.format("%s (From Server,Session ID=%s)",
message,session.getId()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 羣發消息
* @param message
* @throws IOException
*/
public static void BroadCastInfo(String message) throws IOException {
for (Session session : SessionSet) {
if(session.isOpen()){
SendMessage(session, message);
}
}
}
/**
* 指定Session發送消息
* @param sessionId
* @param message
* @throws IOException
*/
public static void SendMessage(String sessionId,String message)
throws IOException {
Session session = null;
for (Session s : SessionSet) {
if(s.getId().equals(sessionId)){
session = s;
break;
}
}
if(session!=null){
SendMessage(session, message);
}
else{
}
}
}