Bootstrap

双向通信之Websocket

介绍

Websocket是一种在单个TCP连接上进行全双工通信的协议。与传统的HTTP协议不同,websocket允许客户端与服务器之间的双向通信,可以在同一条连接上进行多次消息的快速传递。我之前在做一个线上刷题网站的时候,需要设计一个社区讨论模块,当有用户评论了我的动态内容或者回复了我的评论的时候我需要被通知,那么这边我就使用了websocket来实现消息回复的实时通知功能。

Websocket协议

原理

它的工作原理大致分为以下这么几个部分:

1.建立连接:客户端在初次加载的时候通过HTTP向服务端发起建立websocket连接的请求,服务端接收到之后同意连接,那么服务端就会将HTTP连接升级到websocket连接。这里分为两步:

握手请求:客户端向服务端发送的HTTP请求包含关键性头部:Connection:upgrade,upgrade:websocket,代表要求升级到websocket协议;sec-websocket-key是基于base64编码生成的校验值,用于服务端对请求进行校验。

握手响应:服务端在接收到请求之后,校验通过会生成101switching code返回,代表升级协议成功,同时还有sec-websocket-Accept,代表请求校验的合法性。

2.数据传输:建立连接成功之后双方就可以发送数据帧了,数据帧的关键性字段包括有:FIN代表这是最后一帧,Opcode代表数据传输的格式(文本帧/二进制帧等),负载数据负载长度等等。

3.关闭连接:客户端和服务端任何一方都可以关闭连接,也可以是网络中断关闭连接。

4.心跳机制:为了确保双方通信的活跃性,客户端或者服务端可以随时向对方发送ping帧,对方接收到之后返回一个pong帧。

5.安全性:websocket可以运行在TLS协议上确保传输的安全性,比如大家熟知的HTTPS就是HTTP+TLS。

业务代码实现

这里我们将给出我们项目中使用websocket实现的消息回复实时通知的部分代码。

首先我们需要做一些配置,我们要求前端请求中有一个"erp"项用于存放用户的openid,方便我们用于作唯一标识来识别不同的用户。

@Component
public class WebSocketServerConfig extends ServerEndpointConfig.Configurator {

    @Override
    public boolean checkOrigin(String originHeaderValue) {
        return true;
    }

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        Map<String, List<String>> parameterMap = request.getParameterMap();
        List<String> erpList = parameterMap.get("erp");
        if(!CollectionUtils.isEmpty(erpList)){
            sec.getUserProperties().put("erp", erpList.get(0));
        }
    }

}

接着就是我们自定义websocket类的编写,在这边我们将实现消息的推送、异常处理、客户端的连接与关闭的逻辑。首先是连接成功与关闭的逻辑:

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig conf) throws IOException {
        //获取用户信息
        try {
            Map<String, Object> userProperties = conf.getUserProperties();
            String erp = (String) userProperties.get("erp");
            this.erp = erp;
            this.session = session;
            if (clients.containsKey(this.erp)) {
                clients.get(this.erp).session.close();
                clients.remove(this.erp);
                onlineCount.decrementAndGet();
            }
            clients.put(this.erp, this);
            onlineCount.incrementAndGet();
            log.info("有新连接加入:{},当前在线人数为:{}", erp, onlineCount.get());
            sendMessage("连接成功", this.session);
            if(offlineMessages.containsKey(this.erp)){
                ConcurrentLinkedQueue<String> messages = offlineMessages.get(this.erp);
                while (!messages.isEmpty()){
                    sendMessage(messages.poll(),this.session);
                }
            }
        } catch (Exception e) {
            log.error("建立链接错误{}", e.getMessage(), e);
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            if (clients.containsKey(erp)) {
                clients.get(erp).session.close();
                clients.remove(erp);
                onlineCount.decrementAndGet();
                offlineMessages.computeIfAbsent(erp, k -> new ConcurrentLinkedQueue<>()).add("你不在的时候,南师很想你哦!!!");
            }
            log.info("有一连接关闭:{},当前在线人数为:{}", this.erp, onlineCount.get());
        } catch (Exception e) {
            log.error("连接关闭错误,错误原因{}", e.getMessage(), e);
        }
    }

在这边我们维护了一个map缓存存放某个用户断开连接期间其他用户发送的消息,等到用户再次上线时推送,这边我们目前是采用的缓存维护的方式,后续会做一个优化,考虑到如果该服务挂掉,那么缓存里的数据就会丢失,所以我们可以在这边对数据做一个持久化比如存储到数据库,服务挂掉之后重启的时候去表中读取未推送的数据加载到缓存中,这是需要优化的一个点;当然也可以考虑将数据存到redis里面配合redis的数据持久化机制可以保证数据不会丢失,但是需要考虑redis宕机的问题,这些不是本文讨论的重点,感兴趣的小伙伴可以根据可能出现的问题进行下一步思考。

然后是发送消息的逻辑,包括用户在线离线发送和群发消息的逻辑:


    /**
     * 指定发送消息
     */
    public void sendMessage(String message, Session session) {
        if(session!=null){
            log.info("服务端给客户端[{}]发送消息{}", this.erp, message);
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("{}发送消息发生异常,异常原因{}", this.erp, message);
            }
        }
    }
    public void sendMessage(String message, String toId) {
        offlineMessages.computeIfAbsent(toId, k -> new ConcurrentLinkedQueue<>()).add(message);
    }

    /**
     * 群发消息
     */
    public void sendMessage(String message) {
        for (Map.Entry<String, ChickenSocket> sessionEntry : clients.entrySet()) {
            String erp = sessionEntry.getKey();
            ChickenSocket socket = sessionEntry.getValue();
            Session session = socket.session;
            log.info("服务端给客户端[{}]发送消息{}", erp, message);
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("{}发送消息发生异常,异常原因{}", this.erp, message);
            }
        }
    }

总结

在这里我们探讨了websocket的工作原理以及它在我们项目中的具体应用。除了消息回复的实时通知,它的应用也是非常广泛,比如说实时更新(股票行情,游戏数据更新),在线协作等等。相较于HTTP协议,websocket的通信模式是双向通信模式,而HTTP是请求响应模式,并且HTTP每次请求都会带上大量的请求头,websocket在建立连接之后只传输很少的头部信息。所以在不同的场景下我们考虑使用不同的协议,简单场景下HTTP已经足够,如果是双方消息通知或者实时推送的话可以考虑websocket协议。最后如果大家有什么想法,欢迎讨论。

;