<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; WebSocket</title>
	<atom:link href="https://blog.gmem.cc/tag/websocket/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>WebSocket协议</title>
		<link>https://blog.gmem.cc/websocket-protocol</link>
		<comments>https://blog.gmem.cc/websocket-protocol#comments</comments>
		<pubDate>Wed, 20 Sep 2017 01:17:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Network]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16213</guid>
		<description><![CDATA[<p>简介 WebSocket是一种全双工（full-duplex）的双向通信技术，它依赖于单个套接字。使用WebSocket之后，HTTP连接升级为TCP长连接，可以被反复使用以传输数据。WebSocket连接可以在HTTP或者HTTPS之上启动。 WebSocket的出现，让B/S应用的实时性更好，因为服务器可以随时把数据推送到客户端，不需要客户端进行轮询。 WebSocket常常指代一套JavaScript的API，但它也作为一种网络协议（RFC 6455），本文主要探讨WebSocket协议的细节。 协议对比 特性 TCP HTTP WebSocket 寻址方式 IP地址+端口 URL URL 并发传输 全双工 半双工 全双工 载荷格式 二进制流 MIME报文 文本或者二进制消息 <a class="read-more" href="https://blog.gmem.cc/websocket-protocol">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-protocol">WebSocket协议</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">简介</span></div>
<p>WebSocket是一种全双工（full-duplex）的双向通信技术，它依赖于单个套接字。使用WebSocket之后，HTTP连接升级为TCP长连接，可以被反复使用以传输数据。WebSocket连接可以在HTTP或者HTTPS之上启动。</p>
<p>WebSocket的出现，让B/S应用的实时性更好，因为服务器可以随时把数据推送到客户端，不需要客户端进行轮询。</p>
<p>WebSocket常常指代一套JavaScript的API，但它也作为一种网络协议（RFC 6455），本文主要探讨WebSocket协议的细节。</p>
<div class="blog_h3"><span class="graybg">协议对比</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">特性</td>
<td style="text-align: center;">TCP</td>
<td style="text-align: center;">HTTP</td>
<td style="text-align: center;">WebSocket</td>
</tr>
</thead>
<tbody>
<tr>
<td>寻址方式</td>
<td>IP地址+端口</td>
<td>URL</td>
<td>URL</td>
</tr>
<tr>
<td>并发传输</td>
<td>全双工</td>
<td>半双工</td>
<td>全双工</td>
</tr>
<tr>
<td>载荷格式</td>
<td>二进制流</td>
<td>MIME报文</td>
<td>文本或者二进制消息</td>
</tr>
<tr>
<td>消息边界</td>
<td>无</td>
<td>有</td>
<td>有</td>
</tr>
<tr>
<td>面向连接</td>
<td>是</td>
<td>否</td>
<td>是</td>
</tr>
</tbody>
</table>
<p>可以看到，WebSocket包含了消息边界规范，因而比TCP更加简单。使用TCP时，随着网络负载、延迟的变化，TCP报文如何分片是无法预测的，唯一的保证是每个字节的接收顺序和发送顺序一致。而使用WebSocket时，<span style="background-color: #c0c0c0;">多字节的消息会完整、按序的到达</span>。</p>
<div class="blog_h2"><span class="graybg">握手</span></div>
<div class="blog_h3"><span class="graybg">打开握手</span></div>
<p>所有WebSocket连接都是在HTTP连接升级产生的。 客户端打开HTTP连接时，发送类似下面的请求：</p>
<pre class="crayon-plain-tag">GET /h264src HTTP/1.1
Pragma: no-cache
Cache-Control: no-cache
Host: 192.168.0.89:9090
Origin: http://192.168.0.89:9090
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: BHIuTA54YKc80CVB9sfaJw==
Sec-WebSocket-Version: 13</pre>
<p>此请求与普通HTTP请求没有太大差异，关键的不同就是Upgrade头，其取值为websocket，表示升级当前连接的协议为WebSocket。</p>
<p>如果服务器同意升级，则HTTP应答报文类似下面：</p>
<pre class="crayon-plain-tag">HTTP/1.1 101 
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: gJ0vp6zOXy4g/koag0FAJkBCwSU=
Date: Wed, 20 Sep 2017 03:17:27 GMT</pre>
<p>101状态码的含义是切换协议，切换到的协议由Upgrade字段说明。Sec-WebSocket-Accept的取值根据Sec-WebSocket-Key推导，供客户端验证。</p>
<div class="blog_h3"><span class="graybg">关闭握手</span></div>
<p>WebSocket关闭并不总是能正常进行，特别是在因特网或者其它不可考网络中进行通信的时候，底层TCP连接可能突然就断开。</p>
<p>当正常关闭WebSocket时，关闭行为的发起端发送特定的opcode=8的消息给对方，说明关闭的操作代码和原因。</p>
<p>关闭操作代码和原因作为载荷发送。关闭操作代码为16bit整数，关闭原因则是简短的UTF-8字符串。关闭代码如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">关闭代码</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>1000</td>
<td>Normal Close。正常关闭</td>
</tr>
<tr>
<td>1001</td>
<td>Going Away。发起者正在关闭，并且不期望后续再发起连接。出现的原因例如服务器准备停机维护</td>
</tr>
<tr>
<td>1002</td>
<td>Protocol Error。因为协议错误而关闭</td>
</tr>
<tr>
<td>1003</td>
<td>Unacceptable Data Type。消息类型不支持</td>
</tr>
<tr>
<td>1004-1006</td>
<td>保留</td>
</tr>
<tr>
<td>1007</td>
<td>Invalid Data。数据无效，例如错误编码的文本消息</td>
</tr>
<tr>
<td>1008</td>
<td>Message Violates Policy。如果关闭原因不被其它关闭代码覆盖，或者你不希望暴露关闭原因给对方，使用此代码</td>
</tr>
<tr>
<td>1009</td>
<td>Message Too Large。消息长度过大，无法处理</td>
</tr>
<tr>
<td>1010</td>
<td>Extension Required。由客户端发送，如果服务器不支持客户端需要的扩展</td>
</tr>
<tr>
<td>1011</td>
<td>Unexpected Condition。不可预知的原因导致应用程序无法继续处理连接</td>
</tr>
<tr>
<td>1015</td>
<td>TLS Failure。在握手之前TLS处理失败，不要使用此代码</td>
</tr>
<tr>
<td>4000-4999</td>
<td>你可以自定义这些代码的用途</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">报文格式</span></div>
<p>一旦握手完成，之后的通信均基于WebSocket报文。通信双方可以随时发送WebSocket报文，此所谓全双工。</p>
<p>报文在网络中以二进制形式表示，它包含一个报文头。报文头标记了不同帧（Frame）之间的边界并且包含了简单类型信息。<span style="background-color: #c0c0c0;">1-N个帧组成完整的WebSocket消息</span>，<span style="background-color: #c0c0c0;">通常情况下，一个消息总是包含仅仅一个帧</span>，因此，帧和消息这两个术语经常替换使用。</p>
<p>WebSocket帧的格式如下图：</p>
<p><img class="aligncenter size-full wp-image-16233" src="https://blog.gmem.cc/wp-content/uploads/2017/09/websocket-header.png" alt="websocket-header" width="508" height="289" /></p>
<div class="blog_h3"><span class="graybg">FIN</span></div>
<p>一般取值0，除非要发送有多个帧组成的消息。</p>
<p>要发送由多个帧组成的消息，则需要把报文首位FIN置为0。依次发送完所有帧后，将FIN置为1，提示接收方所有帧已经发送完毕。 </p>
<div class="blog_h3"><span class="graybg">RSVx</span></div>
<p>除非协商使用了某种WebSocket扩展，这3bit均设置为0</p>
<div class="blog_h3"><span class="graybg">opcode</span></div>
<p>指定消息载荷的类型，对应第一字节的后4个bit：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 10%; text-align: center;">opcode</td>
<td style="width: 15%; text-align: center;">载荷类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>文本</td>
<td>载荷为文本</td>
</tr>
<tr>
<td>2</td>
<td>二进制</td>
<td>载荷为字节 </td>
</tr>
<tr>
<td>8</td>
<td>关闭连接</td>
<td>客户端或者服务器发起，关闭握手 </td>
</tr>
<tr>
<td>9</td>
<td>Ping</td>
<td>客户端或者服务器发起，Ping消息 </td>
</tr>
<tr>
<td>10</td>
<td>Pong</td>
<td>客户端或者服务器发起，Pong消息 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">掩码</span></div>
<p>从浏览器发送到服务器的WebSocket帧被掩码处理以混淆载荷内容。掩码的意图并非防止窃听，而是出于非一般性的安全考虑，以及增强和即有HTTP代理服务器的兼容性。</p>
<p>报文头第二字节第1位说明报文是否被掩码处理。WebSocket协议要求客户端对所有帧进行掩码处理，服务器收到的任何帧，都需要解除掩码后再进一步处理。</p>
<p>如果报文被掩码，在报文长度头字段之后，会有4字节的掩码键。</p>
<div class="blog_h3"><span class="graybg">载荷长度</span></div>
<p>WebSocket使用可变bit数来标注帧的长度：</p>
<ol>
<li>如果帧小于126字节，使用7bit标注长度</li>
<li>如果帧长度在126-216之间， 使用额外两个字节标注长度</li>
<li>如果帧长度在216以上，使用8字节标注长度</li>
</ol>
<p>其中，第2、3种情况下，最初的7bit被填写为126或者127，作为指示标记。 </p>
<div class="blog_h3"><span class="graybg">文本消息 </span></div>
<p>文本消息的编码为UTF-8，此编码与7bit的ASCII兼容。需要注意UTF-8是WebSocket唯一支持的文本编码格式。 </p>
<div class="blog_h2"><span class="graybg">子协议</span></div>
<p>WebSocket协议支持高层协议、高层协议协商。这些高层协议被称为子协议（Subprotocols）。</p>
<p>要使用子协议，在握手时客户端发送HTTP头<pre class="crayon-plain-tag">Sec-WebSocket-Protocol</pre>，指明它支持的子协议列表。服务器的同名响应头则从列表中选择一个子协议。 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-protocol">WebSocket协议</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/websocket-protocol/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于C/C++的WebSocket库</title>
		<link>https://blog.gmem.cc/websocket-library-for-c-or-cpp</link>
		<comments>https://blog.gmem.cc/websocket-library-for-c-or-cpp#comments</comments>
		<pubDate>Tue, 19 Sep 2017 07:46:33 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16206</guid>
		<description><![CDATA[<p>libwebsockets 简介 libwebsockets是一个纯C语言的轻量级WebSocket库，它的CPU、内存占用很小，同时支持作为服务器端/客户端。其特性包括： 支持ws://和wss://协议 可以选择和OpenSSL、CyaSSL或者WolfSSL链接 轻量和高速，即使在每个线程处理多达250个连接的情况下 支持事件循环、零拷贝。支持poll()、libev（epoll）、libuv libwebsockets提供的API相当底层，实现简单的功能也需要相当冗长的代码。 构建 [crayon-69e20c9f1648e877137595/] Echo示例 CMake项目配置 [crayon-69e20c9f16492233544491/] 客户端 [crayon-69e20c9f16495314227057/] 服务器 [crayon-69e20c9f16499785424610/] 封装 为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。 客户端封装 [crayon-69e20c9f1649c732420646/] 使用客户端封装 <a class="read-more" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">基于C/C++的WebSocket库</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">libwebsockets</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>libwebsockets是一个纯C语言的轻量级WebSocket库，它的CPU、内存占用很小，同时支持作为服务器端/客户端。其特性包括：</p>
<ol>
<li>支持ws://和wss://协议</li>
<li>可以选择和OpenSSL、CyaSSL或者WolfSSL链接</li>
<li>轻量和高速，即使在每个线程处理多达250个连接的情况下</li>
<li>支持事件循环、零拷贝。支持poll()、libev（epoll）、libuv</li>
</ol>
<p>libwebsockets提供的API相当底层，实现简单的功能也需要相当冗长的代码。</p>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
mkdir build &amp;&amp; cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/libwebsockets ..
make &amp;&amp; make install </pre>
<div class="blog_h2"><span class="graybg">Echo示例</span></div>
<div class="blog_h3"><span class="graybg">CMake项目配置</span></div>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 2.8.9)
project(libws-study C)

include_directories(/home/alex/CPP/lib/libwebsockets/include)

set(CMAKE_CXX_FLAGS "-w -pthread")

set(SF_CLIENT client.c)
set(SF_SERVER server.c)

add_executable(client ${SF_CLIENT})
target_link_libraries(client /home/alex/CPP/lib/libwebsockets/lib/libwebsockets.so)


add_executable(server ${SF_SERVER})
target_link_libraries(server /home/alex/CPP/lib/libwebsockets/lib/libwebsockets.so)</pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<pre class="crayon-plain-tag">#include "libwebsockets.h"
#include &lt;signal.h&gt;

static volatile int exit_sig = 0;
#define MAX_PAYLOAD_SIZE  10 * 1024

void sighdl( int sig ) {
    lwsl_notice( "%d traped", sig );
    exit_sig = 1;
}

/**
 * 会话上下文对象，结构根据需要自定义
 */
struct session_data {
    int msg_count;
    unsigned char buf[LWS_PRE + MAX_PAYLOAD_SIZE];
    int len;
};

/**
 * 某个协议下的连接发生事件时，执行的回调函数
 *
 * wsi：指向WebSocket实例的指针
 * reason：导致回调的事件
 * user 库为每个WebSocket会话分配的内存空间
 * in 某些事件使用此参数，作为传入数据的指针
 * len 某些事件使用此参数，说明传入数据的长度
 */
int callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct session_data *data = (struct session_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_CLIENT_ESTABLISHED:   // 连接到服务器后的回调
            lwsl_notice( "Connected to server\n" );
            break;

        case LWS_CALLBACK_CLIENT_RECEIVE:       // 接收到服务器数据后的回调，数据为in，其长度为len
            lwsl_notice( "Rx: %s\n", (char *) in );
            break;
        case LWS_CALLBACK_CLIENT_WRITEABLE:     // 当此客户端可以发送数据时的回调
            if ( data-&gt;msg_count &lt; 3 ) {
                // 前面LWS_PRE个字节必须留给LWS
                memset( data-&gt;buf, 0, sizeof( data-&gt;buf ));
                char *msg = (char *) &amp;data-&gt;buf[ LWS_PRE ];
                data-&gt;len = sprintf( msg, "你好 %d", ++data-&gt;msg_count );
                lwsl_notice( "Tx: %s\n", msg );
                // 通过WebSocket发送文本消息
                lws_write( wsi, &amp;data-&gt;buf[ LWS_PRE ], data-&gt;len, LWS_WRITE_TEXT );
            }
            break;
    }
    return 0;
}

/**
 * 支持的WebSocket子协议数组
 * 子协议即JavaScript客户端WebSocket(url, protocols)第2参数数组的元素
 * 你需要为每种协议提供回调函数
 */
struct lws_protocols protocols[] = {
    {
        //协议名称，协议回调，接收缓冲区大小
        "", callback, sizeof( struct session_data ), MAX_PAYLOAD_SIZE,
    },
    {
        NULL, NULL,   0 // 最后一个元素固定为此格式
    }
};

int main() {
    // 信号处理函数
    signal( SIGTERM, sighdl );

    // 用于创建vhost或者context的参数
    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = CONTEXT_PORT_NO_LISTEN;
    ctx_info.iface = NULL;
    ctx_info.protocols = protocols;
    ctx_info.gid = -1;
    ctx_info.uid = -1;

    // 创建一个WebSocket处理器
    struct lws_context *context = lws_create_context( &amp;ctx_info );

    char *address = "192.168.0.89";
    int port = 9090;
    char addr_port[256] = { 0 };
    sprintf( addr_port, "%s:%u", address, port &amp; 65535 );

    // 客户端连接参数
    struct lws_client_connect_info conn_info = { 0 };
    conn_info.context = context;
    conn_info.address = address;
    conn_info.port = port;
    conn_info.ssl_connection = 0;
    conn_info.path = "/h264src";
    conn_info.host = addr_port;
    conn_info.origin = addr_port;
    conn_info.protocol = protocols[ 0 ].name;

    // 下面的调用触发LWS_CALLBACK_PROTOCOL_INIT事件
    // 创建一个客户端连接
    struct lws *wsi = lws_client_connect_via_info( &amp;conn_info );
    while ( !exit_sig ) {
        // 执行一次事件循环（Poll），最长等待1000毫秒
        lws_service( context, 1000 );
        /**
         * 下面的调用的意义是：当连接可以接受新数据时，触发一次WRITEABLE事件回调
         * 当连接正在后台发送数据时，它不能接受新的数据写入请求，所有WRITEABLE事件回调不会执行
         */
        lws_callback_on_writable( wsi );
    }
    // 销毁上下文对象
    lws_context_destroy( context );

    return 0;
}</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">static int protocol0_callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct session_data *data = (struct session_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_ESTABLISHED:       // 当服务器和客户端完成握手后
            break;
        case LWS_CALLBACK_RECEIVE:           // 当接收到客户端发来的帧以后
            // 判断是否最后一帧
            data-&gt;fin = lws_is_final_fragment( wsi );
            // 判断是否二进制消息
            data-&gt;bin = lws_frame_is_binary( wsi );
            // 对服务器的接收端进行流量控制，如果来不及处理，可以控制之
            // 下面的调用禁止在此连接上接收数据
            lws_rx_flow_control( wsi, 0 );

            // 业务处理部分，为了实现Echo服务器，把客户端数据保存起来
            memcpy( &amp;data-&gt;buf[ LWS_PRE ], in, len );
            data-&gt;len = len;

            // 需要给客户端应答时，触发一次写回调
            lws_callback_on_writable( wsi );
            break;
        case LWS_CALLBACK_SERVER_WRITEABLE:   // 当此连接可写时
            lws_write( wsi, &amp;data-&gt;buf[ LWS_PRE ], data-&gt;len, LWS_WRITE_TEXT );
            // 下面的调用允许在此连接上接收数据
            lws_rx_flow_control( wsi, 1 );
            break;
    }
    // 回调函数最终要返回0，否则无法创建服务器
    return 0;
}

