Tomcat自7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356 ),而在7.0.5版本之前(7.0.2版本之后)则采用自定义API,即WebSocketServlet。本节我们仅介绍Tomcat针对规范的实现。

根据JSR356的规定,Java WebSocket应用由一系列的WebSocket Endpoint组成。Endpoint是一个Java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之于HTTP请求一样(不同之处在于Endpoint每个链接一个实例)。

我们可以通过两种方式定义Endpoint,第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法。第二种是注解式,即定义一个POJO对象,为其添加Endpoint相关的注解。

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。Endpoint接口明确定义了与其生命周期相关的方法,规范实现者确保在生命周期的各个阶段调用实例的相关方法。

Endpoint的生命周期方法如下:

  • onOpen:当开启一个新的会话时调用。这是客户端与服务器握手成功后调用的方法。等同于注解@OnOpen。
  • onClose:当会话关闭时调用。等同于注解@OnClose。
  • onError:当链接过程中异常时调用。等同于注解@OnError。

当客户端链接到一个Endpoint时,服务器端会为其创建一个唯一的会话(javax.websocket.Session)。会话在WebSocket握手之后创建,并在链接关闭时结束。当生命周期中触发各个事件时,都会将当前会话传给Endpoint。

我们通过为Session添加MessageHandler消息处理器来接收消息。当采用注解方式定义Endpoint时,我们还可以通过@OnMessage指定接收消息的方法。发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步消息发送的实例或者通过Session.getAsyncRemote获取异步消息发送的实例。

WebSocket通过javax.websocket.WebSocketContainer接口维护应用中定义的所有Endpoint。它在每个Web应用中只有一个实例,类似于传统Web应用中的ServletContext。

最后,WebSocket规范提供了一个接口javax.websocket.server.ServerApplicationConfig,通过它,我们可以为编程式的Endpoint创建配置(如指定请求地址),还可以过滤只有符合条件的Endpoint提供服务。该接口的实现同样通过SCI机制加载。

介绍完WebSocket规范中的基本概念,我们看一下Tomcat的具体实现。接下来会涉及到Tomcat链接器(Cotyte)和Web应用加载的知识,如不清楚可以阅读Tomcat官方文档。

WebSocket加载

Tomcat提供了一个javax.servlet.ServletContainerInitializer的实现类org.apache.tomcat.websocket.server.WsSci。因此Tomcat的WebSocket加载是通过SCI机制完成的。WsSci可以处理的类型有三种:添加了注解@ServerEndpoint的类、Endpoint的子类以及ServerApplicationConfig的实现类。

Web应用启动时,通过WsSci.onStartup方法完成WebSocket的初始化:

  • 构造WebSocketContainer实例,Tomcat提供的实现类为WsServerContainer。在WsServerContainer构造方法中,Tomcat除了初始化配置外,还会为ServletContext添加一个过滤器org.apache.tomcat.websocket.server.WsFilter,它用于判断当前请求是否为WebSocket请求,以便完成握手。
  • 对于扫描到的Endpoint子类和添加了注解@ServerEndpoint的类,如果当前应用存在ServerApplicationConfig实现,则通过ServerApplicationConfig获取Endpoint子类的配置(ServerEndpointConfig实例,包含了请求路径等信息)和符合条件的注解类,将结果注册到WebSocketContainer上,用于处理WebSocket请求。
  • 通过ServerApplicationConfig接口我们以编程的方式确定只有符合一定规则的Endpoint可以注册到WebSocketContainer,而非所有。规范通过这种方式为我们提供了一种定制化机制。
  • 如果当前应用没有定义ServerApplicationConfig的实现类,那么WsSci默认只将所有扫描到的注解式Endpoint注册到WebSocketContainer。因此,如果采用可编程方式定义Endpoint,那么必须添加ServerApplicationConfig实现。

WebSocket请求处理

当服务器接收到来自客户端的请求时,首先WsFilter会判断该请求是否是一个WebSocket Upgrade请求(即包含Upgrade: websocket头信息)。如果是,则根据请求路径查找对应的Endpoint处理类,并进行协议Upgrade。

在协议Upgrade过程中,除了检测WebSocket扩展、添加相关的转换外,最主要的是添加WebSocket相关的响应头信息、构造Endpoint实例、构造HTTP Upgrade处理类WsHttpUpgradeHandler。

