如何使用Tomcat实现WebSocket即时通信服务服务端

2022年05月11日 阅读数:7
这篇文章主要向大家介绍如何使用Tomcat实现WebSocket即时通信服务服务端,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。
摘要:HTTP协议是“请求-响应”模式,浏览器必须先发请求给服务器,服务器才会响应该请求。即服务器不会主动发送数据给浏览器。

本文分享自华为云社区《Tomcat支持WebSocket吗?》,做者: JavaEdge 。java

HTTP协议是“请求-响应”模式,浏览器必须先发请求给服务器,服务器才会响应该请求。即服务器不会主动发送数据给浏览器。web

实时性要求高的应用,如在线游戏、股票实时报价和在线协同编辑等,浏览器需实时显示服务器的最新数据,所以出现Ajax和Comet技术:编程

  • Ajax本质仍是轮询
  • Comet基于HTTP长链接作了一些hack

但它们实时性不高,频繁请求也会给服务器巨大压力,也浪费网络流量和带宽。因而HTML5推出WebSocket标准,使得浏览器和服务器之间任一方均可主动发消息给对方,这样服务器有新数据时可主动推给浏览器。浏览器

WebSocket原理

网络上的两个程序经过一个双向链路进行通讯,这个双向链路的一端称为一个Socket。一个Socket对应一个IP地址和端口号,应用程序一般经过Socket向网络发出或应答网络请求。安全

Socket不是协议,是对TCP/IP协议层抽象出来的API。服务器

WebSocket跟HTTP协议同样,也是应用层协议。为兼容HTTP协议,它经过HTTP协议进行一次握手,握手后数据就直接从TCP层的Socket传输,与HTTP协议再无关。websocket

这里的握手指应用协议层,不是TCP层,握手时,TCP链接已创建。即HTTP请求里带有websocket的请求头,服务端回复也带有websocket的响应头。网络

浏览器发给服务端的请求会带上跟WebSocket有关的请求头,好比Connection: Upgrade和Upgrade: websocketsession

若服务器支持WebSocket,一样会在HTTP响应加上WebSocket相关的HTTP头部:多线程

这样WebSocket链接就创建好了。

WebSocket的数据传输以frame形式传输,将一条消息分为几个frame,按前后顺序传输出去。为什么这样设计?

  • 大数据的传输能够分片传输,无需考虑数据大小问题
  • 和HTTP的chunk同样,可边生成数据边传输,提升传输效率

Tomcat如何支持WebSocket

WebSocket聊天室案例

浏览器端核心代码:

var Chat = {};
Chat.socket = null;
Chat.connect = (function(host) {

    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        // 若支持,则建立WebSocket JS类
        Chat.socket = new WebSocket(host);
    } else if ('MozWebSocket' in window) {
        Chat.socket = new MozWebSocket(host);
    } else {
        Console.log('WebSocket is not supported by this browser.');
        return;
    }

  	// 再实现几个回调方法
    // 回调函数,当和服务器的WebSocket链接创建起来后,浏览器会回调这个方法
    Chat.socket.onopen = function () {
        Console.log('Info: WebSocket connection opened.');
        document.getElementById('chat').onkeydown = function(event) {
            if (event.keyCode == 13) {
                Chat.sendMessage();
            }
        };
    };

    // 回调函数,当和服务器的WebSocket链接关闭后,浏览器会回调这个方法
    Chat.socket.onclose = function () {
        document.getElementById('chat').onkeydown = null;
        Console.log('Info: WebSocket closed.');
    };

    // 回调函数,当服务器有新消息发送到浏览器,浏览器会回调这个方法
    Chat.socket.onmessage = function (message) {
        Console.log(message.data);
    };
});

服务器端Tomcat实现代码:

Tomcat端的实现类加上**@ServerEndpoint**注解,value是URL路径

@ServerEndpoint(value = "/websocket/chat")
public class ChatEndpoint {

    private static final String GUEST_PREFIX = "Guest";
 