int main() {
    // 信号处理函数
    signal( SIGTERM, sighdl );

    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = 9090;
    ctx_info.iface = NULL; // 在所有网络接口上监听
    ctx_info.protocols = protocols;
    ctx_info.gid = -1;
    ctx_info.uid = -1;
    ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
    struct lws_context *context = lws_create_context( &amp;ctx_info );
    while ( !exit_sig ) {
        lws_service( context, 1000 );
    }
    lws_context_destroy( context );
}</pre>
<div class="blog_h2"><span class="graybg">封装</span></div>
<p>为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。</p>
<div class="blog_h3"><span class="graybg">客户端封装</span></div>
<pre class="crayon-plain-tag">#ifndef LIVE555_WSCLIENT_H
#define LIVE555_WSCLIENT_H

#include "libwebsockets.h"

#ifndef LWS_MAX_PAYLOAD_SIZE
#define LWS_MAX_PAYLOAD_SIZE  1024 * 1024
#endif

#ifndef SPDLOG_CONST
#define SPDLOG_CONST
const auto LOGGER = spdlog::stdout_color_st( "console" );
#endif

/**
 * 通用回调函数签名
 */
typedef void (*lws_callback)( struct lws *wsi, void *user, void *in, size_t len );

// 用户数据对象
typedef struct lws_user_data {
    // 缓冲区
    unsigned char *buf;
    // 缓冲区有效字节数
    int len;
    // 用户自定义数据
    void *user;
    // 读写缓冲区之前需要加锁
    volatile bool locked;
    // 指示当前缓冲区的数据的重要性，如果为真，发送之前不得被覆盖
    volatile bool critical;
    // 本次数据发送类型
    lws_write_protocol type;
    // 回调函数
    lws_callback esta_callback;
    lws_callback recv_callback;
    lws_callback writ_callback;
};

void writ_callback_send_buf( struct lws *wsi, void *user, void *in, size_t len ) {
    struct lws_user_data *data = (struct lws_user_data *) user;
    if ( __sync_bool_compare_and_swap( &amp;data-&gt;locked, 0, 1 )) {
        unsigned char *buf;
        char hex[128]= { 0 };
        int writ_count;

        int len = data-&gt;len;
        if ( len == 0 ) goto cleanup;

        buf = data-&gt;buf + LWS_PRE;
        writ_count = lws_write( wsi, buf, len, data-&gt;type );
        if ( data-&gt;type == LWS_WRITE_BINARY ) {
            char *phex = hex;
            for ( int i = 0; i &lt; 16; i++ ) {
                unsigned char c = *buf++;
                sprintf( phex, "%02x ", c );
                phex += 3;
            }
        }
        LOGGER-&gt;debug( "lws_write {} bytes: {}...", writ_count, hex );
        cleanup:
        data-&gt;locked = 0;
        data-&gt;critical = 0;
        data-&gt;len = 0;
    }
}

static int lws_protocol_0_callback( struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len ) {
    struct lws_user_data *data = (struct lws_user_data *) user;
    switch ( reason ) {
        case LWS_CALLBACK_CLIENT_ESTABLISHED:
            if ( data-&gt;esta_callback )data-&gt;esta_callback( wsi, user, in, len );
            break;
        case LWS_CALLBACK_CLIENT_RECEIVE:
            if ( data-&gt;recv_callback )data-&gt;recv_callback( wsi, user, in, len );
            break;
        case LWS_CALLBACK_CLIENT_WRITEABLE:
            if ( data-&gt;writ_callback )data-&gt;writ_callback( wsi, user, in, len );
            break;
    }
    return 0;
}

typedef struct lws_client {
    struct lws *wsi;
    struct lws_context *context;
    lws_user_data *data;
    int *cycle;

    // 连接参数
    char *address;
    char *path;
    int port;

    void (*fill_buf)( lws_client *client, void *buf, int len, lws_write_protocol type );

    void (*fire_writable)( lws_client *client );
};

void fill_buf( lws_client *client, void *buf, int len, lws_write_protocol type ) {
    lws_user_data *data = client-&gt;data;
    data-&gt;type = type;
    data-&gt;len = len;
    memcpy( data-&gt;buf + LWS_PRE, buf, len );
}

void fire_writable( lws_client *client ) {
    lws_callback_on_writable( client-&gt;wsi );
    // 停止当前事件循环等待
    lws_cancel_service( client-&gt;context );
}

