一、双向通信常用解决方案
常用单向通信,前端发出请求,服务端作出响应。很多场景下需要服务端主动向客户端发出通知,常见解决方案:
1、轮询
1)简单轮询:间隔固定时间,向服务端发送请求来刷新页面数据。缺点:建立连接,资源消耗
2)变间隔轮询:间隔不固定,若返回数据无变化,则延长请求间隔时间,反之有变化则缩短请求间隔时间
3)带缓存的轮询:设置数据失效时间,优先从客户端缓存中获取,失效后从服务端获取。确定:实时性差
2、长连接:客户端与服务端建立一个隐藏的长连接,用于服务端向客户端发送通知,基于http层面,对服务端性能影响较大
3、WebSocket
二、WebSocket实现原理
1、websocket简介
1)本质上是一个TCP连接,浏览器和服务器通过套接字建立一个持久连接
2)支持全双工通信
3)对代理、路由器、防火墙透明
4)流量小、网络负载小、无头部信息、Cookie、身份验证、安全开销
5)按业务场景可分为:广播式和点对点式(通过服务端维护websocket session实现)
2、生命周期
1)TCP三次握手:建立TCP连接
2)websocket握手:对握手端点url发起一个ws协议的get请求,可传参数,头信息如图所示
3)双向消息推送
4)探活:ping/pong
5)关闭:任何一方发送Close控制帧
三、Demo
实现websocket服务有多种方式,常见有基于spring的实现(4.x及以上)、基于tomcat的实现(7.x及以上)、基于netty的实现、基于springboot的实现,本文给出两个基于springboot的demo
1、Demo1:底层使用spring提供的websocket
1)依赖
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2)开启配置
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @EnableWebSocketMessageBroker开启使用STOMP协议来传输基于代理(message broker)的消息,此时控制器支持使用@MessageMapping
*/
@EnableWebSocketMessageBroker
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* Register STOMP endpoints mapping each to a specific URL and (optionally)
* enabling and configuring SockJS fallback options.
* 注册STOMP协议的节点(endpoint),并映射指定的URL
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpointTopic").withSockJS(); // 注册广播节点,并指定使用SocketJS协议
registry.addEndpoint("/endpointP2P").withSockJS(); // 注册点对点节点,并指定使用SocketJS协议
}
/**
* Configure message broker options.
* 配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/**
* Enable a simple message broker and configure one or more prefixes to filter
* destinations targeting the broker (e.g. destinations prefixed with "/topic").
*/
registry.enableSimpleBroker("/topic", "/queue");
}
}
3)控制器业务代码
import com.zhanghao.demo.common.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
public class WebSocketController {
/**
* 通过SimpMessagingTemplate向客户端发送消息
*/
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 广播
* @param name
* @return
* @throws Exception
*/
@MessageMapping("/visit") // 客户端向服务端发送请求的地址
@SendTo("/topic/notify") // 当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
public Result testTopic(String name) throws Exception {
String str = "welcome " + name + " visit!";
return new Result(str);
}
/**
* 点对点
* @param msg
* @return
* @throws Exception
*/
@MessageMapping("/sendMsg")
public void testP2P(String msg) throws Exception {
String toUser = "toUser"; // 接收消息的人,实际中应根据参数等进行解析
String queueURL = "/queue/sendMsg"; // 订阅消息
simpMessagingTemplate.convertAndSendToUser(toUser, queueURL, msg);
}
}
2、Demo2:底层使用JDK自带websocket
1)依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2)开启websocket
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@SpringBootApplication
public class WebsocketServer {
public static void main(String[] args) {
SpringApplication.run(WebsocketServer.class, args);
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3)连接管理、数据通信
import org.springframework.stereotype.Component;
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;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@ServerEndpoint("/ws/test")
public class WebsocketEndpointTest {
private Set<Session> sessionSet = new HashSet<>();
/**
* 握手成功后执行
* @param session
*/
@OnOpen
public void onOpen(Session session) {
// 可通过session.getRequestParameterMap获取建立连接时的订制参数,并通过sessionId与参数进行关联
sessionSet.add(session);
}
/**
* 连接断开时执行
*/
@OnClose
public void onClose(Session session) {
sessionSet.remove(session);
}
/**
* 发生错误时执行
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
// do something
}
/**
* 收到客户端消息时执行
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
// do something
}
/**
* 广播
* @param message
*/
public void sendTopicMessage(String message) {
sessionSet.parallelStream().filter(Session::isOpen).collect(Collectors.toList())
.forEach(session -> {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
// ERROR INFO
}
});
}
/**
* 点对点发送
* @param message
* @param session
*/
public void sendPointMessage(String message, Session session) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
// ERROR INFO
}
}
}