WebSocket实现服务端接收移动端定位在网页前端显示

WebSocket笔记——服务端接收移动端定位在前端显示

一、需求

服务端需要对接APP和网页前端,将APP端的定位信息整理并返回给前端。前端需要展示在线人员位置以及基础信息。

一开始想用netty(网络I/0编程框架)实现,后面考虑到成本和时间问题,最后选择简单的webSocket协议。

二、什么是WebSocket?

1、介绍

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

2、配置

  <!--WebSocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>
@Configuration
public class WebSocketConfig {
   @Bean
   public ServerEndpointExporter serverEndpointExporter(){
       return new ServerEndpointExporter();
   }
}

三、WebSocket和Netty怎么选

主要看并发量。

webSocket和netty本质上是两样不同的东西,webSocket是一种TCP的通信协议,而netty则是一个通信框架,netty不但可以实现webSocket的功能,还可以实现异步I/0操作,解决高并发问题。但需要APP端也编写netty的代码,以客户端和服务端的形式交互。

像这次需求比较简单,时间比较紧的话,使用webSocket就足够了。

四、设计篇

1、交互

APP端发送一串json字符串,后端识别整理并定时推送给前端。

{"userId":10086,"longitude":112.3454897,"latitude":"23.3454897","height":100,"teamName":"2","userName":"张三"}

但考虑到以后需要网页端和APP端都需要连接webSocket,以及往后业务的增加webSocket可能不止用于定位的业务

于是又加入了两个字段

{"jobType":"JOBTYPE_GPS","clientType":"APP"}

后续发现APP端需要接受发送成功的反馈,于是加入时间戳字段,并将时间戳推送给APP端。

{"jobType":"JOBTYPE_GPS","clientType":"APP","userId":10086,"longitude":112.3454897,"latitude":"23.3454897","height":100,"teamName":"2","userName":"张三",time:1607339673336}

返回给APP端。

{time:1607339673336}

设计完对应POJO对象也清楚了

父类:保存公共属性

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonWebSocketJson {

    /**
     * 上传时间
     */
    private Long time;

    /**
     * 业务类型
     */
    private String jobType;

    /**
     * 客户端类型
     */
    private String clientType;
}

业务类:定义交互的格式

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GpsToWebJson extends CommonWebSocketJson{

    /**
     * 接入的人员的id
     */
    private String userId;

    /**
     * 经度
     */
    private Double longitude;

    /**
     * 纬度
     */
    private Double latitude;

    /**
     * 高程
     */
    private Double height;

    /**
     * 队名
     */
    private String teamName;

    /**
     * 人名
     */
    private String userName;
    
}

2、开发

因为要用到定时任务

所以要加启动类上加上@EnableScheduling

配置定时线程池

 @Bean
    public TaskScheduler taskScheduler(){

        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(10);//线程池的基本大小
        taskScheduler.initialize();
        return taskScheduler;
    }

clientId :接入的客户端的唯一id,一般用用户id即可

clientType : 接入的客户端类型 ,这里定义两种类型 移动端:APP|网页端:WEB

@ServerEndpoint("/webSocket/{clientId}/{clientType}")
@Component
@Slf4j
public class WebSocketServer {

    /**
     * 定位业务 在线成员列表
     */
    private static Map<String,GpsToWebJson> gpsOnlineMap = new ConcurrentHashMap<>();

    /**
     * 定位业务 clientId 列表 用来保存需要广播的客户端
     */
    private static Set<String> gpsWebClient = new ConcurrentHashSet<>();

    /**
     * 在线客户端总数
     */
    private static int onlineCount = 0;
    
    /**
     *    移动端WebSocket连接
     */
    private static Map<String,WebSocketServer> appMap = new ConcurrentHashMap<>();
    /**
     *    网页端WebSocket连接
     */
    private static Map<String,WebSocketServer> webMap = new ConcurrentHashMap<>();

    /**
     * session
     */
    private Session session;

    /**
     * 客户端id
     */
    private String clientId;

    /**
     * 客户端类型
     */
    private String clientType;

    @OnOpen
    public void onOpen(@PathParam("clientId") String clientId,@PathParam("clientType") String clientType, Session session){

        this.clientId = clientId;
        this.session = session;
        this.clientType = clientType;

        switch (clientType) {
            case ClientType.CLIENT_TYPE_APP:
                this.addClientToMap(appMap);

                break;
            case ClientType.CLIENT_TYPE_WEB:
            default:
                this.addClientToMap(webMap);

        }
    }

    /**
     * 将客户端添加到对应的Map中
     * @param webSocketMap
     */
    private void addClientToMap(Map<String,WebSocketServer> webSocketMap){
        if (webSocketMap.containsKey(clientId)) {
            webSocketMap.remove(clientId);
            webSocketMap.put(clientId, this);
            //加入set中
        } else {
            webSocketMap.put(clientId, this);
            //加入set中
            addOnlineCount();
            //在线数加1
        }
        webSocketMap.put(clientId, this);
        log.info("用户" + clientId + "连接上webSocket,当前连接人数为" + getOnlineCount());
        log.info("当前在线APP数为" + appMap.size());
        log.info("当前在线Web数为" + webMap.size());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        switch (clientType) {
            case ClientType.CLIENT_TYPE_APP:
                this.removeClientFromMap(appMap);
                //定位业务中去掉
                gpsOnlineMap.remove(clientId);
                break;
            case ClientType.CLIENT_TYPE_WEB:
            default:
                this.removeClientFromMap(webMap);
                gpsWebClient.remove(clientId);
        }
    }