void *lws_service_thread_func( void *arg ) {
    lws_client *client = (lws_client *) arg;

    struct lws_context_creation_info ctx_info = { 0 };
    ctx_info.port = CONTEXT_PORT_NO_LISTEN;
    ctx_info.iface = NULL;
    const struct lws_protocols protocols[] = {
        {
            "", lws_protocol_0_callback, sizeof( struct lws_user_data ), LWS_MAX_PAYLOAD_SIZE, 0, 0, LWS_MAX_PAYLOAD_SIZE
        },
        {
            NULL, NULL,                  0
        }
    };
    static const struct lws_extension exts[] = {
        {
            "permessage-deflate",
            lws_extension_callback_pm_deflate,
            "permessage-deflate; client_no_context_takeover; client_max_window_bits"
        },
        { NULL, NULL, NULL /* terminator */ }
    };
    ctx_info.protocols = protocols;
    ctx_info.extensions = exts;
    ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
    ctx_info.gid = -1;
    ctx_info.uid = -1;

    struct lws_context *context = lws_create_context( &amp;ctx_info );
    client-&gt;context = context;

    char addr_port[256] = { 0 };
    sprintf( addr_port, "%s:%u", client-&gt;address, client-&gt;port &amp; 65535 );

    struct lws_client_connect_info conn_info = { 0 };
    conn_info.context = context;
    conn_info.address = client-&gt;address;
    conn_info.port = client-&gt;port;
    conn_info.ssl_connection = 0;
    conn_info.path = client-&gt;path;
    conn_info.host = addr_port;
    conn_info.origin = addr_port;
    conn_info.protocol = protocols[ 0 ].name;
    // 用户数据对象由调用者提供，因为需要提供回调
    conn_info.userdata = client-&gt;data;

    struct lws *wsi = lws_client_connect_via_info( &amp;conn_info );
    client-&gt;wsi = wsi;

    int *loop_cycle = client-&gt;cycle;
    int cycle = *loop_cycle;
    while ( *loop_cycle &gt;= 0 ) {
        lws_service( context, cycle );
    }
    lws_context_destroy( context );
}

/**
 * 连接到WebSocket服务器
 * @param address  IP地址
 * @param path  上下文路径URL
 * @param port 端口
 * @param data 用户数据
 * @param loop_cycle 事件循环周期，如果大于等于0则启动事件循环，后续将其置为-1则导致循环终止
 * @return
 */
lws_client *lws_connect( char *address, char *path, int port, lws_user_data *data, int loop_cycle ) {
    lws_client *client = (lws_client *) malloc( sizeof( lws_client ));
    client-&gt;data = data;
    client-&gt;cycle = (int *) malloc( sizeof( int ));
    *client-&gt;cycle = loop_cycle;
    client-&gt;address = address;
    client-&gt;path = path;
    client-&gt;port = port;
    client-&gt;fill_buf = fill_buf;
    client-&gt;fire_writable = fire_writable;
    pthread_t *lws_service_thread = (pthread_t *) malloc( sizeof( pthread_t ));
    pthread_create( lws_service_thread, NULL, lws_service_thread_func, client );
    return client;

}

#endif</pre>
<div class="blog_h3"><span class="graybg">使用客户端封装</span></div>
<pre class="crayon-plain-tag">// 创建用户数据对象
lws_user_data *data = new lws_user_data();
data-&gt;buf = new unsigned char[LWS_PRE + LWS_MAX_PAYLOAD_SIZE];
data-&gt;writ_callback = writ_callback_send_buf_bin;  // 注册回调

// 创建客户端
lws_client *ws_client = lws_connect( "192.168.0.89", "/h264src", 9090, data, 10 );

// 发送数据，需要同步
lws_user_data *data = client-&gt;data;
// GCC内置CAS语义
if ( __sync_bool_compare_and_swap( &amp;data-&gt;locked, 0, 1 )) {
    client-&gt;fill_buf( client, sink-&gt;recvBuf, frameSize );
    client-&gt;fire_writable( client );
    data-&gt;locked = 0;
}</pre>
<div class="blog_h2"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">error on reading from skt : 104</span></div>
<p><a href="/network-faq#skt-enos">错误代码104</a>的含义是连接被重置，我遇到这个问题的原因是，Spring的WebSocket消息缓冲区大小不足。</p>
<div class="blog_h1"><span class="graybg">WebSocket++</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>WebSocket++是一个仅仅由头文件构成的C++库，它实现了WebSocket协议（RFC6455），通过它，你可以在C++项目中使用WebSocket客户端或者服务器。</p>
<p>WebSocket++使用两个可以相互替换的网络传输模块，其中一个基于C++ I/O流，另一个基于Asio。</p>
<p>WebSocket++的主要特性包括：</p>
<ol>
<li>事件驱动的接口</li>
<li>支持WSS、IPv6</li>
<li>灵活的依赖管理 —— Boost或者C++ 11标准库</li>
<li>可移植性：Posix/Windows、32/64bit、Intel/ARM/PPC</li>
<li>线程安全</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone https://github.com/zaphoyd/websocketpp.git
cd websocketpp
mkdir build &amp;&amp; cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/websocketpp ..
make &amp;&amp; make install </pre>
<div class="blog_h2"><span class="graybg">Echo示例</span></div>
<div class="blog_h3"><span class="graybg">CMake项目配置</span></div>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(websocket__)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "-pthread")
add_definitions(-D_WEBSOCKETPP_CPP11_FUNCTIONAL_)
add_definitions(-D_WEBSOCKETPP_CPP11_THREAD_)
add_definitions(-D_WEBSOCKETPP_CPP11_SYSTEM_ERROR_)
add_definitions(-D_WEBSOCKETPP_CPP11_MEMORY_)


include_directories(/home/alex/CPP/lib/websocketpp/include /home/alex/CPP/lib/boost/1.65.1/include/)

set(SF_CLIENT client.cpp)
add_executable(client ${SF_CLIENT})
target_link_libraries(client /home/alex/CPP/lib/boost/1.65.1/lib/libboost_system.so)

set(SF_SERVER server.cpp)
add_executable(server ${SF_SERVER})
target_link_libraries(server /home/alex/CPP/lib/boost/1.65.1/lib/libboost_system.so)</pre>
<div class="blog_h3"><span class="graybg">客户端 </span></div>
<pre class="crayon-plain-tag">#include &lt;websocketpp/config/asio_no_tls_client.hpp&gt;

#include &lt;websocketpp/client.hpp&gt;

#include &lt;iostream&gt;

typedef websocketpp::client&lt;websocketpp::config::asio_client&gt; client;

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

// 消息指针
typedef websocketpp::config::asio_client::message_type::ptr message_ptr;

// 打开连接时的回调
void on_open( client *c, websocketpp::connection_hdl hdl ) {
    std::string msg = "Hello 1";
    // 发送文本消息
    c-&gt;send( hdl, msg, websocketpp::frame::opcode::text );
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Tx: " + msg );

}

// 连接失败时的回调
void on_fail( client *c, websocketpp::connection_hdl hdl ) {
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Connection Failed" );
}

// 接收到服务器发来的WebSocket消息后的回调
void on_message( client *c, websocketpp::connection_hdl hdl, message_ptr msg ) {
    c-&gt;get_alog().write( websocketpp::log::alevel::app, "Rx: " + msg-&gt;get_payload());
    // 关闭连接，导致事件循环退出
    c-&gt;close( hdl, websocketpp::close::status::normal, "" );
}

// 关闭连接时的回调
void on_close( client *c, websocketpp::connection_hdl hdl ) {
}

int main( int argc, char *argv[] ) {
    client echo_client;

    // 调整日志策略
    echo_client.clear_access_channels( websocketpp::log::alevel::frame_header );
    echo_client.clear_access_channels( websocketpp::log::alevel::frame_payload );

    std::string uri = "ws://192.168.0.89:9090/h264src";

    try {
        // 初始化ASIO ASIO
        echo_client.init_asio();

        // 注册回调函数
        echo_client.set_open_handler( std::bind( &amp;on_open, &amp;echo_client, ::_1 ));
        echo_client.set_fail_handler( std::bind( &amp;on_fail, &amp;echo_client, ::_1 ));
        echo_client.set_message_handler( std::bind( &amp;on_message, &amp;echo_client, ::_1, ::_2 ));
        echo_client.set_close_handler( std::bind( &amp;on_close, &amp;echo_client, ::_1 ));

        // 在事件循环启动前创建一个连接对象
        websocketpp::lib::error_code ec;
        client::connection_ptr con = echo_client.get_connection( uri, ec );
        echo_client.connect( con );
        con-&gt;get_handle(); // 连接句柄，发送消息时必须要传入

        // 启动事件循环（ASIO的io_service），当前线程阻塞
        echo_client.run();
    } catch ( const std::exception &amp;e ) {
        std::cout &lt;&lt; e.what() &lt;&lt; std::endl;
    } catch ( websocketpp::lib::error_code e ) {
        std::cout &lt;&lt; e.message() &lt;&lt; std::endl;
    } catch ( ... ) {
        std::cout &lt;&lt; "other exception" &lt;&lt; std::endl;
    }
}</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;

#include &lt;websocketpp/config/asio_no_tls.hpp&gt;
#include &lt;websocketpp/server.hpp&gt;

typedef websocketpp::server&lt;websocketpp::config::asio&gt; server;
typedef websocketpp::config::asio::message_type::ptr message_ptr;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

void on_open( server *s, websocketpp::connection_hdl hdl ) {
    // 根据连接句柄获得连接对象
    server::connection_ptr con = s-&gt;get_con_from_hdl( hdl );
    // 获得URL路径
    std::string path = con-&gt;get_resource();
    s-&gt;get_alog().write( websocketpp::log::alevel::app, "Connected to path " + path );
}

void on_message( server *s, websocketpp::connection_hdl hdl, message_ptr msg ) {
    s-&gt;send( hdl, msg-&gt;get_payload(), websocketpp::frame::opcode::text );
}

