HTML5中的服务器‘推送’技术 -WebSocket

HTML5中的服务器‘推送’技术 -WebSocket
转帖:http://www.developersky.net/thread-81-1-1.html

除了Server-Sent Event之外,即将到来的HTML5标准还包含了WebSockets。WebSocket使得我们可以建立双向的通信通道。和Server-Sent Event相反,WebSocket协议不是建立在HTTP之上的。但是WebSocket协议订立了HTTP握手的行为来将已经存在的HTTP连接转换为WebSocket连接。WebSocket没有试图在HTTP之上模拟server推送的通道,而是直接在TCP之上定义了帧协议,因此WebSocket能够支持双向的通信。
和server-Sent Event规范相同,WebSocket定义了API和相应的协议。WebSocket API规范中包含一个新的HTML元素:WebSocket。下面的代码就是一个使用WebSocket的HTML的例子:

<html>
   <head>
     <mce:script type='text/javascript'><!--
        var ws = new WebSocket('ws://localhost:8876/Channel', 'mySubprotocol.example.org');
        ws.onmessage = function (message) {
          var messages = document.getElementById('messages');
          messages.innerHTML += "<br>[in] " + message.data;
        };
       
        sendmsg = function() {
          var message = document.getElementById('message_to_send').value
          document.getElementById('message_to_send').value = ''
          ws.send(message);
          var messages = document.getElementById('messages');
          messages.innerHTML += "<br>[out] " + message;
        };
    
// --></mce:script>
  </head>
  <body>
     <form>
       <input type="text" id="message_to_send" name="msg"/>
       <input type="button" name="btn" id="sendMsg" value="Send" onclick="javascript:sendmsg();">
       <div id="messages"></div>
     </form>
  </body>
</html>

当创建一个WebSocket实例的时候,相应的WebSocket连接就会被建立。构造函数需要两个参数(第二个参数是可选的)。第一个参数是WebSocketURL,该参数定义了要连接的URL。WebSocketURL以ws或wss开头。ws开头的是普通的WebSocket连接,wss开头的是安全的WebSocket连接(类似https)。第二个参数是要使用的子协议,该参数是可选的。
我们可以定义WebSocket实例的onMessage处理函数,每次收到消息的时候,该处理函数都会被调用。如果要发送消息到服务器端,可以通过WebSocket的send()方法发送消息。
当一个新的WebSocket实例被创建的时候,首先底层的user agent会建立一个普通的到指定URL的HTTP(S)连接,然后对该连接进行升级(upgrade)。HTTP规范在消息头中定义了upgrade域来进行该操作。Upgrade头提供了简单的机制来将HTTP协议转换为其他的不兼容的协议。WebSocket利用了HTTP协议的这个能力来讲新创建的HTTP连接转换为WebSocket连接。同时添加了WebSocket-Protocal来制定要使用的子协议。
下面是一个Upgrade的Request和Response消息的例子。

REQUEST:
GET /Channel HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: myServer:8876
Origin: http://myServer:8876
WebSocket-Protocol: mySubprotocol.example.org


RESPONSE:
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://myServer:8876
WebSocket-Location: ws://myServer:8876/Channel
WebSocket-Protocol: mySubprotocol.example.org

当收到HTTP response消息之后,只有WebSocket帧能够通过该连接传送,所有的数据都会根据WebSocket协议进行转换。WebSocket帧能够在任何时候向任何方向发送。WebSocket协议定义了两种类型的帧:文本帧(text frame)和二进制帧(binary frame)。文本帧以字节0x00开头,以字节0xFF结尾。当中的文本内容要转换成UTF8编码。所以为了打包,文本帧需要添加两个额外的字节。下面是两个文本帧的例子:

Text frame of "GetDate":
0x00 0x47 0x65 0x74 0x44 0x61 0x74 0x65 0xFF

Text frame of "Sat Mar 13 14:00:25 CET 2010":
0x00 0x53 0x61 0x74 0x20 0x4D 0x61 0x72 0x20 0x31
0x33 0x20 0x31 0x34 0x3A 0x30 0x30 0x3A 0x32 0x35
0x20 0x43 0x45 0x54 0x20 0x32 0x30 0x31 0x30 0xFF

如果要传送二进制数据,就需要使用二进制帧。二进制帧以字节0x80开始。和文本帧相反,二进制帧没有结束标志。在二进制帧的开始标识字节(0x80)之后就是长度字节。长度字节的字节数是不固定的,根据需要来决定。下面是两个例子,第一个例子中需要传递的数据量很小,因此长度字节就只有一个字节;第二个例子中需要传递的数据量比较大,就使用了2个字节作为长度字节。

binary frame of 0x00 0x44:
0x80 0x02 0x00 0x44

