双向通信之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协议。最后如果大家有什么想法,欢迎讨论。

WebSocket是一种在客户端和服务器之间实现双向通信网络协议。它通过在客户端和服务器之间建立持久连接,允许双方实时地发送消息和数据。 与传统的HTTP协议相比,WebSocket协议具有以下特点: 1. 双向通信WebSocket允许客户端和服务器之间进行实时的双向通信,而不需要客户端发起请求,服务器返回响应的模式。 2. 低延迟:WebSocket建立的连接是持久的,可以在连接保持的时间内随时发送消息,减少了每次请求的开销,从而降低了通信的延迟。 3. 较小的数据传输量:由于WebSocket建立的连接是持久的,只需要在连接建立时进行一次握手,之后的通信只需要发送少量的控制数据和实际的数据,减少了额外的数据传输量。 4. 跨域支持:WebSocket支持跨域通信,可以在不同域名或端口之间进行通信。 5. 适应性强:WebSocket协议可以与HTTP协议共享同一端口,可以通过HTTP代理或反向代理进行部署。 在使用WebSocket进行双向通信时,客户端和服务器需要通过WebSocket API进行交互。客户端可以使用JavaScript中的WebSocket对象进行连接和消息发送,而服务器端需要实现WebSocket协议的处理逻辑来接收和处理客户端发送的消息。 总的来说,WebSocket提供了一种高效、实时的双向通信方式,适用于需要实时数据传输和实时交互的应用场景,如实时聊天、实时数据监控等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值