一般客户端和服务端交互是由客户端发起一个请求,服务端回答响应。但有时候服务端需要主动的推送数据,比如视频、弹幕、新闻实时刷新等,这时候就用到了服务器推送技术。
Ajax短轮询就是前端通过ajax不断向服务端发送请求,这种方式最简单但是性能最低,尤其在服务端未使用netty等高性能框架下。
客户端代码样例:
function showTime(){
…//发送请求
setInterval(showTime, 1000);
长轮询是短轮询的变种,服务器使用我在之前的文章里面介绍的DeferredResult,服务器实现异步处理请求,增大服务器响应并发。有兴趣的可以看下之前的文章。
SSE(Server-Sent Events)全称是服务器发送事件。我们知道HTTP协议无法做到服务器主动推送信息,但是SSE协议中由服务器向客户端声明接下来要发送的是流信息,即数据会不断地发送过来。这时客户端不会关闭连接,会一直等着服务器发过来的新的数据流。
SSE基于HTTP协议,目前浏览器基本都支持(除了IE/Edge)。
SSE协议规范:
服务器向浏览器发送的数据,必须是UTF-8编码并且具有如下的HTTP头信息:
每次响应的信息由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式:
[field]: value\n。
field有四种类型的值:
此外,有冒号开头的行表示注释。通常服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
一个JSON数据的示例:
data: {\n
data: "context": "aaa",\n
data: }\n\n
SSE相对WebSocket优势:
缺点:
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();
}
}
WebSocket可以让客户端和服务器进行双向通信。WebSocket只需要建立一次连接,就可以一直保持连接状态。相比于轮询方式的不停建立连接显然效率要大大提高。使用WebSockets需要客户端和服务器都支持WebSockets协议,但不是所有浏览器都支持。
客户端的请求:
服务器端:
Upgrade:websocket
Connection:Upgrade
告诉客户端即将升级的是Websocket协议。Sec-WebSocket-Accept表示是经过服务器确认,并且加密过后的Sec-WebSocket-Key。
Sec-WebSocket-Protocol表示最终使用的协议。
(1)STOMP
WebSocket是个规范,在实际的实现中有HTML5规范中的WebSocket API、WebSocket的子协议STOMP。
STOMP(Simple Text Oriented Messaging Protocol):
客户端代码:
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("" +
" " +
" "+chatValue.val()+"" +
" " +
" " +userName+
" ");
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);
...//业务处理
});
}
前端页面核心代码:
请选择你是谁:
群聊:
请选择你要发给谁:
单聊:
服务端核心代码:
@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 SessionSet
= new CopyOnWriteArraySet();
/**
* 连接建立成功调用的方法
*/
@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{
}
}
}