int main() {
    server echo_server;
    // 调整日志策略
    echo_server.set_access_channels( websocketpp::log::alevel::all );
    echo_server.clear_access_channels( websocketpp::log::alevel::frame_payload );

    try {
        echo_server.init_asio();

        echo_server.set_open_handler( bind( &amp;on_open, &amp;echo_server, ::_1 ));
        echo_server.set_message_handler( bind( &amp;on_message, &amp;echo_server, ::_1, ::_2 ));
        // 在所有网络接口的9090上监听
        echo_server.listen( 9090 );

        // 启动服务器端Accept事件循环
        echo_server.start_accept();

        // 启动事件循环（ASIO的io_service），当前线程阻塞
        echo_server.run();
    } catch ( websocketpp::exception const &amp;e ) {
        std::cout &lt;&lt; e.what() &lt;&lt; std::endl;
    } catch ( ... ) {
        std::cout &lt;&lt; "other exception" &lt;&lt; std::endl;
    }
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/websocket-library-for-c-or-cpp">基于C/C++的WebSocket库</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/websocket-library-for-c-or-cpp/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>SockJS知识集锦</title>
		<link>https://blog.gmem.cc/sockjs-faq</link>
		<comments>https://blog.gmem.cc/sockjs-faq#comments</comments>
		<pubDate>Tue, 05 Sep 2017 06:08:03 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15858</guid>
		<description><![CDATA[<p>简介 SockJS允许应用程序使用WebSocket来进行通信，但是当WebSocket不可用时，可以使用代替的传输机制，但是保持API不变。 SockJS由以下部分组成： SockJS协议 一个JavaScript客户端 SockJS服务器端实现，例如 spring-websocket SocketJS客户端以针对/info的GET请求发起通信，服务器会返回一些基本信息，在此之后，客户端必须决定使用何种传输机制。SocketJS支持多种传输机制，包括WebSocket、HTTP Streaming、HTTP Long Polling。 从4.1开始，Spring提供SockJS的Java客户端。 客户端 JavaScript客户端 SockJS的API和WebSocket很类似： [crayon-69e20c9f169a0249207232/] Java客户端 参考Spring对WebSocket的支持</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/sockjs-faq">SockJS知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">简介</span></div>
<p>SockJS允许应用程序使用WebSocket来进行通信，但是当WebSocket不可用时，可以使用代替的传输机制，但是保持API不变。</p>
<p>SockJS由以下部分组成：</p>
<ol>
<li>SockJS协议</li>
<li>一个JavaScript客户端</li>
<li>SockJS服务器端实现，例如 spring-websocket</li>
</ol>
<p>SocketJS客户端以针对/info的GET请求发起通信，服务器会返回一些基本信息，在此之后，客户端必须决定使用何种传输机制。SocketJS支持多种传输机制，包括WebSocket、HTTP Streaming、HTTP Long Polling。</p>
<p>从4.1开始，Spring提供SockJS的Java客户端。</p>
<div class="blog_h2"><span class="graybg">客户端</span></div>
<div class="blog_h3"><span class="graybg">JavaScript客户端</span></div>
<p>SockJS的API和WebSocket很类似：</p>
<pre class="crayon-plain-tag">var sock = new SockJS( 'ws://gmem.cc:8888/hello' );
// 当连接打开后的回调
sock.onopen = function () {
    console.log( 'open' );
    // 发送消息
    sock.send( 'Hello there' );
};
// 接收到消息时的回调
sock.onmessage = function ( msg ) {
    // msg.data为消息内容
    console.log( 'message', msg.data );
    // 关闭连接
    sock.close();
};
// 关闭连接时的回调
sock.onclose = function () {
    console.log( 'close' );
};</pre>
<div class="blog_h3"><span class="graybg">Java客户端</span></div>
<p>参考<a href="/ws-support-of-spring#sockjs-java-client">Spring对WebSocket的支持</a></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/sockjs-faq">SockJS知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/sockjs-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Spring对WebSocket的支持</title>
		<link>https://blog.gmem.cc/ws-support-of-spring</link>
		<comments>https://blog.gmem.cc/ws-support-of-spring#comments</comments>
		<pubDate>Tue, 05 Sep 2017 03:38:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[STOMP]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15848</guid>
		<description><![CDATA[<p>简介 Spring 4.x引入了新的模块spring-websocket，对WebSocket提供了全面的支持，Spring的WebSocket实现遵循JSR-356（Java WebSocket API），并且添加了一些额外特性。 绝大部分现代浏览器均支持WebSocket，包括IE 10+。对于不支持WebSocket的浏览器，Spring允许基于 SockJS协议作为备选传输方案。 消息架构  与REST那种大量URL + HTTP方法来区分对象和操作的风格完全不同，WebSocket仅仅使用单个URL。WebSocket更加和传统的MOM类似，它是异步的、 事件驱动的基于消息的架构。 Spring 4 引入了新的模块spring-messaging，抽象出了Message、MessageChannel、MessageHandler等消息架构的基础概念。此模块包含了一些注解，用于将消息映射到方法（类似于Spring MVC把URL映射到方法）。 子协议支持 WebSocket是在TCP之上很薄的一层封装，它仅仅是把比特流转换为消息（文本、二进制）流，解析消息的职责由应用程序负责。我们可以在WebSocket之上提供应用层子协议。 在WebSocket握手阶段，客户端和服务器可以基于Sec-WebSocket-Protocol头来协商子协议。Spring支持STOMP —— 一个简单的消息协议。 WebSocket <a class="read-more" href="https://blog.gmem.cc/ws-support-of-spring">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ws-support-of-spring">Spring对WebSocket的支持</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">简介</span></div>
<p>Spring 4.x引入了新的模块spring-websocket，对WebSocket提供了全面的支持，Spring的WebSocket实现遵循JSR-356（Java WebSocket API），并且添加了一些额外特性。</p>
<p>绝大部分现代浏览器均<a href="http://caniuse.com/#feat=websockets">支持WebSocket</a>，包括IE 10+。对于不支持WebSocket的浏览器，Spring允许基于 SockJS协议作为备选传输方案。</p>
<div class="blog_h3"><span class="graybg">消息架构 </span></div>
<p>与REST那种大量URL + HTTP方法来区分对象和操作的风格完全不同，WebSocket仅仅使用单个URL。WebSocket更加和传统的MOM类似，它是异步的、 事件驱动的基于消息的架构。</p>
<p>Spring 4 引入了新的模块spring-messaging，抽象出了Message、MessageChannel、MessageHandler等消息架构的基础概念。此模块包含了一些注解，用于将消息映射到方法（类似于Spring MVC把URL映射到方法）。</p>
<div class="blog_h3"><span class="graybg">子协议支持</span></div>
<p>WebSocket是在TCP之上很薄的一层封装，它仅仅是把比特流转换为消息（文本、二进制）流，解析消息的职责由应用程序负责。我们可以在WebSocket之上提供应用层子协议。</p>
<p>在WebSocket握手阶段，客户端和服务器可以基于Sec-WebSocket-Protocol头来协商子协议。Spring支持STOMP —— 一个简单的消息协议。</p>
<div class="blog_h2"><span class="graybg">WebSocket API</span></div>
<div class="blog_h3"><span class="graybg">配置WebSocketHandler</span></div>
<p>Spring提供了可以在很多WebSocket引擎中运行的API，支持的引擎包括Tomcat 7.0.47+, Jetty 9.1+, GlassFish 4.1+, WebLogic 12.1.3+等。</p>
<p>要创建一个WebSocket服务器，可以实现WebSocketHandler接口，或者继承TextWebSocketHandler或者BinaryWebSocketHandler类：</p>
<pre class="crayon-plain-tag">import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

public class HelloHandler extends TextWebSocketHandler {
    // 接受消息的回调
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // 发送消息
        session.sendMessage( new TextMessage( payload ) );
    }

}</pre>
<p>每个WebSocketHandler， 处理单个URL。在一个WebSocket端口上可以有多个WebSocketHandler。要注册WebSocketHandler，可以：</p>
<pre class="crayon-plain-tag">import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(helloHander(), "/hello");
    }

    @Bean
    public WebSocketHandler helloHander() {
        return new HelloHander();
    }

}</pre>
<p>也可以使用等价的XML配置：</p>
<pre class="crayon-plain-tag">&lt;beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd"&gt;

    &lt;websocket:handlers&gt;
        &lt;websocket:mapping path="/hello" handler="helloHander"/&gt;
    &lt;/websocket:handlers&gt;

    &lt;bean id="helloHander" class="cc.gmem.study.spring.ws.HelloHandler"/&gt;

&lt;/beans&gt;</pre>
<div class="blog_h3"><span class="graybg">定制WebSocket握手</span></div>
<p>通过HandshakeInterceptor可以对WebSocket最初基于HTTP的握手进行定制，此拦截器暴露beforeHandshake/afterHandshake方法，实现这些方法可以：</p>
<ol>
<li>阻止握手</li>
<li>设置在WebSocketSession中可以使用的属性 </li>
</ol>
<p>拦截器的注册方式为：</p>
<pre class="crayon-plain-tag">@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new HelloHandler(), "/hello")
                // 添加拦截器
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

}</pre>
<p>等价的XML配置：</p>
<pre class="crayon-plain-tag">&lt;websocket:handlers&gt;
    &lt;websocket:mapping path="/hello" handler="helloHandler"/&gt;
    &lt;websocket:handshake-interceptors&gt;
        &lt;bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/&gt;
    &lt;/websocket:handshake-interceptors&gt;
&lt;/websocket:handlers&gt;</pre>
<div class="blog_h3"><span class="graybg">装饰WebSocketHandler</span></div>
<p>使用WebSocketHandlerDecorator来装饰WebSocketHandler，可以实现额外的行为。当基于Java-Config / XML来配置时，日志、异常处理这两个装饰器自动添加。</p>
<p>ExceptionWebSocketHandlerDecorator会捕获任何WebSocketHandler抛出的异常，并以1011状态码（服务器错误）关闭WebSocket会话。</p>
<div class="blog_h3"><span class="graybg">部署</span></div>
<p>WebSocket API可以和Spring MVC一起使用，DispatcherServlet同时负责WebSocket握手和普通HTTP请求的处理。</p>
<p>你也可以独立在其它HTTP服务环境中使用WebSocket API，可以借助WebSocketHttpRequestHandler集成WebSocketHandler到HTTP服务环境中。</p>
<div class="blog_h3"><span class="graybg">WebSocket引擎配置</span></div>
<p>每种底层Servlet引擎都暴露了一些配置属性，进行缓冲区大小、超时时间等参数的配置。</p>
<p>当使用Tomcat/WildFly/GlassFish时，你可以使用ServletServerContainerFactoryBean进行引擎配置：</p>
<pre class="crayon-plain-tag">@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // WebSocket消息缓冲区大小，如果客户端发来的消息较大，需要按需调整
        // 和libwebsockets配合时，客户端报错error on reading from skt : 104，即因为缓冲区不够大
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
}</pre>
<p>当使用Jetty时，你需要提供一个WebSocketServerFactory，并传递给Spring的DefaultHandshakeHandler：</p>
<pre class="crayon-plain-tag">@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(helloHandler(),"/hello")
               // 设置握手处理器
               .setHandshakeHandler(handshakeHandler());
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {

        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);

        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }
}</pre>
<div class="blog_h3"><span class="graybg">Origin配置</span></div>
<p>从 Spring4.1.5开始，WebSocket/SockJS默认仅仅支持同源请求。不同策略下的行为如下：</p>
<ol>
<li>仅仅允许同源请求（默认）。在此模式下，如果启用SockJS，则IFrame的HTTP响应头X-Frame-Options被设置为SAMEORIGIN，JSONP被禁用</li>
<li>允许指定列表的源，每个源必须以http或者https开头。在此模式下，如果启用SocketJS，IFrame、JSONP两种传输都被禁用</li>
<li>设置为*。在此模式下，所有传输都可以使用</li>
</ol>
<p>修改配置的代码：</p>
<pre class="crayon-plain-tag">@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(helloHandler(), "/hello").setAllowedOrigins("*");
}</pre>
<div class="blog_h2"><span class="graybg">SockJS支持</span></div>
<p>WebSocket不受一些老旧的浏览器支持，并且某些网络代理阻止了WebSocket协议。因此Spring将SockJS作为备选实现，模拟WebSocket API。</p>
<p>要启用SockJS支持，调用：</p>
<pre class="crayon-plain-tag">@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(helloHandler(), "/hello").withSockJS();
}

