SpringBoot整合WebSocket

SpringBoot整合WebSocket

  • 简介
  • 特点
  • SpringBoot整合
    • pom
    • WebSocketConfig配置类
    • ServerEncoder自定义编码器
    • WebSocketServer实现类
    • application.yml
  • 测试
  • nginx配置
  • 客户端心跳

简介

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
SpringBoot整合WebSocket_第1张图片
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

特点

  • 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

SpringBoot整合

pom

<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>

WebSocketConfig配置类

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

ServerEncoder自定义编码器

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() {

    }
}

WebSocketServer实现类

@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);
    }
}

application.yml

spring:
  application:
    name: parking-ws-server
server:
  port: 9527

测试

  • WebSocket在线测试地址
  • 输入连接地址ws://127.0.0.1:9527/ws/987654321
    SpringBoot整合WebSocket_第2张图片

nginx配置

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发送心跳,服务端接收到心跳数据后给客户端一个响应。

你可能感兴趣的:(java,SpringBoot)