引言
通过NIO+Continunation+HttpClient可以使Jetty具有异步长连接的功能,但有些应用场景确需要服务器“推”的功能,比如说:聊天室、实时消息提醒、股票行情等对于实时要求比较高的应用,能想到的实时推送的解决方案大致可以分为下面几种:
1、轮询:前台ajax轮询实时消息。
2、applet:已经OUT了不是~而且亦有安全方面的问题
3、长连接:在一次TCP的连接上发送多次数据,除非手动close,但需要在HTTP协议的基础上做协议的转换并应用在客户端和服务端,这些工作需要自己来实现。
我最初接触到websocket是设计一个资源远程加载的平台。设想你在本地开发web应用,你只需要告诉平台你的应用在本地的地址。第三方的人员(主管或者运营人员,亦或是一个项目组的同事)可以随时通过访问平台看到你工作的成果,因为是实时的,所以沟通会更加有效。
本文描述的websocket就是一个非Http的双向连接(其实也跟Http息息相关,下文有详解),有了它你不需要没事去轮询实现推的功能;有了它你可以对注册到平台上的计算机做一些事情(确实有安全隐患,不过都是开发环境也就无所谓了)。
一个简单的实例
为了研究websocket需要搭建一个功能环境,修改了网上的一段实例并调试无误,贴出来主要代码,需要完整工程的同学请留言。
实例流程如下:
A 客户端建立websocket连接后发送给服务端want消息告知服务端。
B 服务端接收到消息,判断如果是want命令的话,返回给所有客户端begin消息。
C 客户端接受消息,判断如果是begin命令的话,即读取本地文件发送给服务端。
D 服务端接收消息,判断如果是非want命令的话,将读取的内容加上后缀返回给客户端。
E 客户端接受消息,判断如果是非begin命令的话,将读取的内容显示在html页面上。
1、前台JS
下面三段js主要是实现服务端读取已注册的用户计算机上的文件,很邪恶的有木有。
//读取本地的文件
function read(file) {
if(typeof window.ActiveXObject != 'undefined') {
var content = "";
try {
var fso = new ActiveXObject("Scripting.FileSystemObject");
var reader = fso.openTextFile(file, 1);
while(!reader.AtEndofStream) {
content += reader.readline();
content += "\n";
}
// close the reader
reader.close();
}
catch (e) {
alert("Internet Explore read local file error: \n" + e);
}
return content;
}
else if(document.implementation && document.implementation.createDocument) {
var content = ""
try {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var lf = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
lf.initWithPath(file);
if (lf.exists() == false) {
alert("File does not exist");
}
var fis = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
fis.init(lf, 0x01, 00004, null);
var sis = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
sis.init(fis);
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
var insis = sis.read(sis.available());
converter.charset = "GBK";
content = converter.ConvertToUnicode(insis);
}
catch (e) {
alert("Mozilla Firefox read local file error: \n" + e);
}
return content;
}
}
</script>
<script type='text/javascript'>
//判断当前浏览器是否支持websocket
if (!window.WebSocket)
alert("window.WebSocket unsuport!");
else
alert("suport!");
function $() {
return document.getElementById(arguments[0]);
}
function $F() {
return document.getElementById(arguments[0]).value;
}
function getKeyCode(ev) {
if (window.event)
return window.event.keyCode;
return ev.keyCode;
}
//websocket主要实现
var server = {
connect : function() {
var location ="ws://localhost:8888/petstore/servlet/a?key=123";
alert("before conn!");
this._ws =new WebSocket(location);
alert("has conned!");
this._ws.onopen =this._onopen;
this._ws.onmessage =this._onmessage;
this._ws.onclose =this._onclose;
server._send("want");
},
_onopen : function() {
},
_send : function(message) {
if (this._ws)
this._ws.send(message);
},
send : function(text) {
if (text !=null&& text.length >0)
server._send(text);
},
_onmessage : function(m) {
if (m.data) {
if (m.data=="begin") {
var res = read("/Users/apple/workspace/apache/chenshuai.html");
server._send(res);
}
else {
var messageBox = $('messageBox');
var spanText = document.createElement('span');
spanText.className ='text';
spanText.innerHTML = m.data;
var lineBreak = document.createElement('br');
messageBox.appendChild(spanText);
messageBox.appendChild(lineBreak);
messageBox.scrollTop = messageBox.scrollHeight
- messageBox.clientHeight;
}
}
},
_onclose : function(m) {
this._ws =null;
}
};
</script>
<script type='text/javascript'>
//显式触发websocket的建立
$('connect').onclick =function(event) {
alert("has clicked!");
server.connect();
return false;
};
</script>
2、后台servlet
public class MyWebSocketServlet extends WebSocketServlet {
private static final long serialVersionUID = -7289719281366784056L;
public static String newLine = System.getProperty("line.separator");
private final Set<TailorSocket> _members = new CopyOnWriteArraySet<TailorSocket>();
private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
public void init() throws ServletException {
super.init();
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
getServletContext().getNamedDispatcher("default").forward(request,
response);
}
public WebSocket doWebSocketConnect(HttpServletRequest request,
String protocol) {
String key = (String) request.getParameter("key");
return new TailorSocket(key);
}
class TailorSocket implements WebSocket.OnTextMessage {
private Connection _connection;
private String key;
public String getKey() {
return key;
}
public TailorSocket(String key) {
this.key = key;
}
public void onClose(int closeCode, String message) {
_members.remove(this);
}
public void sendMessage(String data) throws IOException {
_connection.sendMessage(data);
}
public void onMessage(String data) {
for(TailorSocket member : _members){
System.out.println("Trying to send to Member!");
if(member.isOpen()){
System.out.println("Sending!");
try {
if (data.equals("want")) {
member.sendMessage("begin");
}
else {
member.sendMessage(data+member.getKey());
}
} catch (IOException e) {
}
}
}
System.out.println("Received: "+data);
}
public boolean isOpen() {
return _connection.isOpen();
}
public void onOpen(Connection connection) {
_members.add(this);
_connection = connection;
try {
connection.sendMessage("onOpen:Server received Web Socket upgrade and added it to Receiver List.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、web.xml配置
<servlet>
<servlet-name>WebSocket</servlet-name>
<servlet-class>com.alibaba.myX3.dal.dataobject.MyWebSocketServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>WebSocket</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
4、前台页面
<body>
<div id='messageBox'></div>
<div id='input'>
<div>
<input id='connect' class='button' type='submit' name='Connect'
value='Connect' />
</div>
</div>
<script type='text/javascript'>
$('connect').onclick =function(event) {
alert("has clicked!");
server.connect();
return false;
};
</script>
<p>
JAVA Jetty for WebSocket
</p>
</body>
5、运行结果
1)本机的文件
2)点击连接之后的结果
只要浏览器过关,任何人都可以看到你本地文件的内容了~推送功能算是完成了。
HTTP状态码101
给出状态码的预备知识,对于理解websocket挺有用的。
1xx:这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。
101:服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。
WebSocket原理
客户端不在本文的研究范围,这里只分析Jetty是如何实现的,其实也大同小异,无非是新的协议罢了~
1、WebSocket模型
红色:请求的入口,它定义了WebSocket并持有WebSocketFactory,从而初始化WebSocket连接并设置连接的WebSocket值。
蓝色:相当于HTTP协议的HttpConnection,不需解释。
橙色:新的协议自然需要新的解析和生产规则了。
2、模拟一次连接的建立
A 简要流程:
B 详细流程:
1)客户端请求建立WebSocket连接
var location ="ws://localhost:8888/petstore/servlet/a?key=123";
alert("before conn!");
this._ws =new WebSocket(location);
此时会向服务器发出:http://localhosts:8888/petstore/servlet/a?key=123,自然不是ws协议,服务器也得认识才行啊,所以ws协议的最初的建立也是依赖了http协议。
2)服务端servlet处理请求
//判断客户端是否要求更新协议为websocket
if ("websocket".equalsIgnoreCase(request.getHeader("Upgrade")))
{
String origin = request.getHeader("Origin");
if (origin==null)
origin = request.getHeader("Sec-WebSocket-Origin");
if (!_acceptor.checkOrigin(request,origin))
{
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false;
}
// Try each requested protocol
WebSocket websocket = null;
@SuppressWarnings("unchecked")
Enumeration<String> protocols = request.getHeaders("Sec-
WebSocket-Protocol");
String protocol=null;
while (protocol==null && protocols!=null &&
protocols.hasMoreElements())
{
String candidate = protocols.nextElement();
for (String p : parseProtocols(candidate))
{
websocket = _acceptor.doWebSocketConnect(request, p);
if (websocket != null)
{
protocol = p;
break;
}
}
}
// Did we get a websocket?
if (websocket == null)
{
// 用servlet提供的webSocket实现,上面的实例有详细实现
websocket = _acceptor.doWebSocketConnect(request, null);
if (websocket==null)
{
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return false;
}
}
// 告诉客户端,我已经准备好了切换协议了
upgrade(request, response, websocket, protocol);
return true;
}
return false;
看下upgrade的实现:
AbstractHttpConnection http = AbstractHttpConnection.getCurrentConnection();
if (http instanceof BlockingHttpConnection)
throw new IllegalStateException("Websockets not supported on blocking
connectors");
//用着一样的信道,并木有重新建立socket连接
ConnectedEndPoint endp = (ConnectedEndPoint)http.getEndPoint();
connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft);
// Set the defaults
connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize);
connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize);
// 完成“握手”阶段,总要告诉点客户端什么,大致就是:我已经换好协议了,你那边可以发送新协议格式的数据了啊!
connection.handshake(request, response, protocol);
response.flushBuffer();
// Give the connection any unused data from the HTTP connection.
connection.fillBuffersFrom(((HttpParser)http.getParser()).getHeaderBuffer());
connection.fillBuffersFrom(((HttpParser)http.getParser()).getBodyBuffer());
// 至此换了新的连接,新的协议解析器和生成器,总不至于还用外面的HttpConnection吧,那我就把新的协议放在request里面,把协议发生改变的标示放在reponse里面,后面jetty判断response如果有协议改变的话就会更新endpoint的connection了。
LOG.debug("Websocket upgrade {} {} {} {}",request.getRequestURI(),draft,protocol,connection);
request.setAttribute("org.eclipse.jetty.io.Connection", connection);
3)Jetty替换原来的Http协议为最新的WebSocket协议
// look for a switched connection instance?
if (_response.getStatus()==HttpStatus.SWITCHING_PROTOCOLS_101)
{
Connection switched=
(Connection)_request.getAttribute("org.eclipse.jetty.io.Connection");
if (switched!=null)
connection=switched;
}
C 报文数据
URL:http://localhost:8888/petstore/servlet/a?key=123
状态码:101
报文头信息:
3、模拟请求的接受和发送
1)WebSocketParserRFC6455解析请求的参数
progress=true;
_handler.onFrame(_flags, _opcode, data);
_bytesNeeded=0;
_state=State.START;
2)接着看下ws协议框架处理器
WSFrameHandler是如何处理解析出的data的。
//示例中的websocket就是该类型的,因此由它来处理
if(_onTextMessage!=null)
{
if (_connection.getMaxTextMessageSize()<=0)
{
// No size limit, so handle only final frames
if (lastFrame)
//调用servlet中定义的websocket的回调接口
_onTextMessage.onMessage(buffer.toString(StringUtil.__UTF8));
else
{
LOG.warn("Frame discarded. Text aggregation disabled for
{}",_endp);
_connection.close(WebSocketConnectionD08.CLOSE_BADDATA,"Text frame
aggregation disabled");
}
}
3)看下servlet中定义的websocket:
class TailorSocket implements WebSocket.OnTextMessage {
private Connection _connection;
public void onClose(int closeCode, String message) {
_members.remove(this);
}
public void sendMessage(String data) throws IOException {
_connection.sendMessage(data);
}
public void onMessage(String data) {
for(TailorSocket member : _members){
System.out.println("Trying to send to Member!");
if(member.isOpen()){
System.out.println("Sending!");
try {
if (data.equals("want")) {
member.sendMessage("begin");
}
else {
member.sendMessage(data+member.getKey());
}
} catch (IOException e) {
}
}
}
System.out.println("Received: "+data);
}
public boolean isOpen() {
return _connection.isOpen();
}
public void onOpen(Connection connection) {
_members.add(this);
_connection = connection;
try {
connection.sendMessage("onOpen:Server received Web Socket upgrade and added it to Receiver List.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
4)至于最后的发送数据,无非就是利用WebSocketGenerator生成协议格式的数据flush到信道中,不详述了。
总结
学习了WebSocket感觉对于HttpConnection和Http状态码的认识更加深刻了,以后可以基于Http定制好玩的协议,不过需要客户端的配合。
Jetty实现了服务器推的功能而无需轮询,缺陷就是支持的浏览器太少。大致流程是如此:
1、客户端首先发送一条要求切换协议格式的http请求要求建立websocket连接。
2、服务端的servlet处理请求时发现Http请求中要求切换协议,因此在原有的信道上创建了新的协议连接器,并返回给客户端101状态码,告诉客户端已经建立好了新的协议连接器了,此即上面注释中说的握手。
3、客户端收到101状态码后返回客户端的websocket实例,并开始发送和接受数据。
来源:oschina
链接:https://my.oschina.net/u/947581/blog/112912