<?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; Network</title>
	<atom:link href="https://blog.gmem.cc/category/work/network/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>IPVS模式下ClusterIP泄露宿主机端口的问题</title>
		<link>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode</link>
		<comments>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode#comments</comments>
		<pubDate>Tue, 05 Jan 2021 10:50:51 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[IPVS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35061</guid>
		<description><![CDATA[<p>问题 在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。 这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。 在宿主机上尝试[crayon-69e29a0c0aabb649144780-i/]，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。 定义 如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。 这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。 如果kube-proxy使用iptables模式，不会出现这种异常行为。 原因 启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）： [crayon-69e29a0c0aac2662950578/] <a class="read-more" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</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>在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。</p>
<p>这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。</p>
<p>在宿主机上尝试<pre class="crayon-plain-tag">telnet RegistryClusterIP 443</pre>，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。</p>
<div class="blog_h1"><span class="graybg">定义</span></div>
<p>如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。</p>
<p>这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。</p>
<p>如果kube-proxy使用iptables模式，不会出现这种异常行为。</p>
<div class="blog_h1"><span class="graybg">原因</span></div>
<p>启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）：</p>
<pre class="crayon-plain-tag">5: kube-ipvs0: &lt;BROADCAST,NOARP&gt; mtu 1500 qdisc noop state DOWN group default 
    link/ether fa:d9:9e:37:12:68 brd ff:ff:ff:ff:ff:ff
    inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
       valid_lft forever preferred_lft forever</pre>
<p>这种绑定是必须的，因为IPVS的工作原理是，<span style="background-color: #c0c0c0;">在netfilter挂载点LOCAL_IN上注册钩子ip_vs_in，拦截目的地是VIP（ClusterIP）的封包</span>。而要使得封包进入到LOCAL_IN，它的目的地址必须是本机地址。</p>
<p>每当为网络接口添加一个IP地址，内核都会<span style="background-color: #c0c0c0;">自动</span>在local路由表中增加一条规则，对于上面的10.96.0.10，会增加：</p>
<pre class="crayon-plain-tag"># 对于目的地址是10.96.0.10的封包，从kube-ipvs0发出，如果没有指定源IP，使用10.96.0.10
local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>上述自动添加路由的一个副作用是，<span style="background-color: #c0c0c0;">对于任意一个端口Port，如果不存在匹配ClusterIP:Port的IPVS规则，同时宿主机上某个应用在0.0.0.0:Port上监听，封包就会交由此应用处理</span>。</p>
<p>在宿主机上执行<pre class="crayon-plain-tag">telnet 10.96.0.10 22</pre>，会发生以下事件序列：</p>
<ol>
<li>出站选路，根据local表路由规则，从kube-ipvs0接口发出封包</li>
<li>由于kube-ipvs0是dummy的，封包<span style="background-color: #c0c0c0;">立刻从kube-ipvs0的出站队列移动到入站队列</span></li>
<li>目的地址是本地地址，因此进入LOCAL_IN挂载点</li>
<li>由于22不是ServicePort，封包被转发给本地进程处理，即监听了22的那个进程</li>
</ol>
<p>如果删除内核自动在local表中添加的路由：</p>
<pre class="crayon-plain-tag">ip route del table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>则会出现以下现象：</p>
<ol>
<li>无法访问10.96.0.10:22。这是我们期望的，因为10.96.0.10这个服务没有暴露22端口，此端口理当不通</li>
<li>无法ping 10.96.0.10。这不是我们期望的，但是一般情况下不会有什么问题。iptables模式下ClusterIP就是无法ping的，IPVS模式下可以在本机ping仅仅是绑定ClusterIP到kube-ipvs0的一个副作用。通常应用程序不应该对ClusterIP做ICMP检测，来判断服务是否可用，因为这依赖了kube-proxy的特定工作模式</li>
<li>在宿主机上，可以访问10.96.0.10:53。这是我们期望的，宿主机上可以访问ClusterIP</li>
<li>在某个容器的网络命名空间下，无法访问10.96.0.10:53。这不是我们期望的，相当于Pod无法访问ClusterIP了</li>
</ol>
<p>以上4条，惟独3难以理解。<span style="background-color: #c0c0c0;">为什么路由没了，宿主机仍然能访问ClusterIP:ServicePort</span>？这个我们还没有从源码级别深究，但是很明显和IPVS有关。IPVS在LOCAL_OUT上挂有钩子，它可能在此钩子中检测到来自本机（主网络命名空间）的、访问ClusterIP+ServicePort（即IPVS虚拟服务）的封包，并进行了某种“魔法”处理，从而避开了没有路由的问题。</p>
<p>下面我们进一步验证上述“魔法”处理的可能性。使用<pre class="crayon-plain-tag">tcpdump -i any host 10.96.0.10</pre>来捕获流量，从容器命名空间访问ClusterIP:ServicePort时，可以看到：</p>
<pre class="crayon-plain-tag">#                  容器IP
11:32:00.448470 IP 172.27.0.24.56378 &gt; 10.96.0.10.53: Flags [S], seq 2946888109, win 28200, options...</pre>
<p>但是从宿主机访问ClusterIP:ServicePort时，则捕获不到任何流量。但是，通过iptables logging，我们可以确定，内核的确<span style="background-color: #c0c0c0;">以ClusterIP为源地址和目的地址</span>，发起了封包：</p>
<pre class="crayon-plain-tag">iptables -t mangle -I OUTPUT 1 -p tcp --dport 53 -j LOG --log-prefix 'out-d53: '

# dmesg -w
#                                      源地址          目的地址
# [3374381.426541] out-d53: IN= OUT=lo SRC=10.96.0.100 DST=10.96.0.100 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=18885 DF PROTO=TCP SPT=42442 DPT=53 WINDOW=86 RES=0x00 ACK URGP=0 </pre>
<p>回顾一下数据报出站、入站的处理过程：</p>
<ol>
<li>出站，依次经过 <strong><span style="background-color: #99cc00;">netfilter/iptables</span></strong> ⇨ <strong><span style="background-color: #cc99ff;">tcpdump</span></strong> ⇨ 网络接口 ⇨网线</li>
<li>入站，依次经过 网线 ⇨ 网络接口 ⇨ tcpdump ⇨ netfilter/iptables</li>
</ol>
<p>只有当IPVS在宿主机请求10.96.0.10的封包出站时，在netfilter中对匹配IPVS虚拟服务的封包进行如下处理，才能解释<span style="background-color: #99cc00;"><strong>iptables</strong></span>中能看到10.96.0.10，而紧随其后的<strong><span style="background-color: #cc99ff;">tcpdump</span></strong>中却又看不到的现象：</p>
<ol>
<li>修改目的地址为Service的Endpoint地址，这就是NAT模式的IPVS（即kube-proxy使用NAT模式）应有的行为</li>
<li>修改了源地址为当前宿主机的地址，不这样做，回程报文就无法路由回来</li>
</ol>
<p>另外注意一下，如果从宿主机访问ClusterIP:NonServicePort，则tcpdump能捕获到源或目的地址为ClusterIP的流量。这是因为IPVS发现它不匹配任何虚拟服务，会直接返回NF_ACCEPT，然后封包就按照常规流程处理了。</p>
<div class="blog_h1"><span class="graybg">后果</span></div>
<div class="blog_h2"><span class="graybg">安全问题</span></div>
<p>如果宿主机上有一个在0.0.0.0上监听的、存在安全漏洞的服务，则可能被恶意的工作负载利用。</p>
<div class="blog_h2"><span class="graybg">行为异常</span></div>
<p>少部分的应用程序，例如docker manifest，其行为取决于端口探测的结果，会无法正常工作。</p>
<div class="blog_h1"><span class="graybg">解决</span></div>
<p>可能的解决方案有：</p>
<ol>
<li>在iptables中匹配哪些针对ClusterIP:NonServicePort的流量，Drop或Reject掉</li>
<li>使用基于fwmark的IPVS虚拟服务，这需要在iptables中对针对ClusterIP:ServicePort的流量打fwmark，而且每个ClusterIP都需要占用独立的fwmark，难以管理</li>
</ol>
<p>对于解决方案1，可以使用如下iptables规则： </p>
<pre class="crayon-plain-tag">#                 如果目的地址是ClusterIP    但是目的端口不是ServicePort           则拒绝
iptables -A INPUT -d  10.96.0.0/12 -m set ! --match-set KUBE-CLUSTER-IP dst,dst -j REJECT</pre>
<p>这个规则能够为容器解决宿主机端口泄露的问题，但是会导致宿主机上无法访问ClusterIP。</p>
<p>引起此问题的原因是，在宿主机访问ClusterIP时，会同时使用ClusterIP作为源地址/目的地址。这样，来自Endpoint的回程报文，unNATed后的目的地址，就会匹配到上面的iptables规则，从而导致封包被Reject掉。</p>
<p>要解决此问题，我们可以修改内核自动添加的路由，提示使用其它地址作为源地址：</p>
<pre class="crayon-plain-tag"># 这条路由给出src提示，当访问10.96.0.10时，选取192.168.104.82（节点IP）作为源地址
ip route replace table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 192.168.104.82</pre>
<div class="blog_h1"><span class="graybg">深入</span></div>
<p>上文我们提到了一个“魔法”处理的猜想，这里我们对IPVS的实现细节进行深入学习，证实此猜想。</p>
<p>本节牵涉到的内核源码均来自linux-3.10.y分支。</p>
<div class="blog_h2"><span class="graybg">Netfilter</span></div>
<p>这是从2.4.x引入内核的一个框架，用于实现防火墙、NAT、封包修改、记录封包日志、用户空间封包排队之类的功能。</p>
<p>netfilter运行在内核中，允许内核模块在Linux网络栈的<span style="background-color: #c0c0c0;">不同位置注册钩子（回调函数），当每个封包穿过网络栈时，这些钩子函数会被调用</span>。</p>
<p><a href="/iptables">iptables</a>是经典的，基于netfilter的用户空间工具。它的继任者是nftables，它更加灵活、可扩容、性能好。</p>
<div class="blog_h3"><span class="graybg">钩子挂载点</span></div>
<p>netfilter提供了5套钩子（的挂载点）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">挂载点</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NF_IP_PER_ROUTING</td>
<td>
<p>当封包进入网络栈时调用。封包的目的地可能是本机，或者需要转发</p>
<p>ip_rcv / ipv6_rcv是内核接受并处理IP数据报的入口，此函数会调用这类钩子：</p>
<pre class="crayon-plain-tag">int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	// ...
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
		       ip_rcv_finish);
}</pre>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_IN</td>
<td>
<p>当路由判断封包应该由本机处理时（目的地址是本机地址）调用
<p>ip_local_deliver / ip6_input负责将IP数据报向上层传递，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_FORWARD</td>
<td>
<p>当路由判断封包应该被转发给其它机器（或者网络命名空间）时调用</p>
<p>ip_forward / ip6_forward负责封包转发，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_POST_ROUTING</td>
<td>
<p>在封包即将离开网络栈（进入网线）时调用，不管是转发的、还是本机发出的，都需要经过此挂载点</p>
<p>ip_output / ip6_finish_output2会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_OUT</td>
<td>
<p>当封包由本机产生，需要往外发送时调用</p>
<p>__ip_local_out / __ip6_local_out会调用这类钩子</p>
</td>
</tr>
</tbody>
</table>
<p>这些挂载点，和iptables的各链是对应的。</p>
<div class="blog_h3"><span class="graybg">注册钩子</span></div>
<p>要在内核中使用netfilter的钩子，你需要调用函数：</p>
<pre class="crayon-plain-tag">// 注册钩子
int nf_register_hook(struct nf_hook_ops *reg){}
// 反注册钩子
void nf_unregister_hook(struct nf_hook_ops *reg){}</pre>
<p>入参nf_hook_ops是一个结构：</p>
<pre class="crayon-plain-tag">struct nf_hook_ops {
	// 钩子的函数指针，依据内核的版本不同此函数的签名有所差异
	nf_hookfn		*hook;
	struct net_device	*dev;
	void			*priv;
	// 钩子针对的协议族，PF_INET表示IPv4
	u_int8_t		pf;
	// 钩子类型代码，参考上面的表格
	unsigned int		hooknum;
	// 每种类型的钩子，都可以有多个，此数字决定执行优先级
	int			priority;
};


// 钩子函数的签名
typedef unsigned int nf_hookfn(unsigned int hooknum,
			       struct sk_buff *skb, // 正被处理的数据报
			       const struct net_device *in, // 输入设备
			       const struct net_device *out, // 是出设备
			       int (*okfn)(struct sk_buff *)); // 如果通过钩子检查，则调用此函数，通常用不到</pre>
<div class="blog_h3"><span class="graybg">钩子返回值</span></div>
<pre class="crayon-plain-tag">/* Responses from hook functions. */
// 丢弃该报文，不再继续传输或处理
#define NF_DROP 0
// 继续正常传输报文，如果后面由低优先级的钩子，仍然会调用它们
#define NF_ACCEPT 1
// 告知netfilter，报文被别人偷走处理了，不需要再对它做任何处理
// 下文的分析中，我们有个例子。一个netfilter钩子在内部触发了对netfilter钩子的调用
// 外层钩子返回的就是NF_STOLEN，相当于将封包的控制器转交给内层钩子了
#define NF_STOLEN 2
// 对该数据报进行排队，通常用于将数据报提交给用户空间进程处理
#define NF_QUEUE 3
// 再次调用该钩子函数
#define NF_REPEAT 4
// 继续正常传输报文，不会调用此挂载点的后续钩子
#define NF_STOP 5
#define NF_MAX_VERDICT NF_STOP </pre>
<div class="blog_h3"><span class="graybg">钩子优先级</span></div>
<p>优先级通常以下面的枚举为基准+/-：</p>
<pre class="crayon-plain-tag">enum nf_ip_hook_priorities {
	// 数值越小，优先级越高，越先执行
	NF_IP_PRI_FIRST = INT_MIN,
	NF_IP_PRI_CONNTRACK_DEFRAG = -400,
	// 可以看到iptables各表注册的钩子的优先级
	NF_IP_PRI_RAW = -300,
	NF_IP_PRI_SELINUX_FIRST = -225,
	NF_IP_PRI_CONNTRACK = -200,
	NF_IP_PRI_MANGLE = -150,
	NF_IP_PRI_NAT_DST = -100,
	NF_IP_PRI_FILTER = 0,
	NF_IP_PRI_SECURITY = 50,
	NF_IP_PRI_NAT_SRC = 100,
	NF_IP_PRI_SELINUX_LAST = 225,
	NF_IP_PRI_CONNTRACK_HELPER = 300,
	NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
	NF_IP_PRI_LAST = INT_MAX,
};</pre>
<div class="blog_h2"><span class="graybg"><a id="ipvs"></a>IPVS</span></div>
<div class="blog_h3"><span class="graybg">钩子列表</span></div>
<p> ip_vs模块初始化时，会通过ip_vs_init函数，调用nf_register_hook，注册以下netfilter钩子：</p>
<pre class="crayon-plain-tag">static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
	// 注册到LOCAL_IN，这两个钩子处理外部客户端的报文
	// 转而调用ip_vs_out，用于NAT模式下，处理LVS回复外部客户端的报文，例如修改IP地址
	{
		.hook		= ip_vs_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_IN,
		.priority	= NF_IP_PRI_NAT_SRC - 2,
	},
	// 转而调用ip_vs_in，用于处理外部客户端进入IPVS的请求报文
	// 如果没有对应请求报文的连接，则使用调度函数创建连接结构，这其中牵涉选择RS负载均衡算法
	{
		.hook		= ip_vs_remote_request4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_IN,
		.priority	= NF_IP_PRI_NAT_SRC - 1,
	},

	// 注册到LOCAL_OUT，这两个钩子处理LVS本机的报文
	// 转而调用ip_vs_out，用于NAT模式下，处理LVS回复客户端的报文
	{
		.hook		= ip_vs_local_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_NAT_DST + 1,
	},
	// 转而调用ip_vs_in，调度并转发（给RS）本机的请求
	{
		.hook		= ip_vs_local_request4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_NAT_DST + 2,
	},

	// 这两个函数注册到FORWARD
	// 转而调用ip_vs_in_icmp，用于处理外部客户端发到IPVS的ICMP报文，并转发到RS
	{
		.hook		= ip_vs_forward_icmp,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_FORWARD,
		.priority	= 99,
	},
	// 转而调用ip_vs_out，用于NAT模式下，修改RS给的应答报文的源地址为IPVS虚拟地址
	{
		.hook		= ip_vs_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_FORWARD,
		.priority	= 100,
	}
};</pre>
<div class="blog_h3"><span class="graybg">ip_vs_in</span></div>
<p>从上面的钩子我们可以看到：</p>
<ol>
<li>针对外部发起的、本机发起的，对IPVS的请求（目的是VIP的SYN），钩子的位置是不一样的：
<ol>
<li>对于外部的请求，在LOCAL_IN中处理，钩子函数为ip_vs_remote_request4</li>
<li>对于本机的请求，在LOCAL_OUT中处理，钩子函数为ip_vs_local_request4</li>
</ol>
</li>
<li> 尽管钩子的位置不同，但是函数ip_vs_remote_request4、ip_vs_local_request4都是调用ip_vs_in。实际上，这两个函数的逻辑完全一样：<br />
<pre class="crayon-plain-tag">/*
 *	AF_INET handler in NF_INET_LOCAL_IN chain
 *	Schedule and forward packets from remote clients
 */
static unsigned int
ip_vs_remote_request4(unsigned int hooknum, struct sk_buff *skb,
		      const struct net_device *in,
		      const struct net_device *out,
		      int (*okfn)(struct sk_buff *))
{
	return ip_vs_in(hooknum, skb, AF_INET);
}

/*
 *	AF_INET handler in NF_INET_LOCAL_OUT chain
 *	Schedule and forward packets from local clients
 */