    /**
     * 将客户端从map中移除
     * @param webSocketMap
     */
    private void removeClientFromMap(Map<String, WebSocketServer> webSocketMap) {

        if(webSocketMap.containsKey(clientId)){
            webSocketMap.remove(clientId);
            //从set中删除

            subOnlineCount();
        }
        log.info("用户退出:"+clientId+",当前在线人数为:" + getOnlineCount());
        log.info("当前在线APP数为" + appMap.size());
        log.info("当前在线Web数为" + webMap.size());
    }

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("app端发来的信息是:"+message);
        //根据业务type判断要进行的业务
        this.doJob(message);
    }


    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误:"+this.clientId+",原因:"+error.getMessage());
        error.printStackTrace();
    }

	/**
     * 处理业务的方法
     * @param message
     */
    private void doJob(String message) {

        //将消息转化为json字符串
        CommonWebSocketJson commonJson = JSONObject.parseObject(message, CommonWebSocketJson.class);
        //判断要进行的业务
        switch (commonJson.getJobType()){
            case JobType.GPS:
                //定位业务
                this.doGPS(ClientType.CLIENT_TYPE_APP,message);
                break;
            default:
                log.error("WebSocket错误: app传输数据格式错误,找不到业务代码!");
        }

    }

    /**
     * 业务:gps定位
     * @param message
     */
    private void doGPS(String clientType, String message) {
        //如果是APP
        if(clientType.equals(ClientType.CLIENT_TYPE_APP)){
            GpsToWebJson gpsToWebJson = JSONObject.parseObject(message, GpsToWebJson.class);
            String userId = gpsToWebJson.getUserId();
            if(gpsOnlineMap.containsKey(userId)){
                //假如原本就在线,则进行替换
                gpsOnlineMap.remove(userId);
                gpsOnlineMap.put(userId,gpsToWebJson);
            }else {
                //不在则加入
                gpsOnlineMap.put(userId,gpsToWebJson);
            }
            String result = "{\"time\":"+gpsToWebJson.getTime()+"}";
            this.session.getAsyncRemote().sendText(result);
        }
        else if (clientType.equals(ClientType.CLIENT_TYPE_WEB)){

        }
    }

    /**
     * 定时向前端推送 定位列表
     */
    @Scheduled(cron = "* * * * * ?")
    public void sendGPSMessage(){
        List<GpsToWebJson> onlineList = new ArrayList<>();
        for(String key : gpsOnlineMap.keySet()){
            onlineList.add(gpsOnlineMap.get(key));
        }
        //广播给全部web端
        Iterator<String> iterator = gpsWebClient.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();
            webMap.get(next).sendMessage(onlineList);
        }
    }

    /**
     *    定时清理定位列表(掉线设备)
     */
    @Scheduled(cron = "0 * * * * ?")
    public void refurbishGPSList(){
        gpsOnlineMap = new ConcurrentHashMap<>();
    }

    /**
     * 将web端添加到gpsWebClient
     * @param webId
     */
    public static void addToGpsWebClient(String webId){
        gpsWebClient.add(webId);
    }

    /**
     * 将Web端从gpsWebClient移除
     */
    public static void removeFromGpsWebClient(String webId){
        gpsWebClient.remove(webId);
    }

    /**
     * 向执行某个任务的客户端发送消息
     */
    public void sendMessage(Object message){
        //this.session.getAsyncRemote().sendText(JSONObject.toJSONObject(message));
        this.session.getAsyncRemote().sendText(JSONArray.toJSONString(message));
    }

    public static synchronized int getOnlineAppCount() {
        return appMap.size();
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    public static synchronized void gpsAppOffLine(String clientId) {
         gpsOnlineMap.remove(clientId);
    }

}

因为上面说了要区分业务

所以网页端还需要进行上线和下线的操作。

  /**
     * 添加客户端到gps广播
     * @param clientId
     * @return
     */
    @ApiOperation(value = "添加客户端到gps广播")
    @GetMapping("/addWebToGpsClient/{clientId}")
    public CommonResult addWebToGpsClient(@PathVariable("clientId") @NotNull String clientId){
        WebSocketServer.addToGpsWebClient(clientId);
        log.info("有客户端加入进来啦!");
        return CommonResult.success("该客户端可以接收到GPS业务的广播了!");
    }

    /**
     * 从gps业务中移除客户端
     * @param clientId
     * @return
     */
    @ApiOperation(value = "从gps业务中移除客户端")
    @GetMapping("/removeWebFromGpsClient/{clientId}")
    public CommonResult removeWebFromGpsClient(@PathVariable("clientId") @NotNull String clientId){
        WebSocketServer.removeFromGpsWebClient(clientId);
        return CommonResult.success("该客户端将不再接收到GPS业务的广播!");
    }

五、测试效果

进入webSocket测试网站进行webSocket连接

WEB端连接:

127.0.0.1:8081/webSocket/10086/WEB

APP端连接:

127.0.0.1:8081/webSocket/10000/APP

使用postMan进行网页端上线

127.0.0.1:8081/gpsManage/addWebToGpsClient/10086

五、测试效果

进入webSocket测试网站进行webSocket连接

WEB端连接:

127.0.0.1:8081/webSocket/10086/WEB

APP端连接:

127.0.0.1:8081/webSocket/10000/APP

使用postMan进行网页端上线

127.0.0.1:8081/gpsManage/addWebToGpsClient/10086

预览效果:
在这里插入图片描述

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值