// 等价XML配置 &lt;websocket:sockjs/&gt; </pre>
<div class="blog_h3"><span class="graybg">心跳消息</span></div>
<p>为了防止代理服务器认为连接已经挂起，SockJS Protocol需要发送心跳消息。Spring提供配置参数<pre class="crayon-plain-tag">.withSockJS().setHeartbeatTime( )</pre>来设置心跳频率，默认值25s。</p>
<div class="blog_h3"><span class="graybg">Servlet3异步请求</span></div>
<p>HTTP流/长轮询这两种传输，要求连接打开时间比使用它的时间更长。在Servlet容器中，这依赖于Servlet 3的异步支持实现  —— 允许请求处理线程退出，之后由其它线程继续向响应中写入数据。</p>
<p>异步请求的问题在于，服务器不知道客户端是否已经断开，只有在后续继续写入响应时，才会抛出异常。不管怎么样，心跳还是能够最终发现断开的。</p>
<div class="blog_h3"><span class="graybg">相关CORS头</span></div>
<p>如果允许跨源请求，SockJS协议依赖CORS来支持跨站HTTP流/长轮询，因此CORS头会被自动添加，除非检测到响应头中指定了对应的CORS头。</p>
<p>配置suppressCors可以禁止自动添加CORS头。</p>
<p>SockJS期望的头包括：</p>
<ol>
<li>Access-Control-Allow-Origin，基于Origin请求头初始化</li>
<li>Access-Control-Allow-Credentials总设置为true</li>
<li>Access-Control-Request-Headers从对应的请求头初始化</li>
<li>Access-Control-Allow-Methods传输机制所需要的HTTP方法</li>
<li>Access-Control-Max-Age设置为31536000（一年）</li>
</ol>
<div class="blog_h3"><span class="graybg"><a id="sockjs-java-client"></a>Java客户端</span></div>
<p>Spring实现了SockJS的Java客户端，允许你在服务器中使用SockJS，或者在压力测试中模拟大量客户端。</p>
<p>此客户端支持websocket/xhr-streaming/xhr-polling这三种传输。其中：</p>
<ol>
<li>WebSocketTransport可以连同下面的实现使用：
<ol>
<li>JSR-356的StandardWebSocketClient</li>
<li>Jetty 9的JettyWebSocketClient</li>
<li>Spring的任何WebSocketClient实现类</li>
</ol>
</li>
<li>XhrTransport有两种实现：
<ol>
<li>RestTemplateXhrTransport，基于Spring的RestTemplate</li>
<li>JettyXhrTransport，基于Jetty的HttpClient</li>
</ol>
</li>
</ol>
<p>客户端代码示例：</p>
<pre class="crayon-plain-tag">List&lt;Transport&gt; transports = new ArrayList&lt;&gt;(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new HelloHandler(), "ws://gmem.cc:8888/hello");</pre>
<p>当模拟大量并发客户端时，底层HTTP客户端实现应该配有足够的资源，例如：</p>
<pre class="crayon-plain-tag">HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));</pre>
<div class="blog_h2"><span class="graybg">STOMP支持</span></div>
<p>WebSocket协议定义了两种消息类型：文本和二进制数据，但是消息的内容没有定义。通常情况下，服务器和客户端能够协商使用一种子协议，来定义消息的结构，STOMP是一种常见的选择，其优势在于：</p>
<ol>
<li>浏览器中可以使用<a href="https://github.com/jmesnil/stomp-websocket">stomp.js</a></li>
<li>不需要引入新的消息格式</li>
<li>支持基于destination的消息路由</li>
<li>能够与支持STOMP的MOM集成</li>
</ol>
<div class="blog_h3"><span class="graybg">STOMP简介</span></div>
<p>STOMP是一种文本协议，最初设计供Ruby/Python/Perl之类的脚本语言使用，以连接到企业的消息代理。STOMP被设计用来处理常见的消息模式，可以基于任何双向可靠信道 —— 例如TCP、WebSocket ——传输。</p>
<p>尽管STOMP是基于文本的协议，但是它的载荷部分可以是二进制的。</p>
<p>STOMP是一种基于Frame的协议，其Frame设计理念源于HTTP。一个Frame的结构如下：</p>
<pre class="crayon-plain-tag">COMMAND
header1:value1
header2:value2

Body^@</pre>
<p>客户端可以使用SEND或者SUBSCRIBE命令，可以发送、订阅消息。此时需要指定一个destination头。下面是两个示例：</p>
<pre class="crayon-plain-tag">SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@ </pre><br />
<pre class="crayon-plain-tag">SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@</pre>
<p>STOMP服务器可以使用MESSAGE来广播消息到所有订阅者：</p>
<pre class="crayon-plain-tag">MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@ </pre>
<p>当使用Spring的STOMP支持时，Spring的WebSocket应用相对客户端而言是STOMP代理。消息会被路由给@Controller下的消息处理方法或者一个简单内存消息代理处理。</p>
<p>你也可以配置Spring，让其与支持STOMP的消息中间件（例如RabbitMQ、ActiveMQ）一起工作，这样客户端就可以把消息发送消息中间件网络中。Spring负责维护到MOM的TCP连接、把消息中继到MOM、并且把监听到的消息下发给连接到Spring的那些客户端。</p>
<div class="blog_h3"><span class="graybg">启用STOMP</span></div>
<p>利用spring-messaging和spring-websocket模块，Spring能够支持STOMP over WS。</p>
<p>配置示例：</p>
<pre class="crayon-plain-tag">@Configuration
// 启用基于WebSocket的消息代理（使用某种子协议）
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 在/stomp暴露一个基于WebSocket/SockJS的STOMP端点
        registry.addEndpoint("/stomp").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {

        // 如果destination以/app开头，则消息路由给@Controller下的消息处理方法
        config.setApplicationDestinationPrefixes("/app");

        // 所有destination均由@Controller下的消息@MessageMapping方法处理
        config.setApplicationDestinationPrefixes("/");

        // 下面的两种开头的destination广播给所有其它客户端
        config.enableSimpleBroker("/topic", "/queue");
    }

}

@Controller
@MessageMapping("greeting")
public class GreetingController {
    @Inject 
    private SimpMessagingTemplate template; // 用于发送消息
    // 此消息处理方法处理/app/greeting/hello这一目标
    @MessageMapping("hello") {
    public String hello(String greeting) {
        String msg =  "[" + getTimestamp() + ": " + greeting;
        // 可以向任何地方发送消息
        this.template.convertAndSend("/topic/greetings", msg);
    }
}</pre>
<p>等价XML配置：</p>
<pre class="crayon-plain-tag">&lt;websocket:message-broker application-destination-prefix="/app"&gt;
    &lt;websocket:stomp-endpoint path="/stomp"&gt;
        &lt;websocket:sockjs/&gt;
    &lt;/websocket:stomp-endpoint&gt;
    &lt;websocket:simple-broker prefix="/topic, /queue"/&gt;
&lt;/websocket:message-broker&gt;</pre>
<p>消息目的地，默认的路径分隔好符是  / ，客户端<span style="background-color: #c0c0c0;">发送时，目的地必须以 / 为第一个字符</span>。除非包含多个路径分段，@MessageMapping的路径不需要包含 / 。</p>
<p>如果要使用MOM领域更加通用的点号分隔符，调用：</p>
<pre class="crayon-plain-tag">registry.setPathMatcher(new AntPathMatcher("."));</pre>
<p>等价的XML配置为：</p>
<pre class="crayon-plain-tag">&lt;websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher"&gt;
&lt;/websocket:message-broker&gt;
&lt;bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"&gt;
    &lt;constructor-arg index="0" value="." /&gt;
&lt;/bean&gt;</pre>
<p>即使使用点号分隔符，客户端发送的目的地，也要以 / 开头。 </p>
<div class="blog_h3"><span class="graybg">JS客户端</span></div>
<pre class="crayon-plain-tag">// 可以使用SockJS：
var socket = new SockJS("/stomp");
var client = Stomp.over(socket); 
// 或者直接使用WebSocket
var client = Stomp.over( new WebSocket( 'ws://172.21.0.1:9090/signal' ) );

// 心跳设置
client.heartbeat.outgoing = 20000;   // 每20秒发送一次心跳给服务器
client.heartbeat.incoming = 0;       // 不接受服务器发送来的心跳

// 调试设置
client.debug = function(str) {
    console.log(str);
};

// 连接
client.connect(login, passcode, connectCallback, errorCallback);
client.connect(headers, connectCallback, errorCallback);
function connectCallback( frame ){
}

// 发送消息，目的地、头、体
client.send("/queue/hello", {priority: 9}, "Hello, STOMP");

// 订阅消息
var subscription = client.subscribe("/topic/hello", callback);
function callback( message ){
    console.log( message.body );
}

// 带消息确认设置的订阅：客户端确认
var subscription = client.subscribe("/topic/hello", callback, {ack: 'client'});
function callback( message ){
    // 确认
    message.ack();
}

// 事务支持
var tx = client.begin();
// transaction头必须
client.send("/queue/hello", {transaction: tx.id}, "message in a transaction");
tx.commit();  // 提交事务
tx.abort();   // 撤销事务</pre>
<p>或者直接使用WebSocket：</p>
<pre class="crayon-plain-tag">var socket = new WebSocket("/stomp");
var client = Stomp.over(socket);</pre>
<div class="blog_h3"><span class="graybg">Java客户端</span></div>
<pre class="crayon-plain-tag">WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // 用于发送心跳

// 创建连接
String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new StompSessionHandlerImpl();
class StompSessionHandlerImpl extends StompSessionHandlerAdapter {
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        // 连接成功后，此回调被调用
    }
}
stompClient.connect(url, sessionHandler);