将WsHttpUpgradeHandler传递给具体的Tomcat协议处理器(ProtocolHandler)进行Upgrade。接收到Upgrade的动作后,Tomcat的协议处理器(HTTP协议)不再使用原有的Processor处理请求,而是替换为专门的Upgrade Processor。

根据I/O的不同,Tomcat提供的Upgrade Processor实现如下:

  • org.apache.coyote.http11.upgrade.BioProcessor;
  • org.apache.coyote.http11.upgrade.NioProcessor;
  • org.apache.coyote.http11.upgrade.Nio2Processor;
  • org.apache.coyote.http11.upgrade.AprProcessor;

替换成功后,WsHttpUpgradeHandler会对Upgrade Processor进行初始化(按以下顺序):

  • 创建WebSocket会话。
  • 为Upgrade Processor的输出流添加写监听器。WebSocket向客户端推送消息具体由org.apache.tomcat.websocket.server.WsRemoteEndpointImplServer完成。
  • 构造WebSocket会话,执行当前Endpoint的onOpen方法。
  • 为Upgrade Processor的输入流添加读监听器,完成消息读取。WebSocket读取客户端消息具体由org.apache.tomcat.websocket.server.WsFrameServer完成。

通过这种方式,Tomcat实现了WebSocket请求处理与具体I/O方式的解耦。

基于编程的示例

首先,添加一个Endpoint子类,代码如下:

package org.springframework.samples.websocket.demo3;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session; public class ChatEndpoint extends Endpoint {
private static final Set<ChatEndpoint> connections = new CopyOnWriteArraySet<>();
private Session session; private static class ChatMessageHandler implements MessageHandler.Partial<String> {
private Session session; private ChatMessageHandler(Session session) {
this.session = session;
} @Override
public void onMessage(String message, boolean last) {
String msg = String.format("%s %s %s", session.getId(), "said:", message);
broadcast(msg);
}
}; @Override
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
connections.add(this);
this.session.addMessageHandler(new ChatMessageHandler(session));
String message = String.format("%s %s", session.getId(), "has joined.");
broadcast(message);
} @Override
public void onClose(Session session, CloseReason closeReason) {
connections.remove(this);
String message = String.format("%s %s", session.getId(), "has disconnected.");
broadcast(message);
} @Override
public void onError(Session session, Throwable throwable) {
} private static void broadcast(String msg) {
for (ChatEndpoint client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
}
String message = String.format("%s %s", client.session.getId(), "has been disconnected.");
broadcast(message);
}
}
}
}

为了方便向客户端推送消息,我们使用一个静态集合作为链接池维护所有Endpoint实例。

在onOpen方法中,首先将当前Endpoint实例添加到链接池,然后为会话添加了一个消息处理器ChatMessageHandler,用于接收消息。当接收到客户端消息后,我们将其推送到所有客户端。最后向所有客户端广播一条上线通知。

在onClose方法中,将当前Endpoint从链接池中移除,向所有客户端广播一条下线通知。

然后定义ServerApplicationConfig实现,代码如下:

package org.springframework.samples.websocket.demo3;

import java.util.HashSet;
import java.util.Set; import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig; public class ChatServerApplicationConfig implements ServerApplicationConfig {
@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
return scanned;
} @Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> scanned) {
Set<ServerEndpointConfig> result = new HashSet<>();
if (scanned.contains(ChatEndpoint.class)) {
result.add(ServerEndpointConfig.Builder.create(ChatEndpoint.class, "/program/chat").build());
}
return result;
}
}

在ChatServerApplicationConfig中为ChatEndpoint添加ServerEndpointConfig,其请求链接为“/program/chat”。

最后添加对应的HTML页面,src\main\webapp\chat.html:

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="application/javascript">
var Chat = {};
Chat.socket = null;
Chat.connect = (function(host) {
if ('WebSocket' in window) {
Chat.socket = new WebSocket(host);
} else if ('MozWebSocket' in window) {
Chat.socket = new MozWebSocket(host);
} else {
Console.log('Error: WebSocket is not supported by this browser.');
return;
}
Chat.socket.onopen = function () {
Console.log('Info: WebSocket connection opened.');
document.getElementById('chat').onkeydown = function(event) {
if (event.keyCode == 13) {
Chat.sendMessage();
}
};
};
Chat.socket.onclose = function () {
document.getElementById('chat').onkeydown = null;
Console.log('Info: WebSocket closed.');
};
Chat.socket.onmessage = function (message) {
Console.log(message.data);
};
});
Chat.initialize = function() {
if (window.location.protocol == 'http:') {
Chat.connect('ws://' + window.location.host + '/spring-websocket-test/program/chat');
} else {
Chat.connect('wss://' + window.location.host + '/spring-websocket-test/program/chat');
}
};
Chat.sendMessage = (function() {
var message = document.getElementById('chat').value;
if (message != '') {
Chat.socket.send(message);
document.getElementById('chat').value = '';
}
});
var Console = {};
Console.log = (function(message) {
var console = document.getElementById('console');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.innerHTML = message;
console.appendChild(p);
while (console.childNodes.length > 25) {
console.removeChild(console.firstChild);
}
console.scrollTop = console.scrollHeight;
});
Chat.initialize();
</script>
</head>
<body>
<div>
<p>
<input type="text" placeholder="type and press enter to chat" id="chat" />
</p>
<div id="console-container">
<div id="console"/>
</div>
</div>
</body>
</html>

