使用WebSocket精准感知用户的在线状态

WebSocket

WebSocket技术实现了什么

在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

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

Http 的无状态无连接

无连接

的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
早期这么做的原因是 HTTP 协议产生于互联网,因此服务器需要处理同时面向全世界数十万、上百万客户端的网页访问,但每个客户端(即浏览器)与服务器之间交换数据的间歇性较大(即传输具有突发性、瞬时性),并且网页浏览的联想性、发散性导致两次传送的数据关联性很低,大部分通道实际上会很空闲、无端占用资源。因此 HTTP 的设计者有意利用这种特点将协议设计为请求时建连接、请求完释放连接,以尽快将资源释放出来服务其他客户端。
随着时间的推移,网页变得越来越复杂,里面可能嵌入了很多图片,这时候每次访问图片都需要建立一次 TCP 连接就显得很低效。后来,Keep-Alive 被提出用来解决这效率低的问题。
Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive 功能避免了建立或者重新建立连接。市场上的大部分 Web 服务器,包括 iPlanet、IIS 和 Apache,都支持 HTTP Keep-Alive。对于提供静态内容的网站来说,这个功能通常很有用。但是,对于负担较重的网站来说,这里存在另外一个问题:虽然为客户保留打开的连接有一定的好处,但它同样影响了性能,因为在处理暂停期间,本来可以释放的资源仍旧被占用。当Web服务器和应用服务器在同一台机器上运行时,Keep-Alive 功能对资源利用的影响尤其突出。
这样一来,客户端和服务器之间的 HTTP 连接就会被保持,不会断开(超过 Keep-Alive 规定的时间,意外断电等情况除外),当客户端发送另外一个请求时,就使用这条已经建立的连接。

无状态

无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送数据过来,但是,发送完,不会记录任何信息。
HTTP 是一个无状态协议,这意味着每个请求都是独立的,Keep-Alive 没能改变这个结果。
缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP 协议这种特性有优点也有缺点,优点在于解放了服务器,每一次请求“点到为止”不会造成不必要连接占用,缺点在于每次请求会传输大量重复的内容信息。
客户端与服务器进行动态交互的 Web 应用程序出现之后,HTTP 无状态的特性严重阻碍了这些应用程序的实现,毕竟交互是需要承前启后的,简单的购物车程序也要知道用户到底在之前选择了什么商品。于是,两种用于保持 HTTP 连接状态的技术就应运而生了,一个是 Cookie,而另一个则是 Session。

WebSocket解决我在实际项目中的什么问题?

我需要解决的问题:

获取后台处理业务用户的在线状态

思路的偏差

获取用户的登录状态,这个问题乍一看好像也不是什么难得事情。所以一开始我就想着使用Linstener & Session 来实现这个事情.于是就有了下面的这段代码

/**
 * @Author:Liu
 * @Date:2018/12/24 10:26
 * @Description: 
 * @Versio */
/*@WebListener
@Component*/
public class OnlieLinstner implements HttpSessionAttributeListener,HttpSessionListener {

    @Autowired
    private StringRedisTemplate redisTemplate;
    // 当设置Session属性时候触发该监听方法
    @Override
    public void attributeAdded(HttpSessionBindingEvent se) {
        String onlineUser = se.getSession().getAttribute("user").toString();
        /*System.out.println(onlineUser);*/
        redisTemplate.opsForValue().set(onlineUser+Online.FLAG,Online.ONLINE_VALUE);
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent se) {

    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent se) {

    }

    //监听session的创建 attributeAdded 方法之前
    @Override
    public void sessionCreated(HttpSessionEvent se) {

    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("30秒之后Session失效");
    }
}

其实这样做完,我就感觉到了两处不妥。

  1. 如果用户不点击系统中的退出登录之类的按钮,而是很直接很粗暴的直接点击浏览器的关闭按钮的话,我们是无法瞬间监测到用户的下线状态,因为我们的用户状态其实是通过监听session的生命周期来实现。那么这个时候,游离状态的session在自动结束了自己的生命之后我们才能获取到当前用户已下线的信息.
    2.如果为了解决session默认生命周期时间过长的问题,我曾视图把Session的默认生命周期时长改小,以此来解决这种大颗粒度的问题。这个想法看似可以行得通,其实用户体验会变大差很多,也是一种在实际生产环境中不被允许的事情.

朋友一语点醒梦中人

于是,我在我们的技术交流群中描述了一下我的问题。

“用WebSocket 拉一条专线,很好用的!”

恍然大悟..... 于是有了后面的这个解决方案

 
            org.springframework.boot
            spring-boot-starter-websocket
            2.0.4.RELEASE
        
@Controller
@ServerEndpoint(value = "/websocket",configurator = GetHttpSessionConfigurator.class)
public class MyWebSocket {

    @Autowired
    private StringRedisTemplate redisTemplate;


    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();

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

    private String keyName;
    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig endpointConfig) {
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
        HttpSession httpSession = (HttpSession) endpointConfig.getUserProperties().get(HttpSession.class.getName());
        String onlineUser = httpSession.getAttribute("user").toString();
        keyName = onlineUser;
        System.out.println("redis中设置上线状态 -----"+keyName);
        redisTemplate.opsForValue().set(onlineUser+Online.FLAG,Online.ONLINE_VALUE);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
        System.out.println("redis中的设置下线状态"+keyName);
        redisTemplate.opsForValue().set(keyName+Online.FLAG,Online.NOT_ONLINE_VALUE);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自客户端的消息:" + message);

    }

    /**
     * 发生错误时调用
     * @OnError
     */
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }

    /**
     * 发送消息
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


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

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

    public static synchronized void subOnlineCount() {
        MyWebSocket.onlineCount--;
    }
}
@Configuration
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator implements ApplicationContextAware {

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession=(HttpSession) request.getHttpSession();
        if (httpSession != null){
            sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
            super.modifyHandshake(sec, request, response);
        }

    }

    private static volatile BeanFactory context;

    @Override
    public  T getEndpointInstance(Class clazz) throws InstantiationException
    {
        return context.getBean(clazz);
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("auto load"+this.hashCode());
        GetHttpSessionConfigurator.context = applicationContext;
    }
}

关于使用@ServerEndpoint 后无法@Autowired Bean 的问题

本质原因:spring管理的都是单例(singleton)对象,和 websocket (多对象)相冲突。
详细解释:项目启动时初始化,会创建第一个 websocket (非用户连接),spring 会为其注入 service,该对象的 service 不是 null。但是,由于 spring 默认管理的是单例,所以只会注入一次 service。当新用户进入聊天时,系统又会创建一个新的 websocket 对象,这时矛盾出现了:spring 管理的都是单例,不会给第二个 websocket 对象注入 service,所以导致只要是用户连接创建的 websocket 对象,都不能正常注入.

Http 与 Https 下使用Websocket


localhost和127.0.0.1其实并不是同一个连接

本地调试使用 ws 协议的时候遇到的一个问题 这个注意一下就好了 具体原因是什么我也没有细究,只是把我踩过的坑告诉大家.

Nginx配置wss

由于我们项目上线是以用的https 所以对象的ws 也要切换成wss 那么我们可以在Nginx上进行配置


nginx.conf

测试效果


log

当然了我这里使用的是打印的方式,你也可以使用log.info 进行日志记录我图方便就没这样写。
我们程序员也是有职业素养的哈 哈哈哈 请参考《程序员的自我修养》

你可能感兴趣的:(使用WebSocket精准感知用户的在线状态)