WebSocket使用


前言

WebSocket与前端消息通讯应用,提供连接建立、关闭,发生错误,数据接收、发送,权限校验(待补充)等方法
* 支持监控在线连接数
* 支持发送消息并回调结果,类似HTTP形式


一、WebSocket是什么?

	WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
	WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

websocket的特点

	WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
	WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
	在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
	浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

二、使用步骤

1.消息通讯处理服务代码

import java.io.IOException;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.SendHandler;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.linkcm.sxssy.espms.job.websocket.model.MsgResult;

/***  
     * <b>function: WebSocket服务
     * 提供连接建立、关闭,发生错误,数据接收、发送,权限校验(待补充)等方法
     * 支持监控在线连接数
     * 支持发送消息并回调结果,类似HTTP形式
     * </b>
 	 * @project sxssy-espms-job
 	 * @package com.linkcm.sxssy.espms.job.websocket.Service
 	 * @filename WebSocketService.java
 	 * @createtime 2021年4月13日 下午4:45:15  
 	 * @author peibinchen
 	 */
@ServerEndpoint("/websocket/{token}/{userName}")
@Component
public class WebSocketService {

    private final static Logger log = LoggerFactory.getLogger(WebSocketService.class);

    // 存放每个客户端对应的WebSocket对象
    private final static ConcurrentHashMap<String, WebSocketService> webSocketServices =
            new ConcurrentHashMap<String, WebSocketService>();
    
    // 存放待处理的ICallBack回调对象
    private final static ConcurrentHashMap<String, ICallBack> callBackMap =
            new ConcurrentHashMap<String, ICallBack>();
    
    // 当前在线连接数
    private final static AtomicInteger onlineCount = new AtomicInteger();
    // 回调超时时间,单位:毫秒
    public final static int WAIT_TIME = 30000;
    // 回调超时时间,单位:毫秒
    public final static String REAL_TIME_FALG = "【realTime】";
    // 回调超时时间,单位:毫秒
    public final static String REAL_TIME_KEY_VALUE_SPLIT = "=>";