客户端实现并不复杂,只是要注意浏览器的区别。在添加完所有配置后,可以将应用部署到Tomcat查看效果,与Comet类似,我们可以同时开启两个客户端查看消息推送效果。

基于注解的示例

基于注解的定义要比编程式简单一些,首先定义一个POJO对象,并添加相关注解:

package org.springframework.samples.websocket.demo3;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint; @ServerEndpoint(value = "/anno/chat")
public class ChatAnnotation {
private static final Set<ChatAnnotation> connections = new CopyOnWriteArraySet<>();
private Session session; @OnOpen
public void start(Session session) {
this.session = session;
connections.add(this);
String message = String.format("%s %s", session.getId(), "has joined.");
broadcast(message);
} @OnClose
public void end() {
connections.remove(this);
String message = String.format("%s %s", session.getId(), "has disconnected.");
broadcast(message);
} @OnMessage
public void incoming(String message) {
String msg = String.format("%s %s %s", session.getId(), "said:", message);
broadcast(msg);
} @OnError
public void onError(Throwable t) throws Throwable {
} private static void broadcast(String msg) {
for (ChatAnnotation client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
}
String message = String.format("%s %s", client.session.getId(), "has been disconnected.");
broadcast(message);
}
}
}
}

@ServerEndpoint注解声明该类是一个Endpoint,并指定了请求的地址。

@OnOpen注解的方法在会话打开时调用,与ChatEndpoint类似,将当前实例添加到链接池。@OnClose注解的方法在会话关闭时调用。@OnError注解的方法在链接异常时调用。@OnMessage注解的方法用于接收消息。

使用注解方式定义Endpoint时,ServerApplicationConfig不是必须的,此时直接默认加载所有的@ServerEndpoin注解POJO。

我们可以直接将编程式示例中HTML页面src\main\webapp\chatanno.html中的链接地址改为“/anno/chat”查看效果。

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="application/javascript">
var Chat = {};
Chat.socket = null;
Chat.connect = (function(host) {
if ('WebSocket' in window) {
Chat.socket = new WebSocket(host);
} else if ('MozWebSocket' in window) {
Chat.socket = new MozWebSocket(host);
} else {
Console.log('Error: WebSocket is not supported by this browser.');
return;
}
Chat.socket.onopen = function () {
Console.log('Info: WebSocket connection opened.');
document.getElementById('chat').onkeydown = function(event) {
if (event.keyCode == 13) {
Chat.sendMessage();
}
};
};
Chat.socket.onclose = function () {
document.getElementById('chat').onkeydown = null;
Console.log('Info: WebSocket closed.');
};
Chat.socket.onmessage = function (message) {
Console.log(message.data);
};
});
Chat.initialize = function() {
if (window.location.protocol == 'http:') {
Chat.connect('ws://' + window.location.host + '/spring-websocket-test/anno/chat');
} else {
Chat.connect('wss://' + window.location.host + '/spring-websocket-test/anno/chat');
}
};
Chat.sendMessage = (function() {
var message = document.getElementById('chat').value;
if (message != '') {
Chat.socket.send(message);
document.getElementById('chat').value = '';
}
});
var Console = {};
Console.log = (function(message) {
var console = document.getElementById('console');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.innerHTML = message;
console.appendChild(p);
while (console.childNodes.length > 25) {
console.removeChild(console.firstChild);
}
console.scrollTop = console.scrollHeight;
});
Chat.initialize();
</script>
</head>
<body>
<div>
<p>
<input type="text" placeholder="type and press enter to chat" id="chat" />
</p>
<div id="console-container">
<div id="console"/>
</div>
</div>
</body>
</html>

