WebSocket的实现与应用

说到websocket,就不得不提http协议的连接特点特点与交互模型。

首先,http协议的特点是无状态连接。即http的前一次连接与后一次连接是相互独立的。

其次,http的交互模型是请求/应答模型。即交互是通过C/B端向S端发送一个请求,S端根据请求,返回一个响应。

那么这里就有一个问题了–S端无法主动向C/B端发送消息。而交互是双方的事情,怎么能限定一方发数据,另一方接数据呢。

传统解决方案:
传统的解决方案就俩字:轮询。
客户端(Request):有消息不?

服务端(Response):No

客户端(Request):有消息不?

服务端(Response):No

客户端(Request):有消息不?

服务端(Response):有了。你妈叫你回家吃饭。

客户端(Request):有消息不?

服务端(Response):No

websocket解决方案:
websocket的解决方案,总结一下,就是:建立固定连接
就是C/B端与S端就一个websocket服务建立一个固定的连接,不断开。

大概的场景是这样的:

服务端:我建立了一个chat的websocket,欢迎大家连接。

客户端:我要和你的chat的websocket连接,我的sid(唯一标识)是No.1

服务端:好的,我已经记住你了。如果有发往chat下No.1的消息,我会告诉你的。

客户端:嗯。谢谢了哈。
==================================> 过了一段时间

(有一个请求调用了chat的websocket,并且指名是给No.1的消息)

服务端(发送消息给No.1):No.1,有你的消息。你妈妈叫你回家做作业。

客户端(No.1):好的。我收到了。谢谢。

由于这次只是简单说一下websocket,所以就不深入解读网络相关知识了。
应用场景
既然http无法满足用户的所有需求,那么为之诞生的websocket必然有其诸多应用场景。如:

实时显示网站在线人数
账户余额等数据的实时更新
多玩家网络游戏
多媒体聊天,如聊天室
。。。
其实总结一下,websocket的应用场景就俩字:实时

无论是多玩家网络游戏,网站在线人数等都是由于实时性的需求,才用上了websocket(后面用缩写ws)。

谈几个在我项目中用到的情景:

在线教育项目中的课件系统,通过ws实现学生端课件与教师端课件的实时交互
物联网项目中的报警系统,通过ws实现报警信息的实时推送
大数据项目中的数据展示,通过ws实现数据的实时更新
物联网项目中的硬件交互系统,通过ws实现硬件异步响应的展示
当你的项目中存在需要S端向C/B端发送数据的情形,那就可以考虑上一个websocket了。
实现
服务端开发:
引入依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

添加配置:
忍不住想要吐槽,为什么不可以如eureka等组件那样,直接在启动类写一个注解就Ok了呢。
package com.renewable.center.warning.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * Websocket的配置
 * 说白了就是引入Websocekt至spring容器
 */
@Configuration
public class WebSocketConfig {  
    
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {  
        return new ServerEndpointExporter();
    }  
  
}

代码实现:
WebSocketServer的实现:

package com.renewable.center.warning.controller.websocket;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Description:
 * @Author: jarry
 */
@Component
@Slf4j
@ServerEndpoint("/websocket/warning/{sid}")
public class WarningWebSocketServer {

    // JUC包的线程安全Set,用来存放每个客户端对应的WarningWebSocketServer对象。
    // 用ConcurrentHashMap也是可以的。说白了就是类似线程池中的BlockingQueue那样作为一个容器
    private static CopyOnWriteArraySet<WarningWebSocketServer> warningWebSocketSet = new CopyOnWriteArraySet<WarningWebSocketServer>();

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    // 接收sid
    private String sid="";

    /**
     * 建立websocket连接
     * 看起来很像JSONP的回调,因为前端那里是Socket.onOpne()
     * @param session
     * @param sid
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid){
        this.session = session;
        this.sid = sid;
        warningWebSocketSet.add(this);

        sendMessage("websocket connection has created.");
    }

    /**
     * 关闭websocket连接
     */
    @OnClose
    public void onClose(){
        warningWebSocketSet.remove(this);
        log.info("there is an wsConnect has close .");
    }