static unsigned int
ip_vs_local_request4(unsigned int hooknum, struct sk_buff *skb,
		     const struct net_device *in, const struct net_device *out,
		     int (*okfn)(struct sk_buff *))
{
	return ip_vs_in(hooknum, skb, AF_INET);
}</pre>
</li>
</ol>
<p>回顾一下上文我们关于“魔法”处理的疑惑。对于从宿主机发起对10.96.0.10:53的请求，我们通过iptables logging证实了使用的源IP地址是10.96.0.10：</p>
<ol>
<li>这个请求为什么tcpdump捕获不到？</li>
<li>为什么删除路由不影响宿主机对ClusterIP的请求（却又导致容器无法请求ClusterIP）？</li>
</ol>
<p>这两个问题的答案，很可能就隐藏在ip_vs_in函数中，因为它是处理进入IPVS的数据报的统一入口。如果该函数同时修改了原始封包的源/目的地址，就解释了问题1；如果该函数在内部进行了选路操作，则解释了问题2。</p>
<p>下面分析一下ip_vs_in的代码：</p>
<pre class="crayon-plain-tag">static unsigned int
ip_vs_in(unsigned int hooknum, struct sk_buff *skb, int af)
{
	// 网络命名空间
	struct net *net;
	// IPVS的IP头，其中存有3层头len、协议、标记、源/目的地址
	struct ip_vs_iphdr iph;
	// 持有协议（TCP/UDP/SCTP/AH/ESP）信息，更重要的是带着很多函数指针。这些指针负责针对特定协议的IPVS逻辑
	struct ip_vs_protocol *pp;
	// 每个命名空间一个此对象，包含统计计数器、超时表
	struct ip_vs_proto_data *pd;
	// 当前封包所属的IPVS连接对象，此对象最重要的是packet_xmit函数指针。它负责将封包发走
	struct ip_vs_conn *cp;
	int ret, pkts;
	// 描述当前网络命名空间的IPVS状态
	struct netns_ipvs *ipvs;

	// 如果封包已经被标记为IPVS请求/应答，不做处理，继续netfilter常规流程
	// 后续ip_vs_nat_xmit会让封包“重入”netfilter，那时封包已经打上IPVS标记
	// 这里的判断确保重入的封包走netfilter常规流程，而不是进入死循环
	if (skb-&gt;ipvs_property)
		return NF_ACCEPT;


	// 如果封包目的地不是本机且当前不在LOCAL_OUT
	// 或者封包的dst_entry不存在，不做处理，继续netfilter常规流程
	if (unlikely((skb-&gt;pkt_type != PACKET_HOST &amp;&amp;
		      hooknum != NF_INET_LOCAL_OUT) ||
		     !skb_dst(skb))) {
		ip_vs_fill_iph_skb(af, skb, &amp;iph);
		IP_VS_DBG_BUF(12, "packet type=%d proto=%d daddr=%s"
			      " ignored in hook %u\n",
			      skb-&gt;pkt_type, iph.protocol,
			      IP_VS_DBG_ADDR(af, &amp;iph.daddr), hooknum);
		return NF_ACCEPT;
	}
	// 如果当前IPVS主机是backup，或者当前命名空间没有启用IPVS，不做处理，继续netfilter常规流程
	net = skb_net(skb);
	ipvs = net_ipvs(net);
	if (unlikely(sysctl_backup_only(ipvs) || !ipvs-&gt;enable))
		return NF_ACCEPT;

	// 使用封包的IP头填充IPVS的IP头
	ip_vs_fill_iph_skb(af, skb, &amp;iph);

	// 如果是RAW套接字，不做处理，继续netfilter常规流程
	if (unlikely(skb-&gt;sk != NULL &amp;&amp; hooknum == NF_INET_LOCAL_OUT &amp;&amp;
		     af == AF_INET)) {
		struct sock *sk = skb-&gt;sk;
		struct inet_sock *inet = inet_sk(skb-&gt;sk);

		if (inet &amp;&amp; sk-&gt;sk_family == PF_INET &amp;&amp; inet-&gt;nodefrag)
			return NF_ACCEPT;
	}

	// 处理ICMP报文，和我们的场景无关
	if (unlikely(iph.protocol == IPPROTO_ICMP)) {
		int related;
		int verdict = ip_vs_in_icmp(skb, &amp;related, hooknum);
		if (related)
			return verdict;
	}

	// 如果协议不受IPVS支持，不做处理，继续netfilter常规流程
	pd = ip_vs_proto_data_get(net, iph.protocol);
	if (unlikely(!pd))
		return NF_ACCEPT;
	// 协议被支持，得到pp
	pp = pd-&gt;pp;
	// 尝试获取封包所属的IPVS连接对象
	cp = pp-&gt;conn_in_get(af, skb, &amp;iph, 0);
	// 如果封包属于既有IPVS连接，且此连接的RS（dest）已经设置，且RS的权重为0
	// 认为是无效连接，设为过期
	if (unlikely(sysctl_expire_nodest_conn(ipvs)) &amp;&amp; cp &amp;&amp; cp-&gt;dest &amp;&amp;
	    unlikely(!atomic_read(&amp;cp-&gt;dest-&gt;weight)) &amp;&amp; !iph.fragoffs &amp;&amp;
	    is_new_conn(skb, &amp;iph)) {
		ip_vs_conn_expire_now(cp);
		__ip_vs_conn_put(cp);
		cp = NULL;
	}

	// 调度一个新的IPVS连接，这里牵涉到RS的LB算法
	if (unlikely(!cp) &amp;&amp; !iph.fragoffs) {
		int v;
		if (!pp-&gt;conn_schedule(af, skb, pd, &amp;v, &amp;cp, &amp;iph))
			// 如果返回0，通常v是NF_DROP，这以为这调度失败，封包丢弃
			return v;
	}

	if (unlikely(!cp)) {
		IP_VS_DBG_PKT(12, af, pp, skb, 0,
			      "ip_vs_in: packet continues traversal as normal");
		if (iph.fragoffs) {
			IP_VS_DBG_RL("Unhandled frag, load nf_defrag_ipv6\n");
			IP_VS_DBG_PKT(7, af, pp, skb, 0, "unhandled fragment");
		}
		return NF_ACCEPT;
	}

	// 入站封包 —— 在我们的场景中，这是本地客户端入了IPVS系统的封包
	// 从网络栈的角度来说，我们正在处理的是出站封包...
	IP_VS_DBG_PKT(11, af, pp, skb, 0, "Incoming packet");

	// IPVS连接的RS不可用
	if (cp-&gt;dest &amp;&amp; !(cp-&gt;dest-&gt;flags &amp; IP_VS_DEST_F_AVAILABLE)) {
		// 立即将连接设为过期
		if (sysctl_expire_nodest_conn(ipvs)) {
			ip_vs_conn_expire_now(cp);
		}
		// 丢弃封包
		__ip_vs_conn_put(cp);
		return NF_DROP;
	}
	// 更新计数器
	ip_vs_in_stats(cp, skb);
	// 更新IPVS连接状态机，做的事情包括
	//   根据数据包 tcp 标记字段来更新当前状态机
	//   更新连接对应的统计数据，包括：活跃连接和非活跃连接
	//   根据连接状态，设置超时时间
	ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pd);

	if (cp-&gt;packet_xmit)
		// 调用packet_xmit将封包发走，实际上是重入netfilter的LOCAL_OUT，封包控制权转移走，后续不该再操控skb
		ret = cp-&gt;packet_xmit(skb, cp, pp, &amp;iph);
	else {
		IP_VS_DBG_RL("warning: packet_xmit is null");
		ret = NF_ACCEPT;
	}

	if (cp-&gt;flags &amp; IP_VS_CONN_F_ONE_PACKET)
		pkts = sysctl_sync_threshold(ipvs);
	else
		pkts = atomic_add_return(1, &amp;cp-&gt;in_pkts);

	if (ipvs-&gt;sync_state &amp; IP_VS_STATE_MASTER)
		ip_vs_sync_conn(net, cp, pkts);

	// 放回连接对象，重置连接定时器
	ip_vs_conn_put(cp);
	return ret;
}</pre>
<p>上面这段代码中，“魔法”处理最可能发生在：</p>
<ol>
<li>conn_schedule：在这里需要进行IPVS连接的调度</li>
<li>packet_xmit：在这里发送经过IPVS处理的封包</li>
</ol>
<p>二者都是函数指针，在TCP协议下，conn_schedule指向tcp_conn_schedule。在NAT模式下，packet_xmit指向ip_vs_nat_xmit。packet_xmit指针是在conn_schedule过程中初始化的。</p>
<div class="blog_h3"><span class="graybg">tcp_conn_schedule</span></div>
<p>我们看一下TCP协议下IPVS连接的调度过程。</p>
<pre class="crayon-plain-tag">static int
tcp_conn_schedule(int af, struct sk_buff *skb, struct ip_vs_proto_data *pd,
		  int *verdict, struct ip_vs_conn **cpp,
		  struct ip_vs_iphdr *iph)
{
	// 网络命名空间
	struct net *net;
	// IPVS虚拟服务对象
	struct ip_vs_service *svc;
	struct tcphdr _tcph, *th;

	// 解析L4头，如果失败，提示ip_vs_in丢弃封包
	th = skb_header_pointer(skb, iph-&gt;len, sizeof(_tcph), &amp;_tcph);
	if (th == NULL) {
		*verdict = NF_DROP;
		return 0;
	}
	net = skb_net(skb);
	rcu_read_lock();
	if (th-&gt;syn &amp;&amp;
	    // 根据封包特征，去查找匹配的虚拟服务
	    (svc = ip_vs_service_find(net, af, skb-&gt;mark, iph-&gt;protocol,
				      &amp;iph-&gt;daddr, th-&gt;dest))) {
		int ignored;
		// 如果当前网络命名空间“过载”了，丢弃封包
		if (ip_vs_todrop(net_ipvs(net))) {
			rcu_read_unlock();
			*verdict = NF_DROP;
			return 0;
		}

		// 选择一个RS，建立IPVS连接
		// 如果找不到RS，或者发生致命错误，则ignore为0或-1，这种情况下
		// IPVS连接没有成功创建，提示ip_vs_in丢弃封包，可能附带回复ICMP
		*cpp = ip_vs_schedule(svc, skb, pd, &amp;ignored, iph);
		if (!*cpp &amp;&amp; ignored &lt;= 0) {
			if (!ignored)
				// ignored=0，找不到RS
				*verdict = ip_vs_leave(svc, skb, pd, iph);
			else
				*verdict = NF_DROP;
			rcu_read_unlock();
			return 0;
		}
	}
	rcu_read_unlock();
	// 如果调度成功，IPVS连接对象不为空，返回1
	/* NF_ACCEPT */
	return 1;
}</pre>
<p>到这里我们还没有看到IPVS对封包地址进行更改，需要进一步阅读ip_vs_schedule。 </p>
<div class="blog_h3"><span class="graybg">ip_vs_schedule</span></div>
<p>这是IPVS调度的核心函数，它支持TCP/UDP，它为虚拟服务选择一个RS，创建IPVS连接对象。</p>
<pre class="crayon-plain-tag">struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, struct sk_buff *skb,
	       struct ip_vs_proto_data *pd, int *ignored,
	       struct ip_vs_iphdr *iph)
{
	struct ip_vs_protocol *pp = pd-&gt;pp;
	// IPVS连接对象（connection entry）
	struct ip_vs_conn *cp = NULL;
	struct ip_vs_scheduler *sched;
	struct ip_vs_dest *dest;
	__be16 _ports[2], *pptr;
	unsigned int flags;

	// ...

	*ignored = 0;

	/*
	 *    Non-persistent service
	 */
	// 调度工作委托给虚拟服务的scheduler
	sched = rcu_dereference(svc-&gt;scheduler);
	// 调度器就是选择一个RS（ip_vs_dest）
	dest = sched-&gt;schedule(svc, skb);
	if (dest == NULL) {
		IP_VS_DBG(1, "Schedule: no dest found.\n");
		return NULL;
	}

	flags = (svc-&gt;flags &amp; IP_VS_SVC_F_ONEPACKET
		 &amp;&amp; iph-&gt;protocol == IPPROTO_UDP) ?
		IP_VS_CONN_F_ONE_PACKET : 0;

	// 初始化IPVS连接对象 ip_vs_conn
	{
		struct ip_vs_conn_param p;

		ip_vs_conn_fill_param(svc-&gt;net, svc-&gt;af, iph-&gt;protocol,
				      &amp;iph-&gt;saddr, pptr[0], &amp;iph-&gt;daddr,
				      pptr[1], &amp;p);
		// 操控ip_vs_conn的逻辑包括：
		//   初始化定时器
		//   设置网络命名空间
		//   设置地址、fwmark、端口
		//   根据IP版本、IPVS模式（NAT/DR/TUN）为连接设置一个packet_xmit
		cp = ip_vs_conn_new(&amp;p, &amp;dest-&gt;addr,
				    dest-&gt;port ? dest-&gt;port : pptr[1],
				    flags, dest, skb-&gt;mark);
		if (!cp) {
			*ignored = -1;
			return NULL;
		}
	}

	// ...
	return cp;
}</pre>
<p>到这里我们可以看到， conn_schedule仍然没有对封包做任何修改。看来关键在packet_xmit函数中。</p>
<div class="blog_h3"><span class="graybg">ip_vs_nat_xmit</span></div>
<pre class="crayon-plain-tag">int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
	       struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
	// 路由表项
	struct rtable *rt;		/* Route to the other host */
	// 是否本机    是否输入路由
	int local, rc, was_input;

	EnterFunction(10);

	rcu_read_lock();
	// 是否尚未设置客户端端口
	if (unlikely(cp-&gt;flags &amp; IP_VS_CONN_F_NO_CPORT)) {
		__be16 _pt, *p;

		p = skb_header_pointer(skb, ipvsh-&gt;len, sizeof(_pt), &amp;_pt);
		if (p == NULL)
			goto tx_error;
		// 设置IPVS连接对象的cport
		// caddr cport 客户端地址
		// vaddr vport 虚拟服务地址
		// daddr dport RS地址
		ip_vs_conn_fill_cport(cp, *p);
		IP_VS_DBG(10, "filled cport=%d\n", ntohs(*p));
	}

	was_input = rt_is_input_route(skb_rtable(skb));
	// 出口路由查找，依据是封包、RS的地址、以及若干标识位
	// 返回值提示路由目的地是否是本机
	local = __ip_vs_get_out_rt(skb, cp-&gt;dest, cp-&gt;daddr.ip,
				   IP_VS_RT_MODE_LOCAL |
				   IP_VS_RT_MODE_NON_LOCAL |
				   IP_VS_RT_MODE_RDR, NULL);
	if (local &lt; 0)
		goto tx_error;
	rt = skb_rtable(skb);

	// 如果目的地是本机，RS地址是环回地址，是输入
	if (local &amp;&amp; ipv4_is_loopback(cp-&gt;daddr.ip) &amp;&amp; was_input) {
		IP_VS_DBG_RL_PKT(1, AF_INET, pp, skb, 0, "ip_vs_nat_xmit(): "
				 "stopping DNAT to loopback address");
		goto tx_error;
	}

	// 封包将被修改，执行copy-on-write
	if (!skb_make_writable(skb, sizeof(struct iphdr)))
		goto tx_error;

	if (skb_cow(skb, rt-&gt;dst.dev-&gt;hard_header_len))
		goto tx_error;

	// 修改封包，dnat_handler指向tcp_dnat_handler
	if (pp-&gt;dnat_handler &amp;&amp; !pp-&gt;dnat_handler(skb, pp, cp, ipvsh))
		goto tx_error;
	// 更改目的地址
	ip_hdr(skb)-&gt;daddr = cp-&gt;daddr.ip;
	// 为出站封包生成chksum
	ip_send_check(ip_hdr(skb));

	IP_VS_DBG_PKT(10, AF_INET, pp, skb, 0, "After DNAT");

	skb-&gt;local_df = 1;

	// 发送封包：
	//   如果发送出去了，返回 NF_STOLEN
	//   如果没有发送（local=1，目的地是本机），返回NF_ACCEPT
	rc = ip_vs_nat_send_or_cont(NFPROTO_IPV4, skb, cp, local);
	rcu_read_unlock();

	LeaveFunction(10);
	return rc;

  tx_error:
	kfree_skb(skb);
	rcu_read_unlock();
	LeaveFunction(10);
	return NF_STOLEN;
}

static inline int ip_vs_nat_send_or_cont(int pf, struct sk_buff *skb,  struct ip_vs_conn *cp, int local)
{
	// 注意这个NF_STOLEN的含义，参考上文
	int ret = NF_STOLEN;
	// 给封包设置IPVS标记，NF_HOOK会导致当前封包重入netfilter，此标记会让重入后的封包立即NF_ACCEPT、
	// 重入让修改后的封包有机会被ipables处理
	skb-&gt;ipvs_property = 1;
	if (likely(!(cp-&gt;flags &amp; IP_VS_CONN_F_NFCT)))
		ip_vs_notrack(skb);
	else
		ip_vs_update_conntrack(skb, cp, 1);
	// 如果目的地不是本机
	if (!local) {
		skb_forward_csum(skb);
		// 调用LOCAL_OUT挂载点
		NF_HOOK(pf, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)-&gt;dev, dst_output);
	} else
		ret = NF_ACCEPT;
	return ret;
}</pre>
<p>在ip_vs_nat_xmit中，我们可以了解到，对于宿主机发起的针对ClusterIP:ServicePort的请求</p>
<ol>
<li>封包的目的地址被修改为Endpoint（通常是Pod，IPVS中的RS）的地址</li>
<li>修改后的封包，重新被塞入netfilter（内层），注意当前就正在netfilter（外层）中
<ol>
<li>外层钩子的返回值是NF_STOLEN：封包处理权转移给内层钩子，停止后续netfilter流程</li>
<li>内层钩子的返回值是NF_ACCEPT：不做IPVS相关处理，继续后续netfilter流程。IPVS前、后的LOCAL_OUT、POSTROUTING钩子都会正常执行。也就是说，对于修改后的封包，内核会进行完整、常规的netfilter处理，就像没有IPVS存在一样</li>
</ol>
</li>
</ol>
<p>到这里，我们确定了，IPVS会在LOCAL_OUT中进行DNAT。但是只有同时进行SNAT，才能解释上文的中的疑惑。</p>
<div class="blog_h2"><span class="graybg">SNAT</span></div>
<p>花费了不少时间在IPVS上探究后，我们意识到走错了方向。我们忘记了SANT是kube-proxy会去做的事情。查看一下iptables规则就一目了然了：</p>
<pre class="crayon-plain-tag"># iptables -t nat -L -n -v

Chain OUTPUT (policy ACCEPT 2 packets, 150 bytes)
 pkts bytes target     prot opt in     out     source        destination         
# 所有出站流量都要经过自定义的 KUBE-SERVICES 链
  21M 3825M KUBE-SERVICES  all  --  *  *   0.0.0.0/0         0.0.0.0/0            /* kubernetes service portals */

Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source        destination         
# 如果目的IP:PORT属于K8S服务，则调用KUBE-MARK-MASQ链
    0     0 KUBE-MARK-MASQ  all  --  * *  !172.27.0.0/16     0.0.0.0/0   match-set KUBE-CLUSTER-IP dst,dst

# 给封包打上标记 0x4000
Chain KUBE-MARK-MASQ (5 references)
 pkts bytes target     prot opt in     out     source         destination         
   98  5880 MARK       all  --  *       *  0.0.0.0/0          0.0.0.0/0            MARK or 0x4000

 
Chain POSTROUTING (policy ACCEPT 2 packets, 150 bytes)
 pkts bytes target     prot opt in     out     source         destination         
  44M 5256M KUBE-POSTROUTING  all  --  *  *       0.0.0.0/0   0.0.0.0/0            /* kubernetes postrouting rules */

Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source         destination         
# 仅仅处理 0x4000标记的封包
 1781  166K RETURN     all  --  *      *  0.0.0.0/0           0.0.0.0/0            mark match ! 0x4000/0x4000
# 执行SNAT     
   97  5820 MARK       all  --  *      *  0.0.0.0/0           0.0.0.0/0   MARK xor 0x4000
   97  5820 MASQUERADE  all  --  *     *  0.0.0.0/0           0.0.0.0/0   /* kubernetes service traffic requiring SNAT */</pre>