    // 与某个客户端的连接会话
    private Session session;
    private String token;
    private String userName;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "token") String token, @PathParam(value = "userName") String userName) {
        this.session = session;
        this.token = token;
        this.userName = userName;
        
        boolean security = isAuth(token, userName);
        if (security == false) {
            try {
                session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Illegal token"));
            } catch (IOException e) {
                log.error("close session ocurre error: {} .", e.getMessage());
            }
            return;
        }
        
        webSocketServices.put(this.session.getId() + "_" + userName, this);
        //在线数加1
        addOnlineCount();           
        log.info("有新窗口开始监听:"+session.getId()+", userName:"+this.userName+", 当前在线会话数为:" + getOnlineCount());
    }
    
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketServices.remove(this.session.getId());
        //在线数减1
        subOnlineCount();           
        log.info("有一连接关闭!当前在线会话数为:" + getOnlineCount());
    }

    /**
     * 出现错误时处理方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误",error);
    }
    
    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("【服务端】收到来自窗口"+session.getId()+"的信息:"+message);
        if (isHeartMessage(message)) return;
        
        /*
         * 如果消息格式为:"【realTime】"+{key}+"=>"+{data},则表示是实时消息,特殊处理,并执行回调函数
         */
        if(message.startsWith(REAL_TIME_FALG)) {
            String dataMsg = message.substring(REAL_TIME_FALG.length());
            if(!dataMsg.contains(REAL_TIME_KEY_VALUE_SPLIT)) {
                log.error("实时消息格式有误:{}", dataMsg);
            }
            String[] dataArr = dataMsg.split(REAL_TIME_KEY_VALUE_SPLIT);
            String key = dataArr[0];     // 回调函数存储的key值
            String data = dataArr[1];    // 真实数据部分
            log.info("data:{}", data);
            
            ICallBack callBack = callBackMap.get(key);  // 获取回调函数
            if(callBack != null) {
                callBack.solve(data);    // 执行回调函数
                callBackMap.remove(key); // 移除待处理回调函数
            }
        }
    }
    
    /**
     * 推送消息
     * SendHandler为发送成功后的处理器
     * SendHandler sendHandler = new SendHandler() {
                @Override
                public void onResult(SendResult result) {
                    log.info(result.toString());
                }
            };
     */
    public void sendMessage(String message, SendHandler sendHandler) {
        try {
            // 异步发送消息
            this.session.getAsyncRemote().sendText(message, sendHandler);
        } catch (Exception e) {
            log.error("send to application: {} fail.", message);
        }
    }
    
    /**
     * 推送消息(异步)
     */
    public void sendMessage(String message) {
        try {
            // 异步发送消息
            this.session.getAsyncRemote().sendText(message);

        } catch (Exception e) {
            log.error("send to application: {} fail.", message);
        }
    }
    
    /**
     * 推送消息,anyc=true为异步,anyc为false为同步
     */
    public void sendMessage(String message, boolean anyc) {
        try {
            sendMessage(this.session, message, anyc);
        } catch (Exception e) {
            log.error("send to application: {} fail.", message);
        }
    }
    
    /**
     * 推送消息,anyc=true为异步,anyc为false为同步
     */
    public void sendMessage(Session session, String message, boolean anyc) {
        try {
            if(anyc) {
                // 异步发送消息
                session.getAsyncRemote().sendText(message);
            } else {
                // 同步发送消息
                session.getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            log.error("send to application: {} fail.", message);
        }
    }
    
    /**  
     * sendForBack:      发送消息并支持回调函数
     * @author chenpb
     * @param key        回调key值,用于回调时判断,执行对应回调函数
     * @param msg        发送的消息
     * @param callBack   回调函数
     */
    public void sendForBack(String key, String msg, ICallBack callBack) {
        callBackMap.put(key, callBack);
        this.sendMessage(msg);
    }
    
    /**  
     * sendForBack: 发送消息并等待回调结果
     * 实时消息格式为:"【realTime】"+{key}+"=>"+{data}
     * @author chenpb
     * @param key
     * @param msg
     * @return
     * @throws InterruptedException  
     */
    public MsgResult sendForBack(String key, String message) throws InterruptedException {
        final MsgResult msgResult = new MsgResult();
        // 发送消息
        this.sendForBack(key, message, new ICallBack(){
            @Override
            public void solve(String result) {
                log.info("回调数据:{}", result);
                synchronized (msgResult) {
                    msgResult.setResult(result);
                    msgResult.notifyAll();
                }
            }
        });
        // 等待处理,不管有没超时,最终都会移除回调函数
        synchronized(msgResult) {
            long start = System.currentTimeMillis();
            log.info("等待开始");
            msgResult.wait(WAIT_TIME);   // 超时时间
            long end = System.currentTimeMillis();
            log.info("等待结束,等待时间:{}s", (end - start)/1000);
            
            WebSocketService.getCallBackMap().remove(key);
            log.info("待回调函数数量:{}", WebSocketService.getCallBackMap().size());
        }
        return msgResult;
    }
    
    /**
     * 判断是否是心跳消息
     */
    public boolean isHeartMessage(String message) {
        // ToDo ...
        return false;
    }

    /**
     * 鉴权
     */
    private boolean isAuth(String token, String userName) {
        // ToDo ...
        return true;
    }
    
    public int getOnlineCount() {
        return onlineCount.get();
    }

    private void addOnlineCount() {
        onlineCount.incrementAndGet();
    }

    private void subOnlineCount() {
        onlineCount.decrementAndGet();
    }
    
    public static ConcurrentHashMap<String, WebSocketService> getAgentserverendpoints() {
        return webSocketServices;
    }
    
    public static ConcurrentHashMap<String, ICallBack> getCallBackMap() {
        return callBackMap;
    }
    
    public WebSocketService getAgentServerEndpoint(String userName) {
        for(Entry<String, WebSocketService> entry : webSocketServices.entrySet()) {
            if(entry.getKey().equals(userName)) {
                return entry.getValue();
            }
        }
        return null;
    }
    
    public Session getSession() {
        return session;
    }
}

2.回调接口

代码如下(示例):

/***  
 * <b>function: 回调接口</b>
 * @project sxssy-espms-job
 * @package com.linkcm.sxssy.espms.job.common.job
 * @filename IcallBack.java
 * @createtime 2021年4月13日 下午3:16:04  
 * @author peibinchen
 */
