Websocket实战技术总结

·  阅读 233
Websocket实战技术总结

应用场景

弹幕
消息订阅
多玩家游戏
协同编辑
股票基金实时报价
视频会议
在线教育
聊天室

结论

  1. WebSocket和HTTP都是基于TCP协议。两个完全不同的应用层协议
  2. WebSocket依赖于HTTP连接进行第一次握手
  3. Socket不是协议,它是在程序层面上对传输层协议的接口封装,可以理解为一个能够提供端对端的通信的调用接口(API)

实例

springboot

依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
复制代码

内置tomcat注入bean

import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


@Configuration
@ConditionalOnWebApplication
public class WebSocketConfig {

    //注意:用外置tomcat不需要注入此bean
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
复制代码

websocket实现类

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@ServerEndpoint(value = "/接口名/{userId}")
@Component
public class MyWebSocketService {

    private static final String NULL_KEY = "null";
    /**
     * 心跳连接有效时间(毫秒)
     */
    private static final Long BEAT_HEART_DURATION_TIME_MILLIS = 10 * 60 * 1000L;

    /**
     * 用来记录当前在线连接数
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    /**
     * concurrent包的线程安全Map,用来存放每个客户端对应的Session对象。
     */
    public static Map<String, Session> clients = new ConcurrentHashMap<String, Session>();

    /**
     * concurrent包的线程安全Map,用来存放每个客户端对应的Session对象。
     */
    private static Map<Session, String> sessionMap = new ConcurrentHashMap<Session, String>();

    private static Map<String, Session> oldClients = new ConcurrentHashMap<String, Session>();


    private static Map<Session, Long> sessionBeatheartMap = new ConcurrentHashMap<Session, Long>();

    @Autowired
    private MessageService messageService;

    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {
        if (StringUtils.isEmpty(userId) || NULL_KEY.equalsIgnoreCase(userId)) {
            try {
                log.warn("[key={}]非法,禁止连接!!!", userId);
                session.close();
            } catch (IOException e) {
            }
        }
        if (clients.containsKey(userId)) {
            //删除原有连接
            destroyOldSession(userId);
        }
        //在线数加1
        addOnlineCount();
        clients.put(userId, session);
        sessionMap.put(session, userId);
        sessionBeatheartMap.put(session, System.currentTimeMillis());
        log.info("新连接[userId={}]加入!当前在线连接数为{}", userId, getOnlineCount());

    }

    @OnClose
    public void onClose(Session session) {
        String key = sessionMap.get(session);
        if (StringUtils.isNotEmpty(key)) {
            if (clients.containsKey(key)) {
                clients.remove(key);
                //在线数减1
                subOnlineCount();
            }
            sessionMap.remove(session);
            sessionBeatheartMap.remove(session);
            log.info("连接 [userId={}]关闭!当前在线连接数为{}", key, getOnlineCount());
            /**通知系统断开连接**/
            destroyOldSession(key);
        }
    }


    @Scheduled(cron = " */5 * * * * ?")
    public void processTerminalInformation() {
        if (clients.isEmpty()) {
            return;
        }
        clients.forEach((k, v) -> {
            try {
                List<Message> messages = messageService.getMessageLists(k);

                if (CollectionUtil.isNotEmpty(messages)) {

                    v.getAsyncRemote().sendText(JSON.toJSONString(messages));
                }
            } catch (Exception e) {
                destroyOldSession(k);
            }
        });
    }


    @Scheduled(cron = "0 */1 * * * ?")
    public void processOnlineTime() {

        oldClients.forEach((k, v) -> {
            try {
                Long lastBeatTime = sessionBeatheartMap.get(v);
                if (lastBeatTime == null || (System.currentTimeMillis() - lastBeatTime) > BEAT_HEART_DURATION_TIME_MILLIS) {
                    //超过90秒未收到空消息,KEY 设备已断开连接
                    destroyOldSession(k);
                }
            } catch (Exception e) {
                //连接不可用,清理连接
                destroyOldSession(k);
            }
        });
        oldClients = clients;
    }


    private void destroyOldSession(String key) {
        Session oldSession = clients.get(key);
        if (oldSession != null) {
            if (clients.containsKey(key)) {
                subOnlineCount();
                clients.remove(key);
                if (oldSession != null) {
                    sessionMap.remove(oldSession);
                    sessionBeatheartMap.remove(oldSession);
                }
                try {
                    oldSession.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "已断开连接!"));
                } catch (IOException e) {
                }
            }
        }
    }


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

    /**
     * 增加连接人数
     */
    public static synchronized void addOnlineCount() {
        onlineCount.incrementAndGet();
    }

    /**
     * 减少连接人数
     */
    public static synchronized void subOnlineCount() {
        onlineCount.decrementAndGet();
    }
}
复制代码

Vue

用法

  created() {
            this.initWebSocket();
        },
        methods: {
            initWebSocket() {
                let token = localStorage.getItem('token');
                const url = 'ws://127.0.0.1:端口号/接口名/' + token;
                this.websocket = new WebSocket(url);
                this.websocket.onopen = this.websockOpen;
                this.websocket.onmessage = this.websocketonmessage;
                this.websocket.onclose = this.websocketclose;
            },
            websockOpen() {
                console.log("WebSocket连接成功");
            },
            websocketonmessage(e) { //数据接收
                console.log(e);

            },
            websocketclose(e) { //关闭
                console.log("close..")
            },
            logout() { //这部分是退出的时候关闭websocket链接
                window.localStorage.removeItem('token');
                this.$router.push({path: '/login'})
                this.websocket.close();
            },
            }    
复制代码

结论刨析

Ajax、Long poll、Websocket图示

虽然http1.1默认开启了keep-alive长连接保持了这个TCP通道使得 在一个HTTP连接中,可以发送多个Request,接收多个Response,但是一个request只能有一个response 。而且这个response也是被动的,不能主动发起

协议升级

每个WebSocket连接都始于一个HTTP请求。WebSocket协议在第一次握手连接时,通过HTTP协议在传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin: xxx
Sec-WebSocket-Version: 13

注意,关键的地方是,这里面有个Upgrade首部,用来把当前的HTTP请求升级到WebSocket协议,这是HTTP协议本身的内容,是为了扩展支持其他的通讯协议。如果服务器支持新的协议,则必须返回101:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

至此,HTTP请求物尽其用,如果成功出发onopen事件,否则触发onerror事件,后面的传输则不再依赖HTTP协议。

为什么依赖http

  1. WebSocket设计上就是天生为HTTP 增强 通信(全双工通信等),所以在HTTP协议连接的基础上是很自然的一件事,并因此而能获得HTTP的诸多便利。
  2. 这诸多便利中有一条很重要,基于HTTP连接将获得最大的一个 兼容 支持,比如即使服务器不支持WebSocket也能建立HTTP通信,只不过返回的是onerror而已,这显然比服务器无响应要好的多。

参考文章

理清 WebSocket 和 HTTP 的关系

零距离接触websocket


看到这里就点个赞吧👇分享更多技术文章,去帮助更多的人,这里有我所有知识库哟~ 🐌   www.yuque.com/yingwenerji…
收藏成功!
已添加到「」, 点击更改