HTTP是无状态、单向的协议,用户只能够通过客服端向服务器发送请求并由服务器处理发回一个响应。
一、AJAX长轮询(Long Polling via AJAX)
Comet 翻译为彗星,彗星长长的尾巴形容服务器和客户端需要建立一个“长”链接。基于这种架构开发的应用中,服务器端可以主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。
Comet 架构非常适合事件驱动的 Web 应用,以及对交互性和实时性要求很强的应用,如股票交易行情分析、聊天室和 Web 版在线游戏等。
AJAX长轮询简单来说,需要服务器与客户端需要保持一条长时间的请求。
客户端给服务器发送了一个Ajax(异步JavaScript和XML)请求,然后等待服务器响应,只要一有事件发生,服务器端就会在挂起的请求中送回响应。如果第一次的请求返回数据后,第二次请求会立刻发出,这种技术就称为Ajax 长轮询。
- 优点: 减少轮询次数,低延迟,浏览器兼容性较好。
- 缺点: 服务器需要保持大量连接。
实操
下面是一个使用 Vue 2 和 Java 实现的 AJAX 长轮询的示例:
客户端(Vue 2):
<template>
<div>
<p>服务器时间: {{ serverTime }}</p>
</div>
</template>
<script>
export default {
data() {
return {
serverTime: ''
};
},
mounted() {
this.longPolling();
},
methods: {
longPolling() {
const self = this;
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
self.serverTime = response.time;
self.longPolling();
}
};
xhr.open('GET', '/api/long-polling', true);
xhr.send();
}
}
};
</script>
服务端(spring):
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;
@RestController
public class LongPollingController {
@GetMapping("/api/long-polling")
public ResponseEntity<?> longPolling() {
// 模拟一个长时间的操作
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(5000, 10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
// 构造响应数据
Map<String, Object> response = new HashMap<>();
response.put("time", new Date());
return ResponseEntity.ok(response);
}
}
客户端收到响应后,更新页面上的服务器时间,并再次发起长轮询请求。这样就实现了一个基本的 AJAX 长轮询机制。
长轮询是一种比较老旧的实时通信方式,现在更常用的是 WebSocket 技术。
二、WebSockets协议
与HTTP不同,WebSockets是一种通过TCP工作的有状态通信协议。
通信最初是作为HTTP握手开始的,但如果两个通信方同意,则通过WebSockets通信。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输(全双工)。
- 优点:更强的实时性,由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少
- 缺点:即使Websockets服务端已经发现连接断开,仍然没有办法推送数据,只能被动等待客户端重新建立好连接才能推送,在此之前数据将可能会被采取丢弃的措施处理掉。
实操
以下是一个使用 Spring 框架实现 WebSocket 的简单示例:
首先,需要创建一个 WebSocket 处理器类来处理 WebSocket 连接和消息的交互:
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
@Component
public class MyWebSocketHandler implements WebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("WebSocket连接已建立");
}
@Override
public void handleMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("接收到WebSocket消息:" + message.getPayload());
session.sendMessage(new TextMessage("服务器已收到消息:" + message.getPayload()));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("WebSocket连接已关闭");
}
}
接下来,需要配置 WebSocket 的处理器和端点:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MyWebSocketHandler myWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myWebSocketHandler, "/websocket")
.setAllowedOrigins("*");
}
}
在上述配置中,将 MyWebSocketHandler
注册为 WebSocket 处理器,并将其映射到 /websocket
路径上。
最后,创建一个简单的控制器来返回包含 WebSocket 客户端的页面:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "index.html";
}
}
在上述控制器中,当访问根路径 /
时,返回名为 index.html
的页面。
创建 index.html
文件,并在其中编写 WebSocket 相关的 JavaScript 代码:
通过监听 onmessage
事件来接收服务器发送的消息,并将其显示在页面上。同时提供一个输入框和按钮,用于发送消息给服务器。
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Demo</title>
</head>
<body>
<h1>WebSocket Demo</h1>
<div id="output"></div>
<script>
const socket = new WebSocket("ws://localhost:8080/websocket");
socket.onopen = function(event) {
console.log("WebSocket连接已建立");
};
socket.onmessage = function(event) {
const outputDiv = document.getElementById("output");
outputDiv.innerHTML += "<p>收到服务器消息:" + event.data + "</p>";
};
socket.onclose = function(event) {
console.log("WebSocket连接已关闭");
};
function sendMessage() {
const message = document.getElementById("message").value;
socket.send(message);
}
</script>
<input type="text" id="message" placeholder="请输入消息">
<button onclick="sendMessage()">发送消息</button>
</body>
</html>
三、总结
在弱网情况下,TCP长连接会经常性的断开,使用以上两种方法均有弊端,短轮询是弱网情况下更好的选择。还是要根据不同的使用场景选择服务器推送技术。