结果:

  • tomcat是怎么加载ServerApplicationConfig的配置的,我想做嵌入式tomcat开发,请问您这边清楚的吗?
  • 回复xiaospace1028:通过org.apache.tomcat.websocket.server.WsSci类,这是一个ServletContainerInitializer,容器启动时会自动加载这个类,执行onStartup方法

websocket之三:Tomcat的WebSocket实现的更多相关文章

  1. Java后端WebSocket的Tomcat实现

    转自:http://blog.chenzuhuang.com/archive/28.html 文章摘要随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5 ...

  2. Java用webSocket实现tomcat的日志实时输出到web页面

    原文:http://blog.csdn.net/smile326/article/details/52218264 1.场景需求 后台攻城狮和前端攻城狮一起开发时,经常受到前端攻城狮的骚扰,动不动就来 ...

  3. WebSocket的Tomcat实现

    一.WebSocket简单介绍 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通 ...

  4. websocket采用tomcat方式,IOC类对象无法注入的解决方案

    前言 我采用的spring框架做的,主要用于IOC AOP ,spring之前采用的2.0版本.(2.0版本出错!下面有解释): 要实现websocket 实现后台主动与JSP发送数据. 具体操作 在 ...

  5. Tomcat下WebSocket最大连接数测试

    WebSocket现在很常用,想要测试tomcat的最大连接数,今天试了一个可行的办法和配置(之前是用全公司的设备一起来测试的,真机环境的测试收到网络的影响很大,其实真实环境应用中,网络才是webso ...

  6. Java后端WebSocket的Tomcat实现(转载)

    一.WebSocket简单介绍 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通 ...

  7. Java后端WebSocket的Tomcat实现 html5 WebSocket 实时聊天

    WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据.Tomcat7.0.47上才能运行. 需要添加Tomcat里lib目 ...

  8. 配置nginx+tomcat支持websocket

    问题情景:    最近开发新增加一个项目,需要支持https wss协议 访问https://test.aa.com  使用nginx反向代理到后端tomcat web应用 访问https://tes ...

  9. Java后端WebSocket的Tomcat实现(转)

    文章摘要随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端 ...

随机推荐

  1. 高通LCD驱动调试

    本文转载自:http://www.itgo.me/a/x6305658852004979994/lcd%20qcom 来自 :http://blog.csdn.net/dacaozuo/article ...

  2. MapReduce-join连接

    join连接 MapReduce能够执行大型数据集间的连接(join)操作.连接操作的具体实现技术取决于数据集的规模及分区方式连接操作如果由mapper执行,则称为“map端连接”:如果由reduce ...

  3. MapReduce-文本输入

    1.TextInputFormat TextInputFormat是默认的InputFormat.每条记录是一行输入.键是LongWritable类型,存储该行在整个文件中的字节偏移量.值是这行的内容 ...

  4. Apache Phoenix的序列

    序列作为标准SQL特性,允许生成递增的序列并应用在典型的ID中.为了创建一个序列,可以使用: 0:jdbc:phoenix:SZB-L0023780:2181:/hbase114> CREATE ...

  5. Python之flask总结

    一.flask      a.Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeug本质是Socket服务端,其用于接收htt ...

  6. 闲聊SEO

    SEO 1. SEO 搜索引擎优化 免费(Baidu,Google) SEM 搜索引擎营销 收费 2. IP 独立IP访问的用户 PV 页面的点击量 UV 独立访客数 3. 搜索引擎蜘蛛 权重 去让搜 ...

  7. OpenStack日志搜集分析之ELK

    ELK 安装配置简单,用于管理 OpenStack 日志时需注意两点: Logstash 配置文件的编写 Elasticsearch 日志存储空间的容量规划 另外推荐 ELKstack 中文指南. E ...

  8. nova Rescue 和 Unrescue

    usage: nova rescue [--password <password>] [--image <image>] <server> Reboots a se ...

  9. review21

    Class是java.lang包中的类,该类的实例可以帮助程序创建其他类的实例或者取得其他类的对象的内部信息. 参考代码如下所示: package pack07; import java.util.D ...

  10. MySQL InnoDB与MyISAM存储引擎差异

    言: 之前简单介绍过 MySQL 常用的存储引擎,今天对两个主流的存储简单分析下差异,书上没有参考的笔试题解答注解: 差异: MyISAM 只支持表锁,不支持事务,表损坏率较高.较老的存储引擎.   ...