// 发送消息
session.send("/topic/foo", "payload");
// 订阅消息
session.subscribe("/topic/foo", new StompFrameHandler() {
    public Type getPayloadType(StompHeaders headers) {
        return String.class;
    }
    public void handleFrame(StompHeaders headers, Object payload) {
        // 处理消息
    }
}); </pre>
<div class="blog_h3"><span class="graybg">消息流</span></div>
<p>spring-messaging提供了以下抽象：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">对象</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Message</td>
<td>一个带有头、载荷的消息</td>
</tr>
<tr>
<td>MessageHandler </td>
<td>处理消息的逻辑单元</td>
</tr>
<tr>
<td>MessageChannel </td>
<td>在发送者/接收者之间传输消息的信道的抽象，通道总是单向的</td>
</tr>
<tr>
<td>SubscribableChannel </td>
<td>继承MessageChannel，用于传输消息到所有订阅者</td>
</tr>
<tr>
<td>ExecutorSubscribableChannel</td>
<td>继承SubscribableChannel，使用异步线程池传输消息</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">基于注解的消息处理 </span></div>
<p>你可以在@Controller类的方法上添加@MessageMapping注解，这类方法可以映射某个/某些消息destination。</p>
<p>@MessageMapping对应的URL支持Ant风格的通配符，例如/foo*、/foo/**。路径变量也是支持的，例如/foo/{id}中的id可以通过注解了@DestinationVariable的方法参数访问到。</p>
<p>你可以为@MessageMapping方法注入很多种参数：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Message</td>
<td>访问完整的消息</td>
</tr>
<tr>
<td>@Payload</td>
<td>访问消息的载荷，消息被基于org.springframework.messaging.converter.MessageConverter转换</td>
</tr>
<tr>
<td>@Header</td>
<td>访问消息头</td>
</tr>
<tr>
<td>@Headers</td>
<td>访问所有消息头的Map</td>
</tr>
<tr>
<td>@DestinationVariable</td>
<td>访问路径变量</td>
</tr>
<tr>
<td>Principal</td>
<td>在WS握手阶段登陆的用户</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">身份验证</span></div>
<p>使用STOMP时，身份验证基于HTTP协议的机制进行。</p>
<p>尽管STOMP协议包含login、passcode头，但是它们通常在STOMP over TCP的情况下使用。Spring默认会忽略这些头，并且假设在HTTP升级到WebSocket之前已经完成身份验证。</p>
<p>如果需要基于STOMP头进行身份验证，可以进行如下配置：</p>
<pre class="crayon-plain-tag">@Configuration
@EnableWebSocketMessageBroker
public class AppConfig extends AbstractWebSocketMessageBrokerConfigurer {
  @Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.setInterceptors(new ChannelInterceptorAdapter() {
        @Override
        public Message&lt;?&gt; preSend(Message&lt;?&gt; message, MessageChannel channel) {
            StompHeaderAccessor accessor =  MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
            if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                String login = accessor.getNativeHeader( "login" ).get( 0 );
                Principal user = new PrincipalImpl( login );
                accessor.setUser(user);
            }
            return message;
        }
    });
  }
}</pre>
<p>注意，不进行任何配置的情况下，你不能为@MessageMapping方法注入Principal对象，执行了上述配置则可以注入。其它备选的身份验证方式包括：</p>
<ol>
<li>子类化DefaultHandshakeHandler，覆盖determineUser方法，这样可以在WebSocket握手阶段确定用户。示例：<br />
<pre class="crayon-plain-tag">registry.addEndpoint( "/signal" ).setHandshakeHandler( new DefaultHandshakeHandler() {
    @Override
    protected Principal determineUser( ServerHttpRequest request, WebSocketHandler wsHandler, Map&lt;String, Object&gt; attributes ) {
        Principal principal = request.getPrincipal();
        if ( principal == null ) {
            Collection&lt;SimpleGrantedAuthority&gt; authorities = new ArrayList&lt;&gt;();
            authorities.add( new SimpleGrantedAuthority( AuthoritiesConstants.ANONYMOUS ) );
            principal = new AnonymousAuthenticationToken( "WebsocketConfiguration", "anonymous", authorities );
        }
        return principal;
    }
} );</pre>
</li>
<li>使用基于HTTP的身份验证，Spring会尝试从HttpServletRequest.getUserPrincipal中获得当前用户</li>
</ol>
<div class="blog_h3"><span class="graybg">用户目的地</span></div>
<p>默认情况下，Spring认为<pre class="crayon-plain-tag">/user/</pre>开头的目的地属于用户目的地，每个WebSocket会话都有这种目的地的同名副本。</p>
<p>客户端代码示例：</p>
<pre class="crayon-plain-tag">let client = Stomp.over( new WebSocket( 'ws://172.21.0.1:9090/signal' ) );
client.connect( {}, ( frame ) =&gt; {
    start();
} );
function start() {
    // 客户端订阅用户目的地，需要/user前缀
    client.subscribe( '/user/rtsp/preview/sdpanswer', function ( frame ) {
        console.log( frame.body );
    } );
    client.send( '/app/rtsp/preview/sdpoffer', {}, '1' );
}</pre>
<p>服务器代码示例：</p>
<pre class="crayon-plain-tag">@Controller
@MessageMapping( "/rtsp/preview" )
public class RtspPreviewController {
    @MessageMapping( "/sdpoffer" )
    // 发送到用户目的地（仅仅发送给当前WebSocket会话对应的客户端），需要指定完整路径，/user前缀不需要
    @SendToUser( "/rtsp/preview/sdpanswer" )
    public String connect( String payload ) {
        return payload;
    }
}</pre>
<p>关于@SendToUser需要注意，实际发送到的目的地是/user/{username}/rtsp/preview，Spring按照以下规则确定username：</p>
<ol>
<li>如果当前会话的Principal存在，则取Principal.getName()作为用户名</li>
<li>否则，取会话标识符，会话标识符来自消息头中的simpSessionId字段</li>
</ol>
<p>当允许同一个用户在多个浏览器中登陆时，要注意这个情况，如果Principal存放登陆名，客户端可能接收到不期望的消息。</p>
<div class="blog_h3"><span class="graybg">混合使用STOMP和原始WebSocket</span></div>
<p>配置示例：</p>
<pre class="crayon-plain-tag">@SpringBootApplication
// 两个注解都需要：
@EnableWebSocket
@EnableWebSocketMessageBroker
public class VideoSurveillanceApp extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer {
    public void registerWebSocketHandlers( WebSocketHandlerRegistry registry ) {
        // 下面的端点使用原始WebSocket
        registry.addHandler( helloHandler(), "/hello" );
    }
    public void registerStompEndpoints( StompEndpointRegistry registry ) {
        // 下面的端点使用STOMP
        registry.addEndpoint( "/signal" );
    }
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ws-support-of-spring">Spring对WebSocket的支持</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ws-support-of-spring/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Socket.io学习笔记</title>
		<link>https://blog.gmem.cc/socket-io-study-note</link>
		<comments>https://blog.gmem.cc/socket-io-study-note#comments</comments>
		<pubDate>Tue, 29 Mar 2016 09:09:16 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Node.js]]></category>
		<category><![CDATA[WebSocket]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14330</guid>
		<description><![CDATA[<p>简介 Socket.io是一个Web通信框架框架，同时支持基于浏览器环境的客户端、基于Node.js的服务器端。它实现了实时的、事件驱动的双向通信。使用Socket.io，你可以： 推送数据，让客户端展示实时更新的仪表/图表、文本信息 推送二进制流，从1.0版本开始，Socket.io支持推送图片、音频、视频 实现多用户协作，例如联网游戏、共同编辑文档 Socket.io主要由两个部分组成： socket.io模块，集成到Node.js的http模块的服务器 socket.io-client，在浏览器中运行的客户端 Socket.io支持多种传输机制，例如WebSocket、Adobe Flash Sockets、XHR轮询、JsonP轮询，它们被隔离在统一的接口之下，这意味着任何浏览器都可以作为客户端。 标准的WebSocket服务器并不能和Socket.io客户端进行直接通信，需要注意这一点。  入门 聊天室应用 我们以一个简单的聊天室应用作为入门教程，你会发现利用Socket.io编写这类应用有多简单。特别是前后端都是基于Socket.io实现的时候，连API接口都一样，太方便了。 首先，为工程添加依赖： [crayon-69e20c9f175dc529797840/] 服务器代码 [crayon-69e20c9f175e0507861017/] 客户端代码 [crayon-69e20c9f175e3413854320/] 运行代码 <a class="read-more" href="https://blog.gmem.cc/socket-io-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/socket-io-study-note">Socket.io学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Socket.io是一个Web通信框架框架，同时支持基于浏览器环境的客户端、基于Node.js的服务器端。它实现了实时的、事件驱动的双向通信。使用Socket.io，你可以：</p>
<ol>
<li>推送数据，让客户端展示实时更新的仪表/图表、文本信息</li>
<li>推送二进制流，从1.0版本开始，Socket.io支持推送图片、音频、视频</li>
<li>实现多用户协作，例如联网游戏、共同编辑文档</li>
</ol>
<p>Socket.io主要由两个部分组成：</p>
<ol>
<li>socket.io模块，集成到Node.js的http模块的服务器</li>
<li>socket.io-client，在浏览器中运行的客户端</li>
</ol>
<p>Socket.io支持多种传输机制，例如WebSocket、Adobe Flash Sockets、XHR轮询、JsonP轮询，它们被隔离在统一的接口之下，这意味着任何浏览器都可以作为客户端。</p>
<p>标准的WebSocket服务器并不能和Socket.io客户端进行直接通信，需要注意这一点。 </p>
<div class="blog_h1"><span class="graybg">入门</span></div>
<div class="blog_h2"><span class="graybg">聊天室应用</span></div>
<p>我们以一个简单的聊天室应用作为入门教程，你会发现利用Socket.io编写这类应用有多简单。特别是前后端都是基于Socket.io实现的时候，连API接口都一样，太方便了。</p>
<p>首先，为工程添加依赖：</p>
<pre class="crayon-plain-tag">npm install --save express
npm install --save socket.io</pre>
<div class="blog_h3"><span class="graybg">服务器代码</span></div>
<pre class="crayon-plain-tag">// 创建一个Express应用
var app = require('express')();
app.use(function (req, res, next) {
    next();
})
// 创建一个HTTP服务器
var http = require('http').Server(app);
// 创建一个IO实例，可以看到socket.io和Express可以共享一个HTTP服务器套接字
var io = require('socket.io')(http);
// Express代码，首页
app.get('/', function (req, res) {
    res.sendfile('index.html');
});
// 侦听客户端连接事件
io.on('connection', function (socket) {
    console.log('a user connected');
    // socket变量为代表一个客户端连接的套接字
    socket.on('chat message', function (msg) {
        // 用户输入文本，并点击发送后，该事件由客户端发送
        console.log('message: ' + msg);
        // 服务器把可以把消息发送给所有人：
        io.emit('chat message', msg);
    });
    socket.on('disconnect', function () {
        // 用户刷新浏览器时，会触发该事件，因为socket.io-client做了清理工作
        console.log('user disconnected');
    });
});
// 启动HTTP服务器
http.listen(3000, function () {
    console.log('listening on *:3000');
});</pre>
<div class="blog_h3"><span class="graybg">客户端代码</span></div>
<pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;title&gt;Socket.IO chat&lt;/title&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;style&gt;
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font: 13px Helvetica, Arial; }
        form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
        form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
        form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
        #messages { list-style-type: none; margin: 0; padding: 0; }
        #messages li { padding: 5px 10px; }
        #messages li:nth-child(odd) { background: #eee; }
    &lt;/style&gt;
    &lt;!-- 这个URL由Socket.io服务器负责生成 --&gt;
    &lt;script src="/socket.io/socket.io.js"&gt;&lt;/script&gt;
    &lt;script src="http://code.jquery.com/jquery-1.11.1.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;ul id="messages"&gt;&lt;/ul&gt;
&lt;form action=""&gt;
    &lt;input id="m" autocomplete="off"/&gt;
    &lt;button&gt;Send&lt;/button&gt;
&lt;/form&gt;
&lt;script&gt;
    // 暴露了全局函数io，调用它后会尝试基于WebSocket协议连接到服务器，并得到一个客户端套接字对象
    // 此对象就像Node.js中的套接字一样，是一个EventEmitter
    var socket = io();
    $('form').submit(function(){
        // Socket.io的主要思想就是，你可以接收、发送任何事件，事件的载荷可以是任何数据，包括编码为JSON的对象或者二进制数据
        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
    });
    socket.on('chat message', function(msg){
        // 该事件由服务器发送，它把消息广播到所有客户端
        $('#messages').append($('&lt;li&gt;').text(msg));
    });
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<div class="blog_h3"><span class="graybg">运行代码</span></div>
<p>执行命令<pre class="crayon-plain-tag">node app.js</pre> 启动服务器，然后浏览器打开http://127.0.0.1:3000，多打开几个这样的窗口，就可以聊天了。</p>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">创建IO实例</span></div>
<p>有几种不同的方法：</p>
<pre class="crayon-plain-tag">// 方法一：配合Node.js的HTTP服务
var app = require( 'http' ).createServer( function ( req, res ) {
    /* */
} );
var io = require('socket.io')(app);
app.listen(80);
// 方法二：配合Express框架
var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
server.listen(80);
// 方法三：隐式HTTP服务创建
var io = require('socket.io')(80); </pre>
<div class="blog_h2"><span class="graybg">名字空间</span></div>
<p>你可以使用名字空间（即端点，对应URL路径）实现单个WebSocket的多路复用（multiplexing），这样多个应用模块可以共享单个TCP连接：</p>
<pre class="crayon-plain-tag">var io = require( 'socket.io' )( 80 );
var chat = io
    // 名字空间一
    .of( '/chat' )
    .on( 'connection', function ( socket ) {
        // 发送消息的两种方式：
        socket.emit( 'a message', {
            that: 'only' , '/chat': 'will get'
        } );
        chat.emit( 'a message', {
            everyone: 'in' , '/chat': 'will get'
        } );
    } );

