WEBSOCKET集成指南

WEBSOCKET集成指南

目录

WEBSOCKET集成指南... 1

一、         为何使用websocket. 1

1.         轮询... 1

2.         长轮询... 2

二、         Websocket原理... 2

三、         Websocket与java集成... 2

1.         前置条件... 2

2.         后端集成操作... 3

3.         前端集成操作... 9

四、         Websocket集成遇到的难题... 11

1.         websocket对于spring有要求,至少4.0以上。... 11

2.         在部署nginx的时候,需要加入特殊的配置,nginx 的版本至少1.3以上才行,     proxy_set_header Upgrade $http_upgrade;      proxy_set_header Connection "upgrade";11

3.         部署websocket的容器也有要求,Tomcat从7.0.27开始支持,我使用tomcat 8.5版本可以。      11

4.         前端页面需要定时发送心跳请求,不然会有超时,断了,那么就无法再推送了。      11

五、         Websocket实现session共享方法... 11

1.         前提摘要... 11

2.         集成步骤... 12

 

 

一、   为何使用websocket

随着网络时代的发展,传统的响应式网站越来越不满足项目的需求,比如一些特殊的项目需求是服务器通知客户端,而不是服务器响应式的应答客户端。在这个大的前提下面,websocket未出来之前解决办法:

 

1.      轮询

客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有Http的Header,会很耗流量,也会消耗CPU的利用率。

 

2.      长轮询

长轮询是对轮询的改进版,客户端发送HTTP给服务器之后,有没有新消息,如果没有新消息,就一直等待。当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和CPU利用率等问题。但是这种方式还是有一种弊端:例如假设服务器端的数据更新速度很快,服务器在传送一个数据包给客户端后必须等待客户端的下一个Get请求到来,才能传递第二个更新的数据包给客户端,那么这样的话,客户端显示实时数据最快的时间为2×RTT(往返时间),而且如果在网络拥塞的情况下,这个时间用户是不能接受的,比如在股市的的报价上。另外,由于http数据包的头部数据量往往很大(通常有400多个字节),但是真正被服务器需要的数据却很少(有时只有10个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费。

 

随着H5技术的推进,新的技术websocket完美的解决了上面的难题,真正的实现了WEB的实时通讯。使B/S模式具有了C/S模式的实时通信能力。

 

二、   Websocket原理

Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。Websocket的数据传输是frame形式传输的,比如会将一条消息分为几个frame,按照先后顺序传输出去。这样做会有几个好处:

 

1)      大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不足够的情况。

2)      和http的chunk一样,可以边生成数据边传递消息,即提高传输效率。

三、   Websocket与java集成

1.      前置条件

maven、Spring、SpringMVC、redis

 

2.      后端集成操作

1)      在项目pom文件加入如下依赖

<dependency>
    <
groupId>org.springframework</groupId>
    <
artifactId>spring-websocket</artifactId>
    <
version>${spring.version}</version>
    <
exclusions>
        <
exclusion>
            <
groupId>org.springframework</groupId>
            <
artifactId>spring-web</artifactId>
        </
exclusion>
        <
exclusion>
            <
groupId>org.springframework</groupId>
            <
artifactId>spring-context</artifactId>
        </
exclusion>
        <
exclusion>
            <
groupId>org.springframework</groupId>
            <
artifactId>spring-core</artifactId>
        </
exclusion>
    </
exclusions>
</
dependency>

 

2)      配置websocket请求规则
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

    @Bean
    public WebSocketHandler myHandler() {
        return new MyWebSocketHandler();
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/ws").addInterceptors(new HandShake()).setAllowedOrigins("*");
        //底下的是防止不支持websocket的替代方案
        registry.addHandler(myHandler(), "/ws/sockjs").setAllowedOrigins("*").addInterceptors(new HandShake()).withSockJS();

    }
}
 
3)      配置握手之前如何获取会话主键获取
         新建类实现接口HandshakeInterceptor,重写里面的方法:beforeHandshake,如如下案例,获取userID
 
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                               Map<String, Object> attributes) throws Exception {
    if (request instanceof ServletServerHttpRequest) {
        // 标记用户
        String userId = request.getURI().getQuery();
        if (StringUtils.isBlank(userId)){
            return false;
        }else {
            if (userId.indexOf("=")>=-1){
                userId = userId.split("=")[1];
            }else {
                return false;
            }
        }
        if (userId != null) {
            attributes.put("userId", userId);
        } else {
            return false;
        }
    }
    return true;
}
 
 
 
4)      连接建立后会话处理类

 

        会话处理类需要实现接口WebSocketHandler,实现接口里面的方法即可。具体可看如下代码:
public class MyWebSocketHandler implements WebSocketHandler {

    /*生成logger*/
    private static final Logger logger = LoggerFactory.getLogger(MyWebSocketHandler.class);