public interface ICallBack {
    public void solve(String result);
}

3.http接口服务

提供WebSocket模拟http接口形式的实时消息通讯接口

import io.swagger.annotations.ApiOperation;

import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.linkcm.sxssy.espms.job.common.bean.ResponseResult;
import com.linkcm.sxssy.espms.job.common.util.RandomFlagUtils;
import com.linkcm.sxssy.espms.job.common.util.RestResultUtils;
import com.linkcm.sxssy.espms.job.websocket.Service.WebSocketService;
import com.linkcm.sxssy.espms.job.websocket.model.MsgResult;
import com.linkcm.sxssy.espms.job.websocket.model.ReqMsgData;

/***  
     * <b>function: websocket http接口服务</b>
 	 * @project sxssy-espms-job
 	 * @package com.linkcm.sxssy.espms.job.websocket.Controller
 	 * @filename WebSocketController.java
 	 * @createtime 2021年4月14日 上午9:51:37  
 	 * @author peibinchen
 	 */
@Slf4j
@RestController
@RequestMapping("/websocket")
public class WebSocketController {

	/**  
	 * test: 测试消息发送并回调方法
	 * @author chenpb
	 * @return
	 * @throws InterruptedException  
	 */
	@ApiOperation(value = "", notes = "")
    @GetMapping(value = "/test")
    public ResponseResult<Object> test() throws InterruptedException {
	    log.info("********* websocket实时推送数据开始  **************");
	    
	    /*
	     * 测试数据
	     */
	    String key = "test";
	    String jsonData = "【realTime】" + key + "=>测试";
	    
        ConcurrentHashMap<String, WebSocketService> service = 
                WebSocketService.getAgentserverendpoints();

        MsgResult msgResult = new MsgResult();
        
        for(Entry<String, WebSocketService> entry : service.entrySet()) {
            WebSocketService point = entry.getValue();
            
            // 获取待发送消息
            log.info("发送的消息:" + jsonData);
            
            // 发送消息
            msgResult = point.sendForBack(key, jsonData);
        }
        
        log.info("********* websocket实时推送数据结束 **************");
        return RestResultUtils.successResult(msgResult.getResult());
    }
	
	/**  
     * test: 消息发送并回调
     * @author chenpb
     * @return
     * @throws InterruptedException  
     */
    @ApiOperation(value = "", notes = "")
    @PostMapping(value = "/sendForBack")
    public ResponseResult<Object> sendForBack(@RequestBody ReqMsgData reqMsgData) throws InterruptedException {
        log.info("********* websocket实时推送消息开始  **************");
        
        String key = RandomFlagUtils.randomDateTime();
        String msg = WebSocketService.REAL_TIME_FALG + key + WebSocketService.REAL_TIME_KEY_VALUE_SPLIT + reqMsgData.getData();
        
        ConcurrentHashMap<String, WebSocketService> service = 
                WebSocketService.getAgentserverendpoints();

        MsgResult msgResult = new MsgResult();
        
        for(Entry<String, WebSocketService> entry : service.entrySet()) {
            WebSocketService point = entry.getValue();
            
            // 获取待发送消息
            log.info("发送的消息:" + msg);
            
            // 发送消息
            msgResult = point.sendForBack(key, msg);
        }
        
        log.info("********* websocket实时推送消息结束 **************");
        return RestResultUtils.successResult(msgResult.getResult());
    }
}

4.回调结果对象

import lombok.Data;

/***  
 * <b>function: 回调结果对象</b>
 * @project sxssy-espms-job
 * @package com.linkcm.sxssy.espms.job.common.job
 * @filename MsgResult.java
 * @createtime 2021年4月13日 下午4:05:56  
 * @author peibinchen
 */
@Data
public class MsgResult {

    public String result;
}

5.http请求参数实体

import lombok.Data;

/***  
 * <b>function: http请求参数实体</b>
 * @project sxssy-espms-job
 * @package com.linkcm.sxssy.espms.job.websocket.model
 * @filename ReqMsgData.java
 * @createtime 2021年4月13日 下午5:22:56  
 * @author peibinchen
 */
@Data
public class ReqMsgData {

    private String data;
    
}

参考资料

https://www.jianshu.com/p/4e678e639161


总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值