移动Web应用程序开发HTML5篇 (七) Web Sockets API

介绍

本系列博客将主要介绍如今大红大紫的移动Web应用程序开发最重要的三个工具:HTML5,JavaScript, CSS3。

本篇是HTML5介绍的第七篇,主要介绍HTML5的Web Sockets API。

相关文章:


介绍

Web Sockets API是HTML5的特性之一,可以通过一个Sockets实现在TCP协议层的全双工通信,Web Sockets目的是提供一个非常有效的通信工具,用来替代传统的Comet和Ajax长轮询操作。目前浏览器对其支持情况如下图:




Web Sockets通信原理



在开发Web应用的过程中,很多情况下需要实时更新数据,如股票操作和分析软件,机票酒店预订软件等等。一个实时HTTP请求的Web应用,其信息流程如下图所示:


目前比较常见的有两种解决方法: HTTP的方式和AJAX方式。他们的通信流程分别如下图所示:


左边是HTTP的方式,也是最早出现的通信方式,客户端发送HTTP Request通过Web Server请求数据库,将结果返回给客户端,但是这种方式在网络通信的过程中会阻碍客户端的UI线程,尤其在实时性要求比较高的场合,网络通信的时间长了以后会直接影响用户体验,于是就出现了AJAX。

右边是AJAX的方式,客户端需要HTTP Request的时候,发起一个JavaScript Call 调用AJAX Engine,由AJAX Engine在后台进行数据请求和处理,不阻碍UI线程,用户还可以和Web进行交互,服务器端数据Ready之后,将其返回给客户端进行UI更新。

对于实时性要求高的应用,利用AJAX技术发展出了长轮询操作(Polling)的技术,有很多代表如:Comet, Pushlets等等。长轮询操作的原理如下:


客户端发起一个HTTP Request,服务器端进行验证,验证通过之后,并不立刻返回数据,而是hold这个connection,当服务器端数据有更新的时候,将数据返回,返回之后客户端立刻再起一个HTTP Request从而实现实时数据更新。这种方式能够很好地实现应用数据的实时更新以及不阻碍UI线程,但是其很长时间有个HTTP connection在维持着,不仅对客户端造成很大的资源消耗,尤其对于流量和电池寿命都很有限的手机客户端,对服务器端也是非常大的挑战,没有一个应用上线,就需要维持一个Connection。而且代码量也比较大。

而Web Sockets API的出现很好地解决了这一问题,一旦连接建立之后,直接由Server端想客户端发数据,不需要每次单独见连接,极大地节省了流量和功耗。其原理图如下:


和传统长轮询操作比,使用Web Socket能够显著节省流量。下图为不同数据请求量的情况下,使用Web Sockets和长轮询的对比关系。


使用Web Sockets API

一个典型的Web Sockets应用创建的步骤如下图所示:


1. 创建一个Web Sockets的对象,代码如上图所示。其中url分别代表Scheme,Host,Port和Server。

2. 添加EventListener,可以监听链接状态,如上图中代码所示。链接状态有:onOpen, onMessage, onClose, onError。

3. 客户端发送消息。发送消息通过Web Sockets对象自带的send函数去发送。

4. 确保Server端已经运行。本例子通过python来创建一个本机的Server端。、

4. Web Sockets 例子

首先在WebSockets HTML文件在中创建如下代码:Load一个本地的ws://localhost:8080/echo URL


<!DOCTYPE html>

<title>WebSocket Test Page</title>

<script>


    var log = function(s) {

        if (document.readyState !== "complete") {

            log.buffer.push(s);

        } else {

            document.getElementById("output").innerHTML += (s + "n")

        }

    }

    log.buffer = [];


    url = "ws://localhost:8080/echo";

    w = new WebSocket(url);

    w.onopen = function() {

        log("open");

        w.send("thank you for accepting this Web Socket request");

    }

    w.onmessage = function(e) {

        log(e.data);

    }

    w.onclose = function(e) {

        log("closed");

    }


    window.onload = function() {

        log(log.buffer.join("n"));

        document.getElementById("sendButton").onclick = function() {

            w.send(document.getElementById("inputMessage").value);

        }

    }

</script>


<input type="text" id="inputMessage" value="Hello, Web Socket!"><button id="sendButton">Send</button>

<pre id="output"></pre>


用Python来写一个简单的本地Web Server来相应客户端请求,代码如下:


#!/usr/bin/env python


import asyncore

import socket

import struct

import time

import hashlib