    // 记录当前有多少个用户加入到了聊天室,它是static全局变量。为了多线程安全使用原子变量AtomicInteger
    private static final AtomicInteger connectionIds = new AtomicInteger(0);
 
    //每一个用户用一个CharAnnotation实例来维护,请你注意它是一个全局的static变量,因此用到了线程安全的CopyOnWriteArraySet
    private static final Set<ChatEndpoint> connections =
            new CopyOnWriteArraySet<>();

    private final String nickname;
    private Session session;

    public ChatEndpoint() {
        nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
    }

    //新链接到达时,Tomcat会建立一个Session,并回调这个函数
    @OnOpen
    public void start(Session session) {
        this.session = session;
        connections.add(this);
        String message = String.format("* %s %s", nickname, "has joined.");
        broadcast(message);
    }

    //浏览器关闭链接时,Tomcat会回调这个函数
    @OnClose
    public void end() {
        connections.remove(this);
        String message = String.format("* %s %s",
                nickname, "has disconnected.");
        broadcast(message);
    }

    //浏览器发送消息到服务器时,Tomcat会回调这个函数
    @OnMessage
    public void incoming(String message) {
        // Never trust the client
        String filteredMessage = String.format("%s: %s",
                nickname, HTMLFilter.filter(message.toString()));
        broadcast(filteredMessage);
    }

    // WebSocket链接出错时,Tomcat会回调这个函数
    @OnError
    public void onError(Throwable t) throws Throwable {
        log.error("Chat Error: " + t.toString(), t);
    }

    // 向聊天室中的每一个用户广播消息
    private static void broadcast(String msg) {
        for (ChatAnnotation client : connections) {
            try {
                synchronized (client) {
                    client.session.getBasicRemote().sendText(msg);
                }
            } catch (IOException e) {
              ...
            }
        }
    }
}

根据Java WebSocket规范的规定,Java WebSocket应用程序由一系列的WebSocket Endpoint组成。Endpoint是一个Java对象,表明WebSocket链接的一端,就好像处理HTTP请求的Servlet同样,你能够把它看做是处理WebSocket消息的接口。

跟Servlet不一样的地方在于,Tomcat会给每个WebSocket链接建立一个Endpoint实例。

能够经过两种方式。

定义和实现Endpoint

编程式

编写一个Java类继承javax.websocket.Endpoint,并实现它的onOpen、onClose和onError方法。这些方法跟Endpoint的生命周期有关,Tomcat负责管理Endpoint的生命周期并调用这些方法。而且当浏览器链接到一个Endpoint时,Tomcat会给这个链接建立一个惟一的Session(javax.websocket.Session)。Session在WebSocket链接握手成功以后建立,并在链接关闭时销毁。当触发Endpoint各个生命周期事件时,Tomcat会将当前Session做为参数传给Endpoint的回调方法,所以一个Endpoint实例对应一个Session,咱们经过在Session中添加MessageHandler消息处理器来接收消息,MessageHandler中定义了onMessage方法。在这里Session的本质是对Socket的封装,Endpoint经过它与浏览器通讯。

注解式

实现一个业务类并给它添加WebSocket相关的注解。

@ServerEndpoint(value = "/websocket/chat")

注解,它代表当前业务类ChatEndpoint是个实现了WebSocket规范的Endpoint,而且注解的value值代表ChatEndpoint映射的URL是/websocket/chat。ChatEndpoint类中有@OnOpen、@OnClose、@OnError和在@OnMessage注解的方法,见名知义。

咱们只需关心具体的Endpoint实现,好比聊天室,为向全部人群发消息,ChatEndpoint在内部使用了一个全局静态的集合CopyOnWriteArraySet维护全部ChatEndpoint实例,由于每个ChatEndpoint实例对应一个WebSocket链接,即表明了一个加入聊天室的用户。

当某个ChatEndpoint实例收到来自浏览器的消息时,这个ChatEndpoint会向集合中其余ChatEndpoint实例背后的WebSocket链接推送消息。

  • Tomcat主要作了哪些事情呢?
    Endpoint加载和WebSocket请求处理。