    /**
     * websocket连接出现问题时的处理
     */
    @OnError
    public void onError(Session session, Throwable error){
        log.error("there is an error has happen ! error:{}",error);
    }

    /**
     * websocket的server端用于接收消息的(目测是用于接收前端通过Socket.onMessage发送的消息)
     * @param message
     */
    @OnMessage
    public void onMessage(String message){
        log.info("webSocketServer has received a message:{} from {}", message, this.sid);

        // 调用消息处理方法(此时针对的WarningWebSocektServer对象,只是一个实例。这里进行消息的单发)
        // 目前这里还没有处理逻辑。故为了便于前端调试,这里直接返回消息
        this.sendMessage(message);
    }

    /**
     * 服务器主动推送消息的方法
     */
    public void sendMessage(String message){
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.warn("there is an IOException:{}!",e.toString());
        }
    }

    public static void sendInfo(String sid, String message){
        for (WarningWebSocketServer warningWebSocketServerItem : warningWebSocketSet) {
            if (StringUtils.isBlank(sid)){
                // 如果sid为空,即群发消息
                warningWebSocketServerItem.sendMessage(message);
                log.info("Mass messaging. the message({}) has sended to sid:{}.", message,warningWebSocketServerItem.sid);
            }
            if (StringUtils.isNotBlank(sid)){
                if (warningWebSocketServerItem.sid.equals(sid)){
                    warningWebSocketServerItem.sendMessage(message);
                    log.info("single messaging. message({}) has sended to sid:{}.", message, warningWebSocketServerItem.sid);
                }
            }
        }
    }

}

WesocketController
为了便于调试与展示效果,写一个控制层,用于推送消息
package com.renewable.center.warning.controller.websocket;

import com.renewable.terminal.terminal.common.ServerResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;

/**
 * @Description: 用于测试WebsocketServer
 * @Author: jarry
 */
@Controller
@RequestMapping("/websocket/test/")
public class WarningWebsocketController {

    @GetMapping("link.do")
    @ResponseBody
    public ServerResponse link(@RequestParam(name = "sid") int sid){
        return ServerResponse.createBySuccessMessage("link : "+sid);
    }

    /**
     * 调用WarningWebsocketServer的消息推送方法,从而进行消息推送
     * @param sid 连接WarningWebsocketServer的前端的唯一标识。如果sid为空,即表示向所有连接WarningWebsocketServer的前端发送相关消息
     * @param message 需要发送的内容主体
     * @return
     */
    @ResponseBody
    @RequestMapping("push.do")
    public ServerResponse pushToWeb(@RequestParam(name = "sid", defaultValue = "") String sid, @RequestParam(name = "message")  String message) {
        WarningWebSocketServer.sendInfo(sid, message);
        return ServerResponse.createBySuccessMessage(message+"@"+sid+" has send to target.");
    }
}

WesocketTestIndex
这里建立了一个B端页面,用于与S端进行交互,演示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebsocketTestIndex</title>
</head>
<body>

<h1>Websocket Test</h1>

</body>
</html>

效果展示
B端网页初始化:
在这里插入图片描述
调用S端WarningWebsocketController下pushToWeb()接口,对sid=2的B端发送消息
在这里插入图片描述
B端网页接收到专门发给sid=2的消息后的效果:
在这里插入图片描述
调用S端WarningWebsocketController下pushToWeb()接口,所有连接该websocket的B端群发消息:
在这里插入图片描述
B端网页接收到群发消息后的效果:
在这里插入图片描述
S端接收到消息后的日志打印:
在这里插入图片描述
S端在B端关闭连接后的日志打印:
在这里插入图片描述
总结
至此,websocket的应用就算入门了。至于实际的使用,其实就是服务端自己调用WebSocket的sendInfo接口。当然也可以自己扩展更为细致的逻辑,方法等。

另外,需要注意的是,别忘了及时关闭webocket的连接。尤其在负载较大的情况下,更需要注意即使关闭不必要的连接。

架构的技术选型,需要的不是最好的,而是最适合的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值