WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
public class ServerEncoder implements Encoder.Text<WSMessageData> {
@Override
public String encode(WSMessageData messageData) throws EncodeException {
return JsonMapper.INSTANCE.toJson(messageData);
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
@Slf4j
@Component
@ServerEndpoint(value = "/ws/{parkid}", encoders = {ServerEncoder.class})
public class WebSocketServer {
// 本地缓存连接,保证线程安全
private static ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketServer>> connCache = new ConcurrentHashMap<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 群发消息
*
* @param messageData 消息数据
*/
public void sendMessage(WSMessageData messageData, String sendTo) {
CopyOnWriteArraySet<WebSocketServer> webSocketServers = connCache.get(sendTo);
if (!CollectionUtils.isEmpty(webSocketServers)) {
webSocketServers.parallelStream()
.filter(Objects::nonNull)
.forEach(webSocketServer -> {
webSocketServer.session.getAsyncRemote().sendObject(messageData);
log.info("Message Topic Success,messageData={}", messageData);
});
} else {
log.info("The parking has no online connection,messageData={}", messageData);
}
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(@PathParam("parkid") String parkid, Session session) {
this.session = session;
// 先查询是否存在对应停车场的链接集合,
CopyOnWriteArraySet<WebSocketServer> webSocketServers = connCache.get(parkid);
if (CollectionUtils.isEmpty(webSocketServers)) {// 没有时先创建再添加
webSocketServers = new CopyOnWriteArraySet<>();
webSocketServers.add(this);
connCache.put(parkid, webSocketServers);
} else {// 有时直接添加
webSocketServers.add(this);
connCache.put(parkid, webSocketServers);
}
log.info("Client Connection Success,parkid={},connCache={}", parkid, connCache);
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("Received Client Message,message={}, session={}", message, session);
if (!StringUtils.isEmpty(message) && message.equals("ping")) {
session.getAsyncRemote().sendObject(WSMessageData.builder()
.code(DataTypeEnum.HEARTBEAT_DATA.getCode())
.data(DataTypeEnum.HEARTBEAT_DATA.getTitle())
.build());
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("parkid") String parkid) {
// 先获取对应停车场的链接集合
CopyOnWriteArraySet<WebSocketServer> webSocketServers = connCache.get(parkid);
if (!CollectionUtils.isEmpty(webSocketServers)) {
// 移除set集合中的this
webSocketServers.remove(this);
// set集合为空时从map中移除parkid对应的set
if (CollectionUtils.isEmpty(webSocketServers)) {
connCache.remove(parkid);
} else { // 否则用移除后的set覆盖
connCache.put(parkid, webSocketServers);
}
}
log.info("Connection Closed, parkid={}, cache={}", parkid, connCache);
}
/**
* 发生错误时,调用的方法
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket Service Exception={}, session={}", error, session);
}
}
spring:
application:
name: parking-ws-server
server:
port: 9527
location /ws {
proxy_pass http://172.***.***.58:9527;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}
经过测试,客户端连接成功后,如果60s内没有数据交互,连接就是自动断开,可以通过配置nginx location 中添加一行proxy_read_timeout **来自定义连接存活时间。若为自定义则默认为60s。
proxy_read_timeout 60s
再配合客户端的心跳机制来实现长时间未进行数据交互而造成的连接自动断开。
前端每55s发送心跳,服务端接收到心跳数据后给客户端一个响应。