65.9K
CodeProject 正在变化。 阅读更多。
Home

构建一个基本的HTML5客户端/服务器应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (49投票s)

2012 年 9 月 2 日

CPOL

4分钟阅读

viewsIcon

291415

downloadIcon

16510

本文介绍了一个基本的HTML5客户端/服务器聊天应用程序的实现。

引言

在本文中,我想展示一个基本的HTML5客户端/服务器应用程序的实现。客户端使用HTML5实现,服务器用C++实现。

背景 

客户端/服务器应用程序意味着参与者之间需要发送数据。这项任务将通过使用一项名为WebSocket的新HTML5功能来完成,该功能在 RFC6455 中进行了描述。目前,Mozilla Firefox (15.0)、Google Chrome (21.0.1180.89) 和 Internet Explorer 10 (10.0.8400.0) 网页浏览器对 RFC6455 的支持最为完善,它们被用来测试该应用程序。

服务器端的套接字支持使用 boost.asio 库实现,服务器代码旨在跨平台。

客户端

客户端代码基于Remy Sharp在 www.html5demos.com 上的一个简单的聊天客户端示例。

客户端实现为一个网页,该网页创建一个websocket对象并订阅其事件:open、close、error和message。

要连接到服务器,我们需要使用以下字符串作为参数调用websocket构造函数

var wsUri = "ws://127.0.0.1:7777";  	
websocket = new WebSocket(wsUri);

如您在此处所见,wsUri 字符串包含服务器应用程序的IP地址和端口。在我们的例子中,服务器应用程序运行在localhost上,端口号是7777。

客户端使用以下过程

  1. 初始化变量以访问网页元素。
    connected = document.getElementById("connected");
    log = document.getElementById("log");
    chat = document.getElementById("chat");
    form = chat.form;
    state = document.getElementById("status");
  2. 检查网页浏览器是否支持WebSocket功能。
    if (window.WebSocket === undefined)
    {
        state.innerHTML = "sockets not supported";
        state.className = "fail";
    }
    else
    {
        // ...
        
        window.addEventListener("load", onLoad, false);
    }
  3. 创建websocket对象并订阅其事件。
    function onLoad()
    {
        var wsUri = "ws://127.0.0.1:7777";  
     
        websocket = new WebSocket(wsUri);
        websocket.onopen = function(evt) { onOpen(evt) };
        websocket.onclose = function(evt) { onClose(evt) };
        websocket.onmessage = function(evt) { onMessage(evt) };
        websocket.onerror = function(evt) { onError(evt) };
    }

以下是事件处理程序。

function onOpen(evt)
{
    state.className = "success";
    state.innerHTML = "Connected to server";	
}
  
function onClose(evt)
{
    state.className = "fail";
    state.innerHTML = "Not connected";
    connected.innerHTML = "0";	
}
 
function onMessage(evt)
{
    // There are two types of messages: 
    //     1. a chat participant message itself
    //     2. a message with a number of connected chat participants. 

    var message = evt.data;
	
    if (message.startsWith("log:"))
    {
        message = message.slice("log:".length);
        log.innerHTML = '<li class="message">' + message + "</li>" + log.innerHTML;	
    }
    else if (message.startsWith("connected:"))
    {
        message = message.slice("connected:".length);
        connected.innerHTML = message;	
    }    
}
 
function onError(evt)
{ 
    state.className = "fail";
    state.innerHTML = "Communication error";
}

请注意,服务器向客户端发送两种类型的消息

  1. 聊天参与者消息本身,“log:”消息;
  2. 包含已连接聊天参与者数量的消息,“connected:”消息。

根据消息类型,我们在相应的网页元素中显示其内容。

要将消息发送到服务器,请使用 send 函数

	
function addMessage()
{
    var message = chat.value;
     
    chat.value = "";
	  
    websocket.send(message);
}

服务器

服务器代码基于 boost.asio 库中的 Chat application 示例,感谢 Christopher M. Kohlhoff。

服务器以异步方式处理套接字,使用I/O完成端口,并且可以轻松扩展以运行两个或更多线程。