<p>由于ip_vs_local_request4挂钩在LOCAL_OUT，优先级为NF_IP_PRI_NAT_DST+2 ，因此它是发生在上面nat表OUTPUT链中MARK之后的。也就是说在IPVS处理之前，kube-proxy已经给原始的封包打上标记。</p>
<p>重入的、DNAT后的封包进入LOCAL_OUT，随后进入POSTROUTING。由于标记的缘故，封包被kube-proxy的规则SNAT。</p>
<p>经过POSTROUTING的封包，经过tcpdump，但是由于源、目的IP地址，以及目的端口都改变了，因而我们看到tcpdump没有任何输出。</p>
<div class="blog_h1"><span class="graybg">总结</span></div>
<p>这里做一下小结。</p>
<p>为什么IPVS模式下，能够ping通ClusterIP？ 这是因为IPVS模式下，ClusterIP被配置为宿主机上一张虚拟网卡kube-ipvs0的IP地址。</p>
<p>为什么IPVS模式下，宿主机端口被ClusterIP泄漏？每当添加一个ClusterIP给网络接口后，内核自动在local表中增加一条路由，此路由保证了针对ClusterIP的访问，在没有IPVS干涉的情况下，路由到本机处理。这样，在0.0.0.0上监听的进程，就接收到报文并处理。</p>
<p>为什么删除内核添加的路由后：</p>
<ol>
<li>宿主机上访问ClusterIP:NonServicePort不通了？因为没有路由了</li>
<li>没有路由了，为什么宿主机上访问ClusterIP:ServicePort仍然畅通？如上文分析，IPVS在ip_vs_nat_xmit中仍然会进行选路操作</li>
<li>那为什么从容器网络命名空间访问ClusterIP:ServicePort不通呢？IPVS处理本地、远程客户端的代码路径不一样。容器网络命名空间是远程客户端，需要首先进入PER_ROUTING，然后选路，路由目的地是本机，才会进入LOCAL_IN，IPVS才有介入的时机。由于路由被删掉了，选路那一步就会出问题</li>
</ol>
<p>为什么通过--match-set KUBE-CLUSTER-IP匹配目的地址，如果封包目的端口是NonServicePort则Reject：</p>
<ol>
<li>这种方案对容器命名空间有效？容器请求的源地址不会是ClusterIP，因此回程报文的目的地址不会因为匹配规则而Reject</li>
<li>这种方案导致宿主机无法访问ClusterIP？宿主机发起请求时用的是ClusterIP，请求端口是随机的。这种请求的回程报文必然匹配规则导致Reject</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于Calico的CNI</title>
		<link>https://blog.gmem.cc/calico</link>
		<comments>https://blog.gmem.cc/calico#comments</comments>
		<pubDate>Mon, 12 Feb 2018 07:42:30 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Network]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=18489</guid>
		<description><![CDATA[<p>什么是Calico Calico为容器或虚拟机提供安全的网络连接，它创建一个扁平化的第3层网络，为每个节点分配一个可路由的IP地址。网络中的节点不需要NAT或IP隧道就可以相互通信，因此性能很好，接近于物理网络，不使用网络策略的情况下，可能引入0.01毫秒级别的延迟，带宽影响不明显）。 在需要Overlay的应用场景下，Calico可以支持IP-in-IP隧道，也可以与其它Overlay网络（例如Flannel）配合，IP-in-IP隧道会带来较小的性能下降。 Calico支持动态的网络安全策略（NetPolicy），你可以细粒度的控制容器、虚拟机、物理机端点之间的网络通信。 Calico在每个节点运行一个虚拟路由器（vRouter），vRouter利用Linux内核自带的IP转发功能，工作负载依赖于此路由器和外部通信。节点上的代理组件Felix负责根据分配到节点上的工作负载的IP地址信息为vRouter提供L3转发规则。vRouter基于BIRD实现了边界网关协议（BGP）。通过vRouter，工作负载直接基于物理网络进行通信，甚至可以被分配外网IP并直接暴露到互联网上。 BGP术语 术语 说明 BGP 边界网关协议（Border Gateway Protocol）是互联网上一个核心的去中心化自治路由协议，它通过维护IP路由表或前缀表来实现自治系统（AS）之间的可达性 大多数ISP使用BGP来与其他ISP创建路由连接，特大型的私有IP网络也可以使用BGP BGP的通信对端（对等实体，Peer）通过TCP（端口179）会话交换数据，BGP路由器会周期地发送19字节的保活消息来维护连接。在路由协议中，只有BGP使用TCP作为传输层协议 IBGP 内部边界网关协议。同一个AS内部的两个或多个对等实体之间运行的BGP被称为IBGP IGP 内部网关协议。同一AS内部的对等实体（路由器）之间使用的协议，它存在可扩容性问题： 一个IGP内部应该仅有数十（最多小几百）个对等实体 对于端点数，也存在限制，一般在数百（最多上千）个Endpoint级别 IBGP和IGP都是处理AS内部路由的，仍然需要IGP的原因是： IBGP之间是TCP连接，也就意味着IBGP邻居采用的是逻辑连接的方式，两个IBGP连接不一定存在实际的物理链路。所以需要有IGP来提供路由，以完成BGP路由的递归查找 <a class="read-more" href="https://blog.gmem.cc/calico">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/calico">基于Calico的CNI</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">什么是Calico</span></div>
<p>Calico为容器或虚拟机提供安全的网络连接，它创建一个扁平化的第3层网络，为每个节点分配一个可路由的IP地址。网络中的节点不需要NAT或IP隧道就可以相互通信，因此性能很好，接近于物理网络，不使用网络策略的情况下，<span style="background-color: #c0c0c0;">可能引入0.01毫秒级别的延迟，带宽影响不明显</span>）。 在需要Overlay的应用场景下，Calico可以支持IP-in-IP隧道，也可以与其它Overlay网络（例如Flannel）配合，IP-in-IP隧道会带来<span style="background-color: #c0c0c0;">较小的性能下降</span>。</p>
<p>Calico支持动态的网络安全策略（NetPolicy），你可以细粒度的控制容器、虚拟机、物理机端点之间的网络通信。</p>
<p>Calico在<span style="background-color: #c0c0c0;">每个节点</span>运行一个虚拟路由器（vRouter），<span style="background-color: #c0c0c0;">vRouter利用Linux内核自带的IP转发功能，工作负载依赖于此路由器和外部通信</span>。节点上的代理组件Felix负责根据分配到节点上的工作负载的IP地址信息为vRouter提供L3转发规则。vRouter基于BIRD实现了边界网关协议（BGP）。通过vRouter，工作负载直接基于物理网络进行通信，甚至可以被分配外网IP并直接暴露到互联网上。</p>
<div class="blog_h1"><span class="graybg">BGP术语</span></div>
<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>BGP</td>
<td>
<p>边界网关协议（Border Gateway Protocol）是互联网上一个核心的<span style="background-color: #c0c0c0;">去中心化</span>自治路由协议，它通过维护IP路由表或前缀表来实现自治系统（AS）之间的可达性</p>
<p>大多数ISP使用BGP来与其他ISP创建路由连接，特大型的私有IP网络也可以使用BGP</p>
<p>BGP的<span style="background-color: #c0c0c0;">通信对端（对等实体，Peer）</span>通过TCP（端口179）会话交换数据，BGP路由器会周期地发送19字节的保活消息来维护连接。在路由协议中，只有BGP使用TCP作为传输层协议</p>
</td>
</tr>
<tr>
<td>IBGP</td>
<td>
<p>内部边界网关协议。同一个AS内部的两个或多个对等实体之间运行的BGP被称为IBGP</p>
</td>
</tr>
<tr>
<td>IGP</td>
<td>
<p>内部网关协议。同一AS内部的对等实体（路由器）之间使用的协议，它存在可扩容性问题：</p>
<ol>
<li>一个IGP内部应该仅有数十（最多小几百）个对等实体</li>
<li>对于端点数，也存在限制，一般在数百（最多上千）个Endpoint级别</li>
</ol>
<p>IBGP和IGP都是处理AS内部路由的，仍然需要IGP的原因是：</p>
<ol>
<li>IBGP之间是TCP连接，也就意味着IBGP邻居采用的是逻辑连接的方式，两个IBGP连接不一定存在实际的物理链路。所以需要有IGP来提供路由，以完成BGP路由的递归查找</li>
<li>BGP协议本身实际上并不发现路由，BGP将路由发现的工作全部移交给了IGP协议，它本身着重于路由的控制</li>
</ol>
</td>
</tr>
<tr>
<td>EBGP</td>
<td>
<p>外部边界网关协议。归属不同的AS的对等实体之间运行的BGP称为EBGP</p>
</td>
</tr>
<tr>
<td>Border Router</td>
<td>
<p>边界路由器，在AS边界上与其他AS交换信息的路由器</p>
</td>
</tr>
<tr>
<td>AS </td>
<td>
<p>自治系统（Autonomous system），一个组织（例如ISP）管辖下的所有IP网络和路由器的整体</p>
<p>参与BGP路由的每个AS都被分配一个唯一的自治系统编号（ASN）。对BGP来说ASN是区别整个相互连接的网络中的各个网络的唯一标识。64512到65535之间的ASN编号保留给专用网络使用</p>
</td>
</tr>
<tr>
<td>Route Reflector</td>
<td>
<p>同一AS内如果有<span style="background-color: #c0c0c0;">多个路由器参与BGP路由</span>，则它们之间必须配置成<span style="background-color: #c0c0c0;">全连通的网状结构</span>——任意两个路由器之间都必须配置成对等实体。由于所需要TCP连接数是路由器数量的平方，这就导致了巨大的TCP连接数</p>
<p>为了缓解这种问题，BGP支持两种方案：Route Reflector、Confederations</p>
<p>路由反射器（Route Reflector）是AS内的一台路由器，其它所有路由器都和RR直接连接，作为RR的客户机。RR和客户机之间建立BGP连接，而客户机之间则不需要相互通信</p>
<p>RR的工作步骤如下：</p>
<ol>
<li>从非客户机IBGP对等实体学到的路由，发布给此RR的所有客户机</li>
<li>从客户机学到的路由，发布给此RR的所有非客户机和客户机</li>
<li>从EBGP对等实体学到的路由，发布给所有的非客户机和客户机</li>
</ol>
<p>RR的一个优势是配置方便，因为只需要在反射器上配置</p>
</td>
</tr>
<tr>
<td>工作负载</td>
<td>Workload，即运行在Calico节点上的虚机或容器</td>
</tr>
<tr>
<td>全互联</td>
<td>全互联网络（Full node-to-node Mesh）是指任何两个Calico节点都进行配对的L3连接模式</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">集成K8S</span></div>
<div class="blog_h2"><span class="graybg">安装Calico</span></div>
<p>关于如何快速启用基于Calico的CNI网络连接，参考<a href="/kubernetes-study-note#with-calico">Kubernetes学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">网络策略</span></div>
<p>Calico支持K8S的NetworkPolicy，下面是一个示例。</p>
<div class="blog_h3"><span class="graybg">准备</span></div>
<p>首先创建一个名字空间：</p>
<pre class="crayon-plain-tag">kubectl create ns dev
# 为新名字空间的默认账户添加imagePullSecrets
kubectl --namespace=dev edit serviceaccount default</pre>
<p>然后创建一个5实例的部署：</p>
<pre class="crayon-plain-tag">kubectl --namespace=dev run media-api --replicas=5 --image=docker.gmem.cc/media-api:1.1
kubectl --namespace=dev get pod</pre>
<p>将上述部署暴露为服务：</p>
<pre class="crayon-plain-tag">kubectl --namespace=dev expose deployment media-api --port=8800,7700
kubectl --namespace=dev get service</pre>
<p>执行类似下面的命令，确认服务能够正常响应HTTP请求：</p>
<pre class="crayon-plain-tag">curl http://media-api.dev.svc.k8s.gmem.cc:8800/media/newpub/2017-01-01</pre>
<div class="blog_h3"><span class="graybg">隔离访问</span></div>
<p>下面将上述名字空间dev隔离掉，Calico会阻止访问此名字空间中各的Pod：</p>
<pre class="crayon-plain-tag">kubectl create -f - &lt;&lt;EOF
kind: NetworkPolicy
apiVersion: extensions/v1beta1
metadata:
  name: default-deny
  namespace: dev
spec:
  podSelector:
    # 默认拒绝任何访问
    matchLabels: {}
EOF</pre>
<p>这是再次执行curl命令，你无法得到响应。</p>
<div class="blog_h3"><span class="graybg">准许访问</span></div>
<p>下面我们再创建一个网络策略， 允许API网关层访问上面的服务：</p>
<pre class="crayon-plain-tag">kubectl create -f - &lt;&lt;EOF
kind: NetworkPolicy
apiVersion: extensions/v1beta1
metadata:
  name: allow-ingress-nginx
  namespace: dev
spec:
  podSelector:
    matchLabels:
      # 下面的标签是自动添加的
      run: media-api
  # 允许来自apigateway层的入站连接
  ingress:
    - from:
      - podSelector:
          matchLabels:
            tier: apigateway
EOF</pre>
<div class="blog_h3"><span class="graybg">删除策略</span></div>
<p>要删除网络策略，执行：</p>
<pre class="crayon-plain-tag">kubectl --namespace=dev delete networkpolicy default-deny</pre>
<div class="blog_h3"><span class="graybg">网络策略列表</span></div>
<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>
<pre class="crayon-plain-tag">apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: advanced-policy-demo
spec:
  # 下面的选择器匹配所有Pod
  podSelector:
    matchLabels: {}
  # 策略类型：入站
  policyTypes:
  - Ingress</pre>
</td>
</tr>
<tr>
<td>禁止对特定Pod的入站连接</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    # 允许对Nginx的入站访问
    matchLabels:
      run: nginx
  ingress:
    - from:
      - podSelector:
          matchLabels: {}</pre>
</td>
</tr>
<tr>
<td>禁止所有出站连接</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    matchLabels: {}
  policyTypes:
  - Egress</pre>
</td>
</tr>
<tr>
<td>允许进行DNS查询的出站连接</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    matchLabels: {}
  policyTypes:
  - Egress
  # 允许针对kube-system名字空间中的Pod的53端口进行UDP访问
  egress:
  - to:
    # 名字空间选择器
    - namespaceSelector:
        matchLabels:
          name: kube-system
    # 53端口
    ports:
    - protocol: UDP
      port: 53</pre>
</td>
</tr>
<tr>
<td>允许对Nginx的出站访问</td>
<td>
<pre class="crayon-plain-tag">spec:
  podSelector:
    matchLabels: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          run: nginx</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">静态IP</span></div>
<p>通常你应该使用Service（具有集群静态IP）而不是尝试为Pod指定静态IP。</p>
<p>如果非要这么做，Calico为Pod提供了一些注解：</p>
<pre class="crayon-plain-tag"># 分配给当前Pod的IP地址列表，请求的地址必须位于Calico的IPAM的地址池中
annotations:
    "cni.projectcalico.org/ipAddrs": "[\"192.168.0.1\"]"

# 分配给当前Pod的IP地址列表，绕过IPAM
# 你可以分配任何地址给Pod，但是仅仅当地址位于Calico的地址空间中时，Calico才负责分配路由
# 注意IP冲突问题
annotations:
    "cni.projectcalico.org/ipAddrsNoIpam": "[\"10.0.0.1\"]"</pre>
<div class="blog_h2"><span class="graybg">高可用</span></div>
<p>Calico依赖于Etcd来存放配置信息，缺省情况下它会容器化安装单实例的Etcd。你可以修改其Configmap，指定使用外部的、高可用的Etcd：</p>
<pre class="crayon-plain-tag">kind: ConfigMap
apiVersion: v1
metadata:
  name: calico-config
  namespace: kube-system
data:
  # 可以和K8S共享Etcd集群
  etcd_endpoints: "http://10.0.1.1:2379,http://10.0.2.1:2379,http://10.0.3.1:2379" </pre>
<div class="blog_h1"><span class="graybg">外部连接</span></div>
<div class="blog_h2"><span class="graybg">出站连接</span></div>
<p>所谓出站外部连接，是指从Calico端点到位于Calico集群外部的目的主机的连接。</p>
<p>最简单的实现外部出站连接的方式是为Calico池开启NAT：</p>
<pre class="crayon-plain-tag"># 查看默认IP池的配置
# calicoctl get ipPool default-ipv4-ippool -o yaml
# 输出：
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  creationTimestamp: 2018-02-12T11:33:04Z
  name: default-ipv4-ippool
  resourceVersion: "5"
  uid: 7d7a9461-0fe8-11e8-a715-deadbeef00a0
spec:
  cidr: 192.168.0.0/16
  ipipMode: Always
  # 已经启用出站NAT
  natOutgoing: true</pre>
<p>你可以针对某个IP池进行NAT设置。</p>
<div class="blog_h2"><span class="graybg">入站连接</span></div>
<p>所谓入站外部连接，是指从Calico集群外部主机发起的，针对Calico端点的连接。实现入站外部连接的方式主要由两种。</p>
<div class="blog_h3"><span class="graybg">BGP Peering</span></div>
<p>利用BGP配对，将<span style="background-color: #c0c0c0;">外部网络的基础设施配对到Calico集群中的某些节点</span>。这需要外部网络中包含支持BGP协议的交换机或者路由器，该路由器作为访问Calico集群内部端点的网关。</p>
<p>如果网络规模较小，你可以让外部路由器和所有Calico节点之间建立BGP会话；如果网络规模较大，可能需要利用RR来创建一个第三层拓扑。 </p>
<p>下面是一个基于BIRD（支持BGP协议的软路由）的示例：Calico集群使用AS号65000，外部网络使用AS号65001，Calico集群内部节点10.0.0.100配对到外部路由器（BIRD）10.0.0.1，Calico内部使用节点全互联模式：</p>
<ol>
<li>Calico BGP Peer配置文件：<br />
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-xenial-100-zircon
spec:
  peerIP: 10.0.0.1
  node: xenial-100
  asNumber: 65001 </pre>
</li>
<li>BIRD配置文件：<br />
<pre class="crayon-plain-tag">protocol bgp xenial100 {
    description "10.0.0.100";
    local as 65001;
    neighbor 10.0.0.100 as 65000;
    source address 10.0.0.1;
    graceful restart;
    import all;
    export all;
}</pre>
</li>
</ol>
<p>按照上面方式配置后，可以在BIRD上看到学习到的路由：</p>
<pre class="crayon-plain-tag"># sudo birdc show route
172.27.154.192/26  via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.187.192/26  via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.97.64/26    via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.121.64/26   via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.41.128/26   via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]
172.27.61.64/26    via 10.0.0.100 on virbr0 [xenial100 14:59:05] * (100) [AS65000i]

# 同步的内核路由表
# route
172.27.41.128   xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.61.64    xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.97.64    xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.121.64   xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.154.192  xenial-100      255.255.255.192 UG    0      0        0 virbr0
172.27.187.192  xenial-100      255.255.255.192 UG    0      0        0 virbr0 </pre>
<p>此外，下一章的RR示例也可以支持入站外部链接。</p>
<div class="blog_h3"><span class="graybg">编排器特定方式</span></div>
<p>Calico支持多种容器编排器（Orchestrator）特有的入站连接方式，例如Kubernetes的Service IP。</p>
<div class="blog_h3"><span class="graybg">外部访问K8S服务IP</span></div>
<p>本节内容和Calico无直接关系，Calico仅仅为K8S提供容器连接性，它不知道K8S的服务（Service）是何物。</p>
<p>K8S中Service IP仅仅在集群内部可以访问，集群内节点或者Pod依赖Kube Proxy来访问Service IP。<span style="background-color: #c0c0c0;">外部访问Service的规范化方式</span>是，使用NodePort（可以配合LoadBalancer），向外部暴露服务。</p>
<p>但是，要直接暴露Service的IP地址到集群外部也是可以实现的。你只需要配置适当的路由规则，将Service IP子网的网关设置为K8S集群的任意节点即可：</p>
<pre class="crayon-plain-tag"># K8S 服务网段为10.96.0.0/12，10.0.0.100为集群中一个节点
route add -net 10.96.0.0 netmask 255.240.0.0 gw 10.0.0.100

# 设置完路由后，尝试连接集群的DNS服务：telnet 10.96.0.10 53，不要ping，不支持</pre>
<div class="blog_h1"><span class="graybg">使用RR</span></div>
<p>较大规模的网络拓扑中，启用节点全互联需要大量的TCP连接，可以考虑引入Router Refactor并禁用全互联。实践中<span style="background-color: #c0c0c0;">“较大规模”网络拓扑包含50+节点</span>，但是也有超过100节点的正常运作的全互联网络。</p>
<p>BIRD是一个BGP实现，支持完全的动态路由，本章以BIRD软路由器作为RR。</p>
<div class="blog_h2"><span class="graybg">安装BIRD</span></div>
<pre class="crayon-plain-tag">sudo add-apt-repository ppa:cz.nic-labs/bird
sudo apt-get update
sudo apt-get install bird</pre>
<div class="blog_h2"><span class="graybg">配置BIRD</span></div>
<div class="blog_h3"><span class="graybg">IPv4</span></div>
<pre class="crayon-plain-tag"># 日志配置
log syslog { debug, trace, info, remote, warning, error, auth, fatal, bug };
log stderr all;

# 当前路由器（RR）全局唯一标识，通常设置为路由器的IP地址
router id 10.0.0.1;

filter import_kernel {
    if ( net != 0.0.0.0/0 ) then {
        accept;
    }
    reject;
}

# 所有协议的全局性调试开关
debug protocols all;

# 伪协议，不是和网络中其它路由器通信，而是每60秒将BIRD的路由表同步到OS内核
protocol kernel {
    scan time 60;
    import none;  # 从内核路由表导入路由条目
    export all;   # 将BIRD的路由同步给内核
}
 
# 伪协议，每2秒监控网络接口的信息
protocol device {
	scan time 2;
}


# 为拓扑中每个节点（每个连接到此RR的Peer）添加以下内容
protocol bgp xenial100 {
    # 可选的描述
    description "10.0.0.100";

    # 声明本路由器所属的AS
    #       IP地址 如果不使用179 AS号
    # local [ip] [port number] [as number]
    local as 65000;

    # BGP实例，指定当前路由器与自通信的邻居路由器的信息
    #                                            如果和当前路由器AS一样则自动切换为IBGP
    #                                                        可以不指定AS号而指定internal
    #                指定网络前缀而非精确的IP，则启用动态BGP行为，当前路由器会在BGP端口监听，当匹配
    #                前缀的BGP连接到来后，spawn一个BGP实例处理它。普通BGP实例/动态BGP实例可混合使用
    # neighbor [ip | range prefix] [port number] [as number] [internal|external]
    # neighbor range 10.0.3.0/24 as 65000;
    neighbor 10.0.0.100 as 65000;
    # 提示此邻居和本路由器（的某个接口）是直接相连的
    direct;
    # 本路由器作为路由反射器，邻居作为RR客户端
    rr client;
    # 当一个BGP speaker重启/崩溃后，邻居会丢弃从它接收到的所有路径。即使该speaker的转发平面仍然
    # 继续工作，也会出现封包转发被干扰的情况。该选项用于缓和这个问题
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial101 {
    description "10.0.0.101";
    local as 65000;
    neighbor 10.0.0.101 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial102 {
    description "10.0.0.102";
    local as 65000;
    neighbor 10.0.0.102 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial103 {
    description "10.0.0.103";
    local as 65000;
    neighbor 10.0.0.103 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial104 {
    description "10.0.0.104";
    local as 65000;
    neighbor 10.0.0.104 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}
protocol bgp xenial105 {
    description "10.0.0.105";
    local as 65000;
    neighbor 10.0.0.105 as 65000;
    direct;
    rr client;
    graceful restart;
    import all;
    export all;
}</pre>
<p>配置完毕后，重启BIRD：<pre class="crayon-plain-tag">sudo service bird restart</pre></p>
<p>很快，你就可以看到RR从Calico节点同步了路由信息过来：</p>
<pre class="crayon-plain-tag">Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         localhost       0.0.0.0         UG    0      0        0 wlan1
10.0.0.0        *               255.255.0.0     U     0      0        0 virbr0
172.17.0.0      *               255.255.0.0     U     0      0        0 docker0
172.18.0.0      *               255.255.0.0     U     0      0        0 docker_gwbridge
172.21.0.0      *               255.255.0.0     U     0      0        0 br-29f4509ebfd6
172.27.0.0      xenial-101      255.255.255.192 UG    0      0        0 virbr0
172.27.0.0      xenial-101      255.255.0.0     UG    0      0        0 virbr0
172.27.0.0      xenial-100      255.255.0.0     UG    0      0        0 virbr0
192.168.142.0   *               255.255.254.0   U     9      0        0 wlan1</pre>
<div class="blog_h2"><span class="graybg">配置Calico节点</span></div>
<p>首先禁用全互联，查看Calico的BGP选项，如果启用全互联则禁用之：</p>
<pre class="crayon-plain-tag"># calicoctl get bgpconfig -o yaml &gt; /tmp/bgp.yaml

apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
  kind: BGPConfiguration
  metadata:
    creationTimestamp: 2018-02-14T02:58:56Z
    name: default
    resourceVersion: "790"
    uid: ff92b98a-1132-11e8-937a-deadbeef00a0
  spec:
    asNumber: 65000
    logSeverityScreen: Info
    # 这里设置为false
    nodeToNodeMeshEnabled: false   
kind: BGPConfigurationList
metadata:
  resourceVersion: "826"
# calicoctl replace -f /tmp/bgp.yaml</pre>
<p>然后，添加一个全局的Peer，指向先前创建的RR：</p>
<pre class="crayon-plain-tag">cat &lt;&lt; EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-global-1
spec:
  peerIP: 10.0.0.1
  asNumber: 65000
EOF

# calicoctl get BGPPeer -o yaml &gt; /tmp/bgpeer.yaml
# calicoctl replace -f /tmp/bgpeer.yaml</pre>
<p>现在，登陆到某个Calico节点，查看其配对情况，应该仅仅和10.0.0.1进行了配对：</p>
<pre class="crayon-plain-tag"># calicoctl node status

Calico process is running.

IPv4 BGP status
+--------------+-----------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE |  SINCE   |    INFO     |
+--------------+-----------+-------+----------+-------------+
| 10.0.0.1     | global    | up    | 03:43:38 | Established |
+--------------+-----------+-------+----------+-------------+

IPv6 BGP status
No IPv6 peers found.</pre>
<p>可以尝试在集群内连接某个Pod，如果可以连接，说明配置成功。 </p>
<div class="blog_h2"><span class="graybg">更简单的方法</span></div>
<p>现在，你可以在任何物理节点上，执行calico node run命令，运行一个容器，将节点加入Calico CNI网络：</p>
<pre class="crayon-plain-tag">sudo calicoctl node run --ip-autodetection-method interface=virbr0</pre>
<p>这样的节点可以直接作为路由反射器使用，也可以直连Pod。</p>
<div class="blog_h2"><span class="graybg">集群内RR</span></div>
<p>从3.3开始，任何Calico节点均可以作为RR来运行，不需要基础设施或外部的专用RR节点。</p>
<div class="blog_h3"><span class="graybg">节点作为RR</span></div>
<p>最简单的配置，为Node加上routeReflectorClusterID字段即可：</p>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: Node
metadata:
  name: node-hostname
  # 标签影响配对
  labels:
    routeReflector: 10.0.0.1
spec:
  bgp:
    asNumber: 64512
    ipv4Address: 10.244.0.1/24
    ipv6Address: 2001:db8:85a3::8a2e:370:7334/120
    # 提示此节点是一个RR（同时也是一个普通Calico节点）
    # 将它的BGP Peers看作RR客户端，除非节点具有相同的routeReflectorClusterID，
    # 这会在BGP协议层次上产生一系列影响
    routeReflectorClusterID: 10.0.0.1</pre>
<div class="blog_h3"><span class="graybg">配对设置</span></div>
<p>3.3为BGPPeer资源添加了字段：</p>
<ol>
<li>nodeSelector：配对应该在该选择器匹配的节点上发生。原先只能指定节点名称</li>
<li>peerSelector：配对应该在该选择器匹配的RR上发生。原先只能指定RR的IP</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: cluster1
spec:
  # 所有节点
  nodeSelector: "all()"
  # 所有具有routeReflector的RR
  peerSelector: "has(routeReflector)"</pre>
<p>一种典型的BGP拓扑划分方式：</p>
<ol>
<li>将节点分组，每个组包含一个RR</li>
<li>所有RR进行全互联 </li>
</ol>
<div class="blog_h1"><span class="graybg">配置</span></div>
<div class="blog_h2"><span class="graybg"><a id="BGPPeer"></a>BGP对等实体</span></div>
<div class="blog_h3"><span class="graybg">概念</span></div>
<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>Node-to-node mesh</td>
<td>
<p>使用full node-to-node mesh选项，可以自动化的配置所有Calico节点之间的对等实体。该选项默认启用，每个Calico节点都自动和任何其它节点进行BGP Peering</p>
<p>适用于几十个节点的规模</p>
</td>
</tr>
<tr>
<td>Global BGP peers</td>
<td>全局BGP Peer是一个BGP代理，它和网络中所有其它Calico节点进行Peering，典型应用场景是中等规模的部署，所有节点运行在同一个L2网络中，每个节点和RR（一个或一组）配对</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">AS号配置</span></div>
<p>默认AS号为64512。如果所有节点在一个AS内部但是你需要定制AS号（例如需要同边界路由器进行Peering），执行以下命令：</p>
<pre class="crayon-plain-tag"># 检查默认BGP配置资源是否存在
calicoctl get bgpconfig default

# 如果不存在，则：
cat &lt;&lt; EOF | calicoctl create -f -
 apiVersion: projectcalico.org/v3
 kind: BGPConfiguration
 metadata:
   name: default
 spec:
   logSeverityScreen: Info
   nodeToNodeMeshEnabled: false   # 禁用全节点BGP互联。如果没有进行适当配置，一旦禁用立即无法连通现有的Pod
   asNumber: 65000                # 使用另一个AS号
EOF

# 如果存在，则先Dump在Replace
calicoctl get bgpconfig default -o yaml &gt; bgp.yaml
calicoctl replace -f bgp.yaml</pre>
<div class="blog_h3"><span class="graybg">禁用全互联</span></div>
<p>修改sepc.nodeToNodeMeshEnabled即可。</p>
<p>如果你准备从零开始构建网络，并且不希望使用全互联，可以禁用之。如果你准备从全互联切换到其它方式，则需要预先配置好Peering然后再禁用全互联，以保证系统持续的提供对外服务。</p>
<div class="blog_h3"><span class="graybg">全局对等实体</span></div>
<p>如果你的网络拓扑中存在可以和任意Calico节点配对（Peering）的BGP Speakers，这种BGP Speaker被称为全局对等实体（Global Peer）。一旦配置了全局对等实体，<span style="background-color: #c0c0c0;">Calico就会自动将所有节点与之配对</span>。</p>
<p>全局对等实体以下场景中有价值：</p>
<ol>
<li>添加了一个边界路由器，将其配对到全互联网络中</li>
<li>配置使用一个或两个RR的Calico网络，这种情况下每个节点都应该和RR配对，全互联应该禁用</li>
</ol>
<p>配置示例：</p>
<pre class="crayon-plain-tag">cat &lt;&lt; EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-global-3040
spec:
  peerIP: 192.20.30.40
  asNumber: 64567
EOF

# 移除全局对等实体
calicoctl delete bgppeer bgppeer-global-3040</pre>
<div class="blog_h3"><span class="graybg">节点对等实体</span></div>
<p>BGP网络拓扑更加复杂的情况下，你可能考虑针对每个Calico节点配置对等实体。示例：</p>
<pre class="crayon-plain-tag">cat &lt;&lt; EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
  name: bgppeer-node-aabbff
spec:
  # 将位于AS 64514中的Peer aa:bb::ff 和 Calico节点node1配对
  peerIP: aa:bb::ff
  node: node1
  asNumber: 64514
EOF

# 查看配对
calicoctl get bgpPeer bgppeer-node-aabbff
# 移除配对
calicoctl delete bgppeer bgppeer-node-aabbff</pre>
<div class="blog_h3"><span class="graybg">检查配对情况</span></div>
<p>要检查某个节点的BGP Peer，执行命令：</p>
<pre class="crayon-plain-tag"># SSH到node1，然后执行下面的命令，可以看到所有和node1配对的实体
calicoctl node status

# Calico process is running.

# IPv4 BGP status
# +--------------+-------------------+-------+----------+-------------+
# | PEER ADDRESS |     PEER TYPE     | STATE |  SINCE   |    INFO     |
# +--------------+-------------------+-------+----------+-------------+
# | 10.0.0.101   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.102   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.103   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.104   | node-to-node mesh | up    | 15:53:18 | Established |
# | 10.0.0.105   | node-to-node mesh | up    | 15:53:18 | Established |
# +--------------+-------------------+-------+----------+-------------+

# IPv6 BGP status
# No IPv6 peers found. </pre>
<div class="blog_h2"><span class="graybg"><a id="BGPConfiguration"></a>BGP集群选项</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
  # 资源的唯一性名称
  name: default
spec:
  # 全局日志级别：Debug, Info, Warning, Error, Fatal
  logSeverityScreen: Info
  # 是否启用节点全互联
  nodeToNodeMeshEnabled: true
  # AS号
  asNumber: 65000</pre>
<div class="blog_h2"><span class="graybg"><a id="Felix"></a>Felix配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  # 资源的唯一性名称
  name: default
spec:
  # 指定Felix操控内核顶级Iptable规则链的方式
  # Insert 默认，较安全，可以防止Calico的规则被Bypass
  # Append 需要注意规则链中更前面的规则可能导致Calico的规则被跳过
  chainInsertMode: Insert
  # 从工作负载发送到其所属宿主机（通过端点egress策略之后）的流量的默认处理方式，可选值Drop, Return, Accept
  # 默认情况下Calico阻止从工作负载到其宿主机的流量，实现手段是使用Iptables规则的DROP目标
  # Accept：Calico在处理完工作负载端点的Egress Policy之后，无条件的允许这种流量通过
  # Return：使用INPUT链中的其它规则进行处理。Calico默认在INPUT链顶部插入规则，使用该选项后，Calico在处理
  #         完工作负载端点的Egress Policy之后，即把封包归还给INPUT链下一条规则处理
  defaultEndpointToHostAction: Drop
  # UDP/TCP的协议端口对，Felix总是允许这些宿主机端点上这些端口的入站流量
  # 这可以防止意外的错误配置导致宿主机断开和外部的连接，默认允许SSH、etcd、BGP、DHCP
  failsafeInboundHostPorts:
  - protocol: tcp
    port: 22
  - protocol: udp
    port: 68
  - protocol: tcp
    port: 179
  - protocol: tcp
    port: 2379
  - protocol: tcp
    port: 2380
  - protocol: tcp
    port: 6666
  - protocol: tcp
    port: 6667
  # UDP/TCP的协议端口对，Felix总是允许这些宿主机端点上针对这些端口的出站流量
  failsafeOutboundHostPorts:
  - protocol: udp
    port: 53
  - protocol: udp
    port: 67
  - protocol: tcp
    port: 179
  - protocol: tcp
    port: 2379
  - protocol: tcp
    port: 2380
  - protocol: tcp
    port: 6666
  - protocol: tcp
    port: 6667
  # 设置为true则允许Felix运行在具有RPF的系统上
  ignoreLooseRPF: false
  # Felix解析宿主机端点时，需要排除的网络接口列表，逗号分隔
  # 默认值是排除K8S内部使用的设备kube-ipvs0
  interfaceExclude: kube-ipvs0
  # 用于区分工作负载端点和宿主机端点的网络接口名前缀
  interfacePrefix: cali
  # 是否在宿主机上配置一个IPinIP网络接口
  # 如果你通过calico/node或calicoctl配置IPIP-enabled pool会自动设置为true
  ipipEnabled：false
  # 上述隧道接口的MTU
  ipipMTU:1440
  # 是否启用IPv6支持
  ipv6Support: false</pre>
<div class="blog_h2"><span class="graybg"><a id="HostEndpoint"></a>HostEndpoint配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: HostEndpoint
metadata:
  # 宿主机端点名称
  name: some.name
  # 标签，用于应用对应的策略
  labels:
    type: production
spec:
  # 端点对应的网络接口
  interfaceName: eth0
  # 端点所在的宿主机
  node: myhost
  # 期望和此网络接口对应的IP地址
  expectedIPs:
  - 192.168.0.1
  - 192.168.0.2
  # 配置，用于应用对应的策略
  profiles:
  - profile1
  - profile2
  # 命名端口，可以在Policy Rule中引用
  ports:
  - name: some-port
    port: 1234
    protocol: TCP
  - name: another-port
    port: 5432
    protocol: UDP</pre>
<div class="blog_h2"><span class="graybg"><a id="WorkloadEndpoint"></a>WorkloadEndpoint</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: WorkloadEndpoint
metadata:
  # 资源名称
  name: node1-k8s-frontend--5gs43-eth0
  # 资源所属名字空间
  namespace: default
  # 资源的标签
  labels:
    app: frontend
    projectcalico.org/namespace: default
    projectcalico.org/orchestrator: k8s
spec:
  # 此端点所属的工作负载的名字
  workload: nginx
  # 工作负载所属的节点
  node: node1
  # 创建此端点的orchestrator
  orchestrator: k8s
  # 容器网络接口名
  endpoint: eth0
  # 工作负载端点的CNI容器ID
  containerID: 1337495556942031415926535
  # 此工作负载端点所在的Pod名称
  pod: my-nginx-b1337a
  # 在宿主机那一端，对接到工作负载的网络接口名
  interfaceName: cali0ef24ba
  # 网络接口的MAC地址
  mac: ca:fe:1d:52:bb:e9
  # 分配到网络接口的CIDR
  ipNetworks:
  - 192.168.0.0/16
  # 此工作负载的出站流量的网关
  ipv4Gateway: 192.168.0.1
  # 分配到此端点的Calico Profile
  profiles:
  - profile1
  # 命名端口列表
  ports:
  - name: some-port
    port: 1234
    protocol: tcp
  - name: another-port
    port: 5432
    protocol: udp
  # 此端点的NAT规则
  ipNATs:
    # 内部IP地址
    internalIP:
    # 外部IP地址
    externalIP:</pre>
<div class="blog_h2"><span class="graybg"><a id="IPPool"></a>IPPool配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: my.ippool-1
spec:
  # IP地址范围，端点IP从中分配
  cidr: 10.1.0.0/16
  # 何时使用IPinIP模式，Always, CrossSubnet, Never
  ipipMode: CrossSubnet
  # 如果启用，则利用该池中IP的容器的出站流量被IP遮掩
  natOutgoing: true
  # 如果禁用，则Calico IPAM不会从该池中分配IP地址
  disabled: false</pre>
<div class="blog_h2"><span class="graybg"><a id="Node"></a>Node配置</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: Node
metadata:
  name: node-hostname
spec:
  bgp:
    # 此节点所属的AS，不指定则使用全局默认AS号
    asNumber: 64512
    # 此节点的IP和子网，会导出，作为该节点上的Calico端点的next-hop
    ipv4Address: 10.244.0.1/24
    ipv6Address: 2001:db8:85a3::8a2e:370:7334/120
    # IP-in-IP隧道中此节点的IP地址
    ipv4IPIPTunnelAddr: 192.168.0.1
  OrchRefs:
    - nodeName: node-hostname
      orchestrator: k8s</pre>
<div class="blog_h2"><span class="graybg"><a id="NetworkPolicy"></a>NetworkPolicy </span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: allow-tcp-6379
  namespace: production
spec:
  # 匹配的端点
  selector: role == 'database'
  # 此策略针对的流量方向
  types:
  - Ingress
  - Egress
  # 入站规则列表（有序）
  ingress:
  # 允许来自frontend的TCP/6379流量
    # 匹配规则时的行为。可选值：Allow, Deny, Log, Pass
  - action: Allow
    # 匹配协议列表，可选值TCP, UDP, ICMP, ICMPv6, SCTP, UDPLite, 1-255
    protocol: TCP
    # 不匹配协议列表，可选值TCP, UDP, ICMP, ICMPv6, SCTP, UDPLite, 1-255
    notProtocol: UDP
    # 源匹配参数
    source:
      # 根据端点选择器匹配
      selector: role == 'frontend'
      # 根据端点所属CIDR匹配
      nets: 172.21.0.0/16
      # 不匹配的CIDR
      notNets: 172.21.0.0/16
      # 根据名字空间匹配，如果指定，则仅仅目标名字空间中的工作负载端点才能匹配
      namespaceSelector: env = 'dev'
      # 根据端点进行匹配
      ports: 0
      notPorts: 0
    # 目的匹配参数
    destination:
      ports:
      - 6379
    # ICMP匹配规则
    icmp:
      type: 0
      code: 0
    # ICMP不匹配规则
    notICMP:
      # ICMP类型， 0-254
      type: 0
      # ICMP代码， 0-255
      code: 0
  # 出站规则列表
  egress:
  - action: Allow</pre>
<p>Calico选择器语法：</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>all()</td>
<td>匹配所有资源</td>
</tr>
<tr>
<td>k == ‘v’</td>
<td>匹配标签k的值为v的资源</td>
</tr>
<tr>
<td>k != ‘v’</td>
<td>匹配具有标签k，且其值不为v的资源</td>
</tr>
<tr>
<td>has(k)</td>
<td>匹配具有标签k的资源</td>
</tr>
<tr>
<td>!has(k)</td>
<td>匹配不具有标签k的资源</td>
</tr>
<tr>
<td>k in { ‘v1’, ‘v2’ }</td>
<td>匹配标签k的值为v1或v2的资源</td>
</tr>
<tr>
<td>k not in { ‘v1’, ‘v2’ }</td>
<td>匹配标签k的值不为v1或v2的资源，或者没有标签k的资源</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"> <a id="Profile"></a>Profile</span></div>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: Profile
metadata:
  name: profile1
  # 使用此Profile的端点，自动添加如下标签
  labels:
    profile: profile1
spec:
  # 此Profile的网络策略
  ingress:
  - action: Deny
    source:
      nets:
      - 10.0.20.0/24
  - action: Allow
    source:
      selector: profile == 'profile1'
  egress:
  - action: Allow</pre>
<div class="blog_h1"><span class="graybg">calicoctl</span></div>
<p>calicoctl是一个命令行工具，使用它可以创建、修改、删除Calico对象。你可以在任何能够访问Calico数据库的主机上使用该命令。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">cd /usr/local/bin
curl -O -L https://github.com/projectcalico/calicoctl/releases/download/v2.0.0/calicoctl
chmod +x calicoctl</pre>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>很多calicoctl命令都需要访问Calico数据库，你需要通过配置文件来指定数据库的信息：</p>
<pre class="crayon-plain-tag">apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
  datastoreType: "etcdv3"
  etcdEndpoints: "http://10.96.232.136:6666"</pre>
<p>注意：在Kubeadm部署方式下，Calico不会使用K8S的etcd，而是自己创建一个，执行下面的命令可以查看：</p>
<pre class="crayon-plain-tag">kubectl --namespace=kube-system get service
# calico-etcd ClusterIP 10.96.232.136 &lt;none&gt; 6666/TCP 1h</pre>
<p>如果配置正确，下面的命令将会返回节点列表：</p>
<pre class="crayon-plain-tag">calicoctl get nodes
# NAME         
# xenial-100   
# xenial-101   
# xenial-102   
# xenial-103   
# xenial-104   
# xenial-105</pre>
<div class="blog_h2"><span class="graybg">子命令</span></div>
<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 class="blog_h3">create</td>
<td>根据指定的文件名或者从标准输入来创建资源</td>
</tr>
<tr>
<td class="blog_h3">replace</td>
<td>替换资源</td>
</tr>
<tr>
<td class="blog_h3">apply</td>
<td>应用资源：如果资源不存在则创建之，如果存在则替换之</td>
</tr>
<tr>
<td class="blog_h3">delete</td>
<td>删除资源</td>
</tr>
<tr>
<td class="blog_h3">get</td>
<td>
<p>获取资源信息，支持的资源包括：</p>
<p style="padding-left: 30px;">bgpConfiguration<br />bgpPeer<br />felixConfiguration<br />globalNetworkPolicy<br />hostEndpoint<br />ipPool<br />networkPolicy<br />node<br />profile<br />workloadEndpoint</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 把默认IP池的配置输出为yaml
calicoctl get ippool -o yaml</pre>
</td>
</tr>
<tr>
<td class="blog_h3">convert</td>
<td>转换不同API版本的配置文件</td>
</tr>
<tr>
<td class="blog_h3">ipam</td>
<td>IP地址管理器相关命令：<br />
<pre class="crayon-plain-tag"># 释放一个原先被IPAM分配的IP地址，释放完毕后此IP地址可以被重用
calicoctl ipam release --ip=192.168.1.2 

# 显示一个IP地址的信息，例如是否被分配，是否被用户或IPAM保留
calicoctl ipam show --ip=192.168.1.1</pre>
</td>
</tr>
<tr>
<td class="blog_h3">node run</td>
<td>
<p>注意：节点的很多配置可以<a href="https://docs.projectcalico.org/v3.2/reference/node/configuration#ip-setting">通过环境变量提供</a>
<p>在当前节点上启动一个calico/node容器实例，以提供Calico网络、网络策略的支持：</p>
<pre class="crayon-plain-tag"># ip/ip6 当前节点的路由IP地址，如果不指定，使用node资源上配置的值
#        如果没有对应的node资源，则尝试自动检测IP地址，如果设置为autodetect则在
#        节点每次启动时强制检测
# as     使用的AS号，如果不指定则使用全局AS号
calicoctl node run [--ip=&lt;IP&gt;] [--ip6=&lt;IP6&gt;] [--as=&lt;AS_NUM&gt;]
# 节点名称，默认为主机名
                     [--name=&lt;NAME&gt;]
# IP地址自动检测方法
# first-found 使用第一个发现的IP地址，如果具有多网卡不建议使用。默认
# can-reach=ip/hostname 使用能到达目标的网络接口
# interface=regrex 网络接口名字正则式
# skip-interface=regrex 排除的网络接口名字正则式
# 示例：
#   --ip-autodetection-method interface=eth.*
#   --ip-autodetection-method interface=eth0
                     [--ip-autodetection-method=&lt;IP_AUTODETECTION_METHOD&gt;]
                     [--ip6-autodetection-method=&lt;IP6_AUTODETECTION_METHOD&gt;]
# 日志存储路径，默认/var/log/calico
                     [--log-dir=&lt;LOG_DIR&gt;]
                     [--node-image=&lt;DOCKER_IMAGE_NAME&gt;]
# BGP后端，gobgp目前试验阶段
                     [--backend=(bird|gobgp|none)]
# 指定配置文件，默认/etc/calico/calicoctl.cfg
                     [--config=&lt;CONFIG&gt;]
# 启动时不创建默认IP池
                     [--no-default-ippools]
                     [--dryrun]
# 执行适当的命令，以配合init system
                     [--init-system]
# 禁用Docker网络
                     [--disable-docker-networking]
# Docker容器中网络接口的名字前缀
                     [--docker-networking-ifprefix=&lt;IFPREFIX&gt;]
                     [--use-docker-networking-container-labels]


# 示例
calicoctl node run --node-image=docker.gmem.cc/calico/node:latest --ip-autodetection-method interface=virbr0</pre>
</td>
</tr>
<tr>
<td>node status</td>
<td>检查节点的状态</td>
</tr>
<tr>
<td>node diags</td>
<td>收集Calico节点的诊断信息</td>
</tr>
<tr>
<td>node checksystem</td>
<td>检查节点内核，是否可以作为Calico节点</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">资源类型</span></div>
<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 class="blog_h3"><a href="#Node">node</a></td>
<td>
<p>表示运行Calico的节点，当添加一个主机到Calico集群中后，你需要创建相应的Node资源
<p>当启动一个Calico节点时，它的名称必须和此资源的名称一致</p>
<p>默认情况下，启动一个calico/node实例会自动创建Node资源，使用机器的hostname作为节点名称</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#BGPPeer">bgpPeer</a></td>
<td>
<p>BGP Peer资源代表Calico集群中某个节点与之配对的远程BGP Peer</p>
<p>使用BGP Peer你可以将Calico Newtwork和数据中心结构（例如ToR）连接起来</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#BGPConfiguration">bgpConfiguration</a></td>
<td>
<p>表示集群的BGP相关的选项</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#Felix">felixConfiguration</a></td>
<td>
<p>表示集群的Felix相关的选项</p>
<p>Felix是运行在任何提供端点（Endpoint）的机器（运行VM或容器的节点）上的守护程序，它负责编程式的路由和ACL，以及为机器上端点提供预期连接性所需的任何东西</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#HostEndpoint">hostEndpoint</a></td>
<td>
<p>表示运行Calico的主机上的一个网络接口</p>
<p>每个HostEndpoint可以包含一系列的标签、一个Profile列表，Calico基于这些来应用策略到网络接口。如果没有任何标签、Profile则Calico不会应用任何策略</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#WorkloadEndpoint">workloadEndpoint</a></td>
<td>
<p>表示将基于Calico实现网络连接的VM、容器，连接到它们的宿主机的网络接口</p>
<p>WorkloadEndpoint是具有名字空间的资源，只有相同名字空间内定义的networkPolicy才能应用到其上</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#IPPool">ipPool</a></td>
<td>
<p>表示一个IP集合，工作负载端点的IP从此集合中分配</p>
<p>IP-in-IP隧道可以用在网络结构（network fabric ）强制进行Src/Dest地址检查、并丢弃无法识别的地址的流量的场景下。在某些公有云环境下，你可能无法完全控制网络，特别是<span style="background-color: #c0c0c0;">无法进行网络路由器和Calico节点之间的BGP配对、各Calico节点也不能L2直连</span>，使用IP-in-IP封装可以确保跨工作负载（Inter-workload）流量正常传输</p>
<p>当启用IP-in-IP模式时，Calico在路由到IP池范围内的工作负载IP时，会对IP封包进行一次包装</p>
<p>设置ipipMode=Always，则<span style="background-color: #c0c0c0;">从任何启用Calico的主机发往任何基于Calico网络的容器、虚拟机的流量</span>，都会进行包装</p>
<p>设置ipipMode=CrossSubnet，则可以在下面的场景下优化性能：</p>
<ol>
<li>AWS multi-AZ部署</li>
<li>多组L2直连的Node，通过路由器建立L3连接</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">globalNetworkPolicy</td>
<td>全局性的网络策略，不具有名字空间，应用到任何名字空间中的工作负载端点、主机端点</td>
</tr>
<tr>
<td class="blog_h3"><a href="#NetworkPolicy">networkPolicy</a></td>
<td>
<p>应用到匹配标签选择器的端点的网络策略，网络策略是命名空间内部的资源</p>
<p>网络策略是一个有序的规则的集合，它应用到标签选择器所匹配的端点</p>
</td>
</tr>
<tr>
<td class="blog_h3"><a href="#Profile">profile</a></td>
<td>包含应用到分配了Profile的单个的端点的网络策略</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg"> 常见问题</span></div>
<div class="blog_h2"><span class="graybg">node run后网络不通</span></div>
<p>某次操作意外删除了节点上的calico_node容器，重新执行calicoctl node run恢复容器后，发现此节点无法连接到K8S容器网络，无法ping通任何容器地址。</p>
<p>最终发现，是此节点的配置出现问题（可能是calicoctl node  run的参数不对引发），ipv4Address的掩码错误：</p>
<pre class="crayon-plain-tag"># 应该是 10.0.0.1/16，32明显错误
ipv4Address: 10.0.0.1/32</pre>
<p>除了此节点以外，其上运行的虚拟机出现同样的问题。修复上述掩码后问题消失。</p>
<p>需要注意一点，正常的路由，出口应该是calicoctl node run时检测到的网络接口：</p>
<pre class="crayon-plain-tag">172.27.252.128  radon           255.255.255.192 UG    0      0        0 virbr0</pre>
<p>而出现问题时，该节点上的出口从期望的virbr0变成了tunl0。  </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/calico">基于Calico的CNI</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/calico/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>基于Broadway的HTML5视频监控</title>
		<link>https://blog.gmem.cc/html5-vs-with-broadway</link>
		<comments>https://blog.gmem.cc/html5-vs-with-broadway#comments</comments>
		<pubDate>Mon, 09 Oct 2017 10:22:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[视频监控]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16500</guid>
		<description><![CDATA[<p>简介 Broadway是一个基于JavaScript的H.264解码器，支持Baseline Profile，我们在HTML5视频监控技术预研一文中介绍过这个库。如果你的监控摄像头支持Baseline的H.264码流，利用Broadway可以实现不需要重新编码的视频监控，这样服务器的负载可以大大减轻。 本文不进行理论知识的讨论，仅仅给出一个简单的实现。此实现由三个部分组成： 基于live555的C++程序，用来从视频源取RTP流，解析出NALU然后通过WebSocket推送给WebSocket服务器 基于Spring Boot的Java WebSocket服务器，接收C++程序推送来的NALU并广播给客户端 基于Broadway的HTML5视频监控客户端，为了简化开发，我们使用了Broadway的一个封装http-live-player 代码托管于GitHub：https://github.com/gmemcc/h5vs.git C++部分 这部分主要是一个RTSP客户端，功能上面已经介绍过，此客户端依赖于我以前一篇文章中的live555 RTSP客户端封装。 WebSocket客户端 [crayon-69e29a0c0c281935175588/] [crayon-69e29a0c0c285528205586/] 主程序 [crayon-69e29a0c0c288841319160/] Java部分 这部分实现了NALU转发功能，基于Spring Boot实现。 主程序 <a class="read-more" href="https://blog.gmem.cc/html5-vs-with-broadway">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/html5-vs-with-broadway">基于Broadway的HTML5视频监控</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>Broadway是一个基于JavaScript的H.264解码器，支持Baseline Profile，我们在<a href="/research-on-html5-video-surveillance">HTML5视频监控技术预研</a>一文中介绍过这个库。如果你的监控摄像头支持Baseline的H.264码流，利用Broadway可以实现不需要重新编码的视频监控，这样服务器的负载可以大大减轻。</p>
<p>本文不进行理论知识的讨论，仅仅给出一个简单的实现。此实现由三个部分组成：</p>
<ol>
<li>基于live555的C++程序，用来从视频源取RTP流，解析出NALU然后通过WebSocket推送给WebSocket服务器</li>
<li>基于Spring Boot的Java WebSocket服务器，接收C++程序推送来的NALU并广播给客户端</li>
<li>基于Broadway的HTML5视频监控客户端，为了简化开发，我们使用了Broadway的一个封装http-live-player</li>
</ol>
<p>代码托管于GitHub：<a href="https://github.com/gmemcc/h5vs.git">https://github.com/gmemcc/h5vs.git</a></p>
<div class="blog_h2"><span class="graybg">C++部分</span></div>
<p>这部分主要是一个RTSP客户端，功能上面已经介绍过，此客户端依赖于我以前一篇文章中的<a href="/realtime-communication-protocols#rtsp-client-wrapper">live555 RTSP客户端封装</a>。</p>
<div class="blog_h3"><span class="graybg">WebSocket客户端</span></div>
<pre class="crayon-plain-tag">//
// Created by alex on 10/9/17.
//

#ifndef LIVE5555_WEBSOCKETCLIENT_H
#define LIVE5555_WEBSOCKETCLIENT_H

#include &lt;pthread.h&gt;

#include &lt;websocketpp/config/asio_no_tls_client.hpp&gt;
#include &lt;websocketpp/client.hpp&gt;

typedef websocketpp::client&lt;websocketpp::config::asio_client&gt; WebSocketppClient;
typedef websocketpp::connection_hdl WebSocketppConnHdl;

class WebSocketClient {
private:
    char *url;
    pthread_t wsThread;
    WebSocketppClient *wsppClient;
    WebSocketppConnHdl wsppConnHdl;
public:
    WebSocketClient( char *url );

    char *getUrl() const;

    virtual void connect();

    virtual void sendBytes( unsigned char *buf, unsigned size );

    virtual void sendText( char *text );

    virtual ~WebSocketClient();

    pthread_t getWsThread() const;

    WebSocketppClient *getWsppClient();

    void setWsppConnHdl( WebSocketppConnHdl wsppConnHdl );
};


#endif //LIVE5555_WEBSOCKETCLIENT_H</pre><br />
<pre class="crayon-plain-tag">//
// Created by alex on 10/9/17.
//

#include "WebSocketClient.h"

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "WebSocketClient" );

WebSocketClient::WebSocketClient( char *url ) : url( url ), wsppClient( new WebSocketppClient()) {
}

WebSocketClient::~WebSocketClient() {
    delete wsppClient;
}

static void *wsRoutine( void *arg ) {
    WebSocketClient *client = (WebSocketClient *) arg;

    WebSocketppClient *wsppClient = client-&gt;getWsppClient();
    wsppClient-&gt;clear_access_channels( websocketpp::log::alevel::frame_header );
    wsppClient-&gt;clear_access_channels( websocketpp::log::alevel::frame_payload );
    wsppClient-&gt;init_asio();

    websocketpp::lib::error_code ec;
    WebSocketppClient::connection_ptr con = wsppClient-&gt;get_connection( std::string( client-&gt;getUrl()), ec );
    wsppClient-&gt;connect( con );
    client-&gt;setWsppConnHdl( con-&gt;get_handle());
    wsppClient-&gt;run();
}

void WebSocketClient::connect() {
    pthread_create( &amp;wsThread, NULL, wsRoutine, (void *) this );
}

void WebSocketClient::sendBytes( unsigned char *buf, unsigned size ) {
    wsppClient-&gt;send( wsppConnHdl, buf, size, websocketpp::frame::opcode::BINARY );
}

void WebSocketClient::sendText( char *text ) {
    wsppClient-&gt;send( wsppConnHdl, text, strlen( text ), websocketpp::frame::opcode::TEXT );
}

char *WebSocketClient::getUrl() const {
    return url;
}

pthread_t WebSocketClient::getWsThread() const {
    return wsThread;
}

WebSocketppClient *WebSocketClient::getWsppClient() {
    return wsppClient;
};

void WebSocketClient::setWsppConnHdl( WebSocketppConnHdl wsppConnHdl ) {
    this-&gt;wsppConnHdl = wsppConnHdl;
}</pre>
<div class="blog_h3"><span class="graybg">主程序</span></div>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;
#include "live5555/client.h"

#include "spdlog/spdlog.h"

#include "WebSocketClient.h"

static auto LOGGER = spdlog::stdout_color_st( "wspush" );

class VideoSink : public SinkBase {
private:
#ifdef _SAVE_H264_SEQ
    FILE *os = fopen( "./rtsp.h264", "w" );
#endif
    WebSocketClient *wsClient;
    bool firstFrameWritten;
    const char *sPropParameterSetsStr;
    unsigned char const start_code[4] = { 0x00, 0x00, 0x00, 0x01 };
public:
    VideoSink( UsageEnvironment &amp;env, unsigned int recvBufSize, WebSocketClient *wsClient ) : SinkBase( env, recvBufSize ), wsClient( wsClient ) {
        // 缓冲区前面留出起始码4字节
        recvBuf += sizeof( start_code );
    }

    virtual ~VideoSink() {
    }

    virtual void onMediaSubsessionOpened( MediaSubsession *subSession ) {
        sPropParameterSetsStr = subSession-&gt;fmtp_spropparametersets();
    }

    void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override {
        size_t scLen = sizeof( start_code );
        if ( !firstFrameWritten ) {
            // 填写起始码
            memcpy( recvBuf - scLen, start_code, scLen );
            // 防止RTSP源不送SPS/PPS
            unsigned numSPropRecords;
            SPropRecord *sPropRecords = parseSPropParameterSets( sPropParameterSetsStr, numSPropRecords );
            for ( unsigned i = 0; i &lt; numSPropRecords; ++i ) {
                unsigned int propLen = sPropRecords[ i ].sPropLength;
                size_t bufLen = propLen + scLen;
                unsigned char buf[bufLen];
                memcpy( buf, start_code, scLen );
                memcpy( buf + scLen, sPropRecords[ i ].sPropBytes, propLen );
                wsClient-&gt;sendBytes( buf, bufLen );
#ifdef _SAVE_H264_SEQ
                fwrite( buf, sizeof( unsigned char ), bufLen, os );
#endif
            }
            firstFrameWritten = true;
        }
#ifdef _SAVE_H264_SEQ
        fwrite( recvBuf - scLen, sizeof( unsigned char ), frameSize + scLen, os );
#endif
        unsigned naluHead = recvBuf[ 0 ];
        unsigned nri = naluHead &gt;&gt; 5;
        unsigned f = nri &gt;&gt; 2;
        unsigned type = naluHead &amp; 0b00011111;
        wsClient-&gt;sendBytes( recvBuf - scLen, frameSize + scLen );
        LOGGER-&gt;trace( "NALU info: nri {} type {}", nri, type );
        SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
    }
};

class H264RTSPClient : public RTSPClientBase {
private:
    VideoSink *videoSink;
public:
    H264RTSPClient( UsageEnvironment &amp;env, const char *rtspURL, VideoSink *videoSink ) :
        RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {}

protected:
    // 测试用的摄像头（RTSP源）仅仅有一个子会话，因此这里简化了实现：
    bool acceptSubSession( const char *mediumName, const char *codec ) override {
        return true;
    }

    MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession ) override {
        videoSink-&gt;onMediaSubsessionOpened( subSession );
        return videoSink;
    }
};

int main() {
    spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" );
    spdlog::set_level( spdlog::level::trace );

    WebSocketClient *wsClient;
    wsClient = new WebSocketClient( "ws://192.168.0.89:9090/h264src" );
    wsClient-&gt;connect();
    sleep( 3 ); // 等待WebSocket连接建立
    wsClient-&gt;sendText( "ch1" );
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    VideoSink *sink = new VideoSink( *env, 1024 * 1024, wsClient );
    H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink );
    client-&gt;start();
    return 0;
}</pre>
<div class="blog_h2"><span class="graybg">Java部分</span></div>
<p>这部分实现了NALU转发功能，基于Spring Boot实现。</p>
<div class="blog_h3"><span class="graybg">主程序</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.KurentoClient;
import org.kurento.client.KurentoClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import sun.security.acl.PrincipalImpl;

import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SpringBootApplication
@EnableWebSocket
@EnableWebSocketMessageBroker
@EnableScheduling
public class VideoSurveillanceApp extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer {

    private static final Logger LOGGER = LoggerFactory.getLogger( VideoSurveillanceApp.class );

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // WebSocket消息缓冲区大小，如果客户端发来的消息较大，需要按需调整
        container.setMaxTextMessageBufferSize( 1024 * 1024 );
        container.setMaxBinaryMessageBufferSize( 1024 * 1024 );
        return container;
    }

    @Override
    public void registerWebSocketHandlers( WebSocketHandlerRegistry registry ) {
        registry.addHandler( h264FrameSinkHandler(), "/h264sink" );
        registry.addHandler( h264FrameSrcHandler(), "/h264src" );
    }

    @Bean
    public WebSocketHandler h264FrameSrcHandler() {
        return new H264FrameSrcHandler();
    }

    @Bean
    public WebSocketHandler h264FrameSinkHandler() {
        return new H264FrameSinkHandler();
    }

    public static void main( String[] args ) {
        new SpringApplication( VideoSurveillanceApp.class ).run( args );
    }

}</pre>
<div class="blog_h3"><span class="graybg">H264FrameSrcHandler</span></div>
<p>此Bean接受C++程序的NALU推送：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class H264FrameSrcHandler extends AbstractWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( H264FrameSrcHandler.class );

    private Map&lt;String, String&gt; sessionIdToChannel = new ConcurrentHashMap&lt;&gt;();

    @Inject
    private H264FrameSinkHandler sinkHandler;

    public void afterConnectionEstablished( WebSocketSession session ) throws Exception {
        LOGGER.debug( "{} connected.", session.getRemoteAddress() );
    }

    @Override
    protected void handleBinaryMessage( WebSocketSession session, BinaryMessage message ) throws Exception {
        ByteBuffer payload = message.getPayload();
        StringBuilder hex = new StringBuilder();
        byte[] pa = payload.array();
        int len = 16;
        if ( pa.length &lt; 16 ) len = pa.length;
        for ( byte i = 0; i &lt; len; i++ ) {
            hex.append( String.format( "%02x ",Byte.toUnsignedInt( pa[i] )  ) );
        }
        LOGGER.debug( "Received binary message {} bytes: {}...", payload.array().length, hex );
        String chnl = sessionIdToChannel.get( session.getId() );
        if ( chnl != null ) sinkHandler.broadcast( chnl, payload );
    }

    @Override
    protected void handleTextMessage( WebSocketSession session, TextMessage message ) throws Exception {
        String payload = message.getPayload();
        sessionIdToChannel.put( session.getId(), payload );
        LOGGER.debug( "Received text message: {}", payload );
    }
}</pre>
<div class="blog_h3"><span class="graybg">H264FrameSinkHandler</span></div>
<p>此Bean向Web客户端广播NALU：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.mutable.MutableInt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class H264FrameSinkHandler extends TextWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( H264FrameSinkHandler.class );

    public static final String ACTION_INIT = "init";

    private static final String ACTION_INIT_RESP = "initresp";

    public static final String ACTION_PLAY = "play";

    public static final String ACTION_STOP = "stop";

    public static final String KEY_ACTION = "action";


    @Inject
    private ObjectMapper om;

    private Map&lt;String, List&lt;WebSocketSession&gt;&gt; chnlToSessions = new ConcurrentHashMap&lt;&gt;();

    @Override
    protected void handleTextMessage( WebSocketSession session, TextMessage message ) throws Exception {
        String client = session.getId() + '@' + session.getRemoteAddress();
        Map req = om.readValue( message.getPayload(), Map.class );
        Map resp = new LinkedHashMap();
        Object action = req.get( KEY_ACTION );
        if ( ACTION_INIT.equals( action ) ) {
            String channel = (String) req.get( "channel" );
            LOGGER.debug( "{} request to subscribe channel {}", client, channel );
            addPushTarget( channel, session );

            resp.put( KEY_ACTION, ACTION_INIT_RESP );
            resp.put( "width", 352 );
            resp.put( "height", 288 );
            session.sendMessage( new TextMessage( om.writeValueAsString( resp ) ) );
        } else if ( ACTION_PLAY.equals( action ) ) {
            LOGGER.debug( "{} request to receive nalu push", session.getRemoteAddress(), client );
            session.getAttributes().put( ACTION_PLAY, true );
        }
    }

    private synchronized void addPushTarget( String channel, WebSocketSession session ) {
        List&lt;WebSocketSession&gt; sessions = chnlToSessions.get( channel );
        if ( sessions == null ) {
            sessions = new ArrayList&lt;&gt;();
            chnlToSessions.put( channel, sessions );
        }
        sessions.add( session );
    }

    public synchronized void broadcast( String chnl, ByteBuffer payload ) {
        List&lt;WebSocketSession&gt; sessions = chnlToSessions.get( chnl );
        if ( sessions == null ) return;
        sessions.forEach( sess -&gt; {
            try {
                if ( sess.isOpen() &amp;&amp; Boolean.TRUE.equals( sess.getAttributes().get( ACTION_PLAY ) ) ) {
                    sess.sendMessage( new BinaryMessage( payload ) );
                }
            } catch ( Exception e ) {
                LOGGER.error( e.getMessage(), e );
            }
        } );
    }

    @Scheduled( fixedRate = 10000 )
    public synchronized void cleanup() {
        final MutableInt counter = new MutableInt( 0 );
        chnlToSessions.values().forEach( sessions -&gt; {
            Iterator&lt;WebSocketSession&gt; it = sessions.listIterator();
            while ( it.hasNext() ) {
                if ( !it.next().isOpen() ) {
                    it.remove();
                    counter.increment();
                }
            }
        } );
        if ( counter.intValue() &gt; 0 ) LOGGER.debug( "Remove {} invalid websocket session.", counter );
    }
}</pre>
<div class="blog_h2"><span class="graybg">Web部分</span></div>
<p>我们对http-live-player进行了简单的修改，主要是修改其通信方式以配合上述WebSocket服务器。核心代码没有变动，因此这里不张贴其代码。</p>
<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;meta charset="UTF-8"&gt;
    &lt;title&gt;Broadway Video Surveillance&lt;/title&gt;
    &lt;script src="js/broadway/http-live-player.js"&gt;&lt;/script&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class="nvbar"&gt;
    &lt;div class="title"&gt;基于Broadway+WebSocket的视频监控示例&lt;/div&gt;
    &lt;div class="subtitle"&gt;http://192.168.0.89:9090/broadway.html&lt;/div&gt;
&lt;/div&gt;
&lt;div class="videos-wrapper"&gt;
    &lt;div id="videos"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
    var videos = document.getElementById( 'videos' );
    for ( var i = 0; i &lt; 9; i++ ) {
        var canvas = document.createElement( "canvas" );
        videos.appendChild( canvas );
        var player = new WSAvcPlayer( canvas, "webgl", 'ch1', true );
        player.connect( "ws://" + document.location.host + "/h264sink" );
    }
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<div class="blog_h3"><span class="graybg">效果截图</span></div>
<p>下面的截图是开了九画面的视频监控，使用的是子码流，在测试机器上CPU压力不大。</p>
<p><img class="aligncenter size-full wp-image-16511" src="https://blog.gmem.cc/wp-content/uploads/2017/10/html5-h264.png" alt="html5-h264" width="798" height="697" /></p>
<p>注意：如果Broadway来不及解码，http-live-player会把缓冲区中的所有NALU全部丢弃，这可能导致暂时的花屏。选择适当的帧率、码率、画幅可以尽量避免这种情况的发生。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/html5-vs-with-broadway/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<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-69e29a0c0c992989272881/] Echo示例 CMake项目配置 [crayon-69e29a0c0c997890179597/] 客户端 [crayon-69e29a0c0c99a459857788/] 服务器 [crayon-69e29a0c0c99f683379300/] 封装 为了简化编程复杂度，应该考虑对libwebsockets进行适当封装。本节给出一个简单封装的例子。 客户端封装 [crayon-69e29a0c0c9a3160550287/] 使用客户端封装 <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>实时通信协议族</title>
		<link>https://blog.gmem.cc/realtime-communication-protocols</link>
		<comments>https://blog.gmem.cc/realtime-communication-protocols#comments</comments>
		<pubDate>Sat, 09 Sep 2017 04:16:11 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Multimedia]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16006</guid>
		<description><![CDATA[<p>网络和多媒体 实时性问题 因特网最初设计用于数据的传输，例如文件、电子邮件。那时的语音传输由专门的电话网络负责。随着节点数量的增加、网络带宽的提高，基于因特网的多媒体需求逐渐出现，包括在线影视、在线视频会议。为了响应这类需求，研究人员开发出了专门的协议，包括： 实时传输协议（Realtime Transmission Protocol，RTP），用于传输媒体 RTP的控制部分：实时传输控制协议（Realtime Transmission Control Protocol，RTCP） 实时流协议（Realtime Streaming Protocol） 注意：RTP这个术语有时候指RTP协议标准，有时候则指RTP协议标准中的RTP部分（不包含RTCP）。 我们都知道，TCP/IP协议族的基础是IP协议，此协议能很好的处理包的路由递送，但是却无法防止丢包、控制延迟。但是此协议让路由器逻辑很简单、网络易于扩容，这是因特网能繁荣的基础。 为了增强端到端的可靠性，TCP协议被引入，这是因特网上最广泛使用的协议。TCP能够自动重发丢失的包，并且保证包的顺序，TCP还提供了拥塞控制机制。 TCP的某些特性，在用来传递多媒体时，反而成为了障碍，原因是： 很多多媒体应用，例如视频监控，对延迟非常敏感 多媒体传输可以容忍某些丢包情况，其质量不会受到影响 因此，很多多媒体传输都是在UDP协议之上进行的。 多媒体应用程序可以分为两个类别： 交互式应用。例如视频会议、VoIP <a class="read-more" href="https://blog.gmem.cc/realtime-communication-protocols">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/realtime-communication-protocols">实时通信协议族</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>
<div class="blog_h2"><span class="graybg">实时性问题</span></div>
<p>因特网最初设计用于数据的传输，例如文件、电子邮件。那时的语音传输由专门的电话网络负责。随着节点数量的增加、网络带宽的提高，基于因特网的多媒体需求逐渐出现，包括在线影视、在线视频会议。为了响应这类需求，研究人员开发出了专门的协议，包括：</p>
<ol>
<li>实时传输协议（Realtime Transmission Protocol，RTP），用于传输媒体</li>
<li>RTP的控制部分：实时传输控制协议（Realtime Transmission Control Protocol，RTCP）</li>
<li>实时流协议（Realtime Streaming Protocol）</li>
</ol>
<p>注意：RTP<span style="background-color: #c0c0c0;">这个术语有时候指RTP协议标准，有时候则指RTP协议标准中的RTP部分</span>（不包含RTCP）。</p>
<p>我们都知道，TCP/IP协议族的基础是IP协议，此协议能很好的处理包的路由递送，但是却无法防止丢包、控制延迟。但是此协议让路由器逻辑很简单、网络易于扩容，这是因特网能繁荣的基础。</p>
<p>为了增强端到端的可靠性，TCP协议被引入，这是因特网上最广泛使用的协议。TCP能够自动重发丢失的包，并且保证包的顺序，TCP还提供了拥塞控制机制。</p>
<p>TCP的某些特性，在用来传递多媒体时，反而成为了障碍，原因是：</p>
<ol>
<li>很多多媒体应用，例如视频监控，对延迟非常敏感</li>
<li>多媒体传输可以容忍某些丢包情况，其质量不会受到影响</li>
</ol>
<p>因此，很多多媒体传输都是在UDP协议之上进行的。</p>
<p>多媒体应用程序可以分为两个类别：</p>
<ol>
<li><span style="background-color: #c0c0c0;">交互式应用</span>。例如视频会议、VoIP</li>
<li>非交互式应用。又可以细分为：
<ol>
<li><span style="background-color: #c0c0c0;">实时流媒体</span>，例如视频监控预览、网络赛事直播</li>
<li><span style="background-color: #c0c0c0;">非实时流媒体</span>，例如视频点播网站、歌曲点播应用、视频监控回放</li>
</ol>
</li>
</ol>
<p>除了非实时流媒体应用之外，多媒体应用不能容忍过长时间的缓冲以避免抖动，也不允许延迟的出现。</p>
<div class="blog_h2"><span class="graybg">互操作性问题</span></div>
<p>大量多媒体应用程序，使用了不同的编码方式，这些编码方式在媒体质量、带宽消耗、计算资源消耗之间做了不同的权衡。</p>
<p>不同的多媒体应用（例如两个独立开发的VoIP应用）要能够相互通信，就必须以某种双方都能理解的编码方式进行媒体的交换。</p>
<div class="blog_h1"><span class="graybg">实时传输协议</span></div>
<p>直接使用UDP传输流媒体数据包不能满足需求，因为UDP没有任何关于延迟、抖动的语义，或者它不理解何为“实时”。</p>
<p>实时传输协议（RTP）是一种专门处理实时需求的传输协议标准，主要用于处理音频、视频数据。 RTP允许接受者知晓接收到的数据包们在时间维上的关系，这样：</p>
<ol>
<li>在缓冲媒体流并播放时不会出现顺序混乱的情况</li>
<li>多个媒体流（例如音频、视频）能够在播放时保持同步关系</li>
</ol>
<p>此外RTP协议：</p>
<ol>
<li>允许通信双方进行协商（Negotiation），选取两者都认可的编码方式。这解决互操作性问题</li>
<li>具有识别包丢失的问题，这样端点应用程序能够<a href="/webrtc-server-basedon-kurento#interoperability">进行适当的处理</a></li>
<li>允许进行拥塞控制，媒体接收者能够向发送者进行网络拥塞状况的反馈（Feedback），这样发送者可以对码流质量进行调整，以改变带宽占用</li>
<li>支持帧指示（Frame Indication），例如媒体接收者需要知道那些数据包是属于相同的视频场景（Video Scene，帧）的，这样才能进行合适的处理</li>
</ol>
<div class="blog_h2"><span class="graybg">RTP的历史</span></div>
<p>通过网络进行音频传输的尝试从1970年代就开始了，在70-80年代多个语音包传输、时间戳、序列号相关的专利被批准。在1991年DARTnet成功完成了一系列语音传输的尝试，DARTnet使用的音频会议工具最终成为RTP版本0。</p>
<p>1992年RTP版本1发布，包含了若干因特网草案，此操作最终在1995年成为RTP版本2，包含：</p>
<ol>
<li>RFC1889，RTP</li>
<li>RFC1890，RTP Profile —— AVP（用于音视频会议的RTP profile，最小化控制）</li>
</ol>
<p>1996年，网景基于RTP和其他协议发布了Netscape LiveMidea。微软的NetMeeting软件也支持RTP。</p>
<p>RTP的设计确定了一个后续被广泛认同的原则 ——<span style="background-color: #c0c0c0;"> 应用层分帧原则（Application Level Framing ALF ）</span>。ALF认为应用程序更了解自己的需要，网络协议应该尽可能保持简单。例如MPEG解码器明白怎么样处理丢帧，如何从I帧、B帧丢失中恢复。</p>
<p>RTP支持大量种类的应用程序，对于每一类应用程序，RTP定义了一种Profile。Profile可以是：</p>
<ol>
<li>一种对RTP协议头结构的约定</li>
<li>定义对RTP协议的扩展或者修改</li>
</ol>
<p>RTP的载荷格式规定，则解释了RTP头之后的数据的结构。</p>
<div class="blog_h2"><span class="graybg">RTP组成</span></div>
<p>RTP作为一个标准，实际上定义了一对协议：</p>
<ol>
<li>RTP，用于交换媒体数据</li>
<li>RTCP，用于传输的控制，例如周期性的获得数据流传输质量的反馈信息、负责多个媒体流的同步。RTCP也负责传输组会话（Group Session）的参与者信息</li>
</ol>
<p>尽管RTP协议相对于传输层是独立的，但是它通常在UDP/IP之上运行。当基于UDP/IP传输时，RTP和RTCP使用连续的两个端口号。</p>
<p>为了发起一个RTP会话，应用程序需要定义一对特定的目的传输地址（Destination Transport Addresses，DTA）—— 一个网络地址加上两个端口（分别用于RTP、RTCP）。在一个多媒体（eg，音频 + 视频 + 文本）会话中，每个媒体都在单独的RTP会话中传输。</p>
<div class="blog_h3"><span class="graybg">视频会议的例子</span></div>
<p>在视频会议中，音频、视频媒体在不同的RTP会话中传输，这些会话使用的DTA是不同的，也就是它们使用两对不同的UDP端口。</p>
<p>视频会议也可能基于组播技术实现，这种情况下，需要使用两对多播地址的DTA。</p>
<p>音频、视频的RTP会话没有直接的关联，这允许接收者仅仅接收音频或者视频流。为了实现一个源的音视频同步，接收者可以使用RTCP包中的时序信息。</p>
<p>每个会议参与者都使用视频、音频应用程序，以块的方式发送数据。这些数据作为RTP包的载荷，RTP头中有专门的字段识别这些数据是如何编码的。</p>
<p>RTP头包含时序信息、序列号，接收者可以用这些信息重新构造音视频流的时序，不同源（音视频）时序信息都是单独构建的。</p>
<div class="blog_h2"><span class="graybg">RTP结构</span></div>
<p>RTP数据包的整体结构、在网络和应用中的传递方式，如下图所示：</p>
<p><img class="aligncenter size-full wp-image-16018" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rpt-package.png" alt="rpt-package" width="100%" /></p>
<p>关于此图的说明如下：</p>
<ol>
<li>RTP协议通常运行在UDP之上，这意味着数据都是无状态、推送的方式传递</li>
<li>RTP数据包是一个瘦协议，对需要持续传递数据（流式）的应用程序提供支持：
<ol>
<li>时序重构（Timing Reconstruction）</li>
<li>丢帧检测（Frame Loss Detection）</li>
<li>数据安全（Data Security）</li>
<li>内容识别（Content Identification）</li>
</ol>
</li>
<li>RTP协议不负责处理带宽保留（Reserve Bandwidth）和保证QoS</li>
<li>RTP数据包的载荷部分是数字化（由编码器负责）的媒体流</li>
</ol>
<div class="blog_h2"><span class="graybg">转换器/混合器</span></div>
<p>除了发送者、接收者角色之外，RTP协议还定义了另外两个参与此协议处理的角色 —— 转换器（Translater）、混合器（Mixer）它们位于发送者、接收者角色<span style="background-color: #c0c0c0;">之间</span>，对<span style="background-color: #c0c0c0;">经过（Passthrough）</span>它们的RTP包做出处理：</p>
<ol>
<li>转换器：对经过的RTP载荷进行转换，例如可以降低视频码流的比特率，降低带宽需求</li>
<li>混合器：用于混合来自多个媒体源的流，例如可以混合多个视频会议参与者的视频流，形成一个单独的流</li>
</ol>
<p>注意，仅仅当若干RTP流经过混合器，混合器才起作用。例如在一个电话会议的应用场景中，多个音频流通常会经过混合器混合为一个流，以节约带宽占用。</p>
<div class="blog_h2"><span class="graybg">RTP协议头</span></div>
<p>RTP协议的头格式如下图所示：</p>
<p><img class="aligncenter size-full wp-image-16022" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-header.png" alt="rtp-header" width="100%" /></p>
<ol>
<li>最前面的12字节（到SSRC为止）总是存在（上图中1、2、3……表示bit）
<ol>
<li>V：2bit的版本号，一般取值2</li>
<li>P：1bit的补白标记，如果此标记被设置，RTP包的尾部会包含1-N个补白字节。这些字节不属于载荷。补白的最后一个字节记录了补白的总数（包含它自己）。之所以需要补白，是为了满足某些加密算法对块（Block）长度的规定</li>
<li>X：1bit的扩展标记，如果此标记被设置，则在标准头后面包含1个扩展头</li>
<li>CC：4bit的CSRC标识符的计数器。如果载荷包含来自多个源的数据，则此计数大于1</li>
<li>M：1bit的Marker标记，此标记的意义由Profile定义，此标记通常用于提示重要事件的发生，例如帧边界</li>
<li>PT：7bit的载荷类型，指示载荷的数据类型。支持的类型包括PCM、MPEG1、MPEG2、JPEG视频、H.261等等。更多载荷类型可以通过Profile规范、载荷格式规范添加</li>
<li>16bit序列号：每当会话发送一个新的RTP包后，此序列号增加1。接收者可以基于此序列号进行丢包检测。此序列号的初始值是随机的，这样RTP包被加密后，尝试破解变得更加困难。当丢包出现后RTP协议层不做任何操作，应用程序负责对丢包事件做出响应，例如：某些视频应用可能在丢包时自动重放前一帧；另一些视频应用可能因为丢包而降低比特率</li>
<li>32bit时间戳：记录载荷中第一个字节的采样发生的时间。此字段的用途包括：让接收者可以按照适当的时间间隔来播放采样；允许多个媒体流保持同步；在计算抖动平滑（Jitter smoothing）时使用。时间戳使用的时钟解析度必须足够高，以满足同步精度、抖动度量精度。时间戳的初始值也是随机的，RTP没有规定时间戳的计量单位 —— 时间戳仅仅是时钟的tick计数，两个tick之间对应真实时间是多少，也是和应用程序相关的，这些仍然由Profile、载荷格式规定。频率表示每秒内有多少tick，因而tick数量 / 频率即得到对应真实世界的时间</li>
<li>32bit的SSRC标识符：用于识别同步源的标识符，此标识符被随机的生成，确保同一媒体会话中，任何两个同步源的标识符都不同。但是即使随机生成也有一定的概率出现重复，因此RTP实现必须有能力识别、解决冲突。当一个信号源改变自己的传递地址后，SSRC标识符必须也更改</li>
</ol>
</li>
<li> 32bit的CSRC标识符，此标识符最多有15个（取决于CC），用于识别此包的载荷部分由哪些（Contributing ）源构成。CSRC标识符由混合器（Mixer）插入到包头中，其值就是Contributing源的SSRC头</li>
<li>后面是可选的扩展头</li>
<li>在RTP后面，是1-N个音视频帧，作为RTP载荷</li>
</ol>
<div class="blog_h1"><span class="graybg">实时传输控制协议</span></div>
<p>RTCP协议专门用于配合RTP协议使用。</p>
<p>在RTP会话中，参与者定期向RTP会话的所有参与者通过组播发送RTCP包。RTCP包中包含媒体发送者/接收者的报告，其内容包括发送数据包的数量、丢失数据包的数量、抖动信息（Jitter）。</p>
<p>应用程序可能使用RTCP中的信息，来自适应的改变媒体流的质量，以适应可用网络带宽。</p>
<p>RTCP为来自同一个发送者的不同媒体流提供了一种协作、同步的机制。例如，当SSRC取值冲突时，需要某个流改变SSRC，这就是通过RTCP完成的。</p>
<p>对于牵涉到多个单独的多媒体流的应用程序，它们之间的同步基于一个通用的系统时钟完成。最初发起会话的那个系统提供此时钟，RTCP消息可以保证会话的所有参与者都使用相同的时钟。</p>
<p>RTCP还用于传输会话中各成员之间的关系。</p>
<p>当会话参与者越来越多时，RTCP数据报的总量会变多。为了防止影响网络，RTCP包占据会话总数据量不会超过5%，这意味着随着参与者的增加RTCP包发送频率会降低</p>
<div class="blog_h2"><span class="graybg">RTCP头</span></div>
<p><img class="aligncenter size-full wp-image-16037" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtcp-header.png" alt="rtcp-header" width="100%" /></p>
<ol>
<li>2bit的版本号，使用的RTP协议的版本</li>
<li>1bit的补白标记，RTP包的最后是否具有补白</li>
<li>5bit的接收报告计数（Reception Report Count ），此包中包含的接收报告块的数量</li>
<li>8bit的消息类型</li>
<li>16bit的长度，指示此包的总长度</li>
<li>32bit的SSRC，同步源标识</li>
</ol>
<div class="blog_h2"><span class="graybg">RTCP消息类型</span></div>
<p>RFC 3550定义了五种类型的RTCP报文：</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>RR</td>
<td>接收者报告（Receiver Report），由不作为活动发送者的会话参与者生成，包含接收质量反馈信息。具体内容包括：接收到的最高包序列号、丢包数量、抖动情况、用于计算收发者之间延迟的时间戳信息</td>
</tr>
<tr>
<td>SR</td>
<td>发送者报告（Sender Report），由活动的发送者生成，除了RR中的信息外，SR还包含一个发送者信息段。此段提供媒体间（Inter-midea）同步需要的信息、累计发包数量、立即发送字节数</td>
</tr>
<tr>
<td>SDES</td>
<td>源描述条目（Source Description Items），包含对源的描述信息。在RTP包中源由32bit的一个头字段标识，但是这个名字不适合人类阅读，SDES则提供了一个所谓规范化名称（Canonical Names)作为会话参与者的唯一性标识。规范化名称可能包括用户名、电话号码、电子邮件地址或者其他信息</td>
</tr>
<tr>
<td>BYE</td>
<td>提示发送者结束会话的参与</td>
</tr>
<tr>
<td>APP</td>
<td>应用特定功能（Application Specific Functions），主要用于新应用、新特性开发时的实验性功能</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">实时流协议</span></div>
<p>RTSP由RFC2326定义，他是一个应用层多媒体展现协议，支持对实时媒体流进行控制 —— 例如暂停播放、Seek、快进、倒放，这些控制行为类似于DVD播放器。RTSP协议本身通常不进行媒体流的传递。</p>
<p>RTSP服务器为客户端维护一个会话，此会话由一个标识符来识别。RTSP协议支持TCP或者UDP传输，在一个RTSP会话中，客户端可能打开、关闭多个传输连接，以发送RTSP请求。</p>
<p>RTSP需要和低层的RTP或者RSVP之类的协议协同，才能在因特网上提供完整的流媒体服务。RTSP在RTP的基础上提供了选择传输通道（TCP/单播UDP/组播UDP）、传输机制的方法。RTSP报文独立于媒体流发送。</p>
<p>RTSP在客户端和流媒体服务器之间创建、控制音视频媒体流。服务器负责提供回放、录制等服务。</p>
<p>RTSP中的每个展现（Presentation）和媒体流都通过一个RTSP URL来识别，整体的展现信息和媒体属性在一个展现描述文件（Presenttation Description File）中记录，此文件中的信息可能包括编码方式、语言、RTSP URLs、目的地址/端口以及其它参数。客户端可以通过HTTP、电子邮件等方式获得展现描述文件。</p>
<p>RTSP协议有意的模仿HTTP协议的设计，但是两者有以下重要的不同：</p>
<ol>
<li>RTSP是有状态的，它必须维护会话状态，让RTSP请求和某个流关联</li>
<li>RTSP是对称的，媒体服务器和客户端都可以发起请求。例如服务器可以发起请求，来设置流的回放参数</li>
</ol>
<p>RTSP支持以下方法：</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>OPTIONS</td>
<td>
<p>返回服务器接收的请求类型。报文示例：</p>
<pre class="crayon-plain-tag"># 客户端请求
C-&gt;S:  OPTIONS rtsp://gmem.cc/media.mp4 RTSP/1.0
       CSeq: 1
       Require: implicit-play
       Proxy-Require: gzipped-messages
# 服务器应答
S-&gt;C:  RTSP/1.0 200 OK
       CSeq: 1
       # 支持的请求类型列表
       Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE</pre>
</td>
</tr>
<tr>
<td>DESCRIBE</td>
<td>
<p>客户端发起此报文，或者RTSP URL所代表的展现/媒体对象的描述信息。报文示例：
<pre class="crayon-plain-tag"># 客户端请求
C-&gt;S: DESCRIBE rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 2
# 服务器应答
S-&gt;C: RTSP/1.0 200 OK
      CSeq: 2
      Content-Base: rtsp://gmem.cc/media.mp4
      Content-Type: application/sdp
      Content-Length: 460
      # 下面是一个媒体行 —— 基于AVP Profile的RTP传输
      m=video 0 RTP/AVP 96
      a=control:streamid=0
      a=range:npt=0-7.741000
      a=length:npt=7.741000
      # 视频编码方式
      a=rtpmap:96 MP4V-ES/5544
      a=mimetype:string;"video/MP4V-ES"
      a=AvgBitRate:integer;304018
      a=StreamName:string;"hinted video track"
      # 下面是一个媒体行，音频部分
      m=audio 0 RTP/AVP 97
      a=control:streamid=1
      a=range:npt=0-7.712000
      a=length:npt=7.712000
      a=rtpmap:97 mpeg4-generic/32000/2
      a=mimetype:string;"audio/mpeg4-generic"
      a=AvgBitRate:integer;65790
      a=StreamName:string;"hinted audio track"</pre>
</td>
</tr>
<tr>
<td>ANNOUNCE</td>
<td>
<p>当由客户端发起时，更新RTSP URL所代表的展现/媒体对象的描述信息
<p>当由服务器发起时，实时的更新会话描述</p>
</td>
</tr>
<tr>
<td>SETUP</td>
<td>
<p>客户端请求服务器为某个流分配资源，并启动一个RTSP会话。报文示例：</p>
<pre class="crayon-plain-tag">C-&gt;S: SETUP rtsp://gmem.cc/media.mp4/streamid=0 RTSP/1.0
      CSeq: 3
      # 基于AVP Profile的RTP，使用UDP单播，RTP/RTCP端口
      Transport: RTP/AVP;unicast;client_port=8000-8001

S-&gt;C: RTSP/1.0 200 OK
      CSeq: 3
      # 附加服务器端口信息，媒体源唯一标识
      Transport: RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001;ssrc=1234ABCD
      # 分配会话标识符
      Session: 12345678</pre>
</td>
</tr>
<tr>
<td>PLAY</td>
<td>
<p>客户端请求服务器通过SETUP分配的流推送数据。报文示例：
<pre class="crayon-plain-tag">C-&gt;S: PLAY rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 4
      Range: npt=5-20
      Session: 12345678

S-&gt;C: RTSP/1.0 200 OK
      CSeq: 4
      Session: 12345678
      RTP-Info: url=rtsp://gmem.cc/media.mp4/streamid=0;seq=9810092;rtptime=3450012</pre>
</td>
</tr>
<tr>
<td>PAUSE</td>
<td>客户端临时停止流的递送，但是不释放服务器资源</td>
</tr>
<tr>
<td>TEARDOWN</td>
<td>客户端请求服务器停止流的递送，并释放分配的资源</td>
</tr>
<tr>
<td>GET_PARAMETER</td>
<td>
<p>获取RTSP URL所代表的展现/流的某个参数的值。报文示例：
<pre class="crayon-plain-tag">S-&gt;C: GET_PARAMETER rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 9
      Content-Type: text/parameters
      Session: 12345678
      Content-Length: 15
      # 获取两个参数
      packets_received
      jitter

C-&gt;S: RTSP/1.0 200 OK
      CSeq: 9
      Content-Length: 46
      Content-Type: text/parameters

      packets_received: 10
      jitter: 0.3838 </pre>
</td>
</tr>
<tr>
<td>SET_PARAMETER</td>
<td>
<p>设置RTSP URL所代表的展现/流的某个参数的值。报文示例：
<pre class="crayon-plain-tag">C-&gt;S: SET_PARAMETER rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 10
      Content-length: 20
      Content-type: text/parameters
      # 设置的参数名、值
      barparam: barstuff

S-&gt;C: RTSP/1.0 451 Invalid Parameter
      CSeq: 10
      Content-length: 10
      Content-type: text/parameters

      barparam </pre>
</td>
</tr>
<tr>
<td>REDIRECT</td>
<td>
<p>服务器发起，通知客户端，必须重新连接到一个媒体位置，此报文的location头指示媒体的新位置。报文示例：
<pre class="crayon-plain-tag">S-&gt;C: REDIRECT rtsp://gmem.cc/media.mp4 RTSP/1.0
      CSeq: 11
      # 新的位置
      Location: rtsp://cast.gmem.cc.com:8001
      Range: clock=19960213T143205Z-</pre>
</td>
</tr>
<tr>
<td>RECORD</td>
<td>客户端基于展现描述，发起媒体数据某个范围的录制请求</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Live555</span></div>
<p>Live555项目提供了一套C++库，用于RTP/RTCP、RTSP、SIP等标准协议下的多媒体应用开发。Live555库被用来实现Live555媒体服务器、Live555代理服务器。
<p>Live555还可以用来流化、接收、处理MPEG, H.265, H.264, H.263+, DV等视频编码格式以及若干音频编码格式。要支持其它音视频编码格式，你只需要简单的扩展。</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<p>下载Live555后，你得到以下几个组件，它们被位于不同的子目录：</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>UsageEnvironment</td>
<td>
<p>包含类UsageEnvironment、TaskScheduler用于延迟事件的调度，例如异步读事件、输出错误/警告消息</p>
<p>包含类HashTable，一个哈希表实现</p>
</td>
</tr>
<tr>
<td>groupsock</td>
<td>此组件中的类封装了网络接口和套接字。Groupsock对组播的收发行为进行了封装</td>
</tr>
<tr>
<td>liveMedia</td>
<td>定义了一个类层次，其根是Medium。提供对多种流媒体类型、编码方式的支持</td>
</tr>
<tr>
<td>BasicUsageEnvironment</td>
<td>定义了UsageEnvironment的一个具体化子类，主要在简单的、基于控制台的应用程序中使用。读事件和延迟操作在一个select()循环中处理</td>
</tr>
<tr>
<td>testProgs</td>
<td>基于BasicUsageEnvironment实现了一些样例</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>Linux下安装Live555库的步骤如下：</p>
<pre class="crayon-plain-tag">wget http://www.live555.com/liveMedia/public/live.2017.07.18.tar.gz
tar xzf live.2017.07.18.tar.gz 
pushd live
# 如果需要保留调试信息，可以修改config.linux文件，添加编译参数-O0 -g3 
./genMakefiles linux
make &amp;&amp; make install PREFIX=/home/alex/CPP/lib/live555</pre>
<div class="blog_h2"><span class="graybg">RTSP客户端</span></div>
<div class="blog_h3"><span class="graybg">DESCRIBE示例</span></div>
<pre class="crayon-plain-tag">#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"
#include &lt;iostream&gt;

using namespace std;

volatile char eventLoopWatchVariable = 0;

int main() {
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    UsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    const char *url = "rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream";
    RTSPClient *client = RTSPClient::createNew( *env, url, 0 );  // 参数3传入1则控制台会打印调试信息
    // 发送RTSP DESCRIBE命令，注意所有RTSP命令都是异步发送的，其应答后续在事件循环中被处理
    client-&gt;sendDescribeCommand( []( RTSPClient *rtspClient, int resultCode, char *resultString ) -&gt; void {
        // 如果resultCode不为0说明失败
        // 打印SDP
        cout &lt;&lt; resultString &lt;&lt; endl;
    } );

    // 直到eventLoopWatchVariable变为非零之前，下面的事件循环不会停止
    env-&gt;taskScheduler().doEventLoop( &amp;eventLoopWatchVariable );
    return 0;
}</pre>
<p>公司的IP摄像头给出如下应答：</p>
<pre class="crayon-plain-tag">v=0
o=- 1505140878681876 1505140878681876 IN IP4 192.168.0.196
s=Media Presentation
e=NONE
b=AS:5050
t=0 0
a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/
# 视频基于RTP/VAP传输类型，编码方式H264
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:5000
a=recvonly
a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/trackID=1
# 90000表示时钟频率，即每秒内，有多少个时间戳tick，或者说每秒RTP时间戳增加多少
a=rtpmap:96 H264/90000
# H.264 Profile: baseline , constraints 0 , level-idc 4.1 
# level-idc，用于提示自己的解码能力 —— 最大多大的分辨率、帧率、码率
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AFJWoWCWm4CAgIEA=,aO48gA==
a=Media_header:MEDIAINFO=494D4B48010100000400000100000000000000000000000000000000000000000000000000000000;
a=appversion:1.0</pre>
<p>上述内容和WebRTC的SDP Offer类似，都属于会话描述协议。SDP消息可以划分为三个主要的段，分别对会话、时间、媒体进行描述。SDP各字段的含义如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">SDP字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>会话描述</em></strong></td>
</tr>
<tr>
<td>v </td>
<td>协议版本号，总是0</td>
</tr>
<tr>
<td>o</td>
<td>发起者以及会话标识符</td>
</tr>
<tr>
<td>s</td>
<td>会话的名称 </td>
</tr>
<tr>
<td>i</td>
<td>会话的描述和简短信息 </td>
</tr>
<tr>
<td>u </td>
<td>
<p>描述（Description）的URI </p>
</td>
</tr>
<tr>
<td>e </td>
<td>0-N个电子邮件地址，附加联系人名称 </td>
</tr>
<tr>
<td>p</td>
<td>0-N个电话号码，附加联系人名称  </td>
</tr>
<tr>
<td>c </td>
<td>连接信息</td>
</tr>
<tr>
<td>b</td>
<td>0-N个带宽信息行 </td>
</tr>
<tr>
<td>z</td>
<td>时区调整</td>
</tr>
<tr>
<td>k</td>
<td>加密密钥</td>
</tr>
<tr>
<td>a</td>
<td>0-N个会话属性行</td>
</tr>
<tr>
<td colspan="2"><strong><em>时间描述（1-N）</em></strong></td>
</tr>
<tr>
<td>t</td>
<td>
<p>会话活动时间</p>
<p>其中的绝对时间基于网络时间协议（<span style="color: #222222;">NTP）格式，即1900年到目前的秒数</span></p>
<p>开始时间为0表示会话是永久的；结束时间为零表示会话持续时间不限制</p>
</td>
</tr>
<tr>
<td>r</td>
<td>0-N个repeat times</td>
</tr>
<tr>
<td colspan="2"><strong><em>媒体描述（0-N）</em></strong></td>
</tr>
<tr>
<td>m</td>
<td>
<p>媒体名称、传输地址，以及传输协议。示例：</p>
<pre class="crayon-plain-tag"># 音频，在49170端口，基于Profile AVP、载荷类型0（PCMU）的RTP协议传输
m=audio 49170 RTP/AVP 0
# 视频，在51372端口，基于Profile AVP、载荷类型96的RTP协议传输
m=video 51372 RTP/AVP 96</pre>
<p>载荷类型如果是96-127之间，则表示载荷类型是动态分配的，后面会出现a=rtpmap行来映射此载荷类型：</p>
<pre class="crayon-plain-tag">a=rtpmap:96 H264/90000</pre>
<p>载荷类型可以声明若干个，表示这些类型在会话中都可能使用</p>
</td>
</tr>
<tr>
<td>i</td>
<td>媒体的标题或者信息</td>
</tr>
<tr>
<td>c</td>
<td>连接信息</td>
</tr>
<tr>
<td>b</td>
<td>带宽信息</td>
</tr>
<tr>
<td>k</td>
<td>加密密钥</td>
</tr>
<tr>
<td>a</td>
<td>0-N个媒体属性行，可以覆盖会话属性行同名属性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg"><a id="rtsp-client-wrapper"></a>客户端封装</span></div>
<p>为简化开发，下面给出一个live555的RTSP客户端封装。</p>
<p>创建如下CMake项目：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(live5555)

set(CMAKE_CXX_STANDARD 11)

set(LIVE555_HOME /home/alex/CPP/lib/live555)

include_directories(${LIVE555_HOME}/include/UsageEnvironment)
include_directories(${LIVE555_HOME}/include/BasicUsageEnvironment)
include_directories(${LIVE555_HOME}/include/liveMedia)
include_directories(${LIVE555_HOME}/include/groupsock)

include_directories(/home/alex/CPP/lib/spdlog/include)

set(CMAKE_CXX_FLAGS "-w -pthread")

set(LIVE5555_SRC SinkBase.cpp RTSPClientBase.cpp)

add_library(live5555 ${LIVE5555_SRC})
target_link_libraries(
        live5555
        ${LIVE555_HOME}/lib/libliveMedia.a
        ${LIVE555_HOME}/lib/libgroupsock.a
        ${LIVE555_HOME}/lib/libBasicUsageEnvironment.a
        ${LIVE555_HOME}/lib/libUsageEnvironment.a
)

set(WS_PUSH_SRC wspush.cpp)
add_executable(wspush ${WS_PUSH_SRC})
target_link_libraries(
        wspush
        live5555
)</pre>
<p>基础头文件：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_COMMON_H
#define LIVE5555_COMMON_H

#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"

#endif //LIVE5555_COMMON_H</pre>
<p>类RTSPClientBase，对RTSPClient进行扩展，将RTSP命令回调函数指针转换为成员函数，实现基本的取流逻辑：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_RTSPCLIENTBASE_H
#define LIVE5555_RTSPCLIENTBASE_H

#include "common.h"

class RTSPClientBase : public RTSPClient {
private:
    MediaSession *session;
    // 事件循环监控变量，不为零时事件循环退出
    volatile char eventLoopWatchVariable;
    char *rtspURL;
    volatile int acceptedSubSessionCount;
    volatile int preparedSubSessionCount;

    static void onDescribeResponse( RTSPClient *client, int resultCode, char *resultString );

    static void onSetupResponse( RTSPClient *client, int resultCode, char *resultString );

    static void onPlayResponse( RTSPClient *client, int resultCode, char *resultString );

    static void onSubSessionClose( void *clientData );

protected:
    RTSPClientBase( UsageEnvironment &amp;env, const char *rtspURL );

    // 处理RTSP命令DESCRIBE的响应
    virtual void onDescribeResponse( int resultCode, const char *sdp );

    // 处理RTSP命令SETUP的响应
    virtual void onSetupResponse( int resultCode, const char *resultString );

    virtual void onPlayResponse( int resultCode, char *resultString );

    // 是否初始化指定的子会话
    virtual bool acceptSubSession( const char *mediumName, const char *codec )=0;

    virtual MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession )=0;

    // 处理子会话关闭事件
    virtual void onSubSessionClose( MediaSubsession *subsess );

public:
    virtual void start();

    virtual void stop();
};

