tomcat支持websocket吗?-4008云顶国际网站

发表于 2022/05/06 23:23:12 2022/05/06
【摘要】 http协议是“请求-响应”模式,浏览器必须先发请求给服务器,服务器才会响应该请求。即服务器不会主动发送数据给浏览器。实时性要求高的应用,如在线游戏、股票实时报价和在线协同编辑等,浏览器需实时显示服务器的最新数据,因此出现ajax和comet技术:ajax本质还是轮询comet基于http长连接做了一些hack但它们实时性不高,频繁请求也会给服务器巨大压力,也浪费网络流量和带宽。于是html...

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

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

  • ajax本质还是轮询
  • comet基于http长连接做了一些hack

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

websocket原理

网络上的两个程序通过一个双向链路进行通信,这个双向链路的一端称为一个socket。一个socket对应一个ip地址和端口号,应用程序通常通过socket向网络发出或应答网络请求。
socket不是协议,是对tcp/ip协议层抽象出来的api。

websocket跟http协议一样,也是应用层协议。为兼容http协议,它通过http协议进行一次握手,握手后数据就直接从tcp层的socket传输,与http协议再无关。

这里的握手指应用协议层,不是tcp层,握手时,tcp连接已建立。
即http请求里带有websocket的请求头,服务端回复也带有websocket的响应头。

浏览器发给服务端的请求会带上跟websocket有关的请求头,比如connection: upgrade和upgrade: websocket

若服务器支持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应用。

【4008云顶国际集团的版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。