Jetty源码学习9-WebSocket

痴心易碎 提交于 2019-12-01 10:33:14

引言

通过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实例,并开始发送和接受数据。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!