#endif //LIVE5555_RTSPCLIENTBASE_H</pre><br />
<pre class="crayon-plain-tag">#include "live5555/RTSPClientBase.h"
#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "RTSPClientBase" );

static const char *getResultString( char *resultString ) {
    return resultString ? resultString : "N/A";
}

// 父构造函数的第三个参数是调试信息冗余级别
RTSPClientBase::RTSPClientBase( UsageEnvironment &amp;env, const char *rtspURL ) :
    RTSPClient( env, rtspURL, 0, NULL, 0, -1 ) {
}

void RTSPClientBase::start() {
    LOGGER-&gt;trace( "Starting RTSP client..." );
    this-&gt;rtspURL = rtspURL;
    LOGGER-&gt;trace( "Send RTSP command: DESCRIBE" );
    sendDescribeCommand( onDescribeResponse );
    LOGGER-&gt;trace( "Startup live555 eventloop" );
    envir().taskScheduler().doEventLoop( &amp;eventLoopWatchVariable );
}


void RTSPClientBase::onDescribeResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER-&gt;trace( "DESCRIBE response received, resultCode: {}", resultCode );
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    bool ok = false;
    if ( resultCode == 0 ) {
        clientBase-&gt;onDescribeResponse( resultCode, resultString );
    } else {
        LOGGER-&gt;trace( "Stopping due to DESCRIBE failure" );
        clientBase-&gt;stop();
    };
    delete[] resultString;
}