WebSocket加载

Tomcat的WebSocket加载是经过SCI,ServletContainerInitializer,是Servlet 3.0规范中定义的用来接收Web应用启动事件的接口。

为何要监听Servlet容器的启动事件呢?这样就有机会在Web应用启动时作一些初始化工做,好比WebSocket须要扫描和加载Endpoint类。

将实现ServletContainerInitializer接口的类增长HandlesTypes注解,而且在注解内指定的一系列类和接口集合。好比Tomcat为了扫描和加载Endpoint而定义的SCI类以下:

定义好SCI,Tomcat在启动阶段扫描类时,会将HandlesTypes注解指定的类都扫描出来,做为SCI的onStartup参数,并调用SCI#onStartup。

WsSci#HandlesTypes注解定义了ServerEndpoint.class、ServerApplicationConfig.class和Endpoint.class,所以在Tomcat的启动阶段会将这些类的类实例(不是对象实例)传递给WsSci#onStartup。

  • WsSci的onStartup方法作了什么呢?

构造一个WebSocketContainer实例,你能够把WebSocketContainer理解成一个专门处理WebSocket请求的Endpoint容器。即Tomcat会把扫描到的Endpoint子类和添加了注解@ServerEndpoint的类注册到这个容器,而且该容器还维护了URL到Endpoint的映射关系,这样经过请求URL就能找到具体的Endpoint来处理WebSocket请求。

WebSocket请求处理

  • Tomcat链接器的组件图

Tomcat用ProtocolHandler组件屏蔽应用层协议的差别,ProtocolHandler两个关键组件:Endpoint和Processor。

这里的Endpoint跟上文提到的WebSocket中的Endpoint彻底是两回事,链接器中的Endpoint组件用来处理I/O通讯。WebSocket本质是个应用层协议,不能用HttpProcessor处理WebSocket请求,而要用专门Processor,在Tomcat就是UpgradeProcessor。

由于Tomcat是将HTTP协议升级成WebSocket协议的,由于WebSocket是经过HTTP协议握手的,当WebSocket握手请求到来时,HttpProtocolHandler首先接收到这个请求,在处理这个HTTP请求时,Tomcat经过一个特殊的Filter判断该当前HTTP请求是不是一个WebSocket Upgrade请求(即包含Upgrade: websocket的HTTP头信息),若是是,则在HTTP响应里添加WebSocket相关的响应头信息,并进行协议升级。

就是用UpgradeProtocolHandler替换当前的HttpProtocolHandler,相应的,把当前Socket的Processor替换成UpgradeProcessor,同时Tomcat会建立WebSocket Session实例和Endpoint实例,并跟当前的WebSocket链接一一对应起来。这个WebSocket链接不会当即关闭,而且在请求处理中,再也不使用原有的HttpProcessor,而是用专门的UpgradeProcessor,UpgradeProcessor最终会调用相应的Endpoint实例来处理请求。

Tomcat对WebSocket请求的处理没有通过Servlet容器,而是经过UpgradeProcessor组件直接把请求发到ServerEndpoint实例,而且Tomcat的WebSocket实现不须要关注具体I/O模型的细节,从而实现了与具体I/O方式的解耦。

总结

WebSocket技术实现了Tomcat与浏览器的双向通讯,Tomcat能够主动向浏览器推送数据,能够用来实现对数据实时性要求比较高的应用。这须要浏览器和Web服务器同时支持WebSocket标准,Tomcat启动时经过SCI技术来扫描和加载WebSocket的处理类ServerEndpoint,而且创建起了URL到ServerEndpoint的映射关系。

当第一个WebSocket请求到达时,Tomcat将HTTP协议升级成WebSocket协议,并将该Socket链接的Processor替换成UpgradeProcessor。这个Socket不会当即关闭,对接下来的请求,Tomcat经过UpgradeProcessor直接调用相应的ServerEndpoint来处理。

还能够经过Spring来实现WebSocket应用。

 

点击关注,第一时间了解华为云新鲜技术~