    //存放会话
    private static ConcurrentHashMap<String,Set<WebSocketSession>> userSessionMap = new ConcurrentHashMap<>();

    //计数
    private static AtomicLong onlineCount = new AtomicLong(0);

    private String getUserId(WebSocketSession session){
        return (String) session.getAttributes().get("userId");
    }


    /**
     * 连接建立之后,执行方法
     * @param webSocketSession
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {


       String id = getUserId(webSocketSession);


        //如果包含在表里面,那么直接添加
        if (userSessionMap.contains(id)){
            userSessionMap.get(id).add(webSocketSession);
        }else {
            Set<WebSocketSession> sessions = new HashSet<>();
            sessions.add(webSocketSession);
            userSessionMap.put(id,sessions);
        }
        onlineCount.incrementAndGet();
        logger.info("新建立一个连接,建立的连接的主键是:\t"+id+"\t当前在线人数 :\t"+onlineCount);

        sendMessageToUser(id,new TextMessage("连接建立成功"));



    }

    /**
     * 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理
     * @param webSocketSession
     * @param webSocketMessage
     * @throws Exception
     */
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {

        if (webSocketMessage.getPayloadLength()==0){
            return;
        }
        logger.info("服务器接受到"+getUserId(webSocketSession)+"\t的消息:\t"+webSocketMessage.getPayload().toString());

        sendMessage(webSocketSession,new TextMessage(webSocketMessage.getPayload().toString()));

    }

    /**
     *消息传输错误处理
     * @param webSocketSession
     * @param throwable
     * @throws Exception
     */
    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {

        if (webSocketSession.isOpen()){
            webSocketSession.close();
        }
        deleteSessionFromCache(webSocketSession);
    }

    /**
     * 关闭连接后
     * @param webSocketSession
     * @param closeStatus
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
        logger.info("Session {} disconnected. Because of {}", webSocketSession.getId(), closeStatus);

        deleteSessionFromCache(webSocketSession);


    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
    /**
     * 向指定的客户端发送消息
     * @param id
     * @param message
     */
    public void sendMessageToUser(String id,TextMessage message){
       try {
           if (StringUtils.isNotBlank(id)){
               Set<WebSocketSession> sessions = userSessionMap.get(id);
               sendMessage(sessions,message);
           }else {
               logger.info("传入的id为空,所以不发送消息");
           }

       }catch (Exception e){
           logger.error("发送消息失败,失败原因是:\t",e);
       }
    }

    /**
     * 向指定的客户端发送消息
     * @param session
     * @param message
     */
    private void sendMessage(WebSocketSession session, TextMessage message){
            try {
                session.sendMessage(message);
            } catch (IOException e) {
                logger.error("发送消息异常",e);
            }
    }

    /**
     * 发送消息
     * @param sessions
     * @param message
     */
    private void sendMessage(Set<WebSocketSession> sessions, TextMessage message){

        if (sessions==null||sessions.size()==0){
            return;
        }
            for (WebSocketSession session: sessions
                 ) {
                try {
                    session.sendMessage(message);
                } catch (IOException e) {
                    logger.error("发送消息异常",e);
                }
            }

    }

    /**
     * 从缓存里面删除次会话
     * @param webSocketSession
     */
    private void deleteSessionFromCache(WebSocketSession webSocketSession){
        String id = getUserId(webSocketSession);
        logger.info("关闭的连接主键:\t"+id);
        Set<WebSocketSession> sessions = userSessionMap.get(id);
        sessions.remove(webSocketSession);
        //如果客户端的连结数0,直接移除
        if (sessions.size()==0){
            userSessionMap.remove(id);
        }
        onlineCount.decrementAndGet();

        logger.info("当前在线用户数: {}"+onlineCount);
    }

}
 

 

3.      前端集成操作

案例如下,前端js主要做的事情,判断是否支持websocket,支持,那么建立连接,定义weosocket的函数onmessage、onopen、onclose、onerror
var socket;
if(typeof(WebSocket) == "undefined") {
    console.log("您的浏览器不支持WebSocket");
}else {
    console.log("您的浏览器支持WebSocket");
}
 
var contextPath = '${pageContext.request.contextPath}';
var ip = window.location.host;
var url ;
if (contextPath){
    url = "ws://"+ip+"/"+contextPath+"/ws?userId="+parseInt(Math.random()*100);
}else {
    url = "ws://"+ip+"/ws?userId="+parseInt(Math.random()*100);
}
//前面是协议,后面是项目名称/监听的url,加入uid是为了区分不同的页面
console.log(url);
initSocket(url);
function initSocket(url) {
    //初始化一个socket客户端
    socket = new WebSocket(url);
    //打开事件
    socket.onopen = function() {
        console.log("Socket 已打开");
        //socket.send("这是来自客户端的消息" + location.href + new Date());
        };

//获得消息事件
socket.onmessage = function(msg) {
    console.log(msg.data);
    //发现消息进入    调后台获取
    // getCallingList();
};
    //关闭事件
        socket.onclose = function() {
            console.log("Socket已关闭");
        };
    //发生了错误事件
        socket.onerror = function() {
            alert("Socket发生了错误");
        }

}