void RTSPClientBase::onDescribeResponse( int resultCode, const char *sdp ) {
    LOGGER-&gt;debug( "SDP received: \n{}", sdp );
    UsageEnvironment &amp;env = envir();
    LOGGER-&gt;trace( "Create new media session according to SDP" );
    session = MediaSession::createNew( env, sdp );
    if ( session &amp;&amp; session-&gt;hasSubsessions()) {
        MediaSubsessionIterator *it = new MediaSubsessionIterator( *session );
        // 遍历子会话，SDP中的每一个媒体行（m=***）对应一个子会话
        while ( MediaSubsession *subsess = it-&gt;next()) {
            const char *mediumName = subsess-&gt;mediumName();
            // 初始化子会话，导致相应的RTPSource被创建
            LOGGER-&gt;trace( "Initialize sub session {}", mediumName );
            if ( !acceptSubSession( mediumName, subsess-&gt;codecName())) {
                continue;
            }
            acceptedSubSessionCount++;
            bool ok = subsess-&gt;initiate();
            if ( !ok ) {
                LOGGER-&gt;error( "Failed to initialize sub session: {}", mediumName );
                stop();
                break;
            }
            const Boolean muxed = subsess-&gt;rtcpIsMuxed();
            const char *codec = subsess-&gt;codecName();
            const int port = subsess-&gt;clientPortNum();
            LOGGER-&gt;debug( "Initialized sub session... \nRTCP Muxed: {}\nPort: {}\nMedium : {}\nCodec: {}", muxed, port, mediumName, codec );

            LOGGER-&gt;trace( "Send RTSP command: SETUP for subsession {}", mediumName );
            sendSetupCommand( *subsess, onSetupResponse, False, False );
        }
    } else {
        stop();
    }
}