binary frame of 0x30 0x31 0x32 0x33  0x34  0x35 […] 0x39 (1000 bytes):
0x80 0x87 0x68 0x30 0x31 0x32 0x33 0x34 0x35 […] 0x39

因为JavaScript不能操作字节数组形式的二进制数据,因此二进制帧目前无法被JavaScript使用。除了文本帧和二进制帧之外,WebSocket协议将来还有可能引入新的类型的帧格式。WebSocket帧在设计的时候就考虑了支持新的帧类型。
WebSocket的连接可以在任何时候关闭,不需要额外的‘结束连接’字节或帧。
管理WebSocket的额外开销是很小的。如Bayeux和BOSH这样的Comet协议是建立在HTTP协议之上的,这就迫使这些协议要实现复杂的会话和连接的管理。而WebSocket是建立在TCP协议之上的,不会碰到这些由于HTTP协议的局限性引起的麻烦。
另一方面,WebSocket基本上没有实现可靠性的功能。它既没有包括重建连接的处理,也不支持向Server-Sent Event那样的保证消息成功传递的机制。而且,由于WebSocket不是基于HTTP协议的,因此也无法利用HTTP协议内建的可靠性的特性。例如HTTP协议支持当网络故障是的自动重试功能(一个Get方法应该不会改变任何服务器端的资源的状态,因此我们可以重复的执行Get方法而没有任何的副作用。当网络故障的时候,浏览器或HttpClients可以自动重新执行Get方法)。
由于WebSocket没有这些功能,因此在应用程序(或者说子协议)层面上我们就需要实现可靠性的功能,包括发送“维持通信”消息来避免代理服务器在一段时间没有消息之后关闭连接。另外,在多个页面之间共享WebSocket通常会带来麻烦。和Server-Sent Event不同,WebSocket会包含一个难以共享的上游的管道。例如,并发的读写操作必须要同步,这并不是一个简单的任务。因此当使用WebSocket的时候,‘每个服务器一个连接’必须要仔细考虑。
Web浏览器限制浏览器端的编程语言(如JavaScript)连接到其他的服务器,因此web页面上的WebSocket只能连接和当前页面在同一个域中的服务器。对于独立的WebSocket客户端则没有这个限制。如下面的例子中我们通过Java客户端来使用WebSocket:

class MyWebSocketHandler implements IWebSocketHandler {

  public void onConnect(IWebSocketConnection wsCon) throws IOException {
               
  }

  public void onMessage(IWebSocketConnection wsCon) throws IOException {
    IWebMessage msg = wsCon.readMessage();
    System.out.println(msg.toString());
  }
           
  public void onDisconnect(IWebSocketConnection wsCon) throws IOException {
               
  }
}

       
MyWebSocketHandler hdl = new MyWebSocketHandler ();
IWebSocketConnection wsCon = httpClient.openWebSocketConnection(
    "ws://myServer:8876/WebSocketsExample",
    "mySubprotocol.example.org", hdl);
       
wsCon.writeMessage(new TextMessage("GetDate"));
// ...

下面是一个WebSocket服务器实现的简单例子。服务器要实现两个接口:IHttpRequestHandler和IWebSocketHandler。IHttpRequestHandler接口用于处理普通的HTTP请求,IWebSocketHandler据诶和用于处理WebSocket连接和消息。当一个标准的HTTP请求到来的时候(不包含upgrade请求),IHttpRequestHandler的onRequest()方法会被调用。如果客户端打开WebSocket,服务器会收到HTTP upgrade请求,IWebSocketHandler的onConnect()方法会被调用。每次收到WebSocket的消息,IWebSocketHandler的onMessage()方法都会被调用。
在onConnect()方法中,我们可以检查一些先决条件。例如检查要求的子协议是否被支持,下面的例子中如果要求的子协议不被支持,则会返回一个错误状态。下面的例子啊红我们还驾车了请求头中的origin字段。Origin字段是HTTP Origin Header RFC(还是草案)定义的,该字段由浏览器自动设置。