class WebSocketConnection(asyncore.dispatcher_with_send):


    def __init__(self, conn, server):

        asyncore.dispatcher_with_send.__init__(self, conn)


        self.server = server

        self.server.sessions.append(self)

        self.readystate = "connecting"

        self.buffer = ""


    def handle_read(self):

        data = self.recv(1024)

        self.buffer += data

        if self.readystate == "connecting":

            self.parse_connecting()

        elif self.readystate == "open":

            self.parse_frametype()


    def handle_close(self):

        self.server.sessions.remove(self)

        self.close()


    def parse_connecting(self):

        header_end = self.buffer.find("rnrn")

        if header_end == -1:

            return

        else:

            header = self.buffer[:header_end]

            # remove header and four bytes of line endings from buffer

            self.buffer = self.buffer[header_end+4:]

            header_lines = header.split("rn")

            headers = {}


            # validate HTTP request and construct location

            method, path, protocol = header_lines[0].split(" ")

            if method != "GET" or protocol != "HTTP/1.1" or path[0] != "/":

                self.terminate()

                return


            # parse headers

            for line in header_lines[1:]:

                key, value = line.split(": ")

                headers[key] = value


            headers["Location"] = "ws://" + headers["Host"] + path


            self.readystate = "open"

            self.handler = self.server.handlers.get(path, None)(self)


            if "Sec-WebSocket-Key1" in headers.keys():

                self.send_server_handshake_76(headers)

            else:

                self.send_server_handshake_75(headers)


    def terminate(self):

        self.ready_state = "closed"

        self.close()


    def send_server_handshake_76(self, headers):

        """

        Send the WebSocket Protocol v.76 handshake response

        """


        key1 = headers["Sec-WebSocket-Key1"]

        key2 = headers["Sec-WebSocket-Key2"]

        # read additional 8 bytes from buffer

        key3, self.buffer = self.buffer[:8], self.buffer[8:]


        response_token = self.calculate_key(key1, key2, key3)


        # write out response headers

        self.send_bytes("HTTP/1.1 101 Web Socket Protocol Handshakern")

        self.send_bytes("Upgrade: WebSocketrn")

        self.send_bytes("Connection: Upgradern")

        self.send_bytes("Sec-WebSocket-Origin: %srn" % headers["Origin"])

        self.send_bytes("Sec-WebSocket-Location: %srn" % headers["Location"])


        if "Sec-WebSocket-Protocol" in headers:

            protocol = headers["Sec-WebSocket-Protocol"]

            self.send_bytes("Sec-WebSocket-Protocol: %srn" % protocol)


        self.send_bytes("rn")

        # write out hashed response token

        self.send_bytes(response_token)


    def calculate_key(self, key1, key2, key3):

        # parse keys 1 and 2 by extracting numerical characters

        num1 = int("".join([digit for digit in list(key1) if digit.isdigit()]))

        spaces1 = len([char for char in list(key1) if char == " "])

        num2 = int("".join([digit for digit in list(key2) if digit.isdigit()]))

        spaces2 = len([char for char in list(key2) if char == " "])


        combined = struct.pack(">II", num1/spaces1, num2/spaces2) + key3

        # md5 sum the combined bytes

        return hashlib.md5(combined).digest()


    def send_server_handshake_75(self, headers):

        """

        Send the WebSocket Protocol v.75 handshake response

        """


        self.send_bytes("HTTP/1.1 101 Web Socket Protocol Handshakern")

        self.send_bytes("Upgrade: WebSocketrn")

        self.send_bytes("Connection: Upgradern")

        self.send_bytes("WebSocket-Origin: %srn" % headers["Origin"])

        self.send_bytes("WebSocket-Location: %srn" % headers["Location"])


        if "Protocol" in headers:

            self.send_bytes("WebSocket-Protocol: %srn" % headers["Protocol"])


        self.send_bytes("rn")


    def parse_frametype(self):

        while len(self.buffer):

            type_byte = self.buffer[0]

            if type_byte == "x00":

                if not self.parse_textframe():

                    return


    def parse_textframe(self):

        terminator_index = self.buffer.find("xFF")

        if terminator_index != -1:

            frame = self.buffer[1:terminator_index]

            self.buffer = self.buffer[terminator_index+1:]

            s = frame.decode("UTF8")

            self.handler.dispatch(s)

            return True

        else:

            # incomplete frame

            return false


    def send(self, s):

        if self.readystate == "open":

            self.send_bytes("x00")

            self.send_bytes(s.encode("UTF8"))

            self.send_bytes("xFF")


    def send_bytes(self, bytes):

        asyncore.dispatcher_with_send.send(self, bytes)


class EchoHandler(object):

    """

    The EchoHandler repeats each incoming string to the same Web Socket.

    """


    def __init__(self, conn):

        self.conn = conn


    def dispatch(self, data):

        self.conn.send("echo: " + data)


class WebSocketServer(asyncore.dispatcher):


    def __init__(self, port=80, handlers=None):

        asyncore.dispatcher.__init__(self)

        self.handlers = handlers

        self.sessions = []

        self.port = port

        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)

        self.set_reuse_addr()

        self.bind(("", port))

        self.listen(5)


    def handle_accept(self):

        conn, addr = self.accept()

        session = WebSocketConnection(conn, self)


if __name__ == "__main__":

    print "Starting WebSocket Server"

    WebSocketServer(port=8080, handlers={"/echo": EchoHandler})

    asyncore.loop()

For more complete information about compiler optimizations, see our Optimization Notice.