void RTSPClientBase::onSetupResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER-&gt;trace( "SETUP response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString ));
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    if ( resultCode == 0 ) {
        clientBase-&gt;preparedSubSessionCount++;
        clientBase-&gt;onSetupResponse( resultCode, resultString );
    } else {
        LOGGER-&gt;trace( "Stopping due to SETUP failure" );
        clientBase-&gt;stop();
    }
    delete[] resultString;
}

void RTSPClientBase::onSetupResponse( int resultCode, const char *resultString ) {
    if ( preparedSubSessionCount == acceptedSubSessionCount ) {
        MediaSubsessionIterator *it = new MediaSubsessionIterator( *session );
        while ( MediaSubsession *subsess = it-&gt;next()) {
            const char *mediumName = subsess-&gt;mediumName();
            const char *codec = subsess-&gt;codecName();
            if ( acceptSubSession( mediumName, codec )) {
                MediaSink *sink = createSink( mediumName, codec, subsess );
                // 让Sink回调能够感知Client对象
                subsess-&gt;miscPtr = this;
                // 导致Sink的continuePlaying被调用，准备接受数据推送
                sink-&gt;startPlaying( *subsess-&gt;readSource(), NULL, subsess );
                // 此时数据推送不会立即开始，直到调用STSP命令PLAY
                RTCPInstance *rtcp = subsess-&gt;rtcpInstance();
                if ( rtcp ) {
                    // 正确处理针对此子会话的RTCP命令
                    rtcp-&gt;setByeHandler( onSubSessionClose, subsess );
                }
                LOGGER-&gt;trace( "Send RTSP command: PLAY" );
                // PLAY命令可以针对整个会话，也可以针对每个子会话
                sendPlayCommand( *session, onPlayResponse );
            }
        }
    }
}