以下是聊天应用程序的类图。

其背后的逻辑如下

  1. 服务器包含一个room对象,它是聊天参与者的容器。
  2. 当客户端连接到服务器时,服务器会创建一个chat_session对象并将其添加到room中。
  3. 每个chat_session对象都包含一个与客户端交互的套接字。当客户端消息到达时,它会被传递给所有room的参与者。

WebSocket

客户端代码基于 www.html5demos.com 的示例,服务器代码基于 boost.asio 示例,您可能会问,那么您自己的代码在哪里?就在这里!WebSocket协议的基本实现。

WebSocket会话由两个连续的阶段组成:握手和数据交换。

握手

为了建立WebSocket连接,客户端(网页浏览器)会发送一个带有多个HTTP头部的HTTP GET请求。在这些头部中,有一个Sec-WebSocket-Key头部,它包含一个握手密钥。根据WebSocket协议,服务器应该

  1. 将握手密钥与魔术guid {258EAFA5-E914-47DA-95CA-C5AB0DC85B11} 连接起来。
  2. 对连接结果取SHA1哈希值。
  3. 将哈希值的base64等效值作为HTTP响应发送给客户端。

以下代码说明了握手过程。

namespace websocket {
    namespace http {
 
        void request_handler::handle_request(const request& req, reply& rep)
        {
            std::string key;
            for (std::vector<header>::const_iterator h = req.headers.cbegin();
                h != req.headers.cend(); ++h)
            {
                if (h->name == "Sec-WebSocket-Key")
                {
                    key = h->value;
                    break;
                }
            }
 
            if (key.empty())
            {
                rep = reply::stock_reply(reply::bad_request);
                return;
            }
 
            const std::string magic_guid("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
            std::string buffer(key + magic_guid);
            std::string accept(to_base64(to_sha1(buffer)));
 
            rep.status = reply::switching_protocols;
            rep.headers.resize(3);
            rep.headers[0].name = "Upgrade";
            rep.headers[0].value = "websocket";
            rep.headers[1].name = "Connection";
            rep.headers[1].value = "Upgrade";
            rep.headers[2].name = "Sec-WebSocket-Accept";
            rep.headers[2].value = accept;
        }
 
    } // namespace http
} // namespace websocket

一旦HTTP响应发送给客户端,握手阶段就被认为完成,我们就可以开始数据交换了。回到我们的聊天应用程序,下面是聊天会话的状态图。

数据交换

WebSocket客户端和服务器之间的数据交换是通过消息进行的。每条消息由多个帧组成。下图显示了WebSocket数据帧的结构。

字段的含义可以在 RFC6455 中找到。这是一个用于处理数据帧的结构

namespace websocket {
 
    // A structure to hold websocket frame data. 
    struct dataframe
    {
        dataframe();
 
        bool fin;
        enum operation_code { continuation_frame, text_frame, binary_frame, connection_close, ping, pong, reserved } opcode;
        bool mask;
        boost::int8_t fin_opcode;
        boost::int8_t mask_payload_len;
        boost::int8_t payload_len;
        boost::uint64_t extended_payload_len;
        boost::array<boost::uint8_t, 4> masking_key;
        std::vector<boost::uint8_t> payload;
 
        // Convert the dataframe into a vector of buffers. 
        std::vector<boost::asio::const_buffer> to_buffers();
    };
 
} // namespace websocket

演示

运行演示

  1. 使用以下命令行启动服务器应用程序
    server 0.0.0.0 7777
  2. 在 Google Chrome 或 Mozilla Firefox 网页浏览器中打开 chat.html 网页。

欢迎提问、评论和反馈。

历史

  • 2015年3月5日 - 修复了关于payload长度的bug。
  • 2012年9月26日 - 添加了对Internet Explorer 10的支持。
  • 2012年9月15日 - 添加了Ubuntu演示。
  • 2012年9月2日 - 初始版本。
构建一个基本的HTML5客户端/服务器应用程序 - CodeProject - 代码之家
© . All rights reserved.