/**
 * 当用户离开了页面,关闭连接
 * 点击某个离开页面的链接
 在地址栏中键入了新的 URL
 使用前进或后退按钮
 关闭浏览器
 重新加载页面
 *
 */
$(window).unload(function(){
    socket.close();
});

//定时发送心跳,防止客户端与服务器因为超时时间发生断开
window.setInterval(function(){
    socket.send('发送心跳');
},40000);

 

 

到此java与websocket集成完成。

 

四、   Websocket集成遇到的难题

1.      websocket对于spring有要求,至少4.0以上。

 

2.      在部署nginx的时候,需要加入特殊的配置,nginx 的版本至少1.3以上才行,
    proxy_set_header Upgrade$http_upgrade;
    proxy_set_header Connection"upgrade";

 

3.      部署websocket的容器也有要求,Tomcat从7.0.27开始支持,我使用tomcat 8.5版本可以。

 

4.      前端页面需要定时发送心跳请求,不然会有超时,断了,那么就无法再推送了。

 

五、   Websocket实现session共享方法

1.      前提摘要

因为单例的服务器到后期无法满足越来越大的客户量请求,所以开始部署多台服务器,组成集群环境,缓解高并发请求压力。但是这样会造成会话请求的session无法进行共享。所以以前很容易碰到我明明登录了系统,但是刷新了下页面,又需要重新登录的情况。这就是session没有共享出现的典型情况。而websocket不会出现这种情况,其链接一但建立,那么就会切换到tcp通道。每一次刷新页面都是断开重新建立连接操作。标题说的session共享是服务器向客户端推送的共享。

这里我采用的是使用redis消息订阅与发布功能,可能还有其他更好的办法,暂不讨论。

 

2.      集成步骤

1)      配置redis

案例如下

 

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="100" />
    <property name="maxTotal" value="600" />
</bean>

<bean id="jedisConnectionFactory"
      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
    <constructor-arg ref="jedisPoolConfig"/>
    <!--<constructor-arg ref="redisClusterConfiguration"/>-->
    <property name="hostName" value="${redis.name}"/>
    <property name="port" value="${redis.port}"/>
    <property name="password" value="${redis.password}"/>
</bean>

<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory"/>
    <property name="enableTransactionSupport" value="true"/>
    <property name="keySerializer" ref="stringRedisSerializer"/>
    <property name="hashKeySerializer" ref="stringRedisSerializer"/>
    <property name="valueSerializer" ref="stringRedisSerializer"/>
    <property name="hashValueSerializer" ref="stringRedisSerializer"/>
</bean>

 

2)      书写redis的订阅者与发布者类

订阅者:需要实现接口M e s sa ge Listener,实现里面的方法onMessage

案例如下:

StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

private InitService initService;

//当生产者发送一条数据的时候,监听事件
@Override
public void onMessage(Message message, byte[] pattern) {
    byte[] body = message.getBody();
    byte[] channel = message.getChannel();

    String msg = (String)stringRedisSerializer.deserialize(body);
    String topic = (String)stringRedisSerializer.deserialize(channel);
    logger.info("我是sub,监听"+topic+",我收到消息:"+msg);
    //接受到消息后,需要处理
    initService.addQuene(msg);

 

发布者:将需要推送的消息,发布到redis中,供订阅者订阅

案例如下:

        /**
 * 发布者发布消息
 * @param message 发布的消息
 */
public void sendMessage(String message){
    redisTemplate.convertAndSend(config.getTopicName(),message);
}
3)      配置消息订阅与发布的关系

这里主要是配置redis订阅者订阅的主题

<bean id="initService" class="com.iflytek.sgy.websocket.service.impl.InitServiceImpl"/>

<bean id="topicMessageListener" class="com.iflytek.sgy.websocket.pubAndSub.Subscription">
    <property name="initService" ref="initService"/>
</bean>

<bean id="channelTopic" class="org.springframework.data.redis.listener.ChannelTopic">
    <constructor-arg value="${topic.name}" />
</bean>

<!-- SDR Pub/Sub配置 -->
<bean id="topicContainer"
      class="org.springframework.data.redis.listener.RedisMessageListenerContainer"
      destroy-method="destroy">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
    <property name="messageListeners">
        <map>
            <entry key-ref="topicMessageListener">
                <ref bean="channelTopic" />
            </entry>
        </map>
    </property>
</bean>
 
至此,配置完成。当集群中项目运行起来后,订阅同一个redis中主题的订阅者,在监听到发布者发布消息后,会进行操作,session共享解决办法完成。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

woniyu123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值