void RTSPClientBase::onPlayResponse( RTSPClient *client, int resultCode, char *resultString ) {
    LOGGER-&gt;trace( "PLAY response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString ));
    RTSPClientBase *clientBase = (RTSPClientBase *) client;
    if ( resultCode == 0 ) {
        clientBase-&gt;onPlayResponse( resultCode, resultString );
    } else {
        LOGGER-&gt;trace( "Stopping due to PLAY failure" );
        clientBase-&gt;stop();
    }
    delete[] resultString;
}

void RTSPClientBase::onPlayResponse( int resultCode, char *resultString ) {
    // 此时服务器应该开始推送流过来
    // 如果播放的是定长的录像，这里应该注册回调，在时间到达后关闭客户端
    double &amp;startTime = session-&gt;playStartTime();
    double &amp;endTime = session-&gt;playEndTime();
    LOGGER-&gt;debug_if( startTime == endTime, "Session is infinite" );
}

void RTSPClientBase::onSubSessionClose( void *clientData ) {
    MediaSubsession *subsess = (MediaSubsession *) clientData;
    RTSPClientBase *clientBase = (RTSPClientBase *) subsess-&gt;miscPtr;
    clientBase-&gt;onSubSessionClose( subsess );
}

void RTSPClientBase::onSubSessionClose( MediaSubsession *subsess ) {
    LOGGER-&gt;debug( "Stopping subsession..." );
    // 首先关闭子会话的SINK
    Medium::close( subsess-&gt;sink );
    subsess-&gt;sink = NULL;

    // 检查是否所有兄弟子会话均已经结束
    MediaSession &amp;session = subsess-&gt;parentSession();
    MediaSubsessionIterator iter( session );
    while (( subsess = iter.next()) != NULL ) {
        // 存在未结束的子会话，不能关闭当前客户端
        if ( subsess-&gt;sink != NULL ) return;
    }
    // 关闭客户端
    LOGGER-&gt;debug( "All subsession closed" );
    stop();
}

void RTSPClientBase::stop() {
    LOGGER-&gt;debug( "Stopping RTSP client..." );
    // 修改事件循环监控变量
    eventLoopWatchVariable = 0;
    UsageEnvironment &amp;env = envir();
    if ( session != NULL ) {
        Boolean someSubsessionsWereActive = False;
        MediaSubsessionIterator iter( *session );
        MediaSubsession *subsession;
        // 检查是否存在需要处理的子会话
        while (( subsession = iter.next()) != NULL ) {
            if ( subsession-&gt;sink != NULL ) {
                // 强制关闭子会话的SINK
                Medium::close( subsession-&gt;sink );
                subsession-&gt;sink = NULL;
                if ( subsession-&gt;rtcpInstance() != NULL ) {
                    // 服务器可能在处理TEARDOWN时发来RTCP包BYE
                    subsession-&gt;rtcpInstance()-&gt;setByeHandler( NULL, NULL );
                }
                someSubsessionsWereActive = True;
            }
        }

        if ( someSubsessionsWereActive ) {
            // 向服务器发送TEARDOWN命令，让服务器关闭输入流
            sendTeardownCommand( *session, NULL );
        }
    }
    // 关闭客户端
    Medium::close( this );
}</pre>
<p>类SinkBase，一个基础的Sink实现，从流中获取帧：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_SINKBASE_H
#define LIVE5555_SINKBASE_H

#include "common.h"

class SinkBase : public MediaSink {

private:
    static void afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime,
        unsigned /*durationInMicroseconds*/ );

protected:
    unsigned recvBufSize;

    unsigned char *recvBuf;

    SinkBase( UsageEnvironment &amp;env, unsigned recvBufSize );

    // sink-&gt;startPlaying会调用continuePlaying，实现播放逻辑
    virtual Boolean continuePlaying();

    virtual ~SinkBase();

public:
    virtual void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime );
};