class ServerHandler implements IHttpRequestHandler, IWebSocketHandler {
           
           
  // IHttpRequestHandler method
  public void onRequest(IHttpExchange exchange) throws IOException {
  String requestURI = exchange.getRequest().getRequestURI();
               
  if (requestURI.equals("/WebSocketsExample")) {
    sendWebSocketPage(exchange, requestURI);
                   
  } else {
    exchange.sendError(404);
  }
}
           
           
  private void sendWebSocketPage(IHttpExchange exchange, String uri)
          throws IOException {
    String page = "<html>\r\n " +
                  "  <head>\r\n" +
                  "     <mce:script type='text/javascript'><!--
\r\n" +
                  "        var ws = new WebSocket('ws://" +
                           exchange.getRequest().getHost() + "/Channel',
                           'mySubprotocol.example.org');\r\n" +
                  "        ws.onmessage = function (message) {\r\n" +
                  "          var messages = document.getElementById('messages');\r\n" +
                  "          messages.innerHTML += \"<br>[in] \" +
                             message.data;\r\n"+
                  "        };\r\n" +
                  "        \r\n" +
                  "        sendmsg = function() {\r\n" +
                  "          var message = document.getElementById
                             ('message_to_send').value\r\n" +
                  "          document.getElementById('message_to_send').value = ''\r\n" +
                  "          ws.send(message);\r\n" +
                  "          var messages = document.getElementById('messages');\r\n" +
                  "          messages.innerHTML += \"<br>[out] \" + message;\r\n"+
                  "        };\r\n" +
                  "    
// --></mce:script>\r\n" +
                  "  </head>\r\n" +
                  "  <body>\r\n" +
                  "     <form>\r\n" +
                  "       <input type=\"text\" id=\"message_to_send\"
                          name=\"msg\"/>\r\n" +
                  "       <input type=\"button\" name=\"btn\" id=\"sendMsg\"
                          value=\"Send\" onclick=\"javascript:sendmsg();\">\r\n" +
                  "       <div id=\"messages\"></div>\r\n" +
                  "     </form>\r\n" +
                  "  </body>\r\n" +
                  "</html>\r\n ";
               
    exchange.send(new HttpResponse(200, "text/html", page));
  }

           
           
  // IWebSocketHandler method
  public void onConnect(IWebSocketConnection webStream) throws IOException, BadMessageException {
    IHttpRequestHeader header = webStream.getUpgradeRequestHeader();

    // check origin header
    String origin = header.getHeader("Origin");
    if (!isAllowed(origin)) {
      throw new BadMessageException(403);
    }
               
    // check the subprotocol 
    String subprotocol = header.getHeader("WebSocket-Protocol", "");
    if (!subprotocol.equalsIgnoreCase("mySubprotocol.example.org")) {
      throw new BadMessageException(403);
    }
  }

  private boolean isAllowed(String origin) {
    // check the origin
    // ...
    return true;
  }
           
           
  // IWebSocketHandler
  public void onMessage(IWebSocketConnection webStream) throws IOException {
    WebSocketMessage msg = webStream.readMessage();
    if (msg.toString().equalsIgnoreCase("GetDate")) {
      webStream.writeMessage(new TextMessage(new Date().toString()));
    } else {
      webStream.writeMessage(new TextMessage(
              "unknown command (supported: GetDate)"));
    }
  }
           
  // IWebSocketHandler
  public void onDisconnect(IWebSocketConnection webStream)
          throws IOException {  }

}
       
XHttpServer server = new XHttpServer(8876, new ServerHandler());
server.start();

上面的例子中,我们会使用内部的白名单来检查origin header,并拒绝我们不期望的请求。这样,我们可以防止某些攻击者将公共页面上的JavaScript代码拷贝并添加到他们自己的页面中。这种情况下,浏览器会将origin header设置为攻击者自己的页面所在的域,服务器处理升级请求的时候就会拒绝该请求。这个技术用来防范Cross-Site Request攻击。Origin header规范和WebSocket协议规范是相互独立的,但是WebSocket协议定义了WebSocket-Origin header,该字段必须被包含在WebSocket升级请求中。

因为WebSocket连接是有HTTP连接升级而来,因此WebSocket协议也可以和HTTP代理服务器一起工作。浏览器总是和代理服务器通信,代理服务器将HTTP request和response进行转发。当浏览器使用HTTP代理并打开一个WebSocket的时候,首先浏览器会打开一个到代理服务器的通道。通过发送HTTP/1.1连接请求,浏览器要求HTTP代理创建一个到被代理的服务器(WebSocket服务器)的TCP连接。当连接建立以后,HTTP代理服务器的功能就被缩小为到WebSocket服务器的TCP代理。使用这个被代理的连接,浏览器发送WebSocket升级请求到WebSocket服务器。下面的列表描述了整个过程。

1. REQUEST:
CONNECT myServer:8876 HTTP/1.1
Host: myServer:8876
User-Agent: xLightweb/2.12-HTML5Preview6
Proxy-Connection: keep-alive


1. RESPONSE:
HTTP/1.1 200 Connection established
Proxy-agent: myProxy


2. REQUEST:
GET /Channel HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: myServer:8876
Origin: http://myServer:8876
WebSocket-Protocol: mySubprotocol.example.org


2. RESPONSE:
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://myServer:8876
WebSocket-Location: ws://myServer:8876/Channel
WebSocket-Protocol: mySubprotocol.example.org 

转帖:http://www.developersky.net/thread-81-1-1.html

你可能感兴趣的:(应用服务器,html5,浏览器,网络协议,Exchange)