var news = io
    // 名字空间二
    .of( '/news' )
    .on( 'connection', function ( socket ) {
        socket.emit( 'item', { news: 'item' } );
    } );</pre><br />
<pre class="crayon-plain-tag">&lt;script&gt;
    // 名字空间一
    var chat = io.connect( 'http://localhost/chat' );
    // 名字空间二，如果 协议://主机名:端口 与当前网页的相同，可以省略
    var news = io.connect( '/news' );

    chat.on( 'connect', function () {
        chat.emit( 'hi!' );
    } );

    news.on( 'news', function () {
        news.emit( 'woot' );
    } );
&lt;/script&gt;</pre>
<div class="blog_h3"><span class="graybg">默认名字空间</span></div>
<p>Socket.io客户端默认连接到此名字空间，对应URL路径 <pre class="crayon-plain-tag">/</pre> ，Socket.io服务器默认也在此名字空间上监听。</p>
<p>下面的代码都是在默认名字空间上发送消息：</p>
<pre class="crayon-plain-tag">io.sockets.emit('hi', 'everyone');    // 方式一
io.emit('hi', 'everyone');            // 方式二 </pre>
<p>在默认名字空间上，也可以监听connection事件，来监听新的客户端连接：</p>
<pre class="crayon-plain-tag">io.on( 'connection', function ( socket ) {
    socket.on( 'disconnect', function () { } );
} );</pre>
<div class="blog_h3"><span class="graybg">房间</span></div>
<p>在每个名字空间内部，你还可以定义任意数量的房间（频道，channel）。每个Socket都可以自由的<pre class="crayon-plain-tag">join()</pre> 或者<pre class="crayon-plain-tag">leave()</pre> 房间：</p>
<pre class="crayon-plain-tag">// 在连接后立即加入房间
io.on( 'connection', function ( socket ) {
    socket.join( 'some room' );
} );
// 调用to可以随时加入房间
io.to( 'some room' ).emit( 'some event' );
// 离开房间
io.leave( 'some room' );</pre>
<p> Socket.io中的每一个Socket由一个随机的、唯一的、不可猜测的ID来标识。Socket会自动加入到以此ID来标识的房间：</p>
<pre class="crayon-plain-tag">io.on( 'connection', function ( socket ) {
    socket.on( 'say to someone', function ( id, msg ) {
        // 广播到房间
        socket.broadcast.to( id ).emit( 'my message', msg );
    } );
} );</pre>
<p>在断开连接时，Socket会自动离开所有加入过的房间。</p>
<div class="blog_h2"><span class="graybg">易失消息</span></div>
<p>如果某些客户端没有准备好接收消息，原因可能包括：网络缓慢、客户端基于长轮询协议连接且正处于请求-响应周期中间，则允许消息丢失，可以提高性能。</p>
<p>某些消息即使丢失，也不会对应用造成太大影响。例如频繁更新的温湿度监测值，丢失一两个数值，可能不会影响客户端的曲线图渲染。这种情况下，我们可以声明消息是易失的（volatile）：</p>
<pre class="crayon-plain-tag">io.on('connection', function (socket) {
    // 发送一个易失的消息
    socket.volatile.emit('bieber tweet', tweet);
});</pre>
<div class="blog_h2"><span class="graybg">消息确认</span></div>
<p>某些情况下，你可能需要在客户端确认（acknowledgement）接收到消息之后，执行一个回调。这种情况下，你可以为send/emit方法提供函数作为最后一个参数。<span style="background-color: #c0c0c0;">使用emit时，确认操作由消息接收者手工触发并且可以附带数据</span>：</p>
<pre class="crayon-plain-tag">&lt;script&gt;
  var socket = io(); // 不带参数的调用io()可以进行自动服务发现
  socket.on('connect', function () { // 你可以监听具体事件，也可以监听connect
      // 像服务器发送一个消息，消息确认后，执行回调
      socket.emit('ferret', 'tobi', function (data) {
          console.log(data); // 打印服务器返回的确认消息
      });
  });
&lt;/script&gt;</pre><br />
<pre class="crayon-plain-tag">io.on( 'connection', function ( socket ) {
    // 服务器，接收客户端emit来的消息后，可以手工的进行确认
    socket.on( 'ferret', function ( msg, fn ) {
        // 调用第二个参数即可确认，其入参是返回给客户端的确认消息
        fn( 'woot' );
    } );
} );</pre>
<div class="blog_h2"><span class="graybg">广播消息</span></div>
<p>所谓广播，是指像所有Socket发送消息，除了广播消息的这个Socket：</p>
<pre class="crayon-plain-tag">io.on( 'connection', function ( socket ) {
    // 广播一个消息
    socket.broadcast.emit( 'user connected' );
} );</pre>
<div class="blog_h2"><span class="graybg">作为WebSocket客户端</span></div>
<p>如果你仅仅想使用WebSocket语义，只需要调用send方法，然后监听message事件：</p>
<pre class="crayon-plain-tag">io.on( 'connection', function ( socket ) {
    // 接收到客户端消息时：
    socket.on( 'message', function ( msg ) {
    } );
    socket.on( 'disconnect', function () {
    } );
} );</pre><br />
<pre class="crayon-plain-tag">&lt;script&gt;
var socket = io( 'http://localhost/' );
socket.on( 'connect', function () {
    // 发送一个消息
    socket.send( 'hi' );
    // 接收到服务器消息时：
    socket.on( 'message', function ( msg ) {
    } );
} );
&lt;/script&gt;</pre>
<div class="blog_h2"><span class="graybg">多Node.js实例</span></div>
<p>当利用多个Node.js进程或机器来进行负载均衡时，你需要会话关联性（session affinity）。即，关联到特定Session ID的请求需要发送给最初产生会话的那个Node.js进程。</p>
<p>上述要求的原因是，某些Socket.io传输机制——XHR轮询、JSONP轮询——依赖于在Socket的生命周期内发起的多个HTTP请求。WebSocket传输则不存在这个问题，因为整个会话中它只会使用一个TCP长连接。</p>
<p>使用NginX可以很容易的实现会话关联性。如果使用Node.js的Cluster模块，可以结合<a href="https://github.com/indutny/sticky-session">sticky session</a>。</p>
<div class="blog_h3"><span class="graybg">跨进程事件</span></div>
<p>某些情况下，你可能需要在跨越多个Node.js进程向特定名字空间、房间发送消息，或者广播消息。你可以使用<a href="https://github.com/socketio/socket.io-redis">socket.io-redis</a>、<a href="https://github.com/socketio/socket.io-emitter">socket.io-emitter</a>之类的模块。</p>
<p>socket.io-redis是一个适配器，可以实现跨越多个进程来路由消息。使用它来结合Redis，你可以在多个进程中运行多个Socket.io实例，这些实例之间可以正常收发消息：</p>
<pre class="crayon-plain-tag">var io = require( 'socket.io' )( 3000 );
var redis = require( 'socket.io-redis' );
/**
 * 选项：
 * key 发布/订阅事件的前缀，默认socket.io
 * host Redis所在主机
 * port Redis监听端口
 * subEvent 可选，需要订阅的Redis客户端事件名，默认messageBuffer
 *
 * pubClient 可选，用于发布事件的Redis客户端
 * subClient 可选，用于订阅事件的Redis客户端
 * 如果提供上面两者之一，必须使用node_redis或者相同API的客户端
 *
 * requestsTimeout 可选，适配器等待响应的超时时间，默认1000ms
 * withChannelMultiplexing 可选，是否启用频道复用，默认true
 *
 */
io.adapter( redis( { host: 'localhost', port: 6379 } ) );</pre>
<div class="blog_h2"><span class="graybg">调试</span></div>
<p>在v1.0之前，Socket.io直接把调试信息打印在console，当前版本则基于<a href="https://github.com/visionmedia/debug">debug</a>模块打印调试日志。</p>
<p>要查看服务器端日志，以<pre class="crayon-plain-tag">DEBUG=* node app.js</pre> 启动应用。要查看浏览器日志，使用：<pre class="crayon-plain-tag">localStorage.debug = '*';</pre> </p>
<div class="blog_h2"><span class="graybg">通配符事件</span></div>
<p>Socket.io本身不支持对所有事件进行统一的处理，但是你可以通过中间件<a href="https://github.com/hden/socketio-wildcard">socketio-wildcard</a>满足这一需求：</p>
<pre class="crayon-plain-tag">npm install --save socketio-wildcard</pre>
<p>服务器代码示例：</p>
<pre class="crayon-plain-tag">var io = require( 'socket.io' )();
var middleware = require( 'socketio-wildcard' )();
// 启用中间件
io.use( middleware );

io.on( 'connection', function ( socket ) {
    // 监听通配符事件
    socket.on( '*', function ( packet ) {
        packet.data === [ 'foo', 'bar', 'baz' ]
    } );
} );

io.listen( 3000 );</pre>
<p>客户端代码示例：</p>
<pre class="crayon-plain-tag">var io = require( 'socket.io-client' );
var socket = io( 'http://localhost' );
var patch = require( 'socketio-wildcard' )( io.Manager );
patch( socket );
// 监听通配符事件
socket.on( '*', function () {
} ); </pre>
<div class="blog_h1"><span class="graybg">API</span></div>
<div class="blog_h2"><span class="graybg">服务器</span></div>
<div class="blog_h3"><span class="graybg">Server</span></div>
<p>该函数由<pre class="crayon-plain-tag">require('socket.io')</pre> 暴露：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">调用/属性/方法/事件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>( )</td>
<td>创建一个服务器，可以使用new：<br />
<pre class="crayon-plain-tag">var io = require('socket.io')();
// 或者
var Server = require('socket.io');
var io = new Server();</pre>
</td>
</tr>
<tr>
<td>( opts )</td>
<td>创建一个服务器，opts为选项：<br />serveClient  调用serveClient()方法<br />path  调用path()方法</td>
</tr>
<tr>
<td>( http, opts )</td>
<td>创建一个服务器，http为Node.js的HTTP服务器</td>
</tr>
<tr>
<td>( port, opts )</td>
<td>创建一个服务器，在指定端口自动创建Node.js的HTTP服务器</td>
</tr>
<tr>
<td>serveClient( Bool ):Server</td>
<td>如果为true，则关联的HTTP服务器负责给客户端服务静态文件，默认true</td>
</tr>
<tr>
<td>path( String ):Server</td>
<td>在什么路径下为客户端服务静态文件，默认 /socket.io</td>
</tr>
<tr>
<td>adapter( Adapter ):Server</td>
<td>
<p>设置适配器，适配器用于<span style="background-color: #c0c0c0;">实现消息存储</span>，参数为Adapter实例。默认使用的适配器是基于内存的 socket.io-adapter</p>
<p>如果不指定入参，则返回当前使用的适配器</p>
</td>
</tr>
<tr>
<td>origins( String ):Server</td>
<td>设置允许的客户端 Orgin头，如果不指定入参，则返回当前值</td>
</tr>
<tr>
<td>sockets:Namespace</td>
<td>返回默认 / 名字空间对象</td>
</tr>
<tr>
<td>attach( http, opts ):Server<br />attach( port, opts ):Server</td>
<td>
<p>附着到一个engine.io实例，返回得到的Server对象</p>
<p>该函数的别名是listen</p>
</td>
</tr>
<tr>
<td>of( String ):Namespace</td>
<td>根据路径，初始化或者取得一个名字空间对象</td>
</tr>
<tr>
<td>emit()</td>
<td>发送一个消息到所有客户端</td>
</tr>
<tr>
<td>use()</td>
<td>注册中间件</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Namespace</span></div>
<p>该类型代表连接到特定Scope的客户端Socket的池，以路径标识。</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">属性/方法/事件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>⚡ connection   <br />⚡ connect</td>
<td>当一个客户端连接到此名字空间时触发，回调函数的入参时客户端Socket对象</td>
</tr>
<tr>
<td>name:String</td>
<td>返回标识符</td>
</tr>
<tr>
<td>connected:Object</td>
<td>连接到此名字空间的Sockets对象的Hash，键为Socket的ID</td>
</tr>
<tr>
<td>use( Function ):Namespace</td>
<td>注册一个中间件，所谓中间件就是一个函数，它会针对每一个接入的Socket调用：<br />
<pre class="crayon-plain-tag">var io = require( 'socket.io' )();
// next代表下一个中间件，用法与Express中间件类似
io.use( function ( socket, next ) {
    // 调用下一个中间件
    if ( socket.request.headers.cookie ) return next();
    // 向客户端发送error包
    next( new Error( 'Authentication error' ) );
} );</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Socket</span></div>
<p>与客户端通信的基础对象。<span style="background-color: #c0c0c0;">Socket属于特定的名字空间</span>，在底层使用Client对象进行通信。</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">属性/方法/事件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>rooms:Array</td>
<td>套接字加入的房间列表</td>
</tr>
<tr>
<td>client:Client</td>
<td>底层的Client对象</td>
</tr>
<tr>
<td>conn:Socket</td>
<td>底层Client对象使用的连接对象</td>
</tr>
<tr>
<td>request:Request</td>
<td>产生此Socket的HTTP请求对象，用于访问Cookie、User-Agent等</td>
</tr>
<tr>
<td>id:String</td>
<td>Socket会话的ID，来自底层Client对象</td>
</tr>
<tr>
<td>emit(name[, …]):Socket</td>
<td>发布一个名为name的事件，后么可以跟着事件的内容。任何数据结构都被支持，包括Buffer。注意不能发布函数，因为其不可串行化</td>
</tr>
<tr>
<td>join(name[, fn]):Socket</td>
<td>
<p>加入到一个房间中，fn(err)为可选的回调。Socket会自动加入到以其ID命名的房间</p>
<p>加入房间的机制由Adapter实现</p>
</td>
</tr>
<tr>
<td>leave(name[, fn]):Socket</td>
<td>
<p>从房间中移除一个Socket，fn(err)为可选的回调</p>
<p>当断开连接时，Socket自动离开所有房间</p>
</td>
</tr>
<tr>
<td>to(room):Socket</td>
<td rowspan="2">让后续的事件发布仅仅在room范围内广播</td>
</tr>
<tr>
<td>in(room):Socket</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">其它客户端</span></div>
<div class="blog_h2"><span class="graybg">Java</span></div>
<p><a href="https://github.com/socketio/socket.io-client-java">socket.io-client-java</a>提供了全特性支持的Java的Socket.io客户端。</p>
<div class="blog_h3"><span class="graybg">Maven依赖</span></div>
<pre class="crayon-plain-tag">&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.socket&lt;/groupId&gt;
        &lt;artifactId&gt;socket.io-client&lt;/artifactId&gt;
        &lt;version&gt;0.8.3&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;</pre>
<div class="blog_h3"><span class="graybg">用法示例</span></div>
<pre class="crayon-plain-tag">// 建立一个客户端套接字对象
IO.Options opts = new IO.Options();
opts.forceNew = true;   // 当查询参数改变后，是否丢弃既有套接字
opts.reconnection = false;  // 
opts.query = "auth_token=" + authToken;  // 指定查询参数
socket = IO.socket( "http://localhost", opts );
// 监听连接事件
socket.on( Socket.EVENT_CONNECT, new Emitter.Listener() {

    public void call( Object... args ) {
        socket.emit( "foo", "hi" ); // 发送消息
        socket.disconnect();
    }

} ).on( "event", new Emitter.Listener() { // 监听一般性事件

    public void call( Object... args ) {
        // Java客户端使用org.json来进行JSON对象的编码、解码
        JSONObject obj = (JSONObject)args[0];

        obj = new JSONObject();
        obj.put("hello", "server");
        obj.put("binary", new byte[42]);
        socket.emit("foo", obj);
    }

} ).on( Socket.EVENT_DISCONNECT, new Emitter.Listener() { // 监听断开事件

    public void call( Object... args ) {
    }

} );
// 发起连接
socket.connect();

// 在服务器确认收到消息后，执行回调
socket.emit("foo", "woot", new Ack() {
   @Override
   public void call(Object... args) {}
});

// 在收到服务器消息后，予以确认
socket.on("foo", new Emitter.Listener() {
   @Override
   public void call(Object... args) {
   Ack ack = (Ack) args[args.length - 1];
       ack.call();
   }
});</pre>
<div class="blog_h2"><span class="graybg">iOS</span></div>
<p>可以使用<a href="https://github.com/MegaBits/SIOSocket">SIOSocket</a></p>
<div class="blog_h2"><span class="graybg">Cordova应用</span></div>
<p>参考：<a href="http://socket.io/socket-io-with-apache-cordova/">Socket.IO with Apache Cordova</a></p>
<div class="blog_h1"><span class="graybg">其它服务器</span></div>
<p>如果服务器的开发平台不是Node.js，你就需要第三方的Socket.io实现了。注意：单纯的WebSocket服务器不能和Socket.io客户端正常通信。</p>
<div class="blog_h2"><span class="graybg">netty-socketio</span></div>
<p><a href="https://github.com/mrniko/netty-socketio">这是</a>基于Netty网络框架，在Java开发环境下实现Socket.io服务器的开源项目。</p>
<div class="blog_h3"><span class="graybg">Maven依赖</span></div>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.corundumstudio.socketio&lt;/groupId&gt;
    &lt;artifactId&gt;netty-socketio&lt;/artifactId&gt;
    &lt;version&gt;1.7.7&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">服务器示例</span></div>
<pre class="crayon-plain-tag">// 服务器配置，主机名和端口
Configuration config = new Configuration();
config.setHostname( "localhost" );
config.setPort( 9092 );
// SocketIO服务器对象
final SocketIOServer server = new SocketIOServer( config );

// 监听新的客户端连接
server.addConnectListener(new ConnectListener() {
    @Override
    public void onConnect(SocketIOClient client) {
       // 参数为新的客户端
    }
});

// 监听客户端断开
server.addDisconnectListener( new DisconnectListener() {
    @Override
    public void onDisconnect( SocketIOClient socketIOClient ) {
        // 参数为断开的客户端
    }
} );

// 监听一个SocketIO消息事件，把JSON反串行化为ChatObject这个Java类型
server.addEventListener( "chatevent", ChatObject.class, new DataListener&lt;ChatObject&gt;() {
    @Override
    public void onData( SocketIOClient client, ChatObject data, AckRequest ackRequest ) {
        // 广播一个事件到所有客户端
        server.getBroadcastOperations().sendEvent( "chatevent", data );
    }
} );
// 启动服务器
server.start();

Thread.sleep( Integer.MAX_VALUE );

// 停止服务器
server.stop();</pre>
<div class="blog_h3"><span class="graybg">消息确认示例</span></div>
<pre class="crayon-plain-tag">server.addEventListener( "ackevent1", ChatObject.class, new DataListener&lt;ChatObject&gt;() {
    @Override
    public void onData( final SocketIOClient client, ChatObject data, final AckRequest ackRequest ) {

        // 检查客户端是否要求消息确认，可选
        if ( ackRequest.isAckRequested() ) {
            // 发送确认消息给客户端
            ackRequest.sendAckData( "client message was delivered to server!", "yeah!" );
        }

        // 发送一个消息给客户端，要求确认
        ChatObject ackChatObjectData = new ChatObject( data.getUserName(), "message with ack data" );
        client.sendEvent( "ackevent2", new AckCallback&lt;String&gt;( String.class ) {
            @Override
            public void onSuccess( String result ) {
                // 此回调执行时，客户端已经确认，可以检查确认结果
                System.out.println( "ack from client: " + client.getSessionId() + " data: " + result );
            }
        }, ackChatObjectData );

        // 发送一个消息给客户端，要求确认，但是不关心确认后返回的消息
        ChatObject ackChatObjectData1 = new ChatObject( data.getUserName(), "message with void ack" );
        client.sendEvent( "ackevent3", new VoidAckCallback() {

            protected void onSuccess() {
                System.out.println( "void ack from: " + client.getSessionId() );
            }

        }, ackChatObjectData1 );
    }
} );</pre>
<div class="blog_h3"><span class="graybg">二进制消息</span></div>
<pre class="crayon-plain-tag">Configuration config = new Configuration();
config.setHostname("localhost");
config.setPort(9092);
// 限制WebSocket帧最大长度
config.setMaxFramePayloadLength(1024 * 1024);
// 限制HTTP报文最大长度
config.setMaxHttpContentLength(1024 * 1024);

final SocketIOServer server = new SocketIOServer(config);
// 把二进制消息转换为数组
server.addEventListener("msg", byte[].class, new DataListener&lt;byte[]&gt;() {
    @Override
    public void onData(SocketIOClient client, byte[] data, AckRequest ackRequest) {
        client.sendEvent("msg", data);
    }
});</pre>
<div class="blog_h3"><span class="graybg">使用名字空间</span></div>
<pre class="crayon-plain-tag">final SocketIOServer server = new SocketIOServer( config );
// 创建一个新的名字空间
final SocketIONamespace chat1namespace = server.addNamespace( "/chat1" );
// 在此名字空间上监听
chat1namespace.addEventListener( "message", ChatObject.class, new DataListener&lt;ChatObject&gt;() {
    @Override
    public void onData( SocketIOClient client, ChatObject data, AckRequest ackRequest ) {
    }
} );</pre>
<div class="blog_h3"><span class="graybg">SSL支持</span></div>
<pre class="crayon-plain-tag">Configuration config = new Configuration();
config.setHostname( "localhost" );
config.setPort( 10443 );

config.setKeyStorePassword( "test1234" );
InputStream stream = SslChatLauncher.class.getResourceAsStream( "/keystore.jks" );
// 设置密钥存储库
config.setKeyStore( stream );

final SocketIOServer server = new SocketIOServer( config ); </pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/socket-io-study-note">Socket.io学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/socket-io-study-note/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
	</channel>
</rss>