#endif //LIVE5555_SINKBASE_H</pre><br />
<pre class="crayon-plain-tag">#include "live5555/SinkBase.h"
#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "SinkBase" );

SinkBase::SinkBase( UsageEnvironment &amp;env, unsigned recvBufSize ) : MediaSink( env ) {
    this-&gt;recvBufSize = recvBufSize;
    this-&gt;recvBuf = new unsigned char[recvBufSize];
}

SinkBase::~SinkBase() {
    delete[] this-&gt;recvBuf;
}

// 缺省实现：保存已分帧源的下一帧到缓冲区中，然后执行回调
Boolean SinkBase::continuePlaying() {
    if ( fSource == NULL ) return False;
    fSource-&gt;getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this );
    return True;
};

// 由于getNextFrame需要的是一个函数指针，因此这里用静态函数。此函数简单的转调对应的成员函数
void SinkBase::afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime,
    unsigned /*durationInMicroseconds*/ ) {
    SinkBase *sink = (SinkBase *) clientData;
    sink-&gt;afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
}

// 缺省实现：递归获取下一帧
void SinkBase::afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) {
    LOGGER-&gt;trace( "Frame of {} bytes received",frameSize );
    fSource-&gt;getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this );
} </pre>
<div class="blog_h3"><span class="graybg">封装应用示例</span></div>
<p>下面的客户端基于上节的封装：</p>
<pre class="crayon-plain-tag">#ifndef LIVE5555_LIVE5555_H
#define LIVE5555_LIVE5555_H

#include "RTSPClientBase.h"
#include "SinkBase.h"

#endif //LIVE5555_LIVE5555_H</pre><br />
<pre class="crayon-plain-tag">#include &lt;iostream&gt;
#include "live5555/client.h"
#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "wspush" );

class VideoSink : public SinkBase {
public:
    VideoSink( UsageEnvironment &amp;env, unsigned int recvBufSize ) : SinkBase( env, recvBufSize ) {}

    void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override {
        unsigned naluHead = recvBuf[ 0 ];
        unsigned nri = naluHead &gt;&gt; 5;
        unsigned f = nri &gt;&gt; 2;
        unsigned type = naluHead &amp; 0b00011111;
        LOGGER-&gt;trace( "NALU info: nri {} type {}", nri, type );
        SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
    }
};

class H264RTSPClient : public RTSPClientBase {
private:
    MediaSink *videoSink;
public:
    H264RTSPClient( UsageEnvironment &amp;env, const char *rtspURL, MediaSink *videoSink ) :
        RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {}

protected:
    // 测试用的摄像头（RTSP源）仅仅有一个子会话，因此这里简化了实现：
    bool acceptSubSession( const char *mediumName, const char *codec ) override {
        return true;
    }

    MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession) override {
        return videoSink;
    }
};

int main() {
    spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" );
    spdlog::set_level( spdlog::level::trace );

    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    VideoSink *sink = new VideoSink( *env, 1024 * 1024 );
    H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink );
    client-&gt;start();
    return 0;
}</pre>
<p>此客户端很简单，它建立RTSP会话，然后依次执行DESCRIBE、SETUP、PLAY命令，最终建立RTP会话，开始视频流的传输。</p>
<p>live555的RTSP客户端中的所有网络事件 —— 包括RTSP请求响应、RTP推送 —— 都由BasicTaskScheduler的事件循环处理，该事件循环会不断的执行select()系统调用，监听网络事件的到达。事件循环由RTSPClientBase.start()启动。</p>
<p>在SETUP响应回调中，发送PLAY命令之前，我们调用了VideoSink.startPlaying()方法，此方法会转调VideoSink.continuePlaying()方法，后者则调用H264RTPSource.getNextFrame()注册下一帧的处理回调。getNextFrame()会将解析得到的帧存放到你指定的缓冲区中，测试时打印前几帧：</p>
<p style="padding-left: 30px;"><span class="monospace">67 42 00 1F 95 A8 14 01 6E 9B 80 80 80 81 </span><br /><span class="monospace">68 CE 3C 80 </span><br /><span class="monospace">61 E4 A0 4F F3 7A 06 B9 36 39 80 07 4C 9A ...</span></p>
<p>可以看到，这些帧都是标准的H264 NALU格式（不带起始码），第一个是SPS，第二个是PPS，后续是普通切片。</p>
<div class="blog_h3"><span class="graybg">H264RTPSource</span></div>
<p>RTP包到达后，事件循环委托H264RTPSource进行如下处理：</p>
<ol>
<li>检查RTP包头的合法性</li>
<li>剔除RTP包头和Padding</li>
<li>抽取H264帧，然后针对自己调用afterGetting(this)，并导致之前通过getNextFrame()注册的回调函数被调用</li>
</ol>
<div class="blog_h1"><span class="graybg">术语列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Jitter</td>
<td>
<p>抖动（Jitter）是TCP/IP网络和组件天生具有的一种不被期望的“倾向”。它是数据报被接收到的延迟（Delay）时间的变化性（Variation）</p>
<p>发送方以固定的频率发送数据报，但是由于网络拥塞、不适当的数据报排队或者配置错误，接收者接收到数据报的频率可能会在较大范围变动</p>
<p>抖动会影响流媒体的回放体验，在等待延迟到达的数据报时，可能出现gap</p>
</td>
</tr>
<tr>
<td>NPT</td>
<td>
<p>正常播放时间（Normal Play Time），指示相对于展现（presentation）开始点的，流的当前位置（时间偏移）</p>
<p>NPT使用一个浮点数表示，整数部分可能按秒数、或者小时+分钟+秒数来解释，小数部分则进行一秒内度量（例如.500表示当前秒过了一半）</p>
<p>展现的开始点的NPT定义为0.0，负数的意义没有定义</p>
<p>当x倍速播放时，NPT的增长速度增加为x倍</p>
</td>
</tr>
<tr>
<td>Presentation</td>
<td>
<p>展现，一个或者多个被渲染到客户端流，它们共同组成了一个Media feed</p>
</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/realtime-communication-protocols">实时通信协议族</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/realtime-communication-protocols/feed</wfw:commentRss>
		<slash:comments>0</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-69e29a0c0d692975779007/] 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>AsyncHttpClient知识集锦</title>
		<link>https://blog.gmem.cc/async-http-client-faq</link>
		<comments>https://blog.gmem.cc/async-http-client-faq#comments</comments>
		<pubDate>Fri, 28 Jul 2017 04:01:18 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[HTTP]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15162</guid>
		<description><![CDATA[<p>简介 本文所指的AsyncHttpClient是指基于Netty的一个开源项目，该项目基于Java8编写，用于简化HTTP客户端的开发。该项目还支持WebSocket协议。 要使用该AsyncHttpClient，引入以下Maven依赖： [crayon-69e29a0c0e076284265160/] 代码示例 配置客户端 [crayon-69e29a0c0e07a591110058/] 异步GET请求 [crayon-69e29a0c0e07c379587735/] ListenableFuture是java.util.concurrent.Future的子类型，你可以使用Java8并发框架提供的任何特性，例如： [crayon-69e29a0c0e07f202067725/] 查询参数 [crayon-69e29a0c0e081347038649/] 上传文件 [crayon-69e29a0c0e083931655431/] 发送JSON请求  [crayon-69e29a0c0e085724602862/] 响应生命周期控制 [crayon-69e29a0c0e087241598005/] WebSocket [crayon-69e29a0c0e08a966446023/] &#160; <a class="read-more" href="https://blog.gmem.cc/async-http-client-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/async-http-client-faq">AsyncHttpClient知识集锦</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>本文所指的AsyncHttpClient是指基于Netty的一个<a href="https://github.com/AsyncHttpClient/async-http-client">开源项目</a>，该项目基于Java8编写，用于简化HTTP客户端的开发。该项目还支持WebSocket协议。</p>
<p>要使用该AsyncHttpClient，引入以下Maven依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
   &lt;groupId&gt;org.asynchttpclient&lt;/groupId&gt;
   &lt;artifactId&gt;async-http-client&lt;/artifactId&gt;
   &lt;version&gt;LATEST_VERSION&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h2"><span class="graybg">代码示例</span></div>
<div class="blog_h3"><span class="graybg">配置客户端</span></div>
<pre class="crayon-plain-tag">AsyncHttpClientConfig cf = new DefaultAsyncHttpClientConfig
    .Builder()
    // 设置代理服务器
    .setProxyServer(new ProxyServer.Builder("127.0.0.1", 8087))
    .build();

// 为客户端提供配置项
AsyncHttpClient http = new DefaultAsyncHttpClient(cf);</pre>
<div class="blog_h3"><span class="graybg">异步GET请求</span></div>
<pre class="crayon-plain-tag">ListenableFuture&lt;Response&gt; future =
http.prepareGet( "http://ip:port/path" ).execute( new AsyncCompletionHandler&lt;Response&gt;() {

    @Override
    public Response onCompleted( Response response ) throws Exception {
        String resp = response.getResponseBody();
        return response;
    }

    @Override
    public void onThrowable( Throwable t ) {
        // Something wrong happened.
    }
} );</pre>
<p>ListenableFuture是java.util.concurrent.Future的子类型，你可以使用Java8并发框架提供的任何特性，例如：</p>
<pre class="crayon-plain-tag">future.get();   // 阻塞等待处理完毕

// 转换为CompletableFuture
CompletableFuture&lt;Response&gt; promise = future.toCompletableFuture();
promise.exceptionally(t -&gt; { /* 当错误发生时 */  } )
       .thenApply(resp -&gt; { /*  处理请求 */ return resp; });</pre>
<div class="blog_h3"><span class="graybg">查询参数</span></div>
<pre class="crayon-plain-tag">http.preparePost( "http://ip:port/path" )
    // 添加请求参数
    .addQueryParam( "name", alex )
    .addQueryParam( "feature", "0" )
    .execute()</pre>
<div class="blog_h3"><span class="graybg">上传文件</span></div>
<pre class="crayon-plain-tag">http.preparePost( "http://ip:port/path" )
    // 上传多个文件
    .addBodyPart( new FilePart( "imageDatas", new ClassPathResource( "alex1.jpg" ).getFile() ) )
    .addBodyPart( new FilePart( "imageDatas", new ClassPathResource( "alex2.jpg" ).getFile() ) )
    .execute()</pre>
<div class="blog_h3"><span class="graybg">发送JSON请求</span> </div>
<pre class="crayon-plain-tag">http.preparePost( "http://192.168.0.89:9090/pems/stpush" )
    .addHeader( "Content-Type", "application/json; charset=utf-8" )
    .setBody( json.getBytes( "utf-8" ) )
    .execute().get();</pre>
<div class="blog_h3"><span class="graybg">响应生命周期控制</span></div>
<pre class="crayon-plain-tag">ByteArrayOutputStream bytes = new ByteArrayOutputStream();
String resp = http.prepareGet( "http://www.example.com/" ).execute( new AsyncHandler&lt;String&gt;() {

    public void onThrowable( Throwable t ) {

    }

    public State onBodyPartReceived( HttpResponseBodyPart bodyPart ) throws Exception {
        // 接收到一个上传文件后
        bytes.write( bodyPart.getBodyPartBytes() );
        return State.CONTINUE;
    }

    public State onStatusReceived( HttpResponseStatus responseStatus ) throws Exception {
        // 仅仅获得响应码
        int statusCode = responseStatus.getStatusCode();
        return State.ABORT;
    }

    public State onHeadersReceived( HttpHeaders headers ) throws Exception {
        // 仅仅接收响应头
        return State.ABORT;
    }

    public String onCompleted() throws Exception {
        // 给出此回调的返回值
        return bytes.toString( "UTF-8" );
    }
} ).get();</pre>
<div class="blog_h3"><span class="graybg">WebSocket</span></div>
<pre class="crayon-plain-tag">WebSocket websocket = http.prepareGet( "http://wsep" )
      .execute( new WebSocketUpgradeHandler.Builder().addWebSocketListener(
              new WebSocketTextListener() {
                  @Override
                  public void onMessage( String message ) {
                      // 接收到消息时的回调
                  }

                  @Override
                  public void onOpen( WebSocket websocket ) {
                      // WebSocket打开时的回调
                      websocket.sendTextMessage( "..." ).sendMessage( "..." );
                  }

                  @Override
                  public void onClose( WebSocket websocket ) {
                      // 关闭时的回调 
                  }

                  @Override
                  public void onError( Throwable t ) {
                  }
              } ).build() ).get();</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/async-http-client-faq">AsyncHttpClient知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/async-http-client-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ZeroMQ学习笔记</title>
		<link>https://blog.gmem.cc/zeromq-study-note</link>
		<comments>https://blog.gmem.cc/zeromq-study-note#comments</comments>
		<pubDate>Mon, 10 Jul 2017 04:07:14 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Network]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16540</guid>
		<description><![CDATA[<p>简介 ZeroMQ是一个高性能的分布式网络库，它的特性包括： 看起来像是嵌入式网络库，行为更像是分布式并发库 对于集群产品、超级计算领域来说，比TCP更快 支持以进程内、IPC、TCP、组播方式携带载荷 支持fanout, pubsub, pipeline, request-reply等N-to-N通信模式  Apache Storm 0.9之前的版本默认使用ZeroMQ作为节点通信库。 起步 安装 libzmq 基于C++实现的ZeroMQ核心引擎。安装步骤参考： [crayon-69e29a0c0e37d806516685/] 请求/应答 EchoServer [crayon-69e29a0c0e381699741832/] EchoClient [crayon-69e29a0c0e384436274057/] <a class="read-more" href="https://blog.gmem.cc/zeromq-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/zeromq-study-note">ZeroMQ学习笔记</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>ZeroMQ是一个高性能的分布式网络库，它的特性包括：</p>
<ol>
<li>看起来像是嵌入式网络库，行为更像是分布式并发库</li>
<li>对于集群产品、超级计算领域来说，比TCP更快</li>
<li>支持以进程内、IPC、TCP、组播方式携带载荷</li>
<li>支持fanout, pubsub, pipeline, request-reply等N-to-N通信模式 </li>
</ol>
<p>Apache Storm 0.9之前的版本默认使用ZeroMQ作为节点通信库。</p>
<div class="blog_h1"><span class="graybg">起步</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">libzmq</span></div>
<p>基于C++实现的ZeroMQ核心引擎。安装步骤参考：</p>
<pre class="crayon-plain-tag">mkdir libzmq &amp;&amp; pushd libzmq
git clone https://github.com/zeromq/libzmq src
pushd src
mkdir build &amp;&amp; pushd build
cmake -DCMAKE_INSTALL_PREFIX:STRING=/home/alex/CPP/lib/libzmq -DCMAKE_BUILD_TYPE:STRING=Debug ..
make &amp;&amp; make install</pre>
<div class="blog_h2"><span class="graybg">请求/应答</span></div>
<div class="blog_h3"><span class="graybg">EchoServer</span></div>
<pre class="crayon-plain-tag">#include "zmq.h"
#include &lt;stdbool.h&gt;
#include &lt;string.h&gt;

int main() {
    // 创建一个线程安全的上下为，返回一个非透明句柄(Opaque Handle)
    void *context = zmq_ctx_new();
    // 在指定的上下文中创建zmq套接字，返回一个非透明句柄(Opaque Handle)
    // 第二个参数为套接字类型，和传统套接字不同，zmq套接字是一种异步消息队列的抽象，队列的语义由套接字类型确定
    // ZMQ_REP用于请求-应答模式的服务器端，用于接受请求并给予应答。此类型仅仅支持成对的zmq_recv/zmq_send调用
    void *responder = zmq_socket( context, ZMQ_REP );  // REP是REPLY的意思
    // 绑定，监听客户端连接
    int rc = zmq_bind( responder, "tcp://*:5555" );
    if ( rc != 0 ) return -1;
    while ( true ) {
        char buf[32];
        memset( buf, 0, sizeof( buf ));
        /**
         *  int zmq_recv (void *socket, void *buf, size_t len, int flags)
         *  从套接字socket中接收消息，存放到buf中，参数：
         *  len   超过此长度的消息被截断
         *  flags 不阻塞，如果没有可用消息则调用失败，设置errno为EAGAIN（资源暂时不可用）
         *  返回值：返回实际接收的消息长度，如果接收失败返回-1
         */
        int len = zmq_recv( responder, buf, 32, ZMQ_DONTWAIT );
        if ( len &gt; -1 ) {
            printf( "Received message: %s\n", buf );
            zmq_send( responder, buf, len, 0 );
        }
    }
}</pre>
<div class="blog_h3"><span class="graybg">EchoClient</span></div>
<pre class="crayon-plain-tag">#include "zmq.h"

int main() {
    void *context = zmq_ctx_new();
    // 套接字类型ZMQ_REQ和ZMQ_REP配合使用
    void *requester = zmq_socket( context, ZMQ_REQ );
    // 连接到服务器
    zmq_connect( requester, "tcp://localhost:5555" );

    int req_count;
    for ( req_count = 0; req_count != 10; req_count++ ) {
        char buf[32];
        int len = sprintf( buf, "Hello %d", req_count );
        // 发送消息
        zmq_send( requester, buf, len, 0 );
        // 接收消息，此时服务器可以尚未启动，客户端会阻塞在此
        zmq_recv( requester, buf, 32, 0 );
    }
    // 关闭套接字
    zmq_close( requester );
    // 销毁上下文
    zmq_ctx_destroy( context );
    return 0;
}</pre>
<div class="blog_h3"><span class="graybg">收发行为</span></div>
<p>测试上述程序时，你可以先启动客户端。由于服务器没有启动，客户端的zmq_recv调用会阻塞，这种行为和常见的网络库不同。</p>
<p>收发消息时是否阻塞，可以通过zmq_send/zmq_recv的第三个参数flags设置。</p>
<div class="blog_h3"><span class="graybg">字符串</span></div>
<p>ZeroMQ不理解你发送的消息的格式，它仅仅知道消息的字节长度。ZeroMQ发送字符串时，必须指定恰当的长度，此长度不应包括结尾的\0字符。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/zeromq-study-note">ZeroMQ学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/zeromq-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
