<?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>Tony Bai &#187; 微信</title>
	<atom:link href="http://tonybai.com/tag/%e5%be%ae%e4%bf%a1/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sat, 04 Apr 2026 00:51:31 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Gopher直通大厂，就从这第一课开始！</title>
		<link>https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory/</link>
		<comments>https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory/#comments</comments>
		<pubDate>Wed, 03 Sep 2025 00:52:21 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1兼容性]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[main]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[云服务]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[大厂]]></category>
		<category><![CDATA[字节跳动]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[显式]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[测试覆盖率]]></category>
		<category><![CDATA[滴滴]]></category>
		<category><![CDATA[生产力]]></category>
		<category><![CDATA[简单]]></category>
		<category><![CDATA[类型参数]]></category>
		<category><![CDATA[类型约束]]></category>
		<category><![CDATA[组合]]></category>
		<category><![CDATA[腾讯]]></category>
		<category><![CDATA[阿里]]></category>
		<category><![CDATA[面向工程]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5116</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory 大家好，我是Tony Bai。 很多计算机专业的同学们都在问：想进大厂，要先学好哪门编程语言？ 从应用广泛程度来说，学好Go语言肯定错不了！我们来看一下大厂们都用Go在做哪些开发： 阿里用于基础服务、网关、容器、服务框架等开发。 字节跳动用于即时通信（IM）、K8s、微服务等开发。 腾讯用于微信后台、云服务、游戏后端等开发。 滴滴用于数据平台、调度系统、消息中间件等开发。 此外，美团、百度、京东、小米等都在业务中大量使用Go语言做开发。可见，同学们只要玩转Go语言，大厂都会张开双臂欢迎你们。 大厂为何如此青睐Go语言呢？有三点重要原因： 简单易上手： Go语法简洁，学习成本低，代码易维护； 生产力与性能有效结合： Go拥有卓越的并发性能，内置调度器和非抢占式模型，保证了超高的稳定性； 使用快乐且前景广阔： 优良的开发体验，包括得心应手的工具链、丰富健壮的标准库、广泛的社区支持等。 总的来说，Go相对于C/C++，性能并没有明显差距，可维护性还更好；相对于Python，Go性能大幅领先，入门难度则相差无几。 直通大厂，同学们请看《Go 语言第一课》这本书，书中详细介绍了Go的设计哲学与核心理念，全面讲解了Go的重要语法特性。没有基础也完全不必担心，本书手把手式教学，小白立即轻松上手。 扫描上方二维码，即可五折购书(在有效期内) 现在，让我们进入课堂，开始Go语言学习的第一课吧。 Part.1 零基础起步，Go开发全掌握 本书为读者设计了一条循序渐进的学习路线，可以分为三个部分。 首先讲述Go语言的起源与设计哲学； 然后说明开发环境的搭建方法； 最后详细介绍Go的重要语法与语言特性，以及工程实施的一些细节。 初次学习Go开发的同学们一定要注意，动手实践是学习编程的不二法门，在进入第二部分学习时，就要根据书中内容同步搭建实验环境，一步一个脚印地走稳走好。 Go的设计哲学 本部分先介绍了Go语言在谷歌公司内部孵化的过程，描述了其在当今云计算时代的广泛应用。 Go的第一版官网 重点说明了Go的5个核心设计哲学： 简单： 仅有25个关键字，摒弃了诸多复杂的特性，便于快速上手； 显式： 要求代码逻辑清晰明确，避免隐式处理带来的不确定性； 组合： 通过类型嵌入提供垂直扩展能力，通过接口实现水平组合，灵活扩展功能； 并发： 原生支持并发，用户层轻量级线程，轻松支持高并发访问； 面向工程： 注重解决实际问题，围绕Go的库、工具、惯用法和软件工程方法，都为开发提供全面支持。 读者理解了Go的设计哲学就能明确它擅长的方向，澄清心中的疑问，也掌握了使用Go进行编程的指导原则。 Part.2 搭建Go开发环境 这部分先针对Windows、macOS、Linux三种主流操作系统给出了多种安装方法，包括使用安装包、使用预编译二进制包、通过源码编译，说明如何管理多个Go版本。 然后基于经典的“Hello World”示例，演示编译运行的方法，讲解Go的基本程序结构，包括包声明、导入包、main函数等内容。接着深入讲解Go包的定义、导入、初始化与编译单元。 // ch3/helloworld/main.go package main [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory">本文永久链接</a> &#8211; https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory</p>
<p>大家好，我是Tony Bai。</p>
<p>很多计算机专业的同学们都在问：想进大厂，要先学好哪门编程语言？</p>
<p>从应用广泛程度来说，学好Go语言肯定错不了！我们来看一下大厂们都用Go在做哪些开发：</p>
<blockquote>
<p>阿里用于基础服务、网关、容器、服务框架等开发。</p>
<p>字节跳动用于即时通信（IM）、K8s、微服务等开发。</p>
<p>腾讯用于微信后台、云服务、游戏后端等开发。</p>
<p>滴滴用于数据平台、调度系统、消息中间件等开发。</p>
</blockquote>
<p>此外，美团、百度、京东、小米等都在业务中大量使用Go语言做开发。可见，同学们只要玩转Go语言，大厂都会张开双臂欢迎你们。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-2.png" alt="" /></p>
<p>大厂为何如此青睐Go语言呢？有三点重要原因：</p>
<ul>
<li><strong>简单易上手：</strong> Go语法简洁，学习成本低，代码易维护；</li>
<li><strong>生产力与性能有效结合：</strong> Go拥有卓越的并发性能，内置调度器和非抢占式模型，保证了超高的稳定性；</li>
<li><strong>使用快乐且前景广阔：</strong> 优良的开发体验，包括得心应手的工具链、丰富健壮的标准库、广泛的社区支持等。</li>
</ul>
<p>总的来说，Go相对于C/C++，性能并没有明显差距，可维护性还更好；相对于Python，Go性能大幅领先，入门难度则相差无几。</p>
<p>直通大厂，同学们请看《<a href="https://book.douban.com/subject/37499496/">Go 语言第一课</a>》这本书，书中详细介绍了Go的设计哲学与核心理念，全面讲解了Go的重要语法特性。没有基础也完全不必担心，本书手把手式教学，小白立即轻松上手。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" alt="" /><br />
<center>扫描上方二维码，即可五折购书(在有效期内)</center></p>
<hr />
<p>现在，让我们进入课堂，开始Go语言学习的第一课吧。</p>
<h2>Part.1 零基础起步，Go开发全掌握</h2>
<p>本书为读者设计了一条循序渐进的学习路线，可以分为三个部分。</p>
<p>首先讲述Go语言的起源与设计哲学；</p>
<p>然后说明开发环境的搭建方法；</p>
<p>最后详细介绍Go的重要语法与语言特性，以及工程实施的一些细节。</p>
<p>初次学习Go开发的同学们一定要注意，动手实践是学习编程的不二法门，在进入第二部分学习时，就要根据书中内容同步搭建实验环境，一步一个脚印地走稳走好。</p>
<h3>Go的设计哲学</h3>
<p>本部分先介绍了Go语言在谷歌公司内部孵化的过程，描述了其在当今云计算时代的广泛应用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-3.png" alt="" /><br />
<center>Go的第一版官网</center></p>
<p>重点说明了Go的5个核心设计哲学：</p>
<ul>
<li><strong>简单：</strong> 仅有25个关键字，摒弃了诸多复杂的特性，便于快速上手；</li>
<li><strong>显式：</strong> 要求代码逻辑清晰明确，避免隐式处理带来的不确定性；</li>
<li><strong>组合：</strong> 通过类型嵌入提供垂直扩展能力，通过接口实现水平组合，灵活扩展功能；</li>
<li><strong>并发：</strong> 原生支持并发，用户层轻量级线程，轻松支持高并发访问；</li>
<li><strong>面向工程：</strong> 注重解决实际问题，围绕Go的库、工具、惯用法和软件工程方法，都为开发提供全面支持。</li>
</ul>
<p>读者理解了Go的设计哲学就能明确它擅长的方向，澄清心中的疑问，也掌握了使用Go进行编程的指导原则。</p>
<h2>Part.2 搭建Go开发环境</h2>
<p>这部分先针对Windows、macOS、Linux三种主流操作系统给出了多种安装方法，包括使用安装包、使用预编译二进制包、通过源码编译，说明如何管理多个Go版本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-4.png" alt="" /></p>
<p>然后基于经典的“Hello World”示例，演示编译运行的方法，讲解Go的基本程序结构，包括包声明、导入包、main函数等内容。接着深入讲解Go包的定义、导入、初始化与编译单元。</p>
<pre><code class="go">// ch3/helloworld/main.go
package main
import "fmt"
func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>详细讲解Go Module的核心概念，结合创世项目案例、社区共识、官方指南，给出清晰的项目布局建议。梳理了Go依赖管理的演化历程，重点讲解基于Go Module的依赖管理操作，包括添加、升级/降级、移除、替换等操作。</p>
<p>经过这部分的学习，读者可以掌握Go的编译与运行方法、项目的组织与管理，具备工程化的能力。</p>
<h2>Part.3 Go语言特性详解</h2>
<p>这部分是本书的重点，覆盖基础语法知识、并发、泛型、测试等内容；在结构上由浅入深，层层递进，读者只要坚持学练结合，就能全盘掌握Go的关键知识。</p>
<p>基础语法知识包含以下内容：</p>
<ul>
<li><strong>变量与类型：</strong> 说明变量的声明方法、变量的作用域。</li>
<li><strong>基本数据类型：</strong> 详细讲解布尔型、数值型、字符串型的特性与常用操作。</li>
<li><strong>常量：</strong> 重点讲解Go常量的创新性设计，包括无类型常量、隐式转换、实现枚举。</li>
<li><strong>复合数据类型：</strong> 讲解数组、切片、map类型、结构体的声明与操作。</li>
<li><strong>指针类型：</strong> 解释指针的概念，说明其用途与使用限制。</li>
<li><strong>控制结构：</strong> 详细介绍if、for、switch语句的用法，实现分支、循环功能。</li>
<li><strong>函数：</strong> 说明函数的声明、参数、多返回值特性，以及defer的使用与注意事项。</li>
<li><strong>错误处理：</strong> 讲解了error接口的错误处理，以及异常处理的panic机制。</li>
<li><strong>方法：</strong> 详解Go方法的声明与本质，通过类型嵌入模拟“实现继承”。</li>
<li><strong>接口：</strong> 说明接口类型的定义、实现方法与注意事项。</li>
</ul>
<p>并发是Go的“杀手锏”级高阶特性，书中详述了Go并发的原理，给出了并发实现方案，即通过channel通信实现goroutine间同步，而非共享内存。说明channel与select结合使用的惯用法。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-5.png" alt="" /><br />
<center>CSP模型</center></p>
<p>泛型是Go 1.18版本的新增特性，解决了为不同类型编写重复代码的痛点。书中介绍了Go泛型设计演化简史，讲解泛型语法、类型参数、类型约束，并给出了代码示例。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-6.png" alt="" /><br />
<center>接口类型的扩展定义</center></p>
<p>最后讨论Go代码的质量保障方法，介绍了Go内置的测试框架，包括单元测试、示例测试、测试覆盖率以及性能基准测试，帮助读者快速且方便地组织、编写、执行测试，并得到详尽的测试结果反馈。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-7.png" alt="" /><br />
<center>Go测试覆盖率报告</center></p>
<h2>Part.4 作者介绍</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-8.png" alt="" /></p>
<p>本书作者Tony Bai（白明），资深架构师，行业经验超20年，现于汽车行业某独角兽Tier1企业担任车云平台架构组技术负责人。</p>
<p>出于对技术的追求与热爱，他发起了Gopher部落技术社群，也是tonybai.com的博主。</p>
<p>Tony Bai老师早在2011年Go语言还没发布Go 1.0稳定版本时，他就在跟随、实践。当Go在大规模生产环境中逐渐替代了C、Python，Go便成为他编写生产系统的第一语言。</p>
<p>后来，Tony Bai老师在极客时间上开设课程讲解Go语言开发，引领学员从入门到建立思维框架，走向大厂。累计2.4w名学员学习这门课程并纷纷给出高分评价。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-9.png" alt="" /></p>
<p>如今，Tony Bai老师基于在线课程将内容整理成书，并补充了之前缺失的重要语法点（如指针、测试、泛型等），并对已有内容进行了精炼，同时更新至Go 1.24版本。</p>
<p>相信这本书会帮助更多读者轻松学会Go语言，解决实际工作问题，获得职业成功。</p>
<h2>Part.5 结语</h2>
<p>《Go 语言第一课》这本书可以说既懂新手痛点，又懂工程实战。本书从Go的设计哲学入手，然后给出保姆级的环境搭建、代码组织指南，最后通过由浅入深的语法讲解，覆盖从基础到高阶的所有核心特性。</p>
<p>本书具备三大特点。</p>
<p><strong>第一是高屋建翎</strong>，开篇即剖析Go语言的设计哲学和编程思想，帮助读者透彻理解Go的核心理念，了解Go的特长，知道如何使用以获得最佳效果。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-10.png" alt="" /><br />
<center>精彩书摘</center></p>
<p><strong>第二是路径完整</strong>，覆盖Go入门的基础知识与概念，打通基础知识-语法特性-工程实践全流程，助力读者从新手进化为合格的Go开发工程师。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-11.png" alt="" /><br />
<center>精彩书摘</center></p>
<p><strong>第三是保姆级讲解</strong>，搭建环境是一步一图，讲解语法时辅以大量精心设计的示例代码，简洁明了，帮助读者直观地理解和掌握重点与难点内容。书中还针对Go开发中易犯的错误给出了贴心的避坑提示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-12.png" alt="" /><br />
<center>精彩书摘</center></p>
<p>本书适合各个层次的读者。对于Go初学者，可以循序渐进地掌握Go编程；对于动态编程语言的开发者，可以通过本书平滑转投Go阵营；对于Go的技术爱好者，可以增进认知，培养专业开发水准。</p>
<p>现在翻开《Go 语言第一课》，开启Go开发之旅，高并发服务端、云原生应用开发，都将轻松掌控！</p>
<h2><strong>今日互动</strong></h2>
<p>说说你对Go语言的看法？</p>
<p>点击右侧链接，在<a href="https://mp.weixin.qq.com/s/pxIfuxtQN7HTXBxwYQMw3Q">原文留言区</a>参与互动，并点击在看和转发活动到朋友圈，我们将选1名读者获得赠书1本，截止时间9月15日。</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>增值类业务短信收发协议介绍</title>
		<link>https://tonybai.com/2019/08/21/introduction-on-tech-protocol-of-transfering-value-added-sms/</link>
		<comments>https://tonybai.com/2019/08/21/introduction-on-tech-protocol-of-transfering-value-added-sms/#comments</comments>
		<pubDate>Wed, 21 Aug 2019 10:37:16 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[3GPP]]></category>
		<category><![CDATA[5G]]></category>
		<category><![CDATA[cmcc]]></category>
		<category><![CDATA[CMPP]]></category>
		<category><![CDATA[gateway]]></category>
		<category><![CDATA[gocmpp]]></category>
		<category><![CDATA[GSM]]></category>
		<category><![CDATA[mms]]></category>
		<category><![CDATA[sgip]]></category>
		<category><![CDATA[smgp]]></category>
		<category><![CDATA[smpp]]></category>
		<category><![CDATA[sms]]></category>
		<category><![CDATA[smsc]]></category>
		<category><![CDATA[SP]]></category>
		<category><![CDATA[TLV]]></category>
		<category><![CDATA[udhi]]></category>
		<category><![CDATA[wap]]></category>
		<category><![CDATA[中国电信]]></category>
		<category><![CDATA[中国移动]]></category>
		<category><![CDATA[中国联通]]></category>
		<category><![CDATA[基站]]></category>
		<category><![CDATA[增值类短信]]></category>
		<category><![CDATA[彩信]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[微博]]></category>
		<category><![CDATA[拇指族]]></category>
		<category><![CDATA[服务提供商]]></category>
		<category><![CDATA[短信]]></category>
		<category><![CDATA[短信中心]]></category>
		<category><![CDATA[短信平台]]></category>
		<category><![CDATA[短信网关]]></category>
		<category><![CDATA[移动梦网]]></category>
		<category><![CDATA[规范]]></category>
		<category><![CDATA[运营商]]></category>
		<category><![CDATA[验证码]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2739</guid>
		<description><![CDATA[在上一篇《增值类短信业务图文简介》中，我们介绍了什么是增值类短信业务以及增值类短信的收发流程。在这篇中我们将进一步深入介绍增值类短信收发协议的相关内容，不过重点放在短信内容编码对短信呈现的影响。 从近两年大火的5G我们可以看到，在移动通信领域规范和标准先行。虽然第一条短信在1992年在实验室就被发了出来，但是这离真正的短信商用还有很长一段距离。之后作为GSM(Global System for Mobile Communications，全球移动通信系统)技术的一个组成部分，GSM规范中对短信内容编码格式作了进一步说明： SMS消息内容的最大长度为160个字符（GSM字符集中的7比特字符）或140个八位字节，也可以支持其他字符集，例如UCS-2的16位编码的字符，最多支持70个UCS-2字符长度的消息。处理SMS消息的应用程序必须确保争取的字符集映射。 SMS消息的接收者不必是移动电话。它可以是一个可以通过网关来处理SMS消息的服务器。 SMS消息可用于传输几乎任何类型的数据（虽然有一个非常严格的大小限制），唯一标准化的是其基于字符的数据格式，字符按照一定的字符集格式进行编码。SMS消息的最大长度为160个字符 （当使用7位字符编码时）或140 字节。但是，SMS消息可以连接起来以形成更长的消息。但如何连接在此GSM规范中并未规范化的明确描述。 上面GSM规范中关于短信的约束和限制，显然是考虑了短信收发硬件设备、网络带宽等综合因素，但这一约束一直延续至今。当然在短信后续的发展中，通过对短信内容编码格式的扩展，丰富了短信的内容的展现形式，使短信尽可能的满足各种场景的需要。 一. 按短信内容呈现形式分类 短信内容的呈现形式是通过设置短信协议控制字段、短信内容编码和短信内容共同实现的。我们日常常见的三种短信内容形式如下： 普通短信 普通短信是指短信内容在70个中文字符以内（含70个字符，采用UCS-2编码或GB2312编码）或160个英文字符以内（含160个字符，采用7比特字符编码）的短信。由于国内工信部要求发送给手机用户的增值类短信必须有“签名”（比如上面截图中的短信开始处的【美团点评】），因此短信实际承载内容要少于70个中文字符或160个英文字符（7bit编码字符）。因此，一旦SP要发送超过如此规定长度的短信，那么普通短信将无法满足,这就有了下面级联短信的需求。 级联短信（俗称长短信） 对于普通手机用户来说，你可能不会注意到级联短信和普通短信的差别，因为到达手机后，这两种短信都以一条短信的形式呈现，只是级联短信内容较长罢了。我们看到手机上呈现的级联短信虽然是一条，但是其长度已经远超出上述GSM对短信内容长度的规定，这样的短信其实是在手机侧合成的。当某个SP要给某手机用户发送长度超出单条普通短信内容承载长度的短息时，会将超长内容拆分为多条有一定关联性的短信下发。这批短信被手机用户的终端接收后，终端会根据其关联关系将这批短信合并为一条短信显示出来。具体的细节在下面介绍短信协议内容时会详细说明。 WAPPUSH短信 注意这是一条垃圾短信。现在通过wappush短信发送垃圾信息也是一种趋势，运营商正在这方面加强防范和堵漏。 WAPPUSH短信是一类特殊格式的短信，它诞生于2.5G时代，那个时候通过手机浏览互联网尚不十分方便，流量贵，带宽还窄。一些服务为了方便手机用户能快速定位到自己的页面，便将携带服务url的内容通过短信下发给手机用户。手机用户点击链接即可打开服务页面。这类短信还可以在用户阅读WAPPUSH短信时自动加载服务页面，而无需用户手动点击内容中的链接。 彩信通知也是通过这种方式下发到手机用户的。这样手机用户既可以在查看短信时自动在手机上下载并查看彩信，也可以手动点击彩信通知短信中的链接，打开存储彩信内容的服务页面查看彩信内容。 上面是目前可以见到的最常见的三类短信形式，当然还有类似闪信等不太常见的短信呈现形式，这里就不重点描述了。 二. 短信相关规范 在上一篇文章中，我们说过SP是通过各大运营商的专用协议连接到运营商的短信网关进行增值类短信下发的，这里就以中国移动的CMPP协议(China Mobile Peer to Peer)为例（版本3.0)进行举例说明。 CMPP是在TCP之上的基于请求-响应的应用层通信协议，从内容上看，它改编自SMPP规范，但对暴露给SP的字段做了进一步约束；增加了运营商对短信计费相关字段。我们要关注的是CMPP协议的submit包。submit包是SP向短信网关发送的承载短信的协议包，一个submit包可以理解为最终到达手机用户的一条短信（当然submit包也支持群发）。 1. tp_udhi（用户数据头指示器） 这里我们重点关注的是协议字段对短信内容呈现的影响。在CMPP submit包中，字段tp_udhi（用户数据头指示器）、msg_fmt（内容编码格式）、msg_length（内容长度）和msg_content（短信内容）对网关解析短信、手机解析并呈现短信起到了至关重要的作用，因为这几个字段将被后续处理短信的各个网元“透传”直至手机上，并影响着手机对所接受到的短信的解析和呈现。 CMPP规范描述tp_udhi字段时提到了参考GSM03.40 中的 9.2.3.23。在《图解3GPP规范文档组织结构与编号规则》一文中，我们提到过03.40中的03系列文档仅适用于早期GSM系统，如今已经进化到4G、5G时代，我们可以直接参考对应该规范的新版规范23.040，你也可以看到23.040和03.40的Title是一致的，都是”Technical realization of the Short Message Service (SMS)”。 我们直接打开23.040（这里使用的版本是v12.2.0)文档，定位到9.2.3.23小节，我们看到的就是对tp_udhi字段的说明。在3GPP规范中，tp_udhi只是短信协议数据单元（PDU）第一个字节中的一个bit位，它只有两个值：0和1。当我们将cmpp submit包中的tp_udhi设置为1时，3GPP中短信PDU中的tp_udhi bit位将被置为1，也就是表明在短信内容中携带有短信头结构。如果为0，则内容里不包含短信头结构： 判断逻辑： tp_udhi bit位是否置为 1? no -&#62; [...]]]></description>
			<content:encoded><![CDATA[<p>在上一篇<a href="https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/">《增值类短信业务图文简介》</a>中，我们介绍了什么是增值类短信业务以及增值类短信的收发流程。在这篇中我们将进一步深入介绍增值类短信收发协议的相关内容，不过重点放在短信内容编码对短信呈现的影响。</p>
<p>从近两年大火的5G我们可以看到，在移动通信领域<strong>规范和标准先行</strong>。虽然第一条短信在1992年在实验室就被发了出来，但是这离真正的短信商用还有很长一段距离。之后作为GSM(Global System for Mobile Communications，全球移动通信系统)技术的一个组成部分，<a href="https://www.ietf.org/rfc/rfc5724.txt">GSM规范</a>中对短信内容编码格式作了进一步说明：</p>
<ul>
<li>
<p>SMS消息内容的最大长度为160个字符（GSM字符集中的7比特字符）或140个八位字节，也可以支持其他<a href="https://tonybai.com/2007/11/03/also-talk-about-char-encoding/">字符集</a>，例如UCS-2的16位编码的字符，最多支持70个UCS-2字符长度的消息。处理SMS消息的应用程序必须确保争取的<a href="https://tonybai.com/2007/11/03/also-talk-about-char-encoding/">字符集映射</a>。</p>
</li>
<li>
<p>SMS消息的接收者不必是移动电话。它可以是一个可以通过网关来处理SMS消息的服务器。 SMS消息可用于传输几乎任何类型的数据（虽然有一个非常严格的大小限制），唯一标准化的是其<strong>基于字符的数据格式</strong>，字符按照一定的字符集格式进行编码。SMS消息的最大长度为160个字符 （当使用7位字符编码时）或140 字节。但是，SMS消息可以连接起来以形成更长的消息。但如何连接在此GSM规范中并未规范化的明确描述。</p>
</li>
</ul>
<p>上面GSM规范中关于短信的约束和限制，显然是考虑了短信收发硬件设备、网络带宽等综合因素，<strong>但这一约束一直延续至今</strong>。当然在短信后续的发展中，通过对短信内容编码格式的扩展，丰富了短信的内容的展现形式，使短信尽可能的满足各种场景的需要。</p>
<h2>一. 按短信内容呈现形式分类</h2>
<p>短信内容的呈现形式是通过设置短信协议控制字段、短信内容编码和短信内容共同实现的。我们日常常见的三种短信内容形式如下：</p>
<ul>
<li>普通短信</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-normal-sms.png" alt="img{512x368}" /></p>
<p>普通短信是指短信内容在70个中文字符以内（含70个字符，采用UCS-2编码或GB2312编码）或160个英文字符以内（含160个字符，采用7比特字符编码）的短信。由于国内工信部要求发送给手机用户的增值类短信必须有“签名”（比如上面截图中的短信开始处的【美团点评】），因此短信实际承载内容要少于70个中文字符或160个英文字符（7bit编码字符）。因此，一旦SP要发送超过如此规定长度的短信，那么普通短信将无法满足,这就有了下面级联短信的需求。</p>
<ul>
<li>级联短信（俗称长短信）</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-concat-sms.png" alt="img{512x368}" /></p>
<p>对于普通手机用户来说，你可能不会注意到级联短信和普通短信的差别，因为到达手机后，这两种短信都以一条短信的形式呈现，只是级联短信内容较长罢了。我们看到手机上呈现的级联短信虽然是一条，但是其长度已经远超出上述GSM对短信内容长度的规定，这样的短信其实是在手机侧合成的。当某个SP要给某手机用户发送长度超出单条普通短信内容承载长度的短息时，会将超长内容拆分为多条有一定关联性的短信下发。这批短信被手机用户的终端接收后，终端会根据其关联关系将这批短信合并为一条短信显示出来。具体的细节在下面介绍短信协议内容时会详细说明。</p>
<ul>
<li>WAPPUSH短信</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-wappush-mms-notify.png" alt="img{512x368}" /></p>
<p>注意这是一条垃圾短信。现在通过wappush短信发送垃圾信息也是一种趋势，运营商正在这方面加强防范和堵漏。</p>
<p>WAPPUSH短信是一类特殊格式的短信，它诞生于2.5G时代，那个时候通过手机浏览互联网尚不十分方便，流量贵，带宽还窄。一些服务为了方便手机用户能快速定位到自己的页面，便将携带服务url的内容通过短信下发给手机用户。手机用户点击链接即可打开服务页面。这类短信还可以在用户阅读WAPPUSH短信时自动加载服务页面，而无需用户手动点击内容中的链接。</p>
<p>彩信通知也是通过这种方式下发到手机用户的。这样手机用户既可以在查看短信时自动在手机上下载并查看彩信，也可以手动点击彩信通知短信中的链接，打开存储彩信内容的服务页面查看彩信内容。</p>
<p>上面是目前可以见到的最常见的三类短信形式，当然还有类似<strong>闪信</strong>等不太常见的短信呈现形式，这里就不重点描述了。</p>
<h2>二. 短信相关规范</h2>
<p>在<a href="https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/">上一篇文章</a>中，我们说过SP是通过各大运营商的专用协议连接到运营商的短信网关进行增值类短信下发的，这里就以中国移动的<a href="https://tonybai.com/wp-content/uploads/sms/CMPP3.0.pdf">CMPP协议(China Mobile Peer to Peer)</a>为例（版本3.0)进行举例说明。</p>
<p>CMPP是在TCP之上的<strong>基于请求-响应的应用层通信协议</strong>，从内容上看，它改编自SMPP规范，但对暴露给SP的字段做了进一步约束；增加了运营商对短信计费相关字段。我们要关注的是CMPP协议的submit包。submit包是SP向短信网关发送的承载短信的协议包，一个submit包可以理解为最终到达手机用户的一条短信（当然submit包也支持群发）。</p>
<h3>1. tp_udhi（用户数据头指示器）</h3>
<p>这里我们重点关注的是协议字段对短信内容呈现的影响。<strong>在CMPP submit包中，字段tp_udhi（用户数据头指示器）、msg_fmt（内容编码格式）、msg_length（内容长度）和msg_content（短信内容）</strong>对网关解析短信、手机解析并呈现短信起到了至关重要的作用，因为这几个字段将被后续处理短信的各个网元“透传”直至手机上，并影响着手机对所接受到的短信的解析和呈现。</p>
<p>CMPP规范描述tp_udhi字段时提到了参考<strong><a href="https://www.3gpp.org/DynaReport/0340.htm">GSM03.40</a> 中的 9.2.3.23</strong>。在<a href="https://tonybai.com/2019/07/25/illustrate-3gpp-spec-docs-structure-and-numbering">《图解3GPP规范文档组织结构与编号规则》</a>一文中，我们提到过03.40中的03系列文档仅适用于早期GSM系统，如今已经进化到4G、5G时代，我们可以直接参考对应该规范的新版规范<a href="https://www.3gpp.org/DynaReport/23040.htm">23.040</a>，你也可以看到23.040和03.40的<strong>Title是一致的</strong>，都是”Technical realization of the Short Message Service (SMS)”。</p>
<p>我们直接打开23.040（这里使用的版本是v12.2.0)文档，定位到9.2.3.23小节，我们看到的就是对tp_udhi字段的说明。在3GPP规范中，tp_udhi只是短信协议数据单元（PDU）第一个字节中的一个bit位，它只有两个值：0和1。当我们将cmpp submit包中的tp_udhi设置为1时，3GPP中短信PDU中的tp_udhi bit位将被置为1，也就是表明在短信内容中携带有<strong>短信头结构</strong>。如果为0，则内容里不包含短信头结构：</p>
<pre><code>判断逻辑：

tp_udhi bit位是否置为 1?

    no -&gt;  普通短信；

    yes -&gt; 内容带有短信头结构的短信（可能是级联短信、可能是wappush短信）

</code></pre>
<p>对于普通短信，短信接收侧仅需要根据msg_fmt、msg_length对短信内容进行解析呈现即可。但对于带有短信头结构的短信（tp_udhi bit位置1），还需要进一步分析。</p>
<h3>2. 短信内容中的用户短信头结构</h3>
<p>用户短信头结构是一组类TLV格式的数据段。注意这里明确是一组，也就是说短信内容头中支持放置多个数据段（如下图中的part1~partN)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-content-header-structure.png" alt="img{512x368}" /></p>
<p>如图所示，cmpp submit的msg_length标识了整个短信内容的长度。如果tp_udhi被置为1，即短信内容中包含短信头结构，那么短信内容的第一个字节是UDHL，即后面短信头的长度。短信头可由多个part组成，每个part都是一个类TLV格式的连续数据：IEI、IEIDL、IED。IEI：信息元素标识符，大小为一个字节，相当于TLV中的T(Type)；IEIDL(信息元素数据长度)指示本part中IED的长度，相当于TLV中的L；IED（信息元素数据）是本part中承载的有价值数据，相当于TLV中的V。</p>
<p>3GPP 23.040定义了一组已知的IEI标准值(9.2.3.24)，我们从中取出几个我们关心的：</p>
<ul>
<li>
<p>0&#215;00 &#8211; Concatenated short messages, 8-bit reference number</p>
</li>
<li>
<p>0&#215;04 &#8211; Application port addressing scheme, 8 bit address</p>
</li>
<li>
<p>0&#215;05 &#8211; Application port addressing scheme, 16 bit address</p>
</li>
<li>
<p>0&#215;08 &#8211; Concatenated short message, 16-bit reference number</p>
</li>
</ul>
<p>其中0&#215;00和0&#215;08对应的是级联短信；0&#215;04、0&#215;05对应的是WAPPUSH消息，下面我们来逐一详细说明。</p>
<h3>3. 级联短信</h3>
<p>前面对级联短信做了简单的诠释：SP将超出普通短信长度的短信拆分为多条短信（每条短信称为该批次级联短信的一个segment），这些短信之间存在关联，当手机用户收到这些短信后，手机上的短信接收程序会将它们重新组装为一条长长的短信并呈现给用户。这里提到的“短信间的关联”就是通过附着在短信内容中的短信头结构实现的。</p>
<p>3GPP规范定义了将短信连接在一起形成更长短信的标准方式（参考23.040 9.2.3.24.1和9.2.3.24.8）。根23.040规范中的描述，IEI = 0&#215;00和0&#215;08的part是为级联短信服务的。但二者只能选择一种，不能共存。我们分别来说说：</p>
<h4>1) IEI = 0&#215;00即reference number为一个字节的级联短信</h4>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-concat-8bit-refnum.png" alt="img{512x368}" /></p>
<p>上图是一个IEI=0&#215;00的级联短信的例子。当短信为IEI=0&#215;00级联短信的一个segment时，短信内容头部的IEIDL为0&#215;03，IED由三部分组成，每个部分一个字节。它们依次是：</p>
<ul>
<li>
<p>reference number &#8211; 该批次级联短信的唯一标识（0~255），手机端重组短信时，就是使用该字段将一批segment重组在一起的；</p>
</li>
<li>
<p>max number &#8211; 该批次级联短信共多少条（0~255）</p>
</li>
<li>
<p>sequence number &#8211; 当前短信segment是该批次级联短信的第几条（从1开始）,该字段用于在重组短信时为短信segment排序。（0~255）</p>
</li>
</ul>
<h4>2) IEI = 0&#215;08即reference number为两个字节的级联短信</h4>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-concat-16bit-refnum.png" alt="img{512x368}" /></p>
<p>为了减少两条不同的级联短信因reference number的值空间过小导致ref number一致而冲突的情况，3GPP还增加了一个IEI=0&#215;80的增强级联长短信类别。与8bit的ref number相比，仅仅是ref number的长度变长了,由一个字节变为两个字节（值空间由256个变为65536个）。而其他字段的位置和含义完全不变。这里就不赘述了。不过要注意的是打包或解析ref number时要注意<a href="https://tonybai.com/2005/09/28/also-talk-about-byte-order/">字节序</a>转换。</p>
<h3>4. 端口应用类短信</h3>
<p>很多朋友会提出：上面图中每条消息的内容组成和网络协议栈怎么很相像呢？都是header + payload！没错！基于短信头结构，我们还可以通过在头结构中放置应用端口号，手机收到短信后，会根据目的应用端口将消息发送给对应的应用或启动对应的应用来处理这条短信，<strong>而短信的内容(payload)则是应用所需的数据</strong>，这类短信我称之为<strong>端口应用类短信</strong>。在3GPP 23.040的标准IEI定义表中，IEI=0&#215;04和IEI=0&#215;05就是用于在短信头中携带应用端口信息的，两者不同的是端口号所占字节不同，IEI=0&#215;04对应的port占用1个字节（端口号表示的值空间较小，最大255），而IEI=0&#215;05对应的port占用2个字节（扩展了端口号表示的值空间，最大65535）。我们用一幅图来诠释一下IEI=0&#215;04和IEI=0&#215;05时，短信头结构的样式：</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-appport.png" alt="img{512x368}" /></p>
<p>由于IEI=0&#215;04对应的port值空间有限，因此在实际使用中并不广泛。更多的采用短信协议承载应用数据的使用的是IEI=0&#215;05，即应用端口采用16bit表示。</p>
<p>WAPPush短信是端口应用类短信的一种，它属于基于短信递送网络(Bearer Network)实现的WAP协议族中的push类应用。所谓Push类应用是用于向驻留在WAP设备（比如手机）上的应用程序传输数据的。这和我们在上面的理解一致，通过短信向手机上的某些应用传递数据，短信内容（去头后）就是应用所需的数据。IANA list一些标准的服务名和端口，可以在<a href="https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt">这里</a>查询。</p>
<p>下面我们就以WAPPush类短信为例，看看要传输的应用数据是如何打包在一条短信的内容中的。</p>
<h4>1) WAP协议栈与短信的映射</h4>
<p>我们即将从<a href="https://tonybai.com/2019/07/25/illustrate-3gpp-spec-docs-structure-and-numbering/">3GPP规范</a>转换到<a href="http://www.openmobilealliance.org/wp/Affiliates/WAP.html">WAP相关规范</a>。WAP（Wireless Application Protocol）是一套基于无线协议的应用协议栈。在2G或2.5G时代以及3G初期，它是无线网络应用的主流。下面是WAP的完整协议栈示意图(来自网络)，也可参考规范<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-210-waparch-20010712-a.pdf">《Wireless Application Protocol Architecture Specification》:wap-210-waparch-20010712-a.pdf</a>中的协议栈全图Figure-7（不过不是很清晰）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/wap-stack.jpg" alt="img{512x368}" /></p>
<p>接下来我们要明确WAP协议栈在wappush短信应用时是如何与短信进行映射的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-wappush-stack-mapping.png" alt="img{512x368}" /></p>
<p>在图中我们看到了WAP Push应用与短信的映射关系：</p>
<ul>
<li>
<p>底层递送网络使用SMS；</p>
</li>
<li>
<p>Transport Layer即WDP对应到短信内容头部的一个IE part，在这部分数据中，我们能找到源port和目的port，这与IP网络协议栈中UDP十分类似（参考：<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-259-wdp-20010614-a.pdf">《Wireless Datagram Protocol Specification</a>》WAP-259-WDP-20010614-a.pdf 6.3.1和6.3.2）。</p>
</li>
<li>
<p>安全层和Transaction layer被省略了，暂无对应。</p>
</li>
<li>
<p>WSP(Session layer)对应到短信内容头之后的第一段自定义数据段。这段数据的形式由Type字段确定。以Push类(type=0&#215;06)为例，这段数据包含：tid, type,headerslen,contenttype,headers和data。而data就是真正应用层的数据。（参考：<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-230-wsp-20010705-a.pdf">《Wireless Session Protocol Specification》</a> WAP-230-WSP-20010705-a.pdf 8.1.2、8.2.1、8.2.4.1、8.4.1)</p>
</li>
<li>
<p>WAE（application layer)对应的就是wsp承载的data字段，以push为例，这里存放的是应用所需的数据。这里的数据究竟是什么，要根据wsp层的ContentType确定。如果是 “application/vnd.wap.mms-message”，那么data中存放的就是mms notification(彩信通知短信）。</p>
</li>
</ul>
<h4>2) WSP PDU介绍</h4>
<p>这里把WSP PDU单独介绍一下，该PDU的字段涉及的内容还是略微复杂的。下面是一个push类的WSP PDU的构成字段示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-wsp-pdu-push-type.png" alt="img{512x368}" /></p>
<ul>
<li>
<p>TID &#8211; Transaction ID uint8类型，标识该PDU所属transaction；</p>
</li>
<li>
<p>Type &#8211; 标识PDU的类型，uint8类型。该字段直接决定了该PDU后面的数据组成格式。WAP规范定义了标准的Type值列表，可参考：<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-230-wsp-20010705-a.pdf">《Wireless Session Protocol Specification》</a> 附录A 表34 PDU Type Assignments；这里我们用push类型举例，因此该字段为0&#215;06。</p>
</li>
</ul>
<p>接下来的数据字段是Push类型wsp pdu特有的，其他type pdu会有不同，但构成类似。熟悉了push类型的pdu字段的解析方式后，其他type的pdu也不是问题了。</p>
<ul>
<li>
<p>HeadersLen 这个字段指示了push类pdu的header的长度：包括后面的ContentType和Headers的长度之和。值得注意的是该字段是uintvar类型，这是一种带有continue bit的7 bit编码的类型，其解析算法参见<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-230-wsp-20010705-a.pdf">《Wireless Session Protocol Specification》</a> WAP-230-WSP-20010705-a.pdf 8.1.2。uintvar类型在WSP规范中大量出现，可以实现一个独立的函数来读取一个uintvar或写入一个uintvar，便于重用；</p>
</li>
<li>
<p>ContentType 指示后面Data中的内容类型。ContentType是一个多字节的数据。它也是WSP PDU头部解析的一个难点。WSP要求客户机和服务器之间交换的信息都采用紧缩的编码格式，很多常见字段的name使用了well-known value作替代了，这样可以压缩存储空间，提高传输效率。ContentType字段本身就支持多种值格式，包括well-known value，变长字节数据(以uintvar开头的)或纯文本字符串形式。在WAP-230-WSP-20010705-a.pdf的 8.4.2.24小节有关于ContentType字段值格式的定义。在8.4.1.2中有关于header中field value第一个octet的值以及对应的含义，以帮助你解析Header field value，ContentType也是一个Header field value。这里摘录如下：</p>
</li>
</ul>
<pre><code>the first octet in all the field values can be interpreted as follows:

Value      Interpretation of First Octet

0 - 30     This octet is followed by the indicated number (0 ¨C30) of data octets

31         This octet is followed by a uintvar, which indicates the number of data octets after it

32 - 127   The value is a text string, terminated by a zero octet (NUL character)

128 - 255  It is an encoded 7-bit value; this header has no more data.

</code></pre>
<p>因此，解析ContentType我们要区分多种情况。</p>
<h4>3) ContentType解析举例</h4>
<p>我们以两种情况为例，一种是Well-known value形式; 另外一种形式是text string形式。</p>
<p>先来看Well-known value形式。如果我们解析到ContentType时遇到一组字节：03AE81EA。</p>
<ul>
<li>
<p>0&#215;03在[0,30]范围内，按照WSP规范，这个0&#215;03是一个是一个length，表明后面的三个octets都是ContentType的值。我们看到03后面的三个字节分别为0xAE、0&#215;81和0xEA；</p>
</li>
<li>
<p>按照WSP规范8.2.4.1关于 Short-integer的定义：</p>
</li>
</ul>
<pre><code>Short-integer = OCTET
; Integers in range 0-127 shall be encoded as a one octet value with the most significant bit set ; to one (1xxx xxxx) and with the value in the remaining least significant bits.

</code></pre>
<p>位于[0,127]区间的数字，在编码这些数字的时候，需要将字节最高bit置1。因此，我们需要将0xAE、0&#215;81和0xEA还原为原先的值，通过 n &amp; 0x7F计算 还原为0x2E、0&#215;01和0x6A。这三个值都是well-known value，我们需要查表找到其对应的含义。根据<a href="http://www.wapforum.org/wina/wsp-content-type.htm">content type assignment(WSP 附录Table 40)</a>、parameter(WSP规范 附录Table 38)以及该parameter对应的assignment(WSP规范附录 Table 42)的顺序，我们分别在表中确定三个字节对应的text：</p>
<pre><code>0x2E - "application/vnd.wap.sic"

0x01 -  "Charset"

0x6A - "utf-8"

</code></pre>
<p>因此该ContentType的值的文本形式是：”application/vnd.wap.sic; charset=utf-8&#8243;。</p>
<p>我们再来看看ContentType直接采用文本形式值的情况，这种情况较为简单：</p>
<pre><code>0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61 , 0x74 , 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64,

0x2e, 0x77, 0x61, 0x70, 0x2e, 0x6d, 0x6d, 0x73 , 0x2d , 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x00

</code></pre>
<p>该段数据的第一个字节为0&#215;61，其值在[32, 127]区间内，表明这是一个以零值结尾的字符串。我们将其以字符形式输出，得到的是：”application/vnd.wap.mms-message”。</p>
<p>得到ContentType数据后，我们再结合HeaderLen字段的值，可以计算出后面的Headers的长度。Headers中可能有多个字段，其构成格式与解析方式与ContentType的类似，这里不赘述了。</p>
<h4>3) 应用数据举例：彩信通知消息介绍</h4>
<p>在WSP头解析后，我们剩下的就是Data这个字段了。这个字段承载的是应用真正需要的数据。我们以ContentType=”application/vnd.wap.mms-message”为例，即彩信通知短信。来看看wappush承载的彩信通知短信的解析。</p>
<p>在<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-209-mmsencapsulation-20020105-a.pdf">《Multimedia Messaging Service Encapsulation Specification》</a> wap-209-mmsencapsulation-20020105-a.pdf 7.1 中有关于彩信通知短信字段编码的规则，彩信通知仅仅是包含彩信的Headers字段。因此，我们仅适用mms header的编码规则解析即可，这里摘录如下：</p>
<pre><code>MMS-header = MMS-field-name MMS-value

MMS-field-name = Short-integer

MMS-value =
Bcc-value |
Cc-value | Content-location-value |... ...

</code></pre>
<p>彩信通知的字段列表在<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-209-mmsencapsulation-20020105-a.pdf">《Multimedia Messaging Service Encapsulation Specification》</a>规范的6.2小节。</p>
<p>有了之前ContentType的解析经验后，解析这些字段便轻车熟路了。要注意几点：</p>
<ul>
<li>
<p>header field name的well-known value在<a href="http://www.openmobilealliance.org/tech/affiliates/wap/wap-209-mmsencapsulation-20020105-a.pdf">《Multimedia Messaging Service Encapsulation Specification》</a>规范的7.3小节表中</p>
</li>
<li>
<p>header field name都是short integer，因此要注意与上0x7F的转换，转换后的值与7.3小节表中的值进行比对。</p>
</li>
<li>
<p>某个具体header field的值的形式，是零值结尾字符串、uintvar还是特定值，查看对应header field的具体说明即可。</p>
</li>
</ul>
<h2>三. 小结</h2>
<p>到这里我们了解了短信协议对短信内容在手机端呈现形式的影响，我们知道了级联短信让我们可以接收到超过70个汉字字符的超长短信，我们知道了通过短信承载wap push协议，我们可以让手机上的应用接收到服务数据（比如一个服务的url或是一条彩信的访问地址）甚至可以在打开短信的时候自动加载彩信，并在手机端呈现彩信内容。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/08/21/introduction-on-tech-protocol-of-transfering-value-added-sms/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>增值类短信业务图文简介</title>
		<link>https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/</link>
		<comments>https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/#comments</comments>
		<pubDate>Tue, 20 Aug 2019 02:49:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[3GPP]]></category>
		<category><![CDATA[cmcc]]></category>
		<category><![CDATA[CMPP]]></category>
		<category><![CDATA[gateway]]></category>
		<category><![CDATA[gocmpp]]></category>
		<category><![CDATA[sgip]]></category>
		<category><![CDATA[smgp]]></category>
		<category><![CDATA[smpp]]></category>
		<category><![CDATA[sms]]></category>
		<category><![CDATA[smsc]]></category>
		<category><![CDATA[SP]]></category>
		<category><![CDATA[中国电信]]></category>
		<category><![CDATA[中国移动]]></category>
		<category><![CDATA[中国联通]]></category>
		<category><![CDATA[基站]]></category>
		<category><![CDATA[增值类短信]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[微博]]></category>
		<category><![CDATA[拇指族]]></category>
		<category><![CDATA[服务提供商]]></category>
		<category><![CDATA[短信]]></category>
		<category><![CDATA[短信中心]]></category>
		<category><![CDATA[短信平台]]></category>
		<category><![CDATA[短信网关]]></category>
		<category><![CDATA[移动梦网]]></category>
		<category><![CDATA[运营商]]></category>
		<category><![CDATA[验证码]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2734</guid>
		<description><![CDATA[以前一提到短信（Short Message），人们会想到“拇指族（在社交移动APP诞生前，专指用手机高频发短信的一个群体）”、“拜年短信”。现在再提到短信，人们想到的变成了“验证码”、“垃圾短信”以及“我好久不发短信了”。短信这一信息承载的媒介是伴随着移动通信工具一并诞生的，它是“古老的” &#8211; 1992年，22岁的加大拿工程师Neil Papworth用电脑给同事Richard Jarvis发出了人类历史上的第一条短信，总共15个字符（包括空格）：“merry christmas”；它也辉煌过，是曾经的“网红” &#8211; 2012年根据中国工信部(MIIT)的数据，中国手机用户共发送9000亿条短信，这是中国短信数量的高峰。2013年开始，短信开始走下坡路，社交APP（如微信）的出现，让短信业务出现断崖式下跌，直到2018年短信业务才有了些许恢复性的增长。 一. 什么是增值类短信 不可否认的是短信依旧（至少目前依然）是我们日常生活中不可缺少的信息获取媒介，只是我们不再主动发送（点对点短信: 手机用户A发给手机用户B的短信），而是被动接收（应用发给手机用户的验证码、通知信息以及垃圾短信）。 而这类被动接收的短信，多数都属于我们要重点说明的“增值类短信业务”。所谓增值类短信业务，说白了就是用于应用与用户互动的短信。 在增值类短信最火爆的年代，你一定听说过电台或电视中出现类似：“请发送XXX到YYYYYYY参与平台互动、抽奖、起名、查询天气预报、交通信息甚至是“算命”&#8230;.”的音频或电视滚动播放的提示文字，这些提供增值类短信应用的商户被称为SP(服务提供商Service Provider)，那个时候各大互联网门户也都是SP，都有自己的短信平台与手机用户互动。在这场以短信为媒介发展起来的增值短信业务市场中，移动运营商（国内的移动、联通、电信）赚的盆满钵满，因为每条发送给SP的短信都要扣费，费用至少包含两部分：通信费和业务费： 通信费是使用运营商短信通道的费用，是运营商收取的，好比汽车上高速要缴纳高速服务费，因为我们的车在行驶过程中占用了高速公路一定面积的路面，并造成了一定的高速路面损害；通信费一般是按条收取的，最常见的费用是1角/条；当然运营商最擅长提供“套餐”，套餐中包含一定数量的短信，如果每月发送的短信条数在套餐数量之下，就无需额外付费。 业务费是使用这个增值短信业务（比如天气预报）产生的费用。这个费用是否都给SP，要看SP与运营商签订的分成协议。多数情况下，运营商还是要从这个费用中扣除一部分分成后，将剩余的打给SP。比如手机用户发送一条查询天气预报的短信花费的业务费为1元；如果运营商和SP的协议是4:6分成的话，那么这1元中，运营商赚4角，提供天气服务的SP赚6角。业务费还可以按条/次或包月收取。以天气预报服务为例，如果是包月，那么手机用户在当月可以无限次给SP发送短信查询天气预报而不用担心逐条扣费。 我们看到在短信增值业务时代，运营商才是最大赢家，他们既收取通信费，还收取部分业务费。在增值类短信最火的几年中，运营商相继推出了自己的移动数据服务品牌，比如中国移动的移动梦网。当然短信增值业务仅是运营商数据业务的一种而已，他们还提供诸如彩信、wap等数据业务。 2013年及以后，随着移动互联网社交APP（诸如微信、移动QQ）的诞生与迅猛发展，短信作为社交工具的职能被彻底剥夺了，点对点短信彻底没落；随着微博、公众号、服务号等平台工具的推出，短信的互动功能、通知服务功能也被大幅削弱，以前的媒体互动平台几乎全部由短信平台迁移到微信、微博平台，大家日常听到最多的是请关注微信公众号或微博参与互动，互动短信被彻底扔到了历史的垃圾桶中。运营商再也不能像以前那样躺着收取手机用户的通道费和业务费了，这也直接导致了运营商在短信业务营收方面的大幅下降，运营商也要开始学勒紧裤腰带过日子了（当然和普通企业相比，运营商还是有钱人）。 目前让增值类短信业务屹立不倒的是验证码短信，这还多亏了国家出台的手机卡办理实名制，实名制让手机与身份几乎一一对应，也助推了短信成为了现存的、可用的最靠谱的（但不是最先进的）身份识别信息载体。可以说目前人们生活离得开“短信”，但离不开增值类的“验证码短信”。 有人会问全国每年发送的验证码短信没有万亿条，也有千百亿条了，运营商怎么没有以前赚钱了呢？这是因为这种从SP发到手机用户的短信，运营商只能收取SP的通道费，对SP收取的通道费原本就很低廉，且在三大运营商疯狂争夺客户的竞争中，SP的通道费还在逐年下降，直接导致运营商的增值短信业务出现量增但收入反降的局面。 二. 增值类短信是如何发送到你的手机上的 接下来，我们将进入偏技术的领域，我们来看看这类增值类短信是如何发送到用户手机上的。这里我们不会深入到运营商无线网络侧作细致说明，我们关注的更多是IP网络侧。国际上通用的关于SP接入运营商进行短信收发的协议是SMPP协议(Short Message Peer-to-Peer)。在最新的5.0版本协议规范中，我们可以看到一幅网络拓扑图，这里将其简化一下： 我们看到，在国际上通行的组网是这样的： SP通过smpp协议与运营商IP侧网络的Routing Entity相连，收发短信； Routing Entity这个网元的作用正如其名，它根据短信的相关信息，将短信路由转发到连接对应SMSC的Routing Entity上，然后下一个Routing Entity负责通过SMPP协议将短信下发到后面的SMSC； SMSC即短信中心，负责接收Routing Entity的短信，并将短信通过运营商的无线侧发到手机用户。 这已经是一个足够简化的网络拓扑图。 在国内，Routing Entity由各大运营商的短信业务网关(SMS Gateway)充当，原则上每个运营商在每个省份会建立一套短信网关。短信会首先由接收该短信的短信网关进行转发（可通过目的号码路由），将短信转发到目的号码归属省的短信中心(SMSC)，然后由SMSC将短信下发到手机用户。这里省略无线侧网元，可以更清晰看到全国短信网关（SMS Gateway）和短信中心(SMSC)组网(先不考虑不同运营商之间的短信收发)： 我们从图中可以看到，这是一个运营商在A省和B省的短信业务网元网络拓扑。SP从A省接入，因此A省也称为SP的接入省。手机用户A和手机用户B分别归属于A省和B省，因此称A省是手机用户A的归属地；B省市手机用户B的归属地。 我们看到：与国际通用方式不同的是，国内SP不是通过SMPP接入短信网关(SMS Gateway)（在没有短信网关之前，SP是通过SMPP直接接入smsc的），而是使用了运营商的专有协议（中国移动CMPP、中国联通SGIP、中国电信SMGP）；短信网关之间是互联的，通信协议也是运营商专有协议。一个省的短信网关只会连接本省的SMSCs。 当SP下发一条短信给手机用户A时，短信流经的网元如下： SP -&#62; A省短信网关(SMS Gateway) -&#62; A省某SMSC实例 -&#62; A省基站 -&#62; 手机用户A [...]]]></description>
			<content:encoded><![CDATA[<p>以前一提到<a href="https://en.wikipedia.org/wiki/Short_Message_Service">短信（Short Message）</a>，人们会想到“拇指族（在社交移动APP诞生前，专指用手机高频发短信的一个群体）”、“拜年短信”。现在再提到短信，人们想到的变成了“验证码”、“垃圾短信”以及“我好久不发短信了”。短信这一信息承载的媒介是伴随着移动通信工具一并诞生的，它是<strong>“古老的”</strong> &#8211; 1992年，22岁的加大拿工程师Neil Papworth用电脑给同事Richard Jarvis发出了人类历史上的第一条短信，总共15个字符（包括空格）：<strong>“merry christmas”</strong>；它也<strong>辉煌</strong>过，是曾经的“网红” &#8211; 2012年根据中国工信部(MIIT)的数据，中国手机用户共发送9000亿条短信，这是中国短信数量的高峰。2013年开始，短信开始走下坡路，社交APP（如微信）的出现，让短信业务出现断崖式下跌，直到2018年短信业务才有了些许恢复性的增长。</p>
<h2>一. 什么是增值类短信</h2>
<p>不可否认的是短信依旧（至少目前依然）是我们日常生活中不可缺少的信息获取媒介，只是我们不再主动发送（点对点短信: 手机用户A发给手机用户B的短信），而是被动接收（应用发给手机用户的验证码、通知信息以及垃圾短信）。</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-1.png" alt="img{512x368}" /></p>
<p>而这类被动接收的短信，多数都属于我们要重点说明的“增值类短信业务”。<strong>所谓增值类短信业务，说白了就是用于应用与用户互动的短信</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-2.png" alt="img{512x368}" /></p>
<p>在增值类短信最火爆的年代，你一定听说过电台或电视中出现类似：“请发送XXX到YYYYYYY参与平台互动、抽奖、起名、查询天气预报、交通信息甚至是“算命”&#8230;.”的音频或电视滚动播放的提示文字，这些提供增值类短信应用的商户被称为SP(服务提供商Service Provider)，那个时候各大互联网门户也都是SP，都有自己的短信平台与手机用户互动。在这场以短信为媒介发展起来的增值短信业务市场中，移动运营商（国内的移动、联通、电信）赚的盆满钵满，因为每条发送给SP的短信都要扣费，费用至少包含两部分：<strong>通信费</strong>和<strong>业务费</strong>：</p>
<ul>
<li>
<p>通信费是使用运营商短信通道的费用，是运营商收取的，好比汽车上高速要缴纳高速服务费，因为我们的车在行驶过程中占用了高速公路一定面积的路面，并造成了一定的高速路面损害；通信费一般是按条收取的，最常见的费用是1角/条；当然运营商最擅长提供“套餐”，套餐中包含一定数量的短信，如果每月发送的短信条数在套餐数量之下，就无需额外付费。</p>
</li>
<li>
<p>业务费是使用这个增值短信业务（比如天气预报）产生的费用。这个费用是否都给SP，要看SP与运营商签订的分成协议。多数情况下，运营商还是要从这个费用中扣除一部分分成后，将剩余的打给SP。比如手机用户发送一条查询天气预报的短信花费的业务费为1元；如果运营商和SP的协议是4:6分成的话，那么这1元中，运营商赚4角，提供天气服务的SP赚6角。业务费还可以按条/次或包月收取。以天气预报服务为例，如果是包月，那么手机用户在当月可以无限次给SP发送短信查询天气预报而不用担心逐条扣费。</p>
</li>
</ul>
<p>我们看到在短信增值业务时代，运营商才是最大赢家，他们既收取通信费，还收取部分业务费。在增值类短信最火的几年中，运营商相继推出了自己的<strong>移动数据服务品牌</strong>，比如中国移动的<a href="http://wap.monternet.com">移动梦网</a>。当然短信增值业务仅是运营商数据业务的一种而已，他们还提供诸如彩信、wap等数据业务。</p>
<p>2013年及以后，随着移动互联网社交APP（诸如微信、移动QQ）的诞生与迅猛发展，短信作为社交工具的职能被彻底剥夺了，点对点短信彻底没落；随着微博、公众号、服务号等平台工具的推出，短信的互动功能、通知服务功能也被大幅削弱，以前的媒体互动平台几乎全部由短信平台迁移到微信、微博平台，大家日常听到最多的是请关注微信公众号或微博参与互动，互动短信被彻底扔到了历史的垃圾桶中。运营商再也不能像以前那样躺着收取手机用户的通道费和业务费了，这也直接导致了运营商在短信业务营收方面的大幅下降，运营商也要开始学勒紧裤腰带过日子了（当然和普通企业相比，运营商还是有钱人）。</p>
<p>目前让增值类短信业务屹立不倒的是验证码短信，这还多亏了国家出台的手机卡办理实名制，实名制让手机与身份几乎一一对应，也助推了短信成为了现存的、可用的最靠谱的（但不是最先进的）身份识别信息载体。可以说目前人们生活离得开“短信”，但离不开增值类的“验证码短信”。</p>
<p>有人会问全国每年发送的验证码短信没有万亿条，也有千百亿条了，运营商怎么没有以前赚钱了呢？这是因为这种从SP发到手机用户的短信，运营商只能收取SP的通道费，对SP收取的通道费原本就很低廉，且在三大运营商疯狂争夺客户的竞争中，SP的通道费还在逐年下降，直接导致运营商的增值短信业务出现量增但收入反降的局面。</p>
<h2>二. 增值类短信是如何发送到你的手机上的</h2>
<p>接下来，我们将进入偏技术的领域，我们来看看这类增值类短信是如何发送到用户手机上的。这里我们不会深入到运营商<strong>无线网络侧</strong>作细致说明，我们关注的更多是<strong>IP网络侧</strong>。国际上通用的关于SP接入运营商进行短信收发的协议是<a href="https://smpp.org/">SMPP协议(Short Message Peer-to-Peer)</a>。在最新的5.0版本协议规范中，我们可以看到一幅网络拓扑图，这里将其简化一下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-3.png" alt="img{512x368}" /></p>
<p>我们看到，在国际上通行的组网是这样的：</p>
<ul>
<li>
<p>SP通过smpp协议与运营商IP侧网络的Routing Entity相连，收发短信；</p>
</li>
<li>
<p>Routing Entity这个网元的作用正如其名，它根据短信的相关信息，将短信路由转发到连接对应SMSC的Routing Entity上，然后下一个Routing Entity负责通过SMPP协议将短信下发到后面的SMSC；</p>
</li>
<li>
<p>SMSC即短信中心，负责接收Routing Entity的短信，并将短信通过运营商的无线侧发到手机用户。</p>
</li>
</ul>
<p>这已经是一个足够简化的网络拓扑图。</p>
<p>在国内，Routing Entity由各大运营商的短信业务网关(SMS Gateway)充当，原则上每个运营商在每个省份会建立一套短信网关。短信会首先由接收该短信的短信网关进行转发（可通过目的号码路由），将短信转发到目的号码归属省的短信中心(SMSC)，然后由SMSC将短信下发到手机用户。这里省略无线侧网元，可以更清晰看到全国短信网关（SMS Gateway）和短信中心(SMSC)组网(先不考虑不同运营商之间的短信收发)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/value-added-sms-intro-4.png" alt="img{512x368}" /></p>
<p>我们从图中可以看到，这是一个运营商在A省和B省的短信业务网元网络拓扑。SP从A省接入，因此A省也称为SP的接入省。手机用户A和手机用户B分别归属于A省和B省，因此称A省是手机用户A的归属地；B省市手机用户B的归属地。</p>
<p>我们看到：与国际通用方式不同的是，国内SP不是通过SMPP接入短信网关(SMS Gateway)（在没有短信网关之前，SP是通过SMPP直接接入smsc的），而是使用了运营商的专有协议（中国移动<a href="https://github.com/bigwhite/gocmpp">CMPP</a>、中国联通SGIP、中国电信SMGP）；短信网关之间是互联的，通信协议也是运营商专有协议。一个省的短信网关只会连接本省的SMSCs。</p>
<p>当SP下发一条短信给手机用户A时，短信流经的网元如下：</p>
<pre><code>SP -&gt; A省短信网关(SMS Gateway) -&gt; A省某SMSC实例 -&gt; A省基站 -&gt; 手机用户A

</code></pre>
<p>当SP下发一条短信给手机用户B时，短信流经的网元如下：</p>
<pre><code>SP -&gt; A省短信网关(SMS Gateway) -&gt; B省短信网关(SMS Gateway) -&gt; B省某SMSC实例 -&gt; B省基站 -&gt; 手机用户B

</code></pre>
<p>如果手机用户收到短信后要与SP互动，即手机用户发送一条短信到SP的号码上时，流程是这样的。</p>
<p>当手机用户A给SP发送一条短信，该短信流经的网元如下：</p>
<pre><code>手机用户A -&gt; A省基站 -&gt; A省某SMSC实例 -&gt; A省短信网关(SMS Gateway) -&gt; SP

</code></pre>
<p>当手机用户B给SP发送一条短信，该短信流经的网元如下：</p>
<pre><code>手机用户B -&gt; B省基站 -&gt; B省某SMSC实例 -&gt; B省短信网关(SMS Gateway) -&gt; A省短信网关(SMS Gateway)  -&gt; SP

</code></pre>
<p>现在我们粗略地知道了我们是如何收到一个SP发送的短信的了以及反向流程了（在同一运营商下面）。</p>
<h2>三. 小结</h2>
<p>从业增值类短信业务平台开发很多年，这算是第一次写有关短信业务相关的文章。这一篇算是一个科普类的增值类短信业务介绍。接下来，我将继续以图文方式介绍增值类短信的协议与不同类型增值类短信打包和解析的难点。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ngrok原理浅析</title>
		<link>https://tonybai.com/2015/05/14/ngrok-source-intro/</link>
		<comments>https://tonybai.com/2015/05/14/ngrok-source-intro/#comments</comments>
		<pubDate>Thu, 14 May 2015 04:46:37 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[firewall]]></category>
		<category><![CDATA[ftp]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[network]]></category>
		<category><![CDATA[ngrok]]></category>
		<category><![CDATA[ngrokd]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[proxy]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[VNC]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[代理]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[远程桌面]]></category>
		<category><![CDATA[防火墙]]></category>
		<category><![CDATA[隧道]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1725</guid>
		<description><![CDATA[之前在进行微信Demo开发时曾用到过ngrok这个强大的tunnel(隧道)工具，ngrok在其github官方页面上的自我诠释是 &#8220;introspected tunnels to localhost&#34;，这个诠释有两层含义： 1、可以用来建立public到localhost的tunnel，让居于内网主机上的服务可以暴露给public，俗称内网穿透。 2、支持对隧道中数据的introspection（内省），支持可视化的观察隧道内数据，并replay（重放）相关请求（诸如http请 求）。 因此ngrok可以很便捷的协助进行服务端程序调试，尤其在进行一些Web server开发中。ngrok更强大的一点是它支持tcp层之上的所有应用协议或者说与应用层协议无关。比如：你可以通过ngrok实现ssh登录到内 网主 机，也可以通过ngrok实现远程桌面(VNC)方式访问内网主机。 今天我们就来简单分析一下这款强大工具的实现原理。ngrok本身是用go语言实现的，需要go 1.1以上版本编译。ngrok官方代码最新版为1.7，作者似乎已经完成了ngrok 2.0版本，但不知为何迟迟不放出最新代码。因此这里我们就以ngrok 1.7版本源码作为原理分析的基础。 一、ngrok tunnel与ngrok部署 网络tunnel（隧道）对多数人都是很&#8221;神秘&#8220;的概念，tunnel种类很多，没有标准定义，我了解的也不多（日常工作较少涉及），这里也就不 深入了。在《HTTP权威指南》中有关于HTTP tunnel（http上承载非web流量）和SSL tunnel的说明，但ngrok中的tunnel又与这些有所不同。 ngrok实现了一个tcp之上的端到端的tunnel，两端的程序在ngrok实现的Tunnel内透明的进行数据交互。 ngrok分为client端(ngrok)和服务端(ngrokd)，实际使用中的部署如下： 内网服务程序可以与ngrok client部署在同一主机，也可以部署在内网可达的其他主机上。ngrok和ngrokd会为建立与public client间的专用通道（tunnel）。 二、ngrok开发调试环境搭建 在学习ngrok代码或试验ngrok功能的时候，我们可能需要搭建一个ngrok的开发调试环境。ngrok作者在ngrok developer guide中给出了步骤： $&#62; git clone https://github.com/inconshreveable/ngrok $&#62; cd ngrok $&#62; make client $&#62; make server make client和make server执行后，会建构出ngrok和ngrokd的debug版本。如果要得到release版本，请使用make release-client和make release-server。debug版本与release版本的区别在于debug版本不打包 assets下的资源文件，执行时通过文件系统访问。 修改/etc/hosts文件，添加两行： 127.0.0.1 ngrok.me 127.0.0.1 test.ngrok.me [...]]]></description>
			<content:encoded><![CDATA[<p>之前在进行<a href="http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/">微信Demo开发</a>时曾用到过<a href="https://github.com/inconshreveable/ngrok">ngrok</a>这个强大的tunnel(隧道)工具，ngrok在其github官方页面上的自我诠释是 &ldquo;introspected tunnels to localhost&quot;，这个诠释有两层含义：<br />
	1、可以用来建立public到localhost的tunnel，让居于内网主机上的服务可以暴露给public，俗称内网穿透。<br />
	2、支持对隧道中数据的introspection（内省），支持可视化的观察隧道内数据，并replay（重放）相关请求（诸如http请 求）。</p>
<p>因此<a href="http://tonybai.com/2015/03/14/selfhost-ngrok-service/">ngrok</a>可以很便捷的协助进行服务端程序调试，尤其在进行一些Web server开发中。ngrok更强大的一点是它支持tcp层之上的所有应用协议或者说与应用层协议无关。比如：你可以通过ngrok实现ssh登录到内 网主 机，也可以通过ngrok实现远程桌面(VNC)方式访问内网主机。</p>
<p>今天我们就来简单分析一下这款强大工具的实现原理。ngrok本身是用<a href="http://tonybai.com/tag/go">go语言</a>实现的，需要go 1.1以上版本编译。ngrok官方代码最新版为1.7，作者似乎已经完成了ngrok 2.0版本，但不知为何迟迟不放出最新代码。因此这里我们就以ngrok 1.7版本源码作为原理分析的基础。</p>
<p><b>一、ngrok tunnel与ngrok部署</b></p>
<p>网络tunnel（隧道）对多数人都是很&rdquo;神秘&ldquo;的概念，tunnel种类很多，没有标准定义，我了解的也不多（日常工作较少涉及），这里也就不 深入了。在《<a href="http://book.douban.com/subject/10746113/">HTTP权威指南</a>》中有关于HTTP tunnel（http上承载非web流量）和SSL tunnel的说明，但ngrok中的tunnel又与这些有所不同。</p>
<p>ngrok实现了一个tcp之上的端到端的tunnel，两端的程序在ngrok实现的Tunnel内透明的进行数据交互。</p>
<p><img alt="" src="/wp-content/uploads/ngrok-tunnel.png" style="height: 170px; width: 500px;" /></p>
<p>ngrok分为client端(ngrok)和服务端(ngrokd)，实际使用中的部署如下：</p>
<p><img alt="" src="/wp-content/uploads/ngrok-deployment.png" style="width: 500px; height: 226px;" /></p>
<p>内网服务程序可以与ngrok client部署在同一主机，也可以部署在内网可达的其他主机上。ngrok和ngrokd会为建立与public client间的专用通道（tunnel）。</p>
<p><b>二、</b><b>ngrok开发调试环境搭建</b></p>
<p>在学习ngrok代码或试验ngrok功能的时候，我们可能需要搭建一个ngrok的开发调试环境。ngrok作者在<a href="https://github.com/inconshreveable/ngrok/blob/master/docs/DEVELOPMENT.md">ngrok developer guide</a>中给出了步骤：</p>
<p><font face="Courier New">$&gt; git clone <a class="moz-txt-link-freetext" href="https://github.com/inconshreveable/ngrok">https://github.com/inconshreveable/ngrok</a><br />
	$&gt; cd ngrok<br />
	$&gt; make client<br />
	$&gt; make server</font></p>
<p>make client和make server执行后，会建构出ngrok和ngrokd的debug版本。如果要得到release版本，请使用<font face="Courier New">make release-client</font>和<font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
      New">make release-server</font>。debug版本与release版本的区别在于debug版本不打包 assets下的资源文件，执行时通过文件系统访问。</p>
<p>修改/etc/hosts文件，添加两行：</p>
<p><font face="Courier New">127.0.0.1 ngrok.me<br />
	127.0.0.1 test.ngrok.me</font></p>
<p>创建客户端配置文件debug.yml：</p>
<p><font face="Courier New">server_addr: ngrok.me:4443<br />
	trust_host_root_certs: false<br />
	tunnels:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http: 8080</font></p>
<p>不过要想让ngrok与ngrokd顺利建立通信，我们还得制作数字证书(自签发)，源码中自带的证书是无法使用的，证书制作方法可参见《<a href="http://tonybai.com/2015/03/14/selfhost-ngrok-service/">搭建自 己的ngrok服务</a>》一文，相关原理可参考《<a href="http://tonybai.com/2015/04/30/go-and-https/">Go和HTTPS</a>》一文，这里就不赘述了。</p>
<p>我直接使用的是release版本(放在bin/release下)，这样在执行命令时可以少传入几个参数：</p>
<p>启动服务端：<br />
	<font face="Courier New">$&gt; sudo ./bin/release/ngrokd -domain ngrok.me<br />
	[05/13/15 17:15:37] [INFO] Listening for public http connections on [::]:80<br />
	[05/13/15 17:15:37] [INFO] Listening for public https connections on [::]:443<br />
	[05/13/15 17:15:37] [INFO] Listening for control and proxy connections on [::]:4443</font></p>
<p><font face="Courier New">启动客户端：<br />
	$&gt; ./bin/release/ngrok -config=debug.yml -log=ngrok.log -subdomain=test 8080</font></p>
<p>有了调试环境，我们就可以通过debug日志验证我们的分析了。</p>
<p>ngrok的源码结构如下：</p>
<p><font face="Courier New">drwxr-xr-x&nbsp;&nbsp; 3 tony&nbsp; staff&nbsp; 102&nbsp; 3 31 16:09 cache/<br />
	drwxr-xr-x&nbsp; 16 tony&nbsp; staff&nbsp; 544&nbsp; 5 13 17:21 client/<br />
	drwxr-xr-x&nbsp;&nbsp; 4 tony&nbsp; staff&nbsp; 136&nbsp; 5 13 15:02 conn/<br />
	drwxr-xr-x&nbsp;&nbsp; 3 tony&nbsp; staff&nbsp; 102&nbsp; 3 31 16:09 log/<br />
	drwxr-xr-x&nbsp;&nbsp; 4 tony&nbsp; staff&nbsp; 136&nbsp; 3 31 16:09 main/<br />
	drwxr-xr-x&nbsp;&nbsp; 5 tony&nbsp; staff&nbsp; 170&nbsp; 5 12 16:17 msg/<br />
	drwxr-xr-x&nbsp;&nbsp; 5 tony&nbsp; staff&nbsp; 170&nbsp; 3 31 16:09 proto/<br />
	drwxr-xr-x&nbsp; 11 tony&nbsp; staff&nbsp; 374&nbsp; 5 13 17:21 server/<br />
	drwxr-xr-x&nbsp;&nbsp; 7 tony&nbsp; staff&nbsp; 238&nbsp; 3 31 16:09 util/<br />
	drwxr-xr-x&nbsp;&nbsp; 3 tony&nbsp; staff&nbsp; 102&nbsp; 3 31 16:09 version/</font></p>
<p>main目录下的ngrok/和ngrokd/分别是ngrok和ngrokd main包，main函数存放的位置，但这里仅仅是一个stub。以ngrok为例：</p>
<p><font face="Courier New">// ngrok/src/ngrok/main/ngrok/ngrok.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;ngrok/client&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; client.Main()<br />
	}</font></p>
<p>真正的&ldquo;main&rdquo;被client包的Main函数实现。</p>
<p>client/和server/目录分别对应ngrok和ngrokd的主要逻辑，其他目录（或包）都是一些工具类的实现。</p>
<p><b>三、第一阶段：Control Connection建立</b></p>
<p>在ngrokd的启动日志中我们可以看到这样一行：</p>
<p><font face="Courier New">[INFO] Listening for control and proxy connections on [::]:4443</font></p>
<p>ngrokd在4443端口（默认）监听control和proxy connection。Control Connection，顾名思义&ldquo;控制连接&rdquo;，有些类似于FTP协议的控制连接（不知道ngrok作者在设计协议时是否参考了FTP协议^_^）。该连接 只用于收发控制类消息。作为客户端的ngrok启动后的第一件事就是与ngrokd建立Control Connection，建立过程序列图如下：</p>
<p><img alt="" src="/wp-content/uploads/ngrok-control-connection.png" style="width: 500px; height: 345px;" /></p>
<p>前面提到过，ngrok客户端的实际entrypoint在ngrok/src/ngrok/client目录下，包名client，实际入口是 client.Main函数。</p>
<p><font face="Courier New">//ngrok/src/ngrok/client/main.go<br />
	func Main() {<br />
	&nbsp;&nbsp;&nbsp; // parse options<br />
	&nbsp;&nbsp;&nbsp; // set up logging<br />
	&nbsp;&nbsp;&nbsp; // read configuration file<br />
	&nbsp;&nbsp;&nbsp; &#8230;. &#8230;<br />
	&nbsp;&nbsp;&nbsp; <b>NewController().</b><b>Run</b><b>(config)</b><br />
	}</font></p>
<p>ngrok采用了MVC模式构架代码，这既包括ngrok与ngrokd之间的逻辑处理，也包括ngrok本地web页面（用于隧道数据的 introspection）的处理。</p>
<p><font face="Courier New">//ngrok/src/ngrok/client/controller.go<br />
	func (ctl *Controller) Run(config *Configuration) {</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; var model *ClientModel</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ctl.model == nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; model = ctl.SetupModel(config)<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; model = ctl.model.(*ClientModel)<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; // init the model<br />
	&nbsp;&nbsp;&nbsp; // init web ui<br />
	&nbsp;&nbsp;&nbsp; // init term ui<br />
	&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp; <b>ctl.Go(ctl.model.Run)</b><br />
	&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;<br />
	}</font></p>
<p>我们来继续看看model.Run都做了些什么。</p>
<p><font face="Courier New">//ngrok/src/ngrok/client/model.go<br />
	func (c *ClientModel) Run() {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // run the control channel<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>c.control()</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if c.connStatus == mvc.ConnOnline {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; wait = 1 * time.Second<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.connStatus = mvc.ConnReconnecting<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.update()<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>Run函数调用c.control来运行Control Connection的主逻辑，并在control connection断开后，尝试重连。</p>
<p>c.control是ClientModel的一个method，用来真正建立ngrok到ngrokd的control connection，并完成基于ngrok的鉴权（用户名、密码配置在配置文件中）。</p>
<p><font face="Courier New">//ngrok/src/ngrok/client/model.go</font><br />
	<font face="Courier New">func (c *ClientModel) control() {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; var (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ctlConn conn.Conn<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err&nbsp;&nbsp;&nbsp;&nbsp; error<br />
	&nbsp;&nbsp;&nbsp; )<br />
	&nbsp;&nbsp;&nbsp; if c.proxyUrl == &quot;&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // simple non-proxied case, just connect to the server<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ctlConn, err = conn.<b>Dial</b>(c.serverAddr, &quot;ctl&quot;, c.tlsConfig)<br />
	&nbsp;&nbsp;&nbsp; } else {&#8230;&#8230;}<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // authenticate with the server<br />
	&nbsp;&nbsp;&nbsp; auth := &amp;msg.Auth{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientId:&nbsp; c.id,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; OS:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; runtime.GOOS,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Arch:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; runtime.GOARCH,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Version:&nbsp;&nbsp; version.Proto,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MmVersion: version.MajorMinor(),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; User:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.authToken,<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if err = msg.WriteMsg(ctlConn, <b>auth</b>); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(err)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // wait for the server to authenticate us<br />
	&nbsp;&nbsp;&nbsp; var authResp msg.AuthResp<br />
	&nbsp;&nbsp;&nbsp; if err = msg.ReadMsgInto(ctlConn, &amp;<b>authResp</b>); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(err)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp; &nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; c.id = authResp.ClientId<br />
	&nbsp;&nbsp;&nbsp; &#8230; ..<br />
	}</font></p>
<p>ngrok封装了connection相关操作，代码在<font face="Courier New">ngrok/src/ngrok/conn</font>下面，包名conn。</p>
<p><font face="Courier New">//ngrok/src/ngrok/conn/conn.go<br />
	func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {<br />
	&nbsp;&nbsp;&nbsp; var rawConn net.Conn<br />
	&nbsp;&nbsp;&nbsp; if rawConn, err = net.Dial(&quot;tcp&quot;, addr); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; conn = wrapConn(rawConn, typ)<br />
	&nbsp;&nbsp;&nbsp; conn.Debug(&quot;New connection to: %v&quot;, rawConn.RemoteAddr())</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if tlsCfg != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>conn.StartTLS(tlsCfg)</b><br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return<br />
	}</font></p>
<p>ngrok首先创建一条TCP连接，并基于该连接创建了TLS client：</p>
<p><font face="Courier New">func (c *loggedConn) StartTLS(tlsCfg *tls.Config) {<br />
	&nbsp;&nbsp;&nbsp; c.Conn = tls.Client(c.Conn, tlsCfg)<br />
	}</font></p>
<p>不过此时并未进行TLS的初始化，即handshake。handshake发生在ngrok首次向ngrokd发送auth消息（<font face="Courier New">msg.WriteMsg, ngrok/src/ngrok/msg/msg.go</font>）时，go标准库的TLS相关函数默默的完成这一handshake过程。我们经常遇到的ngrok证书验证失败等问题，就发生在该过程中。</p>
<p>在AuthResp中，ngrokd为该Control Connection分配一个ClientID，该ClientID在后续Proxy Connection建立时使用，用于关联和校验之用。</p>
<p>前面的逻辑和代码都是ngrok客户端的，现在我们再从ngrokd server端代码review一遍Control Connection的建立过程。</p>
<p>ngrokd的代码放在<font face="Courier New">ngrok/src/ngrok/server</font>下面，entrypoint如下：</p>
<p><font face="Courier New">//ngrok/src/ngrok/server/main.go<br />
	func Main() {<br />
	&nbsp;&nbsp;&nbsp; // parse options<br />
	&nbsp;&nbsp;&nbsp; opts = parseArgs()<br />
	&nbsp;&nbsp;&nbsp; // init logging<br />
	&nbsp;&nbsp;&nbsp; // init tunnel/control registry<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; // start listeners<br />
	&nbsp;&nbsp;&nbsp; listeners = make(map[string]*conn.Listener)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // load tls configuration<br />
	&nbsp;&nbsp;&nbsp; tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(err)<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; // listen for http<br />
	&nbsp;&nbsp;&nbsp; // listen for https<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // ngrok clients<br />
	&nbsp;&nbsp;&nbsp; <b>tunnelListener</b>(opts.tunnelAddr, tlsConfig)<br />
	}</font></p>
<p>ngrokd启动了三个监听，其中最后一个<font face="Courier New">tunnelListenner</font>用于监听ngrok发起的Control Connection或者后续的proxy connection，作者意图通过一个端口，监听两种类型连接，旨在于方便部署。</p>
<p><font face="Courier New">//ngrok/src/ngrok/server/main.go</font><br />
	<font face="Courier New">func tunnelListener(addr string, tlsConfig *tls.Config) {<br />
	&nbsp;&nbsp;&nbsp; // listen for incoming connections<br />
	&nbsp;&nbsp;&nbsp; listener, err := conn.Listen(addr, &quot;tun&quot;, tlsConfig)<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; for c := range listener.Conns {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func(tunnelConn conn.Conn) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var rawMsg msg.Message<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tunnelConn.Warn(&quot;Failed to read message: %v&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tunnelConn.Close()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch m := rawMsg.(type) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case *msg.Auth:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>NewControl</b>(tunnelConn, m)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }(c)<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>从tunnelListener可以看到，当ngrokd在新建立的Control Connection上收到Auth消息后，ngrokd执行NewControl来处理该Control Connection上的后续事情。</p>
<p><font face="Courier New">//ngrok/src/ngrok/server/control.go<br />
	func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {<br />
	&nbsp;&nbsp;&nbsp; var err error</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // create the object<br />
	&nbsp;&nbsp;&nbsp; c := &amp;Control{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // register the clientid<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; // register the control<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // start the writer first so that<br />
	&nbsp;&nbsp;&nbsp; // the following messages get sent<br />
	&nbsp;&nbsp;&nbsp; go c.writer()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // Respond to authentication<br />
	&nbsp;&nbsp;&nbsp; c.out &lt;- &amp;<b>msg.AuthResp</b>{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Version:&nbsp;&nbsp; version.Proto,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MmVersion: version.MajorMinor(),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientId:&nbsp; c.id,<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // As a performance optimization,<br />
	&nbsp;&nbsp;&nbsp; // ask for a proxy connection up front<br />
	&nbsp;&nbsp;&nbsp; c.out &lt;- &amp;msg.ReqProxy{}</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // manage the connection<br />
	&nbsp;&nbsp;&nbsp; go c.manager()<br />
	&nbsp;&nbsp;&nbsp; go c.reader()<br />
	&nbsp;&nbsp;&nbsp; go c.stopper()<br />
	}</font></p>
<p>在NewControl中，ngrokd返回了AuthResp。到这里，一条新的Control Connection建立完毕。</p>
<p>我们最后再来看一下Control Connection建立过程时ngrok和ngrokd的输出日志，增强一下感性认知：</p>
<p>ngrok Server:</p>
<p><font face="Courier New">[INFO] [tun:d866234] <b>New connection</b> from 127.0.0.1:59949<br />
	[DEBG] [tun:d866234] Waiting to read message<br />
	[DEBG] [tun:d866234] Reading message with length: 126<br />
	[DEBG] [tun:d866234] Read message {&quot;Type&quot;:&quot;<b>Auth</b>&quot;,<br />
	&quot;Payload&quot;:{&quot;Version&quot;:&quot;2&quot;,&quot;MmVersion&quot;:&quot;1.7&quot;,&quot;User&quot;:&quot;&quot;,&quot;Password&quot;:&quot;&quot;,&quot;OS&quot;:&quot;darwin&quot;,&quot;Arch&quot;:&quot;amd64&quot;,&quot;ClientId&quot;:&quot;&quot;}}<br />
	[INFO] [ctl:d866234] Renamed connection tun:d866234<br />
	[INFO] [registry] [ctl] Registered control with id ac1d14e0634f243f8a0cc2306bb466af<br />
	[DEBG] [ctl:d866234] [ac1d14e0634f243f8a0cc2306bb466af] Writing message: {&quot;Type&quot;:&quot;<b>AuthResp</b>&quot;,&quot;Payload&quot;:{&quot;Version&quot;:&quot;2&quot;,&quot;MmVersion&quot;:&quot;1.7&quot;,&quot;ClientId&quot;:&quot;<b>ac1d14e0634f243f8a0cc2306bb466af</b>&quot;,&quot;Error&quot;:&quot;&quot;}}</font></p>
<p>Client:</p>
<p><font face="Courier New">[INFO] (ngrok/log.Info:112) Reading configuration file debug.yml<br />
	[INFO] (ngrok/log.(*PrefixLogger).Info:83) [client] Trusting root CAs: [assets/client/tls/ngrokroot.crt]<br />
	[INFO] (ngrok/log.(*PrefixLogger).Info:83) [view] [web] Serving web interface on 127.0.0.1:4040<br />
	[INFO] (ngrok/log.Info:112) Checking for update<br />
	[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [view] [term] Waiting for update<br />
	[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] <b>New connection</b> to: 127.0.0.1:4443<br />
	[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Writing message: {&quot;Type&quot;:&quot;<b>Auth</b>&quot;,&quot;Payload&quot;:{&quot;Version&quot;:&quot;2&quot;,&quot;MmVersion&quot;:&quot;1.7&quot;,&quot;User&quot;:&quot;&quot;,&quot;Password&quot;:&quot;&quot;,&quot;OS&quot;:&quot;darwin&quot;,&quot;Arch&quot;:&quot;amd64&quot;,&quot;ClientId&quot;:&quot;&quot;}}<br />
	[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Waiting to read message<br />
	(ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Reading message with length: 120<br />
	(ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Read message {&quot;Type&quot;:&quot;<b>AuthResp</b>&quot;,&quot;Payload&quot;:{&quot;Version&quot;:&quot;2&quot;,&quot;MmVersion&quot;:&quot;1.7&quot;,&quot;ClientId&quot;:&quot;ac1d14e0634f243f8a0cc2306bb466af&quot;,&quot;Error&quot;:&quot;&quot;}}<br />
	[INFO] (ngrok/log.(*PrefixLogger).Info:83) [client] Authenticated with server, client id: ac1d14e0634f243f8a0cc2306bb466af</font></p>
<p><b>四、Tunnel Creation</b></p>
<p><img alt="" src="/wp-content/uploads/ngrok-tunnel-creation.png" style="width: 500px; height: 267px;" /></p>
<p>Tunnel Creation是ngrok将配置文件中的tunnel信息通过刚刚建立的Control Connection传输给 ngrokd，ngrokd登记、启动相应端口监听（如果配置了remote_port或多路复用ngrokd默认监听的http和https端口）并返回相应应答。ngrok和ngrokd之间并未真正建立新连接。</p>
<p>我们回到ngrok的model.go，继续看ClientModel的control方法。在收到AuthResp后，ngrok还做了如下事情：</p>
<p><font face="Courier New">//ngrok/src/ngrok/client/model.go<br />
	&nbsp;<br />
	&nbsp;&nbsp; // request tunnels<br />
	&nbsp;&nbsp;&nbsp; reqIdToTunnelConfig := make(map[string]*TunnelConfiguration)<br />
	&nbsp;&nbsp;&nbsp; for _, config := range c.tunnelConfig {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // create the protocol list to ask for<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var protocols []string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for proto, _ := range config.Protocols {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; protocols = append(protocols, proto)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; reqTunnel := &amp;msg.<b>ReqTunnel</b>{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // send the tunnel request<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // save request id association so we know which local address<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // to proxy to later<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; reqIdToTunnelConfig[reqTunnel.ReqId] = config<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // main control loop<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var rawMsg msg.Message<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch m := rawMsg.(type) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case *msg.<b>NewTunnel</b>:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tunnel := mvc.Tunnel{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.tunnels[tunnel.PublicUrl] = tunnel<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.connStatus = mvc.ConnOnline<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.update()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p>ngrok将配置的Tunnel信息逐一以ReqTunnel消息发送给ngrokd以注册登记Tunnel，并在随后的main control loop中处理ngrokd回送的NewTunnel消息，完成一些登记索引工作。</p>
<p>ngrokd Server端对tunnel creation的处理是在NewControl的结尾处：</p>
<p><font face="Courier New">//ngrok/src/ngrok/server/control.go<br />
	func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; // manage the connection<br />
	&nbsp;&nbsp;&nbsp; go c.manager()<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p><font face="Courier New">func (c *Control) manager() {<br />
	&nbsp;&nbsp;&nbsp; //&#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; select {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case &lt;-reap.C:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case mRaw, ok := &lt;-c.in:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // c.in closes to indicate shutdown<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !ok {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch m := mRaw.(type) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case *<b>msg.ReqTunnel</b>:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>c.registerTunnel</b>(m)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; .. &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>Control的manager在收到ngrok发来的ReqTunnel消息后，调用registerTunnel进行处理。</p>
<p><font face="Courier New">// ngrok/src/ngrok/server/control.go<br />
	// Register a new tunnel on this control connection<br />
	func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {<br />
	&nbsp;&nbsp;&nbsp; for _, proto := range strings.Split(rawTunnelReq.Protocol, &quot;+&quot;) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tunnelReq := *rawTunnelReq<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tunnelReq.Protocol = proto</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.conn.Debug(&quot;Registering new tunnel&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; t, err := <b>NewTunnel</b>(&amp;tunnelReq, c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.out &lt;- &amp;<b>msg.NewTunnel</b>{Error: err.Error()}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if len(c.tunnels) == 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.shutdown.Begin()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // we&#39;re done<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // add it to the list of tunnels<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.tunnels = append(c.tunnels, t)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // acknowledge success<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b> c.out &lt;- &amp;msg.NewTunnel<u>{</u></b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Url:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; t.url,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Protocol: proto,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ReqId:&nbsp;&nbsp;&nbsp; rawTunnelReq.ReqId,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rawTunnelReq.Hostname = strings.Replace(t.url, proto+&quot;://&quot;, &quot;&quot;, 1)<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>Server端创建tunnel的实际工作由NewTunnel完成：</p>
<p><font face="Courier New">// ngrok/src/ngrok/server/tunnel.go<br />
	func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {<br />
	&nbsp;&nbsp;&nbsp; t = &amp;Tunnel{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; proto := t.req.Protocol<br />
	&nbsp;&nbsp;&nbsp; switch proto {<br />
	&nbsp;&nbsp;&nbsp; case &quot;<b>tcp</b>&quot;:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bindTcp := func(port int) error {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if t.listener, err = <b>net.ListenTCP</b>(&quot;tcp&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &amp;net.TCPAddr{IP: net.ParseIP(&quot;0.0.0.0&quot;),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Port: port}); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // create the url<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; addr := t.listener.Addr().(*net.TCPAddr)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; t.url = fmt.Sprintf(&quot;tcp://%s:%d&quot;, opts.domain, addr.Port)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // register it<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err = tunnelRegistry.RegisterAndCache(t.url, t);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>go t.listenTcp(t.listener)</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // use the custom remote port you asked for<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if t.req.<b>RemotePort</b> != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bindTcp(int(t.req.RemotePort))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; // try to return to you the same port you had before<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cachedUrl := tunnelRegistry.GetCachedRegistration(t)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if cachedUrl != &quot;&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Bind for TCP connections<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>bindTcp(0)</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; case &quot;http&quot;, &quot;https&quot;:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; l, ok := listeners[proto]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !ok {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err = <b>registerVhost(</b>t, proto, l.Addr.(*net.TCPAddr).Port);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; default:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err = fmt.Errorf(&quot;Protocol %s is not supported&quot;, proto)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; metrics.OpenTunnel(t)<br />
	&nbsp;&nbsp;&nbsp; return<br />
	}</font></p>
<p>可以看出，NewTunnel区别对待tcp和http/https隧道：</p>
<p><font face="Courier New">- 对于Tcp隧道，NewTunnel先要看是否配置了remote_port，如果remote_port不为空，则启动监听这个 remote_port。否则尝试从cache里找出你之前创建tunnel时使用的端口号，如果可用，则监听这个端口号，否则bindTcp(0)，即 随机选择一个端口作为该tcp tunnel的remote_port。</font></p>
<p><font face="Courier New">- 对于http/https隧道，ngrokd启动时就默认监听了80和443，如果ngrok请求建立http/https隧道(目前不支持设置remote_port)，则ngrokd通过一种自实现的vhost的机制实现所有http/https请求多路复用到80和443端口上。ngrokd不会新增监听端口。</font></p>
<p>从下面例子，我们也可以看出一些端倪。我们将debug.yml改为：</p>
<p><font face="Courier New">server_addr: ngrok.me:4443<br />
	trust_host_root_certs: false<br />
	tunnels:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http: 8080<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test1:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http: 8081<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ssh1:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; remote_port: 50000<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tcp: 22<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ssh2:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tcp: 22</font></p>
<p>启动ngrok：</p>
<p><font face="Courier New">$./bin/release/ngrok -config=debug.yml -log=ngrok.log start test test1&nbsp; ssh1 ssh2</font></p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; online<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/1.7<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tcp://ngrok.me:50000 -&gt; 127.0.0.1:22<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tcp://ngrok.me:56297 -&gt; 127.0.0.1:22<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http://test.ngrok.me -&gt; 127.0.0.1:8080<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http://test1.ngrok.me -&gt; 127.0.0.1:8081<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040</font></p>
<p>可以看出ngrokd为ssh2随机挑选了一个端口56297进行了监听，而两个http隧道，则都默认使用了80端口。</p>
<p>如果像下面这样配置会发生什么呢？</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp; ssh1:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; remote_port: 50000<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tcp: 22<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ssh2:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; remote_port: 50000<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tcp: 22</font></p>
<p><font face="Courier New">ngrok启动会得到错误信息：<br />
	Server failed to allocate tunnel: [ctl:5332a293] [a87bd111bcc804508c835714c18a5664] Error binding TCP listener: listen tcp 0.0.0.0:50000: bind: address already in use</font></p>
<p>客户端ngrok在ClientModel control方法的main control loop中收到NewTunnel并处理该消息：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; case *msg.NewTunnel:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if m.Error != &quot;&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tunnel := mvc.Tunnel{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PublicUrl: m.Url,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Protocol:&nbsp; c.protoMap[m.Protocol],<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.tunnels[tunnel.PublicUrl] = tunnel<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.connStatus = mvc.ConnOnline<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.Info(&quot;Tunnel established at %v&quot;, tunnel.PublicUrl)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.update()</font></p>
<p><b>五、Proxy Connection</b><b>和Private Connection</b></p>
<p>到目前为止，我们知道了Control Connection：用于ngrok和ngrokd之间传输命令；Public Connection：外部发起的，尝试向内网服务建立的链接。</p>
<p>这节当中，我们要接触到Proxy Connection和Private Connection。</p>
<p>Proxy Connection以及Private Connection的建立过程如下：</p>
<p><img alt="" src="/wp-content/uploads/ngrok-proxy-connection.png" style="width: 500px; height: 325px;" /></p>
<p>前面ngrok和ngrokd的交互进行到了NewTunnel，这些数据都是通过之前已经建立的Control Connection上传输的。</p>
<p>ngrokd侧，NewControl方法的结尾有这样一行代码：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // As a performance optimization, ask for a proxy connection up front<br />
	&nbsp;&nbsp;&nbsp; c.out &lt;- &amp;msg.ReqProxy{}</font></p>
<p>服务端ngrokd在Control Connection上向ngrok发送了&quot;ReqProxy&quot;的消息，意为请求ngrok向ngrokd建立一条Proxy Connection，该链接将作为隧道数据流的承载者。</p>
<p>客户端ngrok在ClientModel control方法的main control loop中收到ReqProxy并处理该消息：</p>
<p><font face="Courier New">case *msg.ReqProxy:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c.ctl.Go(c.proxy)</font></p>
<p><font face="Courier New">// Establishes and manages a tunnel proxy connection with the server<br />
	func (c *ClientModel) proxy() {<br />
	&nbsp;&nbsp;&nbsp; if c.proxyUrl == &quot;&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; remoteConn, err = conn.Dial(c.serverAddr, &quot;pxy&quot;, c.tlsConfig)<br />
	&nbsp;&nbsp;&nbsp; }&#8230;&#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; err = msg.WriteMsg(remoteConn, &amp;msg.RegProxy{ClientId: c.id})<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; remoteConn.Error(&quot;Failed to write RegProxy: %v&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p>ngrok客户端收到ReqProxy后，创建一条新连接到ngrokd，该连接即为Proxy Connection。并且ngrok将RegProxy消息通过该新建立的Proxy Connection发到ngrokd，以便ngrokd将该Proxy Connection与对应的Control Connection以及tunnel关联在一起。</p>
<p>// ngrok服务端<br />
	<font face="Courier New">func tunnelListener(addr string, tlsConfig *tls.Config) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;. &#8230;<br />
	&nbsp;&nbsp;&nbsp; case *msg.RegProxy:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NewProxy(tunnelConn, m)<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p>到目前为止, tunnel、Proxy Connection都已经建立了，万事俱备，就等待Public发起Public connection到ngrokd了。</p>
<p>下面我们以Public发起一个http连接到ngrokd为例，比如我们通过curl 命令，向test.ngrok.me发起一次http请求。</p>
<p>前面说过，ngrokd在启动时默认启动了80和443端口的监听，并且与其他http/https隧道共同多路复用该端口（通过vhost机制)。ngrokd server对80端口的处理代码如下：</p>
<p><font face="Courier New">// ngrok/src/ngrok/server/main.go<br />
	func Main() {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;// listen for http<br />
	&nbsp;&nbsp;&nbsp; if opts.httpAddr != &quot;&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; listeners["http"] =<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>startHttpListener</b>(opts.httpAddr, nil)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p>startHttpListener针对每个连接，启动一个goroutine专门处理：</p>
<p><font face="Courier New">//ngrok/src/ngrok/server/http.go<br />
	func startHttpListener(addr string,<br />
	&nbsp;&nbsp;&nbsp; tlsCfg *tls.Config) (listener *conn.Listener) {<br />
	&nbsp;&nbsp;&nbsp; // bind/listen for incoming connections<br />
	&nbsp;&nbsp;&nbsp; var err error<br />
	&nbsp;&nbsp;&nbsp; if listener, err = conn.Listen(addr, &quot;pub&quot;, tlsCfg);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(err)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; proto := &quot;http&quot;<br />
	&nbsp;&nbsp;&nbsp; if tlsCfg != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; proto = &quot;https&quot;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for conn := range listener.Conns {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go<b> httpHandler</b>(conn, proto)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return<br />
	}</font></p>
<p><font face="Courier New">// Handles a new http connection from the public internet<br />
	func httpHandler(c conn.Conn, proto string) {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; // let the tunnel handle the connection now<br />
	&nbsp;&nbsp;&nbsp; tunnel.HandlePublicConnection(c)<br />
	}</font></p>
<p>我们终于看到server端处理public connection的真正方法了:</p>
<p><font face="Courier New">//ngrok/src/ngrok/server/tunnel.go<br />
	func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; var proxyConn conn.Conn<br />
	&nbsp;&nbsp;&nbsp; var err error<br />
	&nbsp;&nbsp;&nbsp; for i := 0; i &lt; (2 * proxyMaxPoolSize); i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // get a proxy connection<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if proxyConn, err = t.ctl.<b>GetProxy</b>();<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err != nil {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; defer proxyConn.Close()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // tell the client we&#39;re going to<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; // start using this proxy connection<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; startPxyMsg := &amp;msg.<b>StartProxy</b>{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Url:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; t.url,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientAddr: publicConn.RemoteAddr().String(),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err = msg.WriteMsg(proxyConn, startPxyMsg);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; // join the public and proxy connections<br />
	&nbsp;&nbsp;&nbsp; bytesIn, bytesOut := <b>conn.Join(</b>publicConn, proxyConn)<br />
	&nbsp;&nbsp;&nbsp; &#8230;. &#8230;<br />
	}</font></p>
<p><font face="Courier New">HandlePublicConnection通过选出的Proxy connection向ngrok client发送StartProxy信息，告知ngrok proxy启动。然后通过conn.Join方法将publicConn和proxyConn关联到一起。</font></p>
<p><font face="Courier New">// ngrok/src/ngrok/conn/conn.go<br />
	func Join(c Conn, c2 Conn) (int64, int64) {<br />
	&nbsp;&nbsp;&nbsp; var wait sync.WaitGroup</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; pipe := func(to Conn, from Conn, bytesCopied *int64) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; defer to.Close()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; defer from.Close()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; defer wait.Done()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var err error<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *bytesCopied, err = io.Copy(to, from)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; from.Warn(&quot;Copied %d bytes to %s before failing with error %v&quot;, *bytesCopied, to.Id(), err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; from.Debug(&quot;Copied %d bytes to %s&quot;, *bytesCopied, to.Id())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; wait.Add(2)<br />
	&nbsp;&nbsp;&nbsp; var fromBytes, toBytes int64<br />
	&nbsp;&nbsp;&nbsp; go pipe(c, c2, &amp;fromBytes)<br />
	&nbsp;&nbsp;&nbsp; go pipe(c2, c, &amp;toBytes)<br />
	&nbsp;&nbsp;&nbsp; c.Info(&quot;Joined with connection %s&quot;, c2.Id())<br />
	&nbsp;&nbsp;&nbsp; wait.Wait()<br />
	&nbsp;&nbsp;&nbsp; return fromBytes, toBytes<br />
	}</font></p>
<p><font face="Courier New">Join通过io.Copy实现public conn和proxy conn数据流的转发，单向被称作一个pipe，Join建立了两个Pipe，实现了双向转发，每个Pipe直到一方返回EOF或异常失败才会退出。后续在ngrok端，proxy conn和private conn也是通过conn.Join关联到一起的。</font></p>
<p><font face="Courier New">我们现在就来看看ngrok在收到StartProxy消息后是如何处理的。我们回到ClientModel的proxy方法中。在向ngrokd成功建立proxy connection后，ngrok等待ngrokd的StartProxy指令。</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; // wait for the server to ack our register<br />
	&nbsp;&nbsp;&nbsp; var startPxy msg.StartProxy<br />
	&nbsp;&nbsp;&nbsp; if err = msg.ReadMsgInto(remoteConn, &amp;startPxy);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; remoteConn.Error(&quot;Server failed to write StartProxy: %v&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">一旦收到StartProxy，ngrok将建立一条private connection：<br />
	&nbsp;&nbsp;&nbsp; // start up the private connection<br />
	&nbsp;&nbsp;&nbsp; start := time.Now()<br />
	&nbsp;&nbsp;&nbsp; localConn, err := conn.Dial(tunnel.LocalAddr, &quot;prv&quot;, nil)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	并将private connection和proxy connection通过conn.Join关联在一起，实现数据透明转发。</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; m.connTimer.Time(func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; localConn := tunnel.Protocol.WrapConn(localConn,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mvc.ConnectionContext{Tunnel: tunnel,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientAddr: startPxy.ClientAddr})<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bytesIn, bytesOut := <b>conn.Join</b>(localConn, remoteConn)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m.bytesIn.Update(bytesIn)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m.bytesOut.Update(bytesOut)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m.bytesInCount.Inc(bytesIn)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m.bytesOutCount.Inc(bytesOut)<br />
	&nbsp;&nbsp;&nbsp; })</font></p>
<p><font face="Courier New">这样一来，public connection上的数据通过proxy connection到达ngrok，ngrok再通过private connection将数据转发给本地启动的服务程序，从而实现所谓的内网穿透。从public视角来看，就像是与内网中的那个服务直接交互一样。</font></p>
<p style='text-align:left'>&copy; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2015/05/14/ngrok-source-intro/feed/</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
		<item>
		<title>Go和HTTPS</title>
		<link>https://tonybai.com/2015/04/30/go-and-https/</link>
		<comments>https://tonybai.com/2015/04/30/go-and-https/#comments</comments>
		<pubDate>Thu, 30 Apr 2015 15:09:46 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CA]]></category>
		<category><![CDATA[CNNIC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[MD5]]></category>
		<category><![CDATA[ngrok]]></category>
		<category><![CDATA[openssl]]></category>
		<category><![CDATA[SHA]]></category>
		<category><![CDATA[SHA-1]]></category>
		<category><![CDATA[SHA256]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[加密]]></category>
		<category><![CDATA[单项散列]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[摘要]]></category>
		<category><![CDATA[数字证书]]></category>
		<category><![CDATA[私钥]]></category>
		<category><![CDATA[签名]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1718</guid>
		<description><![CDATA[近期在构思一个产品，考虑到安全性的原因，可能需要使用到HTTPS协议以及双向数字证书校验。之前只是粗浅接触过HTTP（使用Golang开 发微信系列）。对HTTPS的了解则始于那次自行搭建ngrok服务，在那个过程中照猫画虎地为服务端生成了一些私钥和证书，虽然结果是好 的：ngrok服务成功搭建起来了，但对HTTPS、数字证书等的基本原理并未求甚解。于是想趁这次的机会，对HTTPS做一些深度挖掘。主要途 径：翻阅网上资料、书籍，并利用golang编写一些实验examples。 一、HTTPS简介 日常生活中，我们上网用的最多的应用层协议就是HTTP协议了，直至目前全世界的网站中大多数依然只支持HTTP访问。 使用Go创建一个HTTP Server十分Easy，十几行代码就能搞定： //gohttps/1-http/server.go package main import ( &#160;&#160;&#160; &#34;fmt&#34; &#160;&#160;&#160; &#34;net/http&#34; ) func handler(w http.ResponseWriter, r *http.Request) { &#160;&#160;&#160; fmt.Fprintf(w, &#160;&#160;&#160;&#160; &#34;Hi, This is an example of http service in golang!&#34;) } func main() { &#160;&#160;&#160; http.HandleFunc(&#34;/&#34;, handler) &#160;&#160;&#160; http.ListenAndServe(&#34;:8080&#34;, nil) } 执行这段代码： $ go run server.go 打开浏览器，在地址栏输入&#34;http://localhost:8080&#34;， 你会看到&#8220; [...]]]></description>
			<content:encoded><![CDATA[<p>近期在构思一个产品，考虑到安全性的原因，可能需要使用到<a href="http://en.wikipedia.org/wiki/HTTPS">HTTPS</a>协议以及双向数字证书校验。之前只是粗浅接触过HTTP（<a href="http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/">使用Golang开 发微信系列</a>）。对HTTPS的了解则始于那次<a href="http://tonybai.com/2015/03/14/selfhost-ngrok-service/">自行搭建ngrok服务</a>，在那个过程中照猫画虎地为服务端生成了一些私钥和证书，虽然结果是好 的：ngrok服务成功搭建起来了，但对HTTPS、数字证书等的基本原理并未求甚解。于是想趁这次的机会，对HTTPS做一些深度挖掘。主要途 径：翻阅网上资料、书籍，并利用golang编写一些实验examples。</p>
<p><b>一、HTTPS简介</b></p>
<p>日常生活中，我们上网用的最多的应用层协议就是HTTP协议了，直至目前全世界的网站中大多数依然只支持HTTP访问。</p>
<p>使用Go创建一个HTTP Server十分Easy，十几行代码就能搞定：</p>
<p><font face="Courier New">//gohttps/1-http/server.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func handler(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Fprintf(w,<br />
	&nbsp;&nbsp;&nbsp;&nbsp; &quot;Hi, This is an example of http service in golang!&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; http.HandleFunc(&quot;/&quot;, handler)<br />
	&nbsp;&nbsp;&nbsp; http.ListenAndServe(&quot;:8080&quot;, nil)<br />
	}</font></p>
<p>执行这段代码：<br />
	<font face="Courier New">$ go run server.go</font></p>
<p>打开浏览器，在地址栏输入&quot;<font face="Courier New"><a class="moz-txt-link-freetext" href="http://localhost:8080">http://localhost:8080</a></font>&quot;， 你会看到&ldquo; <font face="Courier New">Hi, This is an example of http service in golang!</font>&quot;输出到浏览器窗口。</p>
<p>不过HTTP毕竟是明文的，在这样一个不安全的世界里，随时存在着窃听（sniffer工具可以简单办到）、篡改甚至是冒充等风险，因此对于一些 对安全比较care的站点或服务，它们需要一种安全的HTTP协议，于是就有了HTTPS。</p>
<p>HTTPS只是我们在浏览器地址栏中看到协议标识，实际上它可以被理解为运行在SSL（Secure Sockets Layer）或TLS(Transport Layer Security)协议所构建的安全层之上的HTTP协议，协议的传输安全性以及内容完整性实际上是由SSL或TLS保证的。</p>
<p>关于HTTPS协议原理的详细说明，没有个百八十页是搞不定的，后续我会在各个实验之前将相关的原理先作一些说明，整体原理这里就不赘述了。有兴 趣的朋友可以参考以下资料：<br />
	1、《<a href="http://book.douban.com/subject/10746113/">HTTP权威指南</a>》第十四章<br />
	2、《<a href="http://book.douban.com/subject/25863515/">图解HTTP</a>》第七章<br />
	3、阮一峰老师的两篇博文&ldquo;<a href="http://www.ruanyifeng.com/blog/2014/02 /ssl_tls.html">SSL/TLS协议运行机制的概述</a>&quot;和&quot;<a href="http://http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html">图解SSL/TLS协议</a>&quot;。</p>
<p><b>二、实现一个最简单的HTTPS Web Server</b></p>
<p>Golang的标准库net/http提供了https server的基本实现，我们修改两行代码就能将上面的HTTP Server改为一个HTTPS Web Server:</p>
<p><font face="Courier New">// gohttps/2-https/server.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func handler(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Fprintf(w,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Hi, This is an example of https service in golang!&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; http.HandleFunc(&quot;/&quot;, handler)<br />
	&nbsp;&nbsp;&nbsp; http.ListenAndServeTLS(&quot;:8081&quot;, &quot;server.crt&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;server.key&quot;, nil)<br />
	}</font></p>
<p><font face="Courier New">我们用http.ListenAndServeTLS替换掉了http.ListenAndServe，就将一个HTTP Server转换为HTTPS Web Server了。不过</font><font face="Courier New">ListenAndServeTLS 新增了两个参数certFile和keyFile，需要我们传入两个文件路径。到这里，我们不得不再学习一点HTTPS协议的原理了。不过为 了让这个例子能先Run起来，我们先执行下面命令，利用openssl生成server.crt和server.key文件，供程序使用，原 理后续详述：</font></p>
<p><font face="Courier New">$openssl genrsa -out server.key 2048</font></p>
<p><font face="Courier New">Generating RSA private key, 2048 bit long modulus<br />
	&#8230;&#8230;&#8230;&#8230;&#8230;.+++<br />
	&#8230;&#8230;&#8230;&#8230;&#8230;+++<br />
	e is 65537 (0&#215;10001)</font></p>
<p><font face="Courier New">$openssl req -new -x509 -key server.key -out server.crt -days 365</font></p>
<p><font face="Courier New">You are about to be asked to enter information that will be incorporated<br />
	into your certificate request.<br />
	What you are about to enter is what is called a Distinguished Name or a DN.<br />
	There are quite a few fields but you can leave some blank<br />
	For some fields there will be a default value,<br />
	If you enter &#39;.&#39;, the field will be left blank.<br />
	&#8212;&#8211;<br />
	Country Name (2 letter code) [AU]:<br />
	State or Province Name (full name) [Some-State]:<br />
	Locality Name (eg, city) []:<br />
	Organization Name (eg, company) [Internet Widgits Pty Ltd]:<br />
	Organizational Unit Name (eg, section) []:<br />
	Common Name (e.g. server FQDN or YOUR name) []:<u><b>localhost</b></u><br />
	Email Address []:</font></p>
<p><font face="Courier New">执行程序：go run server.go<br />
	通过浏览器访问：https://localhost:8081，chrome浏览器会显示如下画面：</font></p>
<p><font face="Courier New"><img alt="" src="/wp-content/uploads/gohttps-chrome-unsecure-connection.png" style="height: 148px; width: 300px;" /></font></p>
<p><font face="Courier New">忽略继续后，才能看到&quot;</font><font face="Courier New"><font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
        New">Hi, This is an example of https service in golang!&quot;这个结果输出在窗口上。</font></font></p>
<p><font face="Courier New"><font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
        New">也可以使用curl工具验证这个HTTPS server：</font></font></p>
<p><font face="Courier New"><font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
        New">curl -k <a class="moz-txt-link-freetext" href="https://localhost:8081">https://localhost:8081</a><br />
	Hi, This is an example of http service in golang!</font></font></p>
<p><font face="Courier New"><font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
        New">注意如果不加-k，curl会报如下错误：</font></font></p>
<p><font face="Courier New"><font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
        New">$curl <a class="moz-txt-link-freetext" href="https://localhost:8081">https://localhost:8081</a><br />
	curl: (60) SSL certificate problem: Invalid certificate chain<br />
	More details here: <a class="moz-txt-link-freetext" href="http://curl.haxx.se/docs/sslcerts.html">http://curl.haxx.se/docs/sslcerts.html</a></font></font></p>
<p><font face="Courier New"><font face="Courier&lt;br /&gt;&lt;br /&gt;<br />
        New">curl performs SSL certificate verification by default, using a &quot;bundle&quot;<br />
	&nbsp;of Certificate Authority (CA) public keys (CA certs). If the default<br />
	&nbsp;bundle file isn&#39;t adequate, you can specify an alternate file<br />
	&nbsp;using the &#8211;cacert option.<br />
	If this HTTPS server uses a certificate signed by a CA represented in<br />
	&nbsp;the bundle, the certificate verification probably failed due to a<br />
	&nbsp;problem with the certificate (it might be expired, or the name might<br />
	&nbsp;not match the domain name in the URL).<br />
	If you&#39;d like to turn off curl&#39;s verification of the certificate, use<br />
	&nbsp;the -k (or &#8211;insecure) option.</font></font></p>
<p><font face="Courier New"><b>三、非对称加密和数字证书</b></font></p>
<p><font face="Courier New">前面说过，HTTPS的数据传输是加密的。实际使用中，HTTPS利用的是对称与非对称加密算法结合的方式。</font></p>
<p><font face="Courier New">对称加密，就是通信双方使用一个密钥，该密钥既用于数据加密（发送方），也用于数据解密（接收方）。<br />
	非对称加密，使用两个密钥。发送方使用公钥（公开密钥）对数据进行加密，数据接收方使用私钥对数据进行解密。</font></p>
<p><font face="Courier New">实际操作中，单纯使用对称加密或单纯使用非对称加密都会存在一些问题，比如对称加密的密钥管理复杂；非对称加密的处理性能低、资源占用高等，因 此HTTPS结合了这两种方式。</font></p>
<p><font face="Courier New">HTTPS服务端在连接建立过程（ssl shaking握手协议）中，会将自身的公钥发送给客户端。客户端拿到公钥后，与服务端协商数据传输通道的对称加密密钥-对话密钥，随后的这个协商过程则 是基于非对称加密的（因为这时客户端已经拿到了公钥，而服务端有私钥）。一旦双方协商出对话密钥，则后续的数据通讯就会一直使用基于该对话密 钥的对称加密算法了。</font></p>
<p><font face="Courier New">上述过程有一个问题，那就是双方握手过程中，如何保障HTTPS服务端发送给客户端的公钥信息没有被篡改呢？实际应用中，HTTPS并非直接 传输公钥信息，而是使用携带公钥信息的数字证书来保证公钥的安全性和完整性。</font></p>
<p><font face="Courier New">数字证书，又称互联网上的&quot;身份证&quot;，用于唯一标识一个组织或一个服务器的，这就好比我们日常生活中使用的&quot;居民身份证&quot;，用于唯一标识一个 人。服务端将数字证书传输给客户端，客户端如何校验这个证书的真伪呢？我们知道</font>居民身份证是由国家统一制作和颁发的，个人向户 口所在地公安机关申请，国家颁发的身份证才具有法律 效力，任何地方这个身份证都是有效和可被接纳的。大悦城的会员卡也是一种身份标识，但你若用大悦城的会员卡去买机票，对不起， 不卖。航空公司可不认大悦城的会员卡，只认居民身份证。网站的证书也是同样的道理。一般来说数字证书从受信的权威证书授权机构 (Certification Authority，证书授权机构)买来的（免费的很少）。一般浏览器在出厂时就内置了诸多知名CA（如Verisign、GoDaddy、美国国防部、 CNNIC等）的数字证书校验方法，只要是这些CA机构颁发的证书，浏览器都能校验。对于CA未知的证书，浏览器则会报错（就像上面那个截图一 样）。主流浏览器都有证书管理功能，但鉴于这些功能比较高级，一般用户是不用去关心的。</p>
<p>初步原理先讲到这，我们再回到上面的例子。</p>
<p><b>四、服务端私钥与证书</b></p>
<p>接上面的例子，我们来说说服务端私钥与证书的生成。</p>
<p>go的<font face="Courier New">http.ListenAndServeTLS需要两个特别参数，一个是服务端的私钥 文件路径，另外一个是服务端的数字证书文件路径。在测试环境，我们没有必要花钱去购买什么证书，利用openssl工具，我们可以自己生成相 关私钥和自签发的数字证书。</font></p>
<p><font face="Courier New">openssl genrsa -out server.key 2048 用于生成服务端私钥文件server.key，后面的参数2048单位是bit，是私钥的长度。<br />
	openssl生成的私钥中包含了公钥的信息，我们可以根据私钥生成公钥：</font></p>
<p><font face="Courier New">$openssl rsa -in server.key -out server.key.public</font></p>
<p><font face="Courier New">我们也可以根据私钥直接生成自签发的数字证书：</font></p>
<p><font face="Courier New">$openssl req -new -x509 -key server.key -out server.crt -days 365</font></p>
<p><font face="Courier New">server.key和server.crt将作为ListenAndServeTLS的两个输入参数。</font></p>
<p><font face="Courier New">我们编写一个Go程序来尝试与这个HTTPS server建立连接并通信。</font></p>
<p><font face="Courier New">//gohttps/4-https/client1.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;io/ioutil&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; resp, err := http.Get(<a class="moz-txt-link-rfc2396E" href="https://localhost:8081">&quot;https://localhost:8081&quot;</a>)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; defer resp.Body.Close()<br />
	&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(resp.Body)<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	}</font></p>
<p><font face="Courier New">运行这个client，我们得到如下错误：</font></p>
<p><font face="Courier New">$go run client1.go<br />
	error: Get <a class="moz-txt-link-freetext" href="https://localhost:8081">https://localhost:8081</a>: x509: certificate signed by unknown authority</font></p>
<p><font face="Courier New">此时服务端也给出了错误日志提示：<br />
	2015/04/30 16:03:31 http: TLS handshake error from 127.0.0.1:62004: remote error: bad certificate</font></p>
<p><font face="Courier New">显然从客户端日志来看，go实现的Client端默认也是要对服务端传过来的数字证书进行校验的，但客户端提示：这个证书是由不知名CA签发 的！</font></p>
<p><font face="Courier New">我们可以修改一下client1.go的代码，让client端略过对证书的校验：</font></p>
<p><font face="Courier New"><font face="Courier New">//gohttps/4-https/client2.go</font><br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/tls&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;io/ioutil&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; tr := &amp;http.Transport{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>TLSClientConfig:&nbsp;&nbsp;&nbsp; &amp;tls.Config{InsecureSkipVerify: true},</b><br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; client := &amp;http.Client{Transport: tr}<br />
	&nbsp;&nbsp;&nbsp; resp, err := client.Get(<a class="moz-txt-link-rfc2396E" href="https://localhost:8081">&quot;https://localhost:8081&quot;</a>)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; defer resp.Body.Close()<br />
	&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(resp.Body)<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	}</font></p>
<p><font face="Courier New">通过设置tls.Config的InsecureSkipVerify为true，client将不再对服务端的证书进行校验。执行后的结果 也证实了这一点：<br />
	$go run client2.go<br />
	Hi, This is an example of http service in golang!</font></p>
<p><font face="Courier New"><b>五、对服务端的证书进行校验</b></font></p>
<p><font face="Courier New">多数时候，我们需要对服务端的证书进行校验，而不是像上面client2.go那样忽略这个校验。我大脑中的这个产品需要服务端和客户端双向 校验，我们先来看看如何能让client端实现对Server端证书的校验呢？</font></p>
<p><font face="Courier New">client端校验证书的原理是什么呢？回想前面我们提到的浏览器内置了知名CA的相关信息，用来校验服务端发送过来的数字证书。那么浏览器 存储的到底是CA的什么信息呢？其实是CA自身的数字证书(包含CA自己的公钥)。而且为了保证CA证书的真实性，浏览器是在出厂时就内置了 这些CA证书的，而不是后期通过通信的方式获取的。CA证书就是用来校验由该CA颁发的数字证书的。</font></p>
<p><font face="Courier New">那么如何使用CA证书校验Server证书的呢？这就涉及到数字证书到底是什么了！</font></p>
<p><font face="Courier New">我们可以通过浏览器中的&quot;https/ssl证书管理&quot;来查看证书的内容，一般服务器证书都会包含诸如站点的名称和主机名、公钥、签发机构 (CA)名称和来自签发机构的签名等。我们重点关注这个</font><font face="Courier New"><font face="Courier New">来自签发机构的签名，因为对于证书的校验，就是使用客户端CA证书来验证服务端证书的签名是否这 个CA签的。</font></font></p>
<p><font face="Courier New"><font face="Courier New">通过签名验证我们可以来确认两件事：</font><br />
	1、服务端传来的数字证书是由某个特定CA签发的（如果是self-signed，也无妨），数字证书中的签名类似于日常生活中的签名，首先 验证这个签名签的是Tony Bai，而不是Tom Bai， Tony Blair等。<br />
	2、服务端传来的数字证书没有被中途篡改过。这类似于&quot;Tony Bai&quot;有无数种写法，这里验证必须是我自己的那种写法，而不是张三、李四写的&quot;Tony Bai&quot;。</font></p>
<p><font face="Courier New">一旦签名验证通过，我们因为信任这个CA，从而信任这个服务端证书。由此也可以看出，CA机构的最大资本就是其信用度。</font></p>
<p><font face="Courier New">CA在为客户签发数字证书时是这样在证书上签名的：</font></p>
<p><font face="Courier New">数字证书由两部分组成：<br />
	1、C：证书相关信息（对象名称+过期时间+证书发布者+证书签名算法&#8230;.）<br />
	2、S：证书的数字签名</font></p>
<p><font face="Courier New">其中的数字签名是通过公式S = F(Digest(C))得到的。</font></p>
<p><font face="Courier New">Digest为摘要函数，也就是 md5、sha-1或sha256等单向散列算法，用于将无限输入值转换为一个有限长度的&ldquo;浓缩&rdquo;输出值。比如我们常用md5值来验证下载的大文件是否完 整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时，网站会对大文件做一次md5计算，得出一个128bit的值作为大文件的 摘要一同放在网站上。用户在下载文件后，对下载后的文件再进行一次本地的md5计算，用得出的值与网站上的md5值进行比较，如果一致，则大 文件下载完好，否则下载过程大文件内容有损坏或源文件被篡改。</font></p>
<p><font face="Courier New">F为签名函数。CA自己的私钥是唯一标识CA签名的，因此CA用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在RSA加密 系统中，发送端的解密函数就是一个以私钥作 为参数的函数，因此常常被用作签名函数使用。签名算法是与证书一并发送给接收 端的，比如apple的一个服务的证书中关于签名算法的描述是&ldquo;带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )&rdquo;。因此CA用私钥解密函数作为F，对C的摘要进行运算得到了客户数字证书的签名，好比大学毕业证上的校长签名，所有毕业证都是校长签发的。</font></p>
<p><font face="Courier New">接收端接收服务端数字证书后，如何验证数字证书上携带的签名是这个CA的签名呢？接收端会运用下面算法对数字证书的签名进行校验：<br />
	F&#39;(S) ?= Digest(C)</font></p>
<p><font face="Courier New">接收端进行两个计算，并将计算结果进行比对：<br />
	1、首先通过Digest(C)，接收端计算出证书内容（除签名之外）的摘要。<br />
	2、数字证书携带的签名是CA通过CA密钥加密摘要后的结果，因此接收端通过一个解密函数F&#39;对S进行&ldquo;解密&rdquo;。RSA系统中，接收端使用 CA公钥对S进行&ldquo;解密&rdquo;，这恰是CA用私钥对S进行&ldquo;加密&rdquo;的逆过程。</font></p>
<p><font face="Courier New">将上述两个运算的结果进行比较，如果一致，说明签名的确属于该CA，该证书有效，否则要么证书不是该CA的，要么就是中途被人篡改了。</font></p>
<p><font face="Courier New">但对于self-signed(自签发)证书来说，接收端并没有你这个self-CA的数字证书，也就是没有CA公钥，也就没有办法对数字证 书的签名进行验证。因此如果要编写一个可以对self-signed证书进行校验的接收端程序的话，首先我们要做的就是建立一个属于自己的 CA，用该CA签发我们的server端证书，并将该CA自身的数字证书随客户端一并发布。</font></p>
<p><font face="Courier New">这让我想起了在《<a href="http://tonybai.com/2015/03/14/selfhost-ngrok-service/">搭建自己的ngrok服务</a>》一文中为ngrok服务端、客户端生成证书的那几个步骤，我们来重温并分析一下每一步都在做什么。</font></p>
<p><font face="Courier New">(1)openssl genrsa -out rootCA.key 2048<br />
	(2)openssl req -x509 -new -nodes -key rootCA.key -subj &quot;/CN=*.tunnel.tonybai.com&quot; -days 5000 -out rootCA.pem</font></p>
<p><font face="Courier New">(3)openssl genrsa -out device.key 2048<br />
	(4)openssl req -new -key device.key -subj &quot;/CN=*.tunnel.tonybai.com&quot; -out device.csr<br />
	(5)openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000</font></p>
<p><font face="Courier New">(6)cp rootCA.pem assets/client/tls/ngrokroot.crt<br />
	(7)cp device.crt assets/server/tls/snakeoil.crt<br />
	(8)cp device.key assets/server/tls/snakeoil.key</font></p>
<p><font face="Courier New">自己搭建ngrok服务，客户端要验证服务端证书，我们需要自己做CA，因此步骤(1)和步骤(2)就是生成CA自己的相关信息。<br />
	步骤(1) ，生成CA自己的私钥 rootCA.key<br />
	步骤(2)，根据CA自己的私钥生成自签发的数字证书，该证书里包含CA自己的公钥。</font></p>
<p><font face="Courier New">步骤(3)~(5)是用来生成ngrok服务端的私钥和数字证书（由自CA签发）。<br />
	步骤(3)，生成ngrok服务端私钥。<br />
	步骤(4)，生成Certificate Sign Request，CSR，证书签名请求。<br />
	步骤(5)，自CA用自己的CA私钥对服务端提交的csr进行签名处理，得到服务端的数字证书device.crt。</font></p>
<p><font face="Courier New">步骤(6)，将自CA的数字证书同客户端一并发布，用于客户端对服务端的数字证书进行校验。<br />
	步骤(7)和步骤(8)，将服务端的数字证书和私钥同服务端一并发布。</font></p>
<p><font face="Courier New">接下来我们来验证一下客户端对服务端数字证书进行验证（gohttps/5-verify-server-cert）！</font></p>
<p><font face="Courier New">首先我们来建立我们自己的CA，需要生成一个CA私钥和一个CA的数字证书:</font></p>
<p><font face="Courier New">$openssl genrsa -out ca.key 2048<br />
	Generating RSA private key, 2048 bit long modulus<br />
	&#8230;&#8230;&#8230;.+++<br />
	&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;.+++<br />
	e is 65537 (0&#215;10001)</font></p>
<p><font face="Courier New">$openssl req -x509 -new -nodes -key ca.key -subj &quot;/CN=tonybai.com&quot; -days 5000 -out ca.crt</font></p>
<p><font face="Courier New">接下来，生成server端的私钥，生成数字证书请求，并用我们的ca私钥签发server的数字证书：</font></p>
<p><font face="Courier New">openssl genrsa -out server.key 2048<br />
	Generating RSA private key, 2048 bit long modulus<br />
	&#8230;.+++<br />
	&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;.+++<br />
	e is 65537 (0&#215;10001)</font></p>
<p><font face="Courier New">$openssl req -new -key server.key -subj &quot;/CN=localhost&quot; -out server.csr</font></p>
<p><font face="Courier New">$openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000<br />
	Signature ok<br />
	subject=/CN=localhost<br />
	Getting CA Private Key</font></p>
<p><font face="Courier New">现在我们的工作目录下有如下一些私钥和证书文件：<br />
	CA:<br />
	&nbsp;&nbsp;&nbsp; 私钥文件 ca.key<br />
	&nbsp;&nbsp;&nbsp; 数字证书 ca.crt</font></p>
<p><font face="Courier New">Server:<br />
	&nbsp;&nbsp;&nbsp; 私钥文件 server.key<br />
	&nbsp;&nbsp;&nbsp; 数字证书 server.crt</font></p>
<p><font face="Courier New">接下来，我们就来完成我们的程序。</font></p>
<p><font face="Courier New">Server端的程序几乎没有变化：</font></p>
<p><font face="Courier New">// gohttps/5-verify-server-cert/server.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func handler(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Fprintf(w,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Hi, This is an example of http service in golang!&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; http.HandleFunc(&quot;/&quot;, handler)<br />
	&nbsp;&nbsp;&nbsp; http.ListenAndServeTLS(&quot;:8081&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;server.crt&quot;, &quot;server.key&quot;, nil)<br />
	}</font></p>
<p><font face="Courier New">client端程序变化较大，由于client端需要验证server端的数字证书，因此client端需要预先加载ca.crt，以用于服务端数字证书的校验：</font></p>
<p><font face="Courier New"><font face="Courier New">// gohttps/5-verify-server-cert/client.go</font><br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/tls&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/x509&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;io/ioutil&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; pool := x509.NewCertPool()<br />
	&nbsp;&nbsp;&nbsp; caCertPath := &quot;ca.crt&quot;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; caCrt, err := ioutil.ReadFile(caCertPath)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;ReadFile err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; pool.AppendCertsFromPEM(caCrt)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; tr := &amp;http.Transport{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TLSClientConfig: &amp;tls.Config{RootCAs: pool},<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; client := &amp;http.Client{Transport: tr}<br />
	&nbsp;&nbsp;&nbsp; resp, err := client.Get(&quot;https://localhost:8081&quot;)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Get error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; defer resp.Body.Close()<br />
	&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(resp.Body)<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	}</font></p>
<p><font face="Courier New">运行server和client:</font></p>
<p><font face="Courier New">$go run server.go</font></p>
<p><font face="Courier New">go run client.go<br />
	Hi, This is an example of http service in golang!</font></p>
<p><font face="Courier New"><font face="Courier New"><b>六、对客户端的证书进行校验(双向证书校验）</b></font></font></p>
<p><font face="Courier New">服务端可以要求对客户端的证书进行校验，以更严格识别客户端的身份，限制客户端的访问。</font></p>
<p><font face="Courier New">要对客户端数字证书进行校验，首先客户端需要先有自己的证书。我们以上面的例子为基础，生成客户端的私钥与证书。</font></p>
<p><font face="Courier New">$openssl genrsa -out client.key 2048<br />
	Generating RSA private key, 2048 bit long modulus<br />
	&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;..+++<br />
	&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;..+++<br />
	e is 65537 (0&#215;10001)<br />
	$openssl req -new -key client.key -subj &quot;/CN=tonybai_cn&quot; -out client.csr<br />
	$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000<br />
	Signature ok<br />
	subject=/CN=tonybai_cn<br />
	Getting CA Private Key</font></p>
<p><font face="Courier New">接下来我们来改造我们的程序，首先是server端。</font></p>
<p><font face="Courier New">首先server端需要要求校验client端的数字证书，并且加载用于校验数字证书的ca.crt，因此我们需要对server进行更加灵活的控制：</font></p>
<p><font face="Courier New">// gohttps/6-dual-verify-certs/server.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/tls&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/x509&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;io/ioutil&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">type myhandler struct {<br />
	}</font></p>
<p><font face="Courier New">func (h *myhandler) ServeHTTP(w http.ResponseWriter,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Fprintf(w,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Hi, This is an example of http service in golang!\n&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; pool := x509.NewCertPool()<br />
	&nbsp;&nbsp;&nbsp; caCertPath := &quot;ca.crt&quot;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; caCrt, err := ioutil.ReadFile(caCertPath)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;ReadFile err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; pool.AppendCertsFromPEM(caCrt)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; s := &amp;http.Server{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Addr:&nbsp;&nbsp;&nbsp; &quot;:8081&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Handler: &amp;myhandler{},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TLSConfig: &amp;tls.Config{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientCAs:&nbsp; pool,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientAuth: <b>tls.RequireAndVerifyClientCert</b>,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; err = s.ListenAndServeTLS(&quot;server.crt&quot;, &quot;server.key&quot;)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;ListenAndServeTLS err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">可以看出代码通过将tls.Config.ClientAuth赋值为tls.RequireAndVerifyClientCert来实现Server强制校验client端证书。ClientCAs是用来校验客户端证书的ca certificate。</font></p>
<p><font face="Courier New">Client端变化也很大，需要加载client.key和client.crt用于server端连接时的证书校验：</font></p>
<p><font face="Courier New">// </font><font face="Courier New"><font face="Courier New">gohttps/6-dual-verify-certs/client.go</font></font></p>
<p><font face="Courier New">package main<br />
	import (<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/tls&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;crypto/x509&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;io/ioutil&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;net/http&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; pool := x509.NewCertPool()<br />
	&nbsp;&nbsp;&nbsp; caCertPath := &quot;ca.crt&quot;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; caCrt, err := ioutil.ReadFile(caCertPath)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;ReadFile err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; pool.AppendCertsFromPEM(caCrt)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; cliCrt, err := tls.LoadX509KeyPair(&quot;client.crt&quot;, &quot;client.key&quot;)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Loadx509keypair err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; tr := &amp;http.Transport{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TLSClientConfig: &amp;tls.Config{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RootCAs:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pool,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Certificates: []tls.Certificate{cliCrt},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; client := &amp;http.Client{Transport: tr}<br />
	&nbsp;&nbsp;&nbsp; resp, err := client.Get(&quot;https://localhost:8081&quot;)<br />
	&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Get error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; defer resp.Body.Close()<br />
	&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(resp.Body)<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	}</font></p>
<p><font face="Courier New">好了，让我们来试着运行一下这两个程序，结果如下：</font></p>
<p><font face="Courier New">$go run server.go<br />
	2015/04/30 22:13:33 http: TLS handshake error from 127.0.0.1:53542:<br />
	tls: client&#39;s certificate&#39;s extended key usage doesn&#39;t permit it to be<br />
	used for client authentication</font></p>
<p><font face="Courier New">$go run client.go<br />
	Get error: Get https://localhost:8081: remote error: handshake failure</font></p>
<p><font face="Courier New">失败了！从server端的错误日志来看，似乎是client端的client.crt文件不满足某些条件。</font></p>
<p><font face="Courier New">根据server端的错误日志，搜索了Golang的源码，发现错误出自crypto/tls/handshake_server.go。</font></p>
<p><font face="Courier New">k := false<br />
	for _, ku := range certs[0].ExtKeyUsage {<br />
	&nbsp;&nbsp;&nbsp; if ku == x509.ExtKeyUsageClientAuth {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ok = true<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}<br />
	if !ok {<br />
	&nbsp;&nbsp;&nbsp; c.sendAlert(alertHandshakeFailure)<br />
	&nbsp;&nbsp;&nbsp; return nil, errors.New(&quot;tls: client&#39;s certificate&#39;s extended key usage doesn&#39;t permit it to be used for client authentication&quot;)<br />
	}</font></p>
<p><font face="Courier New">大致判断是证书中的ExtKeyUsage信息应该包含clientAuth。翻看openssl的相关资料，了解到自CA签名的数字证书中包含的都是一些basic的信息，根本没有ExtKeyUsage的信息。我们可以用命令来查看一下当前client.crt的内容：</font></p>
<p><font face="Courier New">$ openssl x509 -text -in client.crt -noout<br />
	Certificate:<br />
	&nbsp;&nbsp;&nbsp; Data:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Version: 1 (0&#215;0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Serial Number:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; d6:e3:f6:fa:ae:65:ed:df<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Signature Algorithm: sha1WithRSAEncryption<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Issuer: CN=tonybai.com<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Validity<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Not Before: Apr 30 14:11:34 2015 GMT<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Not After : Jan&nbsp; 6 14:11:34 2029 GMT<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Subject: CN=tonybai_cn<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Subject Public Key Info:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Public Key Algorithm: rsaEncryption<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RSA Public Key: (2048 bit)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Modulus (2048 bit):<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:e4:12:22:50:75:ae:b2:8a:9e:56:d5:f3:7d:31:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 7b:aa:75:5d:3f:90:05:4e:ff:ed:9a:0a:2a:75:15:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Exponent: 65537 (0&#215;10001)<br />
	&nbsp;&nbsp;&nbsp; Signature Algorithm: sha1WithRSAEncryption<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 76:3b:31:3e:9d:b0:66:ad:c0:03:d4:19:c6:f2:1a:52:91:d6:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 13:31:3a:c5:d5:58:ea:42:1d:b7:33:b8:43:a8:a8:28:91:ac:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<div class="page" title="Page 87">
<div class="section">
<div class="layoutArea">
<div class="column">
<p><span style="color: rgb(0, 0, 0); font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 14px; line-height: 21px;"><span style="color: rgb(51, 51, 51); font-family: arial, 宋体, sans-serif; line-height: 24px; text-indent: 28px;">而偏偏golang的tls又要校验ExtKeyUsage，如此我们需要重新生成client.crt，并在生成时指定extKeyUsage。经过摸索，可以用如下方法重新生成client.crt：</span></span></p>
<p><span style="color: rgb(0, 0, 0); font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 14px; line-height: 21px;"><span style="color: rgb(51, 51, 51); font-family: arial, 宋体, sans-serif; line-height: 24px; text-indent: 28px;">1、创建文件client.ext<br />
					内容：<br />
					<font face="Courier New">extendedKeyUsage=clientAuth</font></span></span></p>
<p><span style="color: rgb(0, 0, 0); font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 14px; line-height: 21px;"><span style="color: rgb(51, 51, 51); font-family: arial, 宋体, sans-serif; line-height: 24px; text-indent: 28px;">2、重建client.crt</span></span></p>
<p><font face="Courier New">$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial <b>-extfile client.ext</b> -out client.crt -days 5000<br />
					Signature ok<br />
					subject=/CN=tonybai_cn<br />
					Getting CA Private Key</font></p>
<p>再通过命令查看一下新client.crt：</p>
<p>看到输出的文本中多了这么几行：<br />
					<font face="DIN Alternate"><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X509v3 extensions:<br />
					&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; X509v3 Extended Key Usage:<br />
					&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TLS Web Client Authentication</font></font></p>
<p>这说明client.crt的extended key usage已经添加成功了。我们再来执行一下server和client：</p>
<p><font face="Courier New">$ go run client.go<br />
					Hi, This is an example of http service in golang!</font></p>
<p>client端证书验证成功，也就是说双向证书验证均ok了。</p>
<p><b>七、小结</b></p>
<p>通过上面的例子可以看出，使用golang开发https相关程序十分便利，Golang标准库已经实现了TLS 1.2版本协议。上述所有example代码均放在我的github上的<a href="https://github.com/bigwhite/experiments/tree/master/gohttps">experiments/gohttps</a>中。</p>
</p></div>
</p></div>
</p></div>
</div>
<p style='text-align:left'>&copy; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2015/04/30/go-and-https/feed/</wfw:commentRss>
		<slash:comments>34</slash:comments>
		</item>
		<item>
		<title>搭建自己的ngrok服务</title>
		<link>https://tonybai.com/2015/03/14/selfhost-ngrok-service/</link>
		<comments>https://tonybai.com/2015/03/14/selfhost-ngrok-service/#comments</comments>
		<pubDate>Sat, 14 Mar 2015 06:19:25 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[ngrok]]></category>
		<category><![CDATA[ngrokd]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vps]]></category>
		<category><![CDATA[亚马逊]]></category>
		<category><![CDATA[代理]]></category>
		<category><![CDATA[企业号]]></category>
		<category><![CDATA[公众号]]></category>
		<category><![CDATA[域名]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[隧道]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1702</guid>
		<description><![CDATA[在国内开发微信公众号、企业号以及做前端开发的朋友想必对ngrok都不陌生吧，就目前来看，ngrok可是最佳的在内网调试微信服务的tunnel工 具。记得今年春节前，ngrok.com提供的服务还一切正常呢，但春节后似乎就一切不正常了。ngrok.com无法访问，ngrok虽然能连上 ngrok.com提供的服务，但微信端因为无法访问ngrok.com，导致消息一直无法发送到我们的服务地址上，比如xxxx.ngrok.com。 这一切都表明，ngork被墙了。没有了ngrok tunnel，一切开始变得困难且没有效率起来。内网到外部主机部署和调试是一件慢的让人想骂街的事情。 ngrok不能少。ngrok以及其服务端ngrokd都是开源的，之前我也知道通过源码可以自搭建ngrok服务。请求搜索引擎后，发现国内有个朋友已经搭建了一个www.tunnel.mobi的ngrok公共服务，与ngrok.com类似，我也实验了一下。 编写一个ngrok.cfg，内容如下： server_addr: &#34;tunnel.mobi:44433&#34; trust_host_root_certs: true 用ngrok最新客户端1.7版本执行如下命令： $ngrok -subdomain tonybaiexample -config=ngrok.cfg 80 可以顺利建立一个tunnel，用于本机向外部提供&#34;tonybaiexample.tunnel.mobi&#34;服务。 Tunnel Status&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; online Version&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 1.7/1.7 Forwarding&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; http://tonybaiexample.tunnel.mobi -&#62; 127.0.0.1:80 Forwarding&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; https://tonybaiexample.tunnel.mobi -&#62; 127.0.0.1:80 Web Interface&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 127.0.0.1:4040 # Conn&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 0 Avg Conn Time&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 0.00ms 而且国内的ngrok服务显然要远远快于ngrok.com提供的服务，消息瞬间即达。 但这是在公网上直接访问的结果。放在公司内部，我看到的却是另外一个结果： Tunnel Status&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; reconnecting Version&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 1.7/ Web Interface&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 127.0.0.1:4040 # Conn&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 0 Avg [...]]]></description>
			<content:encoded><![CDATA[<p>在国内开发<a href="https://mp.weixin.qq.com">微信公众号</a>、<a href="http://qydev.weixin.qq.com/">企业号</a>以及做前端开发的朋友想必对<a href="https://github.com/inconshreveable/ngrok">ngrok</a>都不陌生吧，就目前来看，ngrok可是最佳的在内网调试微信服务的tunnel工 具。记得今年春节前，ngrok.com提供的服务还一切正常呢，但春节后似乎就一切不正常了。ngrok.com无法访问，ngrok虽然能连上 ngrok.com提供的服务，但微信端因为无法访问ngrok.com，导致消息一直无法发送到我们的服务地址上，比如xxxx.ngrok.com。 这一切都表明，ngork被墙了。没有了ngrok tunnel，一切开始变得困难且没有效率起来。内网到外部主机部署和调试是一件慢的让人想骂街的事情。</p>
<p>ngrok不能少。ngrok以及其服务端ngrokd都是开源的，之前我也知道通过源码可以自搭建ngrok服务。请求搜索引擎后，发现国内有个朋友已经搭建了一个<font face="Courier New">www.tunnel.mobi</font>的ngrok公共服务，与ngrok.com类似，我也实验了一下。</p>
<p>编写一个ngrok.cfg，内容如下：</p>
<p><font face="Courier New">server_addr: &quot;tunnel.mobi:44433&quot;<br />
	trust_host_root_certs: true</font></p>
<p>用ngrok最新客户端1.7版本执行如下命令：</p>
<p><font face="Courier New">$ngrok -subdomain tonybaiexample -config=ngrok.cfg 80</font></p>
<p>可以顺利建立一个tunnel，用于本机向外部提供&quot;<font face="Courier New">tonybaiexample.tunnel.mobi</font>&quot;服务。</p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; online<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/1.7<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http://tonybaiexample.tunnel.mobi -&gt; 127.0.0.1:80<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; https://tonybaiexample.tunnel.mobi -&gt; 127.0.0.1:80<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0.00ms</font></p>
<p>而且国内的ngrok服务显然要远远快于ngrok.com提供的服务，消息瞬间即达。</p>
<p>但这是在公网上直接访问的结果。放在公司内部，我看到的却是另外一个结果：</p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; reconnecting<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0.00ms</font></p>
<p>我们无法从内网建立tunnel，意味着依旧不方便和低效，因为很多基础服务都在内网部署，内外网之间的交互十分不便。但内网连不上tunnel.mobi也是个事实，且无法知道原因，因为看不到server端的连接错误日志。</p>
<p>于是我决定自建一个ngrok服务。</p>
<p><b>一、准备工作</b></p>
<p>搭建ngrok服务需要在公网有一台vps，去年年末曾经在Amazon申请了一个体验主机<a href="https://aws.amazon.com/cn">EC2</a>，有公网IP一个，这次就打算用这个主机作为ngrokd服务端。</p>
<p>需要一个自己的域名。已有域名的，可以建立一个子域名，用于关联ngrok服务，这样也不会干扰原先域名提供的服务。(不用域名的方式也许可以，但我没有试验过。）</p>
<p>搭建的参考资料主要来自下面三个：<br />
	<font face="Courier New">1) ngrok的官方SELFHOST指南：https://github.com/inconshreveable/ngrok/blob/master/docs/SELFHOSTING.md<br />
	2) 国外一哥们的博客：http://www.svenbit.com/2014/09/run-ngrok-on-your-own-server/<br />
	3) &quot;海运的博客&quot;中的一篇文章：http://www.haiyun.me/archives/1012.html</font></p>
<p><b>二、实操</b><b>步骤</b></p>
<p>我的AWS EC2实例安装的是<a href="http://tonybai.com/tag/ubuntu">Ubuntu </a>Server 14.04 x86_64，并安装了<a href="http://tonybai.com/tag/golang">golang</a> 1.4（go version go1.4 linux/amd64）。Golang是编译ngrokd和ngrok所必须的，建议直接从golang官方下载对应平台的二进制安装包（国内可以从 golangtc.com上下载，速度慢些罢了）。</p>
<p><b>1、下载ngrok源码</b></p>
<p>（GOPATH=~/goproj)<br />
	<font face="Courier New">$ mkdir ~/goproj/src/github.com/inconshreveable<br />
	$ git clone https://github.com/inconshreveable/ngrok.git<br />
	$ export GOPATH=~/goproj/src/github.com/inconshreveable/ngrok</font></p>
<p><b>2、生成自签名证书</b></p>
<p>使用ngrok.com官方服务时，我们使用的是官方的SSL证书。自建ngrokd服务，我们需要生成自己的证书，并提供携带该证书的ngrok客户端。</p>
<p>证书生成过程需要一个<strong style="box-sizing: border-box; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 18.1818180084229px;">NGROK_BASE_DOMAIN</strong>。 以ngrok官方随机生成的地址693c358d.ngrok.com为例，其NGROK_BASE_DOMAIN就是&quot;ngrok.com&quot;，如果你要 提供服务的地址为&quot;example.tunnel.tonybai.com&quot;，那NGROK_BASE_DOMAIN就应该 是&quot;tunnel.tonybai.com&quot;。</p>
<p>我们这里以NGROK_BASE_DOMAIN=&quot;tunnel.tonybai.com&quot;为例，生成证书的命令如下：</p>
<p><font face="Courier New">$ cd ~/goproj/src/github.com/inconshreveable/ngrok<br />
	$ openssl genrsa -out rootCA.key 2048<br />
	$ openssl req -x509 -new -nodes -key rootCA.key -subj &quot;/CN=</font><span style="font-family:courier new,courier,monospace;"><span style="line-height: 20.7999992370605px;">tunnel.tonybai.com</span></span><font face="Courier New">&quot; -days 5000 -out rootCA.pem<br />
	$ openssl genrsa -out device.key 2048<br />
	$ openssl req -new -key device.key -subj &quot;/CN=</font><span style="font-family:courier new,courier,monospace;"><span style="line-height: 20.7999992370605px;">tunnel.tonybai.com</span></span><font face="Courier New">&quot; -out device.csr<br />
	$ openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000</font></p>
<p><font face="Courier New">执行完以上命令，在ngrok目录下就会新生成6个文件：</font></p>
<p><font face="Courier New">-rw-rw-r&#8211; 1 ubuntu ubuntu 1001 Mar 14 02:22 device.crt<br />
	-rw-rw-r&#8211; 1 ubuntu ubuntu&nbsp; 903 Mar 14 02:22 device.csr<br />
	-rw-rw-r&#8211; 1 ubuntu ubuntu 1679 Mar 14 02:22 device.key<br />
	-rw-rw-r&#8211; 1 ubuntu ubuntu 1679 Mar 14 02:21 rootCA.key<br />
	-rw-rw-r&#8211; 1 ubuntu ubuntu 1119 Mar 14 02:21 rootCA.pem<br />
	-rw-rw-r&#8211; 1 ubuntu ubuntu&nbsp;&nbsp; 17 Mar 14 02:22 rootCA.srl</font></p>
<p><font face="Courier New">ngrok通过bindata将ngrok源码目录下的assets目录（资源文件）打包到可执行文件(ngrokd和ngrok)中 去，assets/client/tls和assets/server/tls下分别存放着用于ngrok和ngrokd的默认证书文件，我们需要将它们替换成我们自己生成的：(因此这一步务必放在编译可执行文件之前)</font></p>
<p><font face="Courier New">cp rootCA.pem assets/client/tls/ngrokroot.crt<br />
	cp device.crt assets/server/tls/snakeoil.crt<br />
	cp device.key assets/server/tls/snakeoil.key</font></p>
<p><b>3、编译ngrokd和ngrok</b></p>
<p>在ngrok目录下执行如下命令，编译ngrokd：</p>
<p><font face="Courier New">$ make release-server</font></p>
<p><font face="Courier New">不过在我的AWS上，出现如下错误：</font></p>
<p><font face="Courier New">GOOS=&quot;&quot; GOARCH=&quot;&quot; go get github.com/jteeuwen/go-bindata/go-bindata<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/client/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/client/&#8230;<br />
	make: bin/go-bindata: Command not found<br />
	make: *** [client-assets] Error 127</font></p>
<p>go-bindata被安装到了$GOBIN下了，go编译器找不到了。修正方法是将$GOBIN/go-bindata拷贝到当前ngrok/bin下。</p>
<p><font face="Courier New">$ cp /home/ubuntu/.bin/go14/bin/go-bindata ./bin</font></p>
<p>再次执行make release-server。</p>
<p><font face="Courier New"><a class="moz-txt-link-abbreviated" href="mailto:ubuntu@ip-172-31-4-131:%7E/goproj/src/github.com/inconshreveable/ngrok$">~/goproj/src/github.com/inconshreveable/ngrok$</a> make release-server<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/client/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/client/&#8230;<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/server/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/server/&#8230;<br />
	go get -tags &#39;release&#39; -d -v ngrok/&#8230;<br />
	code.google.com/p/log4go (download)<br />
	go: missing Mercurial command. See <a class="moz-txt-link-freetext" href="http://golang.org/s/gogetcmd">http://golang.org/s/gogetcmd</a><br />
	<u><b>package code.google.com/p/log4go: exec: &quot;hg&quot;: executable file not found in $PATH</b></u><br />
	github.com/gorilla/websocket (download)<br />
	github.com/inconshreveable/go-update (download)<br />
	github.com/kardianos/osext (download)<br />
	github.com/kr/binarydist (download)<br />
	github.com/inconshreveable/go-vhost (download)<br />
	github.com/inconshreveable/mousetrap (download)<br />
	github.com/nsf/termbox-go (download)<br />
	github.com/mattn/go-runewidth (download)<br />
	github.com/rcrowley/go-metrics (download)<br />
	Fetching <a class="moz-txt-link-freetext" href="https://gopkg.in/yaml.v1?go-get=1">https://gopkg.in/yaml.v1?go-get=1</a><br />
	Parsing meta tags from <a class="moz-txt-link-freetext" href="https://gopkg.in/yaml.v1?go-get=1">https://gopkg.in/yaml.v1?go-get=1</a> (status code 200)<br />
	get &quot;gopkg.in/yaml.v1&quot;: found meta tag main.metaImport{Prefix:&quot;gopkg.in/yaml.v1&quot;, VCS:&quot;git&quot;, RepoRoot:<a class="moz-txt-link-rfc2396E" href="https://gopkg.in/yaml.v1">&quot;https://gopkg.in/yaml.v1&quot;</a>} at <a class="moz-txt-link-freetext" href="https://gopkg.in/yaml.v1?go-get=1">https://gopkg.in/yaml.v1?go-get=1</a><br />
	gopkg.in/yaml.v1 (download)<br />
	make: *** [deps] Error 1</font></p>
<p>又出错！提示找不到hg，原来是aws上没有安装hg。install hg后（sudo apt-get install mercurial），再编译。</p>
<p><font face="Courier New">$ make release-server<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/client/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/client/&#8230;<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/server/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/server/&#8230;<br />
	go get -tags &#39;release&#39; -d -v ngrok/&#8230;<br />
	code.google.com/p/log4go (download)<br />
	go install -tags &#39;release&#39; ngrok/main/ngrokd</font></p>
<p>同样编译ngrok:</p>
<p><font face="Courier New">$ make release-client<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/client/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/client/&#8230;<br />
	bin/go-bindata -nomemcopy -pkg=assets -tags=release \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -debug=false \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; -o=src/ngrok/server/assets/assets_release.go \<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; assets/server/&#8230;<br />
	go get -tags &#39;release&#39; -d -v ngrok/&#8230;<br />
	go install -tags &#39;release&#39; ngrok/main/ngrok</font></p>
<p>AWS上ngrokd和ngrok被安装到了$GOBIN下。</p>
<p><b>三、调试</b></p>
<p>1、<b>启动ngrokd</b></p>
<p><font face="Courier New">$ ngrokd -domain=&quot;tunnel.tonybai.com&quot; -httpAddr=&quot;:8080&quot; -httpsAddr=&quot;:8081&quot;</font><br />
	<font face="Courier New">[03/14/15 04:47:24] [INFO] [registry] [tun] No affinity cache specified<br />
	[03/14/15 04:47:24] [INFO] [metrics] Reporting every 30 seconds<br />
	[03/14/15 04:47:24] [INFO] Listening for public http connections on [::]:8080<br />
	[03/14/15 04:47:24] [INFO] Listening for public https connections on [::]:8081<br />
	[03/14/15 04:47:24] [INFO] Listening for control and proxy connections on [::]:4443</font><br />
	&#8230; &#8230;</p>
<p><b>2、公网连接ngrok</b><b>d</b></p>
<p>将生成的ngrok下载到自己的电脑上。</p>
<p>创建一个配置文件ngrok.cfg，内容如下：</p>
<p><font face="Courier New">server_addr: &quot;tunnel.tonybai.com:4443&quot;<br />
	trust_host_root_certs: false</font></p>
<p>执行ngrok：<br />
	<font face="Courier New">$ ngrok -subdomain example -config=ngrok.cfg 80</font></p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; reconnecting<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0.00ms</font></p>
<p><font face="Courier New">连接失败。此刻我的电脑是在公网上。查看ngrokd的日志，没有发现连接到达Server端。试着在本地ping tunnel.tonybai.com这个地址，发现地址不通。难道是DNS设置的问题。之前我只是设置了&quot;*.tunnel.tonybai.com&quot;的A地址，并未设置&quot;tunnel.tonybai.com&quot;。于是到DNS管理页面，添加了&quot;tunnel.tonybai.com&quot;的A记录。</font></p>
<p><font face="Courier New">待DNS记录刷新OK后，再次启动ngrok：</font></p>
<p><font face="Courier New">Tunnel Status online<br />
	Version 1.7/1.7<br />
	Forwarding http://epower.tunnel.tonybai.com:8080 -&gt; 127.0.0.1:80<br />
	Forwarding https://epower.tunnel.tonybai.com:8080 -&gt; 127.0.0.1:80<br />
	Web Interface 127.0.0.1:4040<br />
	# Conn 0<br />
	Avg Conn Time 0.00ms</font></p>
<p><font face="Courier New">这回连接成功了！</font></p>
<p><font face="Courier New"><b>3、内网连接ngrokd</b></font></p>
<p>将ngrok拷贝到内网的一台PC上，这台PC设置了公司的代理。</p>
<p>按照同样的步骤启动ngrok：</p>
<p><font face="Courier New">$ ngrok -subdomain example -config=ngrok.cfg 80</font></p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; reconnecting<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0.00ms</font></p>
<p>不巧，怎么又失败了！从Server端来看，还是没有收到客户端的连接，显然是连接没有打通公司内网。从我自己的squid代理服务器来看，似乎只有443端口的请求被公司代理服务器允许通过，4443则无法出去。</p>
<p><font face="Courier New">1426301143.558 9294 10.10.126.101 TCP_MISS/000 366772 CONNECT api.equinox.io:443 &#8211; DEFAULT_PARENT/proxy.xxx.com -&nbsp;&nbsp; 通过了<br />
	1426301144.441 27 10.10.126.101 TCP_MISS/000 1185 CONNECT tunnel.tonybai.com:4443 &#8211; DEFAULT_PARENT/proxy.xxx.com -&nbsp; 似乎没有通过</font></p>
<p>只能修改server监听端口了。将-tunnelAddr由4443改为443(注意AWS上需要修改防火墙的端口规则，这个是实时生效的，无需重启实例)：</p>
<p><font face="Courier New">$ sudo ngrokd -domain=&quot;tunnel.tonybai.com&quot; -httpAddr=&quot;:8080&quot; -httpsAddr=&quot;:8081&quot;</font> <font face="Courier New">-tunnelAddr=&quot;:443&quot;</font><br />
	<font face="Courier New">[03/14/15 04:47:24] [INFO] [registry] [tun] No affinity cache specified<br />
	[03/14/15 04:47:24] [INFO] [metrics] Reporting every 30 seconds<br />
	[03/14/15 04:47:24] [INFO] Listening for public http connections on [::]:8080<br />
	[03/14/15 04:47:24] [INFO] Listening for public https connections on [::]:8081<br />
	[03/14/15 04:47:24] [INFO] Listening for control and proxy connections on [::]:443</font><br />
	&#8230; &#8230;</p>
<p>将ngrok.cfg中的地址改为443：</p>
<p><font face="Courier New">server_addr: &quot;tunnel.tonybai.com:443&quot;</font></p>
<p>再次执行ngrok客户端：</p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; online<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/1.7<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http://epower.tunnel.tonybai.com:8080 -&gt; 127.0.0.1:80<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; https://epower.tunnel.tonybai.com:8080 -&gt; 127.0.0.1:80<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0.00ms</font></p>
<p>这回成功连上了。</p>
<p><b>4、80端口</b></p>
<p>是否大功告成了呢？我们看看ngrok的结果，总感觉哪里不对呢？噢，转发的地址怎么是8080端口呢？为何不是80？微信公众号/企业号可只是支持80端口啊！</p>
<p>我们还需要修改一下Server端的参数，将-httpAddr从8080改为80。</p>
<p><font face="Courier New">$ sudo ngrokd -domain=&quot;tunnel.tonybai.com&quot; -httpAddr=&quot;:80&quot; -httpsAddr=&quot;:8081&quot;</font> <font face="Courier New">-tunnelAddr=&quot;:443&quot;</font></p>
<p>这回再用ngrok连接一下：<br />
	<font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; online<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/1.7<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; http://epower.tunnel.tonybai.com -&gt; 127.0.0.1:80<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; https://epower.tunnel.tonybai.com -&gt; 127.0.0.1:80<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0.00ms</font></p>
<p>这回与我们的需求匹配上了。</p>
<p><b>5、测试</b></p>
<p>在内网的PC上建立一个简单的http server 程序：hello</p>
<p><font face="Courier New">//hello.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;net/http&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; http.HandleFunc(&quot;/&quot;, hello)<br />
	&nbsp;&nbsp;&nbsp; http.ListenAndServe(&quot;:80&quot;, nil)<br />
	}</font></p>
<p><font face="Courier New">func hello(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp; w.Write([]byte(&quot;hello!&quot;))<br />
	}</font></p>
<p><font face="Courier New">$ go build -o hello hello.go<br />
	$ sudo ./hello</font></p>
<p>通过公网浏览器访问一下&ldquo;<font face="Courier New">http://epower.tunnel.tonybai.com&rdquo;这个地址，如果你看到浏览器返回&quot;hello!&quot;字样，那么你的ngrokd服务就搭建成功了！</font></p>
<p><b>四、注意事项</b></p>
<p>客户端ngrok.cfg中server_addr后的值必须严格与-domain以及证书中的NGROK_BASE_DOMAIN相同，否则Server端就会出现如下错误日志：</p>
<p><font face="Courier New">[03/13/15 09:55:46] [INFO] [tun:15dd7522] New connection from 54.149.100.42:38252<br />
	[03/13/15 09:55:46] [DEBG] [tun:15dd7522] Waiting to read message<br />
	[03/13/15 09:55:46] [WARN] [tun:15dd7522] Failed to read message: remote error: <b>bad certificate</b><br />
	[03/13/15 09:55:46] [DEBG] [tun:15dd7522] Closing</font></p>
<p style='text-align:left'>&copy; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2015/03/14/selfhost-ngrok-service/feed/</wfw:commentRss>
		<slash:comments>43</slash:comments>
		</item>
		<item>
		<title>2014小结</title>
		<link>https://tonybai.com/2014/12/31/2014-summary/</link>
		<comments>https://tonybai.com/2014/12/31/2014-summary/#comments</comments>
		<pubDate>Wed, 31 Dec 2014 12:21:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[思考控]]></category>
		<category><![CDATA[2014]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[A股]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[QQ]]></category>
		<category><![CDATA[QZ8501]]></category>
		<category><![CDATA[RCS]]></category>
		<category><![CDATA[中国移动]]></category>
		<category><![CDATA[企业号]]></category>
		<category><![CDATA[创业]]></category>
		<category><![CDATA[周鸿祎]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[平台]]></category>
		<category><![CDATA[年终总结]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[招行]]></category>
		<category><![CDATA[服务号]]></category>
		<category><![CDATA[生态圈]]></category>
		<category><![CDATA[用友网络]]></category>
		<category><![CDATA[用友软件]]></category>
		<category><![CDATA[短信]]></category>
		<category><![CDATA[精益创业]]></category>
		<category><![CDATA[终端技术]]></category>
		<category><![CDATA[腾讯]]></category>
		<category><![CDATA[融合通信]]></category>
		<category><![CDATA[订阅号]]></category>
		<category><![CDATA[转型]]></category>
		<category><![CDATA[运营商]]></category>
		<category><![CDATA[麦肯锡报告]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1677</guid>
		<description><![CDATA[2014年的最后一个工作日，这里写下有关2014年的一份小结。 年终总结本无固定格式，但写了若干年后，便有了自己的格式。但今年不打算遵循这个格式了，跳出自己的舒适区，随意写写。 2014年12月底，随着亚航QZ8501航班的最后一掉，航空史上都为数不多的灾难年终于画上了句号，留给人们的是久久的惊恐不安，留给遇难者 家属们的是无法释怀的悲伤。2014年12月31日15点，随着A股上证指数最后一个交易日收涨68.86点，稳稳站上3200点，让广大股民们 看到了2015年牛市持续赚钱的希望。不知为何，这个世界几乎总是同时上演着冰与火两种剧本。 短信与微信(包括其他X信)的博弈亦是如此。 短信，这一红极一时的让移动运营商赚得盆满钵满的廉价沟通工具如今却早已成明日黄花。不妨打开手机，翻看一下你的手机通信录，短信列表中是不是除 了验证码（登录、支付业务），就是各种营销垃圾广告，或者是移动运营商自有的客服信息呢。我相信我的情况应该可以代表广大群众了。 随着微信今年推出&#8220;企业号&#8221;，微信几乎完成了对短信的业务合围： 点对点短信 vs. 联系人、朋友圈、群 SP短信&#160;&#160;&#160; vs. 订阅号、服务号 行业短信&#160; vs. 服务号、企业号 （营销、售后、内部OA、CRM等） 今年年初招商银行信用卡将300以内的消费提醒短信取消，改为微信提醒，其实就是一个看高微信，看空短信的行为。只是考虑到到达率(用户未开网络时)，没 有将大额消费全部转到微信上，而是短信和微信都做提醒。一旦无线网络接入、资费门槛下降、网络速度提升、终端实时在线不再是问题，达到率也将 不是问题时，微信会对短信发起最后的总攻。 这么对比其实也不公平，因为短信和微信本不是一个重量级的对手。从出生的那天起，微信就被赋予了崇高的使命，非短信可比。微信试图连接一切，做统 一入口，建立庞大生态圈；而短信仅仅是一个通道工具罢了。 面对移动短信市场的衰败，移动运营商也在挣扎，也在试图翻盘，或至少平起平坐，但就我了解到的移动运营商产品开发与运营的风格，想和互联网巨头T 掰手腕，下场必输无疑。中国移动年初也蛮拼的，喊出了&#34;RCS（融合通信）&#34;与微信抢手机社交入口，但这都到了2014最后一天了，RCS依旧不见踪 影。 短信免费或退出历史舞台就像周鸿祎在其书《周鸿祎自述：我的互联网方法论》中说的那样是&#8220;趋势&#8221;，不可违！ 而我们就是为中国移动短信业务提供服务端软件和方案的。短信若是没了(或变成鸡肋)，我们干啥？冰冷的现实摆在大家面前，领导跟我 们说：&#8220;转型&#8221;。 2014年，至少我们依旧在转型中。老板们把&#8220;转型&#8221;依旧约束在&#8220;移动运营商&#8221;这棵大树下面，这让我们转的不那么纯粹，有些拖泥带水，可持续盈利 的业务方向并不明显。从目前来看，今年收入依旧靠传统业务渠道获得。 虽说要&#8220;转型&#8221;，但领导今年给我的任务却是做好守门员，守住现有市场份额，保证产品线上无事。这并非如我所愿，在一个业务线耕耘多年，业务和技术 能力均到达了天花板，对我个人来说，这不是一个很好的发展规划。但考虑到下面的技术负责人、员工在技术和业务火候儿还欠缺那么一些，我答应了留 守，但会投入部分精力做个人技术转型储备。 业务的转型需要技术做支撑，局限于传统后台服务系统的我们需要张开怀抱，拥抱那些&#8220;流行&#8221;的新玩意儿。我首先试水！从2014年我的博客中你也许 可以看得出来我试水过的技术，我在尝试跳出自己的各种舒适区，向一些近两年兴起的、将来比较有前途的技术方向靠拢，学习移动互联网的思维和潮流。 上半年曾尝试过终端产品开发的技术，还为此购入若干数码装备，但试过后才发现这仍然不是我的主菜，就和10年前Windows GUI程序开发不是我的菜一样。但这个过程并非没有收获，未来任何业务不与终端开发打交道是不可能的，这个接触过程让我了解到了终端开发的重点和难点，于 是总结经验，整理教训。 正当准备调整方向、重新上路之际，家里出现重大变故，耗费了我整整1个多月的时间，一切几乎都停滞了，直到10月份我才渐渐重新进入状态。 在公司内部技术社区看到公司CTO的一篇文章，讲述移动互联网正在由消费者驱动向企业驱动转变（来自麦肯锡报告），结合微信推出企业号、用友软件的转型来 看(今天听说用友软件更名为&#8220;用友网络&#8221;了，决心向互联网转型)，这个趋势也是我比较认同的，这个方向以及相关技术也是我在正在涉猎以及即将涉猎的。不过 关于企业互联网服务以及平台，自己的相关业务经验、技术和积累还是甚少，征途必然坎坷，自己还需&#8220;拼&#8221;一下！关于微信这个平台，这个入口，它是腾讯未来战 略的核心，靠着腾讯这棵大树，至少未来几年发展应该还是不错的。 公司的大BOSS这两年一直提倡&#8220;创业者的精神&#34;，学会在逆境中成长，在困境中成功。但作为在短信这个行业内浸淫了十多年的部门，我们不免产生一些惰性， 更愿意躺在现有的温床上&#8220;享受生活&#8221;，立足于现有的平台做舒服的事情。经历过2014年的严峻形势，现在的我们应该清醒的认识到这样的舒服生活，温床和平 台都可能将远离我们。如果我们再不主动站起来，我们将再无力站起了。 2014年在个人发展方面做出了&#8220;妥协&#8221;，2015我打算轻装前行，这对我、对团队成员的成长都是有好处的。年底给领导发总结时，已经和领导书面提出退出 当前业务线的想法。虽然目前还没有收到回复，不过无论怎样，我都坚定了决心，自己作为这个产品线的负责人，已经起不到领路的作用了，是时候退出了。 2015，给自己的关键字是&#8220;创业&#8221;。《精益创业》一书中作者似乎有这样一句话：&#8220;你不一定非要在车库里折腾才算是创业&#8221;，在企业内部也可以&#8220;创业&#8221;，为创造某种新产品或新服务为目的而组建的一个团队或组织内的人都是&#8220;创业者&#8221;。 以往年份的小结，我总会总结一些数据，比如blog文章、读过多少本书等等。但今年这些数据就不统计了，自己对自己的考核指标&#34;KPI&#34;有所调整，以前哪些指标已经不算数了，列出也就无意义了。 2014这一年，LP给了我很大压力！我能理解，她期望我能取得更大的成功。这让我&#8220;亚历山大&#8221;啊，这回可是真的。 要说新年的愿望是什么？希望2015年年末时能为自己2015年的所作所为，所取得的进步和成果点个赞！ &#169; [...]]]></description>
			<content:encoded><![CDATA[<p>2014年的最后一个工作日，这里写下有关2014年的一份小结。</p>
<p>年终总结本无固定格式，但写了若干年后，便有了自己的格式。但今年不打算遵循这个格式了，跳出自己的舒适区，随意写写。</p>
<p><b>2014年</b>12月底，随着亚航QZ8501航班的最后一掉，航空史上都为数不多的灾难年终于画上了句号，留给人们的是久久的惊恐不安，留给遇难者 家属们的是无法释怀的悲伤。2014年12月31日15点，随着A股上证指数最后一个交易日收涨68.86点，稳稳站上3200点，让广大股民们 看到了2015年牛市持续赚钱的希望。不知为何，这个世界几乎总是同时上演着冰与火两种剧本。</p>
<p>短信与<a href="http://weixin.qq.com">微信</a>(包括其他X信)的博弈亦是如此。</p>
<p>短信，这一红极一时的让移动运营商赚得盆满钵满的廉价沟通工具如今却早已成明日黄花。不妨打开手机，翻看一下你的手机通信录，短信列表中是不是除 了验证码（登录、支付业务），就是各种营销垃圾广告，或者是移动运营商自有的客服信息呢。我相信我的情况应该可以代表广大群众了。</p>
<p>随着微信今年推出&ldquo;<a href="http://qy.weixin.qq.com">企业号</a>&rdquo;，微信几乎完成了对短信的业务合围：</p>
<p><font face="Courier New">点对点短信 vs. 联系人、朋友圈、群<br />
	SP短信&nbsp;&nbsp;&nbsp; vs. 订阅号、服务号<br />
	行业短信&nbsp; vs. 服务号、企业号 （营销、售后、内部OA、CRM等）</font></p>
<p>今年年初招商银行信用卡将300以内的消费提醒短信取消，改为微信提醒，其实就是一个看高微信，看空短信的行为。只是考虑到到达率(用户未开网络时)，没 有将大额消费全部转到微信上，而是短信和微信都做提醒。一旦无线网络接入、资费门槛下降、网络速度提升、终端实时在线不再是问题，达到率也将 不是问题时，微信会对短信发起最后的总攻。</p>
<p>这么对比其实也不公平，因为短信和微信本不是一个重量级的对手。从出生的那天起，微信就被赋予了崇高的使命，非短信可比。微信试图连接一切，做统 一入口，建立庞大生态圈；而短信仅仅是一个通道工具罢了。</p>
<p>面对移动短信市场的衰败，移动运营商也在挣扎，也在试图翻盘，或至少平起平坐，但就我了解到的移动运营商产品开发与运营的风格，想和互联网巨头T 掰手腕，下场必输无疑。中国移动年初也蛮拼的，喊出了&quot;<a href="http://zh.wikipedia.org/wiki/RCS">RCS</a>（融合通信）&quot;与微信抢手机社交入口，但这都到了2014最后一天了，RCS依旧不见踪 影。</p>
<p>短信免费或退出历史舞台就像周鸿祎在其书《<a href="http://book.douban.com/subject/25928983/">周鸿祎自述：我的互联网方法论</a>》中说的那样是&ldquo;趋势&rdquo;，不可违！</p>
<p><b>而</b>我们就是为中国移动短信业务提供服务端软件和方案的。短信若是没了(或变成鸡肋)，我们干啥？冰冷的现实摆在大家面前，领导跟我 们说：<b>&ldquo;转型</b>&rdquo;。</p>
<p><b>2014年</b>，至少我们依旧在转型中。老板们把&ldquo;转型&rdquo;依旧约束在&ldquo;移动运营商&rdquo;这棵大树下面，这让我们转的不那么纯粹，有些拖泥带水，可持续盈利 的业务方向并不明显。从目前来看，今年收入依旧靠传统业务渠道获得。</p>
<p>虽说要&ldquo;转型&rdquo;，但领导今年给我的任务却是做好守门员，守住现有市场份额，保证产品线上无事。这并非如我所愿，在一个业务线耕耘多年，业务和技术 能力均到达了天花板，对我个人来说，这不是一个很好的发展规划。但考虑到下面的技术负责人、员工在技术和业务火候儿还欠缺那么一些，我答应了留 守，但会投入部分精力做个人技术转型储备。</p>
<p>业务的转型需要技术做支撑，局限于传统后台服务系统的我们需要张开怀抱，拥抱那些&ldquo;流行&rdquo;的新玩意儿。我首先试水！从2014年我的博客中你也许 可以看得出来我试水过的技术，我在尝试跳出自己的各种舒适区，向一些近两年兴起的、将来比较有前途的技术方向靠拢，学习移动互联网的思维和潮流。</p>
<p>上半年曾尝试过终端产品开发的技术，还为此购入若干数码装备，但试过后才发现这仍然不是我的主菜，就和10年前Windows GUI程序开发不是我的菜一样。但这个过程并非没有收获，未来任何业务不与终端开发打交道是不可能的，这个接触过程让我了解到了终端开发的重点和难点，于 是总结经验，整理教训。</p>
<p>正当准备调整方向、重新上路之际，家里出现重大变故，耗费了我整整1个多月的时间，一切几乎都停滞了，直到10月份我才渐渐重新进入状态。</p>
<p>在公司内部技术社区看到公司CTO的一篇文章，讲述移动互联网正在由消费者驱动向企业驱动转变（来自<a href="http://www.aliresearch.com/?m-cms-q-view-id-76846.html">麦肯锡报告</a>），结合微信推出企业号、用友软件的转型来 看(今天听说用友软件更名为&ldquo;用友网络&rdquo;了，决心向互联网转型)，这个趋势也是我比较认同的，这个方向以及相关技术也是我在正在涉猎以及即将涉猎的。不过 关于企业互联网服务以及平台，自己的相关业务经验、技术和积累还是甚少，征途必然坎坷，自己还需&ldquo;拼&rdquo;一下！关于微信这个平台，这个入口，它是腾讯未来战 略的核心，靠着腾讯这棵大树，至少未来几年发展应该还是不错的。</p>
<p>公司的大BOSS这两年一直提倡&ldquo;创业者的精神&quot;，学会在逆境中成长，在困境中成功。但作为在短信这个行业内浸淫了十多年的部门，我们不免产生一些惰性， 更愿意躺在现有的温床上&ldquo;享受生活&rdquo;，立足于现有的平台做舒服的事情。经历过2014年的严峻形势，现在的我们应该清醒的认识到这样的舒服生活，温床和平 台都可能将远离我们。如果我们再不主动站起来，我们将再无力站起了。</p>
<p>2014年在个人发展方面做出了&ldquo;妥协&rdquo;，2015我打算轻装前行，这对我、对团队成员的成长都是有好处的。年底给领导发总结时，已经和领导书面提出退出 当前业务线的想法。虽然目前还没有收到回复，不过无论怎样，我都坚定了决心，自己作为这个产品线的负责人，已经起不到领路的作用了，是时候退出了。</p>
<p>2015，给自己的关键字是&ldquo;创业&rdquo;。《<a href="http://book.douban.com/subject/10945606/">精益创业</a>》一书中作者似乎有这样一句话：&ldquo;你不一定非要在车库里折腾才算是创业&rdquo;，在企业内部也可以&ldquo;创业&rdquo;，为创造某种新产品或新服务为目的而组建的一个团队或组织内的人都是&ldquo;创业者&rdquo;。</p>
<p>以往年份的小结，我总会总结一些数据，比如blog文章、读过多少本书等等。但今年这些数据就不统计了，自己对自己的考核指标&quot;KPI&quot;有所调整，以前哪些指标已经不算数了，列出也就无意义了。</p>
<p>2014这一年，LP给了我很大压力！我能理解，她期望我能取得更大的成功。这让我&ldquo;亚历山大&rdquo;啊，这回可是真的。</p>
<p>要说新年的愿望是什么？希望2015年年末时能为自己2015年的所作所为，所取得的进步和成果<b>点个赞</b>！</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/12/31/2014-summary/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>使用Golang开发微信公众平台-发送客服消息</title>
		<link>https://tonybai.com/2014/12/30/send-custom-service-text-msg-for-wechat-public-platform-dev-in-golang/</link>
		<comments>https://tonybai.com/2014/12/30/send-custom-service-text-msg-for-wechat-public-platform-dev-in-golang/#comments</comments>
		<pubDate>Tue, 30 Dec 2014 08:02:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[CDATA]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[marshal]]></category>
		<category><![CDATA[ngrok]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Tcpdump]]></category>
		<category><![CDATA[unmarshal]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[wireshark]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[公众平台]]></category>
		<category><![CDATA[开放平台]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[服务号]]></category>
		<category><![CDATA[编码]]></category>
		<category><![CDATA[解码]]></category>
		<category><![CDATA[订阅号]]></category>
		<category><![CDATA[调试]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1672</guid>
		<description><![CDATA[关注并使用过微信&#8220;飞常准&#8221;公众号的朋友们都有过如下体验：查询一个航班情况后，这个航班的checkin、登机、起降等信息都会在后续陆续异步发给你，这个服务就是通过微信公众平台的客服消息实现的。 微信公众平台开发文档中关于客服消息的解释如下：&#8220;当用户主动发消息给公众号的时候（包括发送信息、点击自定义菜单、订阅事件、扫描二维码事件、支付成功 事件、用户维权），微信将会把消息数据推送给开发者，开发者在一段时间内（目前修改为48小时）可以调用客服消息接口，通过POST一个JSON数据包来 发送消息给普通用户，在48小时内不限制发送次数。此接口主要用于客服等有人工消息处理环节的功能，方便开发者为用户提供更加优质的服务&#8221;。 这篇文章我们就来说说如何用golang实现发送文本客服消息。 一、获取access_token access_token是公众号的全局唯一票据，公众号调用微信平台各接口时都需使用access_token。我们要主动给微信平台发送客服消息，该access_token就是我们的凭证。在构造和下发客服消息前，我们需要获取这个access_token。 access_token的有效期为2小时（7200s），我们获取一次，两小时内均可使用。微信公众平台开发文档也给出了access_token获取、保存以及刷新的技术建议。但我们这里仅是Demo，无需考虑这么多。 通过https GET请求，我们可以得到属于我们的access_token，请求line为： https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&#38;appid=APPID&#38;secret=APPSECRET golang提供了默认的http client实现，通过默认的client实现我们可以很容器的获取access_token。 const ( &#160;&#160;&#160;&#160;&#160;&#160;&#160; token&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = &#34;wechat4go&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; appID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = &#34;wx8e0fb2659c2eexxx&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; appSecret&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = &#34;22746009b0162fe50cb915851c53fyyy&#34; &#160;&#160;&#160;&#160;&#160;&#160;&#160; accessTokenFetchUrl = &#34;https://api.weixin.qq.com/cgi-bin/token&#34; ) func fetchAccessToken() (string, float64, error) { &#160;&#160;&#160;&#160;&#160;&#160;&#160; requestLine := strings.Join([]string{accessTokenFetchUrl, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;?grant_type=client_credential&#38;appid=&#34;, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; appID, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#34;&#38;secret=&#34;, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; appSecret}, &#34;&#34;) &#160;&#160;&#160;&#160;&#160;&#160;&#160; resp, err := [...]]]></description>
			<content:encoded><![CDATA[<p>关注并使用过微信&ldquo;飞常准&rdquo;公众号的朋友们都有过如下体验：查询一个航班情况后，这个航班的checkin、登机、起降等信息都会在后续陆续异步发给你，这个服务就是通过微信公众平台的客服消息实现的。</p>
<p><a href="http://mp.weixin.qq.com/wiki">微信公众平台开发文档</a>中关于客服消息的解释如下：&ldquo;当用户主动发消息给公众号的时候（包括发送信息、点击自定义菜单、订阅事件、扫描二维码事件、支付成功 事件、用户维权），微信将会把消息数据推送给开发者，开发者在一段时间内（目前修改为48小时）可以调用客服消息接口，通过POST一个JSON数据包来 发送消息给普通用户，在48小时内不限制发送次数。此接口主要用于客服等有人工消息处理环节的功能，方便开发者为用户提供更加优质的服务&rdquo;。</p>
<p>这篇文章我们就来说说如何用<a href="http://tonybai.com/tag/golang">golang</a>实现发送文本客服消息。</p>
<p><b>一、获取access_token</b></p>
<p>access_token是公众号的全局唯一票据，公众号调用微信平台各接口时都需使用access_token。我们要主动给微信平台发送客服消息，该access_token就是我们的凭证。在构造和下发客服消息前，我们需要获取这个access_token。</p>
<p>access_token的有效期为2小时（7200s），我们获取一次，两小时内均可使用。微信公众平台开发文档也给出了access_token获取、保存以及刷新的技术建议。但我们这里仅是Demo，无需考虑这么多。</p>
<p>通过https GET请求，我们可以得到属于我们的access_token，请求line为：</p>
<p><font face="Courier New">https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&amp;appid=APPID&amp;secret=APPSECRET</font></p>
<p>golang提供了默认的http client实现，通过默认的client实现我们可以很容器的获取access_token。</p>
<p><font face="Courier New">const (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; token&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = &quot;wechat4go&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; appID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = &quot;wx8e0fb2659c2eexxx&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; appSecret&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = &quot;22746009b0162fe50cb915851c53fyyy&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; accessTokenFetchUrl = &quot;https://api.weixin.qq.com/cgi-bin/token&quot;<br />
	)</font></p>
<p><font face="Courier New">func fetchAccessToken() (string, float64, error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; requestLine := strings.Join([]string{accessTokenFetchUrl,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;?grant_type=client_credential&amp;appid=&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; appID,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;&amp;secret=&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; appSecret}, &quot;&quot;)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; resp, err := http.Get(requestLine)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil || resp.StatusCode != http.StatusOK {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return &quot;&quot;, 0.0, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; defer resp.Body.Close()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(resp.Body)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return &quot;&quot;, 0.0, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p>无论成功与否，微信平台都会返回一个包含json数据的应答：</p>
<p>如果获取正确，那么应答里的Json数据为：</p>
<p><font face="Courier New">{&quot;access_token&quot;:&quot;0QCeHwiRtPRUCiM5MM0cSPYIP5QOUNYdb8usRSgVZcsFuVF6mu3vQq41OIifJdrtJPGn7b1x90HdvUanpb7eZHxg40B6bU_Sgszh2byyF40&quot;,&quot;expires_in&quot;:7200}</font></p>
<p>如果获取错误，那么应答里的Json数据为：</p>
<p><font face="Courier New">{&quot;errcode&quot;:40001,&quot;errmsg&quot;:&quot;invalid credential&quot;}</font></p>
<p>和xml数据包一样，golang也提供了json格式数据包的Marshal和Unmarshal方法，且使用方式相同，也是将一个json数据包与一 个struct对应起来。从上面来看，通过http response，我们无法区分出是否成功获取了token，因此我们需要首先判断试下body中是否包含某些特征字符串，比 如&quot;access_token&quot;：</p>
<p><font face="Courier New">if bytes.Contains(body, []byte(&quot;access_token&quot;)) {<br />
	&nbsp;&nbsp;&nbsp; //unmarshal to AccessTokenResponse struct<br />
	} else {<br />
	&nbsp;&nbsp;&nbsp; //unmarshal to AccessTokenErrorResponse struct<br />
	}</font></p>
<p>针对获取成功以及失败的两种Json数据，我们定义了两个结构体：</p>
<p><font face="Courier New">type AccessTokenResponse struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AccessToken string&nbsp; `json:&quot;access_token&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ExpiresIn&nbsp;&nbsp; float64 `json:&quot;expires_in&quot;`<br />
	}</font></p>
<p><font face="Courier New">type AccessTokenErrorResponse struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Errcode float64<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Errmsg&nbsp; string<br />
	}</font></p>
<p>Json unmarshal的代码片段如下：</p>
<p><font face="Courier New">//Json Decoding<br />
	if bytes.Contains(body, []byte(&quot;access_token&quot;)) {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; atr := AccessTokenResponse{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err = json.Unmarshal(body, &amp;atr)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; return &quot;&quot;, 0.0, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return atr.AccessToken, atr.ExpiresIn, nil<br />
	} else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;return err&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ater := AccessTokenErrorResponse{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err = json.Unmarshal(body, &amp;ater)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp; return &quot;&quot;, 0.0, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return &quot;&quot;, 0.0, fmt.Errorf(&quot;%s&quot;, ater.Errmsg)<br />
	}</font></p>
<p><font face="Courier New">我们的main函数如下：<br />
	func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; accessToken, expiresIn, err := fetchAccessToken()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Get access_token error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(accessToken, expiresIn)<br />
	}</font></p>
<p>编译执行，成功获取access_token的输出如下：</p>
<p><font face="Courier New">0QCeHwiRtPRUCiM5MM0cSPYIP5QOUNYdb8usRSgVZcsFuVF6mu3vQq41OIifJdrtJPGn7b1x90HdvUanpb7eZHxg40B6bU_Sgszh2byyF40 7200</font></p>
<p>失败时，输出如下：</p>
<p><font face="Courier New">2014/12/30 12:39:56 Get access_token error: invalid credential</font></p>
<p><b>二、发送客服消息</b></p>
<p>平台开发文档中定义了文本客服消息的body格式，一个json数据：</p>
<p><font face="Courier New">{<br />
	&nbsp;&nbsp;&nbsp; &quot;touser&quot;:&quot;OPENID&quot;,<br />
	&nbsp;&nbsp;&nbsp; &quot;msgtype&quot;:&quot;text&quot;,<br />
	&nbsp;&nbsp;&nbsp; &quot;text&quot;:<br />
	&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;content&quot;:&quot;Hello World&quot;<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>其中的touser填写的是openid。之前的文章中提到过，每个微信用户针对某一个订阅号/服务号都有唯一的OpenID，这个ID可以在微信订阅号 /服务号管理页面中看到，也可以在收到的微信平台转发的消息中看到(FromUserName)。比如我个人订阅的我的测试体验号后得到的OpenID 为：</p>
<p><font face="Courier New">BQcwuAbKpiSAbbvd_DEZg7q27QI</font></p>
<p>我们要做的就是构造这样一个json数据，并放入HTTP Post包中，发到：</p>
<p><font face="Courier New">https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN</font></p>
<p>从平台开发文档给出的json数据包样例来看，这是个嵌套json数据包，我们通过下面方法marshall：</p>
<p><font face="Courier New">type CustomServiceMsg struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUser&nbsp; string&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `json:&quot;touser&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgType string&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `json:&quot;msgtype&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Text&nbsp;&nbsp;&nbsp; TextMsgContent `json:&quot;text&quot;`<br />
	}</font></p>
<p><font face="Courier New">type TextMsgContent struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Content string `json:&quot;content&quot;`<br />
	}</font></p>
<p><font face="Courier New">func pushCustomMsg(accessToken, toUser, msg string) error {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; csMsg := &amp;CustomServiceMsg{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUser:&nbsp; toUser,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgType: &quot;text&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Text:&nbsp;&nbsp;&nbsp; TextMsgContent{Content: msg},<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; body, err := json.MarshalIndent(csMsg, &quot; &quot;, &quot;&nbsp; &quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p>如果单纯输出上面marshal的结果，可以看到：</p>
<p><font face="Courier New">{<br />
	&nbsp;&nbsp; &quot;touser&quot;: &quot;oBQcwuAbKpiSAbbvd_DEZg7q27QI&quot;,<br />
	&nbsp;&nbsp; &quot;msgtype&quot;: &quot;text&quot;,<br />
	&nbsp;&nbsp; &quot;text&quot;: {<br />
	&nbsp;&nbsp;&nbsp;&nbsp; &quot;content&quot;: &quot;你好&quot;<br />
	&nbsp;&nbsp; }<br />
	&nbsp;}</font></p>
<p>接下来将marshal后的[]byte放入一个http post的body中，发送到指定url中：</p>
<p><font face="Courier New">var openID = &quot;oBQcwuAbKpiSAbbvd_DEZg7q27QI&quot;</font></p>
<p><font face="Courier New">func pushCustomMsg(accessToken, toUser, msg string) error {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; postReq, err := http.NewRequest(&quot;POST&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strings.Join([]string{customServicePostUrl, &quot;?access_token=&quot;, accessToken}, &quot;&quot;),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bytes.NewReader(body))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; postReq.Header.Set(&quot;Content-Type&quot;, &quot;application/json; encoding=utf-8&quot;)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; client := &amp;http.Client{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; resp, err := client.Do(postReq)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; resp.Body.Close()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	}</font></p>
<p>我们在main函数中加上客服消息的发送环节：</p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Fetch access_token<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; accessToken, expiresIn, err := fetchAccessToken()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Get access_token error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(accessToken, expiresIn)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Post custom service message<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; msg := &quot;你好&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err = pushCustomMsg(accessToken, openID, msg)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Push custom service message err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>编译执行，手机响起提示音，打开观看，微信公众平台测试号发来消息：&ldquo;你好&rdquo;。</p>
<p>上述Demo完整代码在<a href="https://github.com/bigwhite/experiments/tree/master/wechat_examples/public/4-customservicetextmsg">这里</a>可以看到，别忘了appID，appSecret改成你自己的值。</p>
<p><b>目前客服接口仅提供给认证后的订阅号以及服务号，对于未认证的订阅号，无法发送客服消息。</b></p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/12/30/send-custom-service-text-msg-for-wechat-public-platform-dev-in-golang/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>使用Golang开发微信公众平台-接收加密消息</title>
		<link>https://tonybai.com/2014/12/24/recv-encrypted-text-msg-for-wechat-public-platform-dev-in-golang/</link>
		<comments>https://tonybai.com/2014/12/24/recv-encrypted-text-msg-for-wechat-public-platform-dev-in-golang/#comments</comments>
		<pubDate>Wed, 24 Dec 2014 05:21:59 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[base64]]></category>
		<category><![CDATA[CDATA]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[ngrok]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Tcpdump]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[wireshark]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[公众平台]]></category>
		<category><![CDATA[加密]]></category>
		<category><![CDATA[开放平台]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[服务号]]></category>
		<category><![CDATA[解密]]></category>
		<category><![CDATA[订阅号]]></category>
		<category><![CDATA[调试]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1669</guid>
		<description><![CDATA[在上一篇&#8220;接收文本消息&#8221;一文中，我们了解到：公众服务与微信服务器间的消息是&#8220;裸奔&#8221;的（即明文传输，通过抓包可以看到）。显然这对于一些对安 全性要求较高的大企业服务号来说，比如银行、证券、电信运营商或航空客服等是不能完全满足要求的。于是乎就有了微信服务器与公众服务间的数据加密 通信流程。 公众号管理员可以在公众号&#8220;开发者中心&#8221;选择是否采用&#34;安全模式&#34;(区别于明文模式)： 一旦选择了&#8220;安全模式&#8221;，微信服务器在向公众号服务转发消息时会对XML数据包部分内容进行加密处理。这类加密后的请求Body中的XML数据变 成了下面这样： xml数据基本结构变成了: &#60;xml&#62; &#160;&#160;&#160; &#60;ToUserName&#62;xx&#60;/ToUserName&#62; &#160;&#160;&#160; &#60;Encrypt&#62;xx&#60;/Encrypt&#62; &#60;/xml&#62; 另外在&#8220;安全模式&#8221;下，Http Post Request line中也增加了两个字段：encrypt_type和msg_signuature，用于消息类型判断以及加密消息内容有效性校验： POST /?signature=891789ec400309a6be74ac278030e472f90782a5&#38;timestamp=1419214101&#38;nonce=788148964&#38;encrypt_type=aes&#38;msg_signature=87d7b127fab3771b452bc6a592f530cd8edba950 HTTP/1.1\r\n 其中： encrypt_type = &#34;aes&#34;，说明是加密消息，否则为&#34;raw&#8221;，即未加密消息。 msg_signature=sha1(sort(Token, timestamp, nonce, msg_encrypt)) 对于测试号，测试号配置页面没有加密相关配置，因此只能通过&#8220;微信公众平台接口调试工具&#8221;来进行相关加密接口调试。 一、消息签名验证 对于&#8220;安全模式&#8221;下的消息交互，首先要做的就是消息签名验证，只有通过验证的消息才会进行下一步解密、解析和处理。 消息签名验证的原理是比较微信平台HTTP Post Line中携带的msg_signature与通过Token、timestamp、nonce和msg_encrypt等四个字段值计算出的 msg_signture是否一致，一致则通过消息签名验证。 我们依旧在procRequest中完成对&#8220;安全模式&#8221;下消息的签名验证。 //recvencryptedtextmsg.go type EncryptRequestBody struct { &#160;&#160;&#160;&#160;&#160;&#160;&#160; XMLName&#160;&#160;&#160; xml.Name `xml:&#34;xml&#34;` &#160;&#160;&#160;&#160;&#160;&#160;&#160; ToUserName string &#160;&#160;&#160;&#160;&#160;&#160;&#160; Encrypt&#160;&#160;&#160; string } func makeMsgSignature(timestamp, nonce, [...]]]></description>
			<content:encoded><![CDATA[<p>在上一篇&ldquo;<a href="http://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/">接收文本消息</a>&rdquo;一文中，我们了解到：公众服务与微信服务器间的消息是&ldquo;裸奔&rdquo;的（即明文传输，通过抓包可以看到）。显然这对于一些对安 全性要求较高的大企业服务号来说，比如银行、证券、电信运营商或航空客服等是不能完全满足要求的。于是乎就有了微信服务器与公众服务间的数据加密 通信流程。</p>
<p>公众号管理员可以在公众号&ldquo;开发者中心&rdquo;选择是否采用&quot;安全模式&quot;(区别于明文模式)：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-wiki/wechat-wiki-public-encypted-settings.png" style="width: 400px; height: 185px;" /></p>
<p>一旦选择了&ldquo;安全模式&rdquo;，微信服务器在向公众号服务转发消息时会对XML数据包部分内容进行加密处理。这类加密后的请求Body中的XML数据变 成了下面这样：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-wiki/wechat-wiki-public-encrypted-msg.png" style="width: 400px; height: 147px;" /></p>
<p>xml数据基本结构变成了:</p>
<p><font face="Courier New">&lt;xml&gt;<br />
	&nbsp;&nbsp;&nbsp; &lt;ToUserName&gt;xx&lt;/ToUserName&gt;<br />
	&nbsp;&nbsp;&nbsp; &lt;Encrypt&gt;xx&lt;/Encrypt&gt;<br />
	&lt;/xml&gt;</font></p>
<p>另外在&ldquo;安全模式&rdquo;下，Http Post Request line中也增加了两个字段：<font face="Courier New">encrypt_type</font>和<font face="Courier New">msg_signuature</font>，用于消息类型判断以及加密消息内容有效性校验：</p>
<p><font face="Courier New">POST /?signature=891789ec400309a6be74ac278030e472f90782a5&amp;timestamp=1419214101&amp;nonce=788148964&amp;encrypt_type=aes&amp;msg_signature=87d7b127fab3771b452bc6a592f530cd8edba950 HTTP/1.1\r\n</font></p>
<p>其中：</p>
<p><font face="Courier New">encrypt_type = &quot;aes&quot;，说明是加密消息，否则为&quot;raw&rdquo;，即未加密消息。<br />
	msg_signature=sha1(sort(Token, timestamp, nonce, msg_encrypt))</font></p>
<p>对于测试号，测试号配置页面没有加密相关配置，因此只能通过&ldquo;<a href="https://mp.weixin.qq.com/debug">微信公众平台接口调试工具</a>&rdquo;来进行相关加密接口调试。</p>
<p><b>一、消息签名验证</b></p>
<p>对于&ldquo;安全模式&rdquo;下的消息交互，首先要做的就是消息签名验证，只有通过验证的消息才会进行下一步解密、解析和处理。</p>
<p>消息签名验证的原理是比较微信平台HTTP Post Line中携带的msg_signature与通过Token、timestamp、nonce和msg_encrypt等四个字段值计算出的 msg_signture是否一致，一致则通过消息签名验证。</p>
<p>我们依旧在procRequest中完成对&ldquo;安全模式&rdquo;下消息的签名验证。</p>
<p><font face="Courier New">//recvencryptedtextmsg.go<br />
	type EncryptRequestBody struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; XMLName&nbsp;&nbsp;&nbsp; xml.Name `xml:&quot;xml&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Encrypt&nbsp;&nbsp;&nbsp; string<br />
	}</font></p>
<p><font face="Courier New">func makeMsgSignature(timestamp, nonce, msg_encrypt string) string {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sl := []string{token, timestamp, nonce, msg_encrypt}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sort.Strings(sl)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s := sha1.New()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; io.WriteString(s, strings.Join(sl, &quot;&quot;))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fmt.Sprintf(&quot;%x&quot;, s.Sum(nil))<br />
	}</font></p>
<p><font face="Courier New">func validateMsg(timestamp, nonce, msgEncrypt, msgSignatureIn string) bool {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; msgSignatureGen := makeMsgSignature(timestamp, nonce, msgEncrypt)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if msgSignatureGen != msgSignatureIn {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return true<br />
	}</font></p>
<p><font face="Courier New">func parseEncryptRequestBody(r *http.Request) *EncryptRequestBody {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(r.Body)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatal(err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; requestBody := &amp;EncryptRequestBody{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Unmarshal(body, requestBody)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return requestBody<br />
	}</font></p>
<p><font face="Courier New">func procRequest(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r.ParseForm()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; timestamp := strings.Join(r.Form["timestamp"], &quot;&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nonce := strings.Join(r.Form["nonce"], &quot;&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signature := strings.Join(r.Form["signature"], &quot;&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptType := strings.Join(r.Form["encrypt_type"], &quot;&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; msgSignature := strings.Join(r.Form["msg_signature"], &quot;&quot;)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; f r.Method == &quot;POST&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if encryptType == &quot;aes&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: in safe mode&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptRequestBody := parseEncryptRequestBody(r)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //Validate msg signature<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !validateMsg(timestamp, nonce, encryptRequestBody.Encrypt, msgSignature) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: msg_signature is invalid&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: msg_signature validation is ok!&quot;)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}</font></p>
<p>程序编译执行结果如下：<br />
	<font face="Courier New">$sudo ./recvencryptedtextmsg<br />
	2014/12/22 13:15:56 Wechat Service: Start!</font></p>
<p><font face="Courier New">用手机微信发送一条消息给公众号，程序输出如下结果：</font></p>
<p><font face="Courier New">2014/12/22 13:17:35 Wechat Service: in safe mode<br />
	2014/12/22 13:17:35 Wechat Service: msg_signature validation is ok!</font></p>
<p><b>二、数据包解密</b></p>
<p>到目前为止，我们已经得到了经过消息验证ok的加密数据包<font face="Courier New">EncryptRequestBody 的Encrypt。要想得到真正的消息内容，我们需要对Encrypt字段的值进行解密处理。</font>微信采用的是<a href="http://zh.wikipedia.org/wiki/AES">AES</a>加解密方案， 下面我们就来看看如何做AES解密。</p>
<p>在开发者中心选择转换为&ldquo;安全模式&rdquo;时，有一个字段EncodingAESKey需要填写，这个字段固定为43个字符，它就是我们在运用AES算 法时需要的那个Key。不过这个EncodingAESKey是被编了码的，真正用来加解密的AESKey需要我们自己通过解码得到。解码方法 为：</p>
<p><font face="Courier New">AESKey=Base64_Decode(EncodingAESKey + &ldquo;=&rdquo;)</font></p>
<p>Base64 decode后，我们就得到了一个32个字节的AESKey，可以看出微信加密解密用的是AES-256算法(256=32x8bit)。</p>
<p>在Golang中，我们可以通过下面代码得到真正的AESKey：</p>
<p><font face="Courier New">const (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; token = &quot;wechat4go&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; appID = &quot;wx5b5c2614d269ddb2&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encodingAESKey = &quot;kZvGYbDKbtPbhv4LBWOcdsp5VktA3xe9epVhINevtGg&quot;<br />
	)</font></p>
<p><font face="Courier New">var aesKey []byte</font></p>
<p><font face="Courier New">func encodingAESKey2AESKey(encodingKey string) []byte {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; data, _ := base64.StdEncoding.DecodeString(encodingKey + &quot;=&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return data<br />
	}</font></p>
<p><font face="Courier New">func init() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; aesKey = encodingAESKey2AESKey(encodingAESKey)<br />
	}</font></p>
<p>有了AESKey，我们再来解密数据包。微信公众平台开发文档给出了加密数据包的解析步骤：</p>
<p><font face="Courier New">1. aes_msg=Base64_Decode(msg_encrypt)<br />
	2. rand_msg=AES_Decrypt(aes_msg)<br />
	3. 验证尾部$AppId是否是自己的AppId，相同则表示消息没有被篡改，这里进一步加强了消息签名验证<br />
	4. 去掉rand_msg头部的16个随机字节，4个字节的msg_len和尾部的$AppId即为最终的xml消息体</font></p>
<p>微信Wiki中如果能用一个简单的图来说明Base64_Decode后的数据格式就更好了。这里进一步说明一下，解密后的数据，我们称之 plainData，它由四部分组成，按先后顺序排列分别是：</p>
<p><font face="Courier New">1、随机值&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 16字节<br />
	2、xml包长度&nbsp;&nbsp;&nbsp; 4字节 （注意以BIG_ENDIAN方式读取）<br />
	3、xml包&nbsp; （*这部分数据的长度由上一个字段标识，这个包等价于一个完整的文本接收消息体数据，从ToUsername到MsgID都 有）<br />
	4、appID</font></p>
<p>其中第三段xml包是一个完整的接收文本数据包，与&ldquo;接收消息&rdquo;一文中的标准文本数据包格式一致，这就方便我们解析了。好了，下面用代码阐述解 密、解析过程以及appid验证：</p>
<p>在<font face="Courier New">procRequest</font>中，增加如下代码：</p>
<p><font face="Courier New"><b>// Decode base64</b><br />
	cipherData, err := base64.StdEncoding.DecodeString(encryptRequestBody.Encrypt)<br />
	if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: Decode base64 error:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	}</font></p>
<p><font face="Courier New"><b>// AES Decrypt</b><br />
	plainData, err := aesDecrypt(cipherData, aesKey)<br />
	if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	}</font></p>
<p><font face="Courier New"><b>//Xml decod</b><b>ing</b><br />
	textRequestBody, _ := parseEncryptTextRequestBody(plainData)<br />
	fmt.Printf(&quot;Wechat Service: Recv text msg [%s] from user [%s]!&quot;,<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; textRequestBody.Content,<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; textRequestBody.FromUserName)</font></p>
<p>根据解密方法，我们先对<font face="Courier New">encryptRequestBody.Encrypt进行base64 decode操作得到cipherData，再用aesDecrypt对cipherData进行解密得到上面提到的由四部分组成的plainData。plainData经过xml decoding后就得到我们的TextRequestBody struct。</font></p>
<p><font face="Courier New">这里难点显然在aesDecrypt的实现上了。微信的加密包采用aes-256算法，秘钥长度32B，采用PKCS#7 Padding方式。<a href="http://tonybai.com/tag/golang">Golang</a>提供了强大的AES加密解密方法，我们利用这些方法实现微信包的解密：</font></p>
<p><font face="Courier New">func aesDecrypt(cipherData []byte, aesKey []byte) ([]byte, error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; k := len(aesKey) //<b>PKCS#7</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if len(cipherData)%k != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil, errors.New(&quot;crypto/cipher: ciphertext size is not multiple of aes key length&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; block, err := aes.NewCipher(aesKey)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iv := make([]byte, aes.BlockSize)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, err := io.ReadFull(rand.Reader, iv); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; blockMode := cipher.NewCBCDecrypter(block, iv)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; plainData := make([]byte, len(cipherData))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; blockMode.CryptBlocks(plainData, cipherData)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return plainData, nil<br />
	}</font></p>
<p><font face="Courier New">对于解密后的plainData做appID校验以及xml Decoding处理如下：</font></p>
<p><font face="Courier New">func parseEncryptTextRequestBody(plainText []byte) (*TextRequestBody, error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(string(plainText))</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>// Read length</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; buf := bytes.NewBuffer(plainText[16:20])<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var length int32<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; binary.Read(buf, binary.BigEndian, &amp;length)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(string(plainText[20 : 20+length]))</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b> // appID validation</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; appIDstart := 20 + length<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id := plainText[appIDstart : int(appIDstart)+len(appID)]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !validateAppId(id) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: appid is invalid!&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil, errors.New(&quot;Appid is invalid&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: appid validation is ok!&quot;)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; <b>// xml Decoding</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody := &amp;TextRequestBody{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Unmarshal(plainText[20:20+length], textRequestBody)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return textRequestBody, nil<br />
	}</font></p>
<p>编译执行输出textRequestBody：</p>
<p><font face="Courier New">&amp;{{ xml} gh_6ebaca4bb551 on95ht9uPITsmZmq_mvuz4h6f6CI 1.419239875s text <b>Hello, Wechat</b> 6095588848508047134}</font></p>
<p><b>三、响应消息的数据包加密</b></p>
<p>微信公众平台开发文档要求：公众账号对密文消息的回复也要求加密。</p>
<p>对比一下普通的响应消息格式和加密后的响应消息格式：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-wiki/wechat-wiki-public-reply-text-msg.png" style="width: 400px; height: 236px;" /></p>
<p>加密后：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-wiki/wechat-wiki-public-encypted-reply-text-msg.png" style="width: 400px; height: 98px;" /></p>
<p>我们定义一个结构体映射响应消息数据包：</p>
<p><font face="Courier New">type EncryptResponseBody struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; XMLName&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Name `xml:&quot;xml&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Encrypt&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CDATAText<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgSignature CDATAText<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TimeStamp&nbsp;&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Nonce&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CDATAText<br />
	}</font></p>
<p><font face="Courier New">type CDATAText struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Text string `xml:&quot;,innerxml&quot;`<br />
	}</font></p>
<p>我们要做的就是给EncryptResponseBody的实例逐一赋值，然后通过xml.MarshalIndent转成xml数据流即可，各字 段值生成规则如下：</p>
<p><font face="Courier New">Encrypt = Base64_Encode(AES_Encrypt [random(16B)+ msg_len(4B) + msg + $AppId])<br />
	MsgSignature=sha1(sort(Token, timestamp, nonce, msg_encrypt))<br />
	TimeStamp = 用请求中的值或新生成<br />
	Nonce = 用请求中的值或新生成</font></p>
<p>微信公众接口的加密复杂度要比解密高一些，关键问题在于加密结果的判定和加密逻辑的调试，AES加密出的结果每次都不同，我们要么通过微信平台真实操作验证，要么通过微信提供的在线调试工具验证加密是否正确。这里强烈建议使用在线调试工具(测试号只能选择这一种)。</p>
<p>在线调试工具的配置参考如下，ToUserName和FromUserName建议填写真实的（通过解密Post包打印输出得到）：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-debug-settings.png" style="width: 400px; height: 471px;" /></p>
<p>如果在线调试工具收到你的应答，并解密成功，会给出如下反馈：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-debug-encrypt-text-result.png" style="width: 400px;" /></p>
<p>在procRequest中，我们在接收解析完Http Request后，通过下面几行代码构造一个加密的Response返回给微信平台或调试工具：</p>
<p><font face="Courier New">responseEncryptTextBody, _ := <b>makeEncryptResponseBody</b>(textRequestBody.ToUserName,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody.FromUserName,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Hello, &quot;+textRequestBody.FromUserName,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nonce,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; timestamp)<br />
	w.Header().Set(&quot;Content-Type&quot;, &quot;text/xml&quot;)<br />
	fmt.Fprintf(w, string(responseEncryptTextBody))</font></p>
<p><font face="Courier New">func makeEncryptResponseBody(fromUserName, toUserName, content, nonce, timestamp string) ([]byte, error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptBody := &amp;EncryptResponseBody{}</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptXmlData, _ := <b>makeEncryptXmlData</b>(fromUserName, toUserName, timestamp, content)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptBody.Encrypt = value2CDATA(encryptXmlData)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptBody.MsgSignature = value2CDATA(makeMsgSignature(timestamp, nonce, encryptXmlData))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptBody.TimeStamp = timestamp<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; encryptBody.Nonce = value2CDATA(nonce)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return xml.MarshalIndent(encryptBody, &quot; &quot;, &quot;&nbsp; &quot;)<br />
	}</font></p>
<p><font face="Courier New">应答Xml包中只有Encrypt字段是加密的，该字段的生成方式如下：</font></p>
<p><font face="Courier New">func makeEncryptXmlData(fromUserName, toUserName, timestamp, content string) (string, error) {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; // <b>Encrypt part3: Xml Encoding</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody := &amp;TextResponseBody{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.FromUserName = value2CDATA(fromUserName)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.ToUserName = value2CDATA(toUserName)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.MsgType = value2CDATA(&quot;text&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.Content = value2CDATA(content)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.CreateTime = timestamp<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; body, err := xml.MarshalIndent(textResponseBody, &quot; &quot;, &quot;&nbsp; &quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return &quot;&quot;, errors.New(&quot;xml marshal error&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; // <b>Encrypt part2: Length bytes</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; buf := new(bytes.Buffer)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err = binary.Write(buf, binary.BigEndian, int32(len(body)))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Binary write err:&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bodyLength := buf.Bytes()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; // <b>Encr</b><b>ypt part1: Random bytes</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; randomBytes := []byte(&quot;abcdefghijklmnop&quot;)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; <b>// Encrypt Part, with part4 </b><b>- appID</b><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; plainData := bytes.Join([][]byte{randomBytes, bodyLength, body, []byte(appID)}, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cipherData, err := <b>aesEncrypt</b>(plainData, aesKey)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return &quot;&quot;, errors.New(&quot;aesEncrypt error&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return base64.StdEncoding.EncodeToString(cipherData), nil<br />
	}</font></p>
<p><font face="Courier New">func aesEncrypt(plainData []byte, aesKey []byte) ([]byte, error) {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; k := len(aesKey)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if len(plainData)%k != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; plainData = PKCS7Pad(plainData, k)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; block, err := aes.NewCipher(aesKey)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iv := make([]byte, aes.BlockSize)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, err := io.ReadFull(rand.Reader, iv); err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil, err<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cipherData := make([]byte, len(plainData))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; blockMode := cipher.NewCBCEncrypter(block, iv)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; blockMode.CryptBlocks(cipherData, plainData)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return cipherData, nil<br />
	}</font></p>
<p>根据官方文档： 微信所用的AES采用的时CBC模式，秘钥长度为32个字节（aesKey），数据采用PKCS#7填充；PKCS#7：K为秘钥字节数（采用32），buf为待加密的内容，N为其字节数。<strong>Buf需要被填充为K的整数倍</strong>。因此我们pad要加密的数据时，<b>务必pad为k(=32)的整数倍</b>，而不是aes.BlockSize(=16)的整数倍。</p>
<p>采用安全模式后的公众号消息交互性能似乎下降了，发送<font face="Courier New">&quot;hello, wechat&quot;</font>给公众号后好长时间才收到响应。</p>
<p>微信公众号接收加密消息的代码在<a href="https://github.com/bigwhite/experiments/tree/master/wechat_examples/public/3-encryptedtextmsg">这里</a>可以下载。这些代码只是演示代码，结构上绝不算优化，大家可以将这些代码封装成通用的接口为后续微信公众平台接口开发奠定基础。</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/12/24/recv-encrypted-text-msg-for-wechat-public-platform-dev-in-golang/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Golang开发微信公众平台-接收文本消息</title>
		<link>https://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/</link>
		<comments>https://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/#comments</comments>
		<pubDate>Sat, 20 Dec 2014 09:18:36 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[CDATA]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[ngrok]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Tcpdump]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[wireshark]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[公众平台]]></category>
		<category><![CDATA[开放平台]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[微信]]></category>
		<category><![CDATA[服务号]]></category>
		<category><![CDATA[订阅号]]></category>
		<category><![CDATA[调试]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1650</guid>
		<description><![CDATA[一旦接入验证成功，成为正式开发者，你可能会迫不及待地想通过手机微信发送一条&#34;Hello, Wechat&#8221;到你的公众号服务器。不过上一篇的那个程序还无法处理手机提交的文本消息，本篇将介绍如何用Golang编写公众号程序来接收手机端发送的 文本消息以及回复响应消息。 根据微信公众平台开发文档中描述：&#8220;当普通微信用户向公众账号发消息时，微信服务器将POST消息的XML数据包到开发者填写的URL上&#8221;。我们 用一个示意图展示一下这个消息流程： 微信服务器通过一个HTTP Post请求将终端用户发送的消息转发给公众号服务器，消息内容被包装在HTTP Post Request的Body中。数据包以XML格式存储，文本类消息XML格式样例如下（引自微信公众平台开发文档）： 数据包中各个字段的含义都显而易见，我们重点关注的时Content这个字段填写的内容，也就是终端用户发送的消息内容。为了得到这个字段值，我 们需要解析微信服务器发来的HTTP Post包的Body。 在&#8220;接入验证&#8221;一文中我们提到过，微信服务器发起的请求都带有验证字段，可被公众号服务用于验证HTTP Request是否来自于微信服务器，避免恶意请求。这些用于验证来源的信息，不仅仅在接入验证阶段会发给公众号服务器，在后续微信服务器与公众号服务器 的消息交互过程中，HTTP Request中也都会携带这些信息(注意：没有echostr参数了)。 下面我们来看接收文本消息的Golang程序。 一、接收文本消息 公众号所用的HTTP Server可以沿用&#8220;接入验证&#8221;一文中的那个main中的Server，我们需要修改的是procRequest函数。 在procRequest函数中，我们保留validateUrl，用于校验请求是否来自于微信服务器。 func procRequest(w http.ResponseWriter, r *http.Request) { &#160;&#160;&#160;&#160;&#160;&#160;&#160; r.ParseForm() &#160;&#160;&#160;&#160;&#160;&#160;&#160; if !validateUrl(w, r) { &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; log.Println(&#34;Wechat Service: this http request is not from Wechat platform!&#34;) &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; return &#160;&#160;&#160;&#160;&#160;&#160;&#160; } &#160;&#160;&#160;&#160;&#160;&#160;&#160; log.Println(&#34;Wechat Service: validateUrl Ok!&#34;) &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>一旦<a href="http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/">接入验证</a>成功，成为正式开发者，你可能会迫不及待地想通过手机微信发送一条&quot;Hello, Wechat&rdquo;到你的公众号服务器。不过上一篇的那个程序还无法处理手机提交的文本消息，本篇将介绍如何用<a href="http://tonybai.com/tag/golang">Golang</a>编写公众号程序来接收手机端发送的 文本消息以及回复响应消息。</p>
<p>根据<a href="http://mp.weixin.qq.com/wiki/home/index.html">微信公众平台开发文档</a>中描述：&ldquo;当普通微信用户向公众账号发消息时，微信服务器将POST消息的XML数据包到开发者填写的URL上&rdquo;。我们 用一个示意图展示一下这个消息流程：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-public-receive-text-message.png" style="width: 400px; height: 221px;" /></p>
<p>微信服务器通过一个HTTP Post请求将终端用户发送的消息转发给公众号服务器，消息内容被包装在HTTP Post Request的Body中。数据包以XML格式存储，文本类消息XML格式样例如下（引自微信公众平台开发文档）：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-wiki/wechat-wiki-public-recv-text-msg.png" style="width: 388px; height: 174px;" /></p>
<p>数据包中各个字段的含义都显而易见，我们重点关注的时Content这个字段填写的内容，也就是终端用户发送的消息内容。为了得到这个字段值，我 们需要解析微信服务器发来的HTTP Post包的Body。</p>
<p>在&ldquo;<a href="http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/">接入验证</a>&rdquo;一文中我们提到过，微信服务器发起的请求都带有验证字段，可被公众号服务用于验证HTTP Request是否来自于微信服务器，避免恶意请求。这些用于验证来源的信息，不仅仅在接入验证阶段会发给公众号服务器，在后续微信服务器与公众号服务器 的消息交互过程中，HTTP Request中也都会携带这些信息(注意：没有echostr参数了)。</p>
<p>下面我们来看接收文本消息的Golang程序。</p>
<p><b>一、接收文本消息</b></p>
<p>公众号所用的HTTP Server可以沿用&ldquo;接入验证&rdquo;一文中的那个main中的Server，我们需要修改的是<font face="Courier New">procRequest</font>函数。</p>
<p>在procRequest函数中，我们保留validateUrl，用于校验请求是否来自于微信服务器。</p>
<p><font face="Courier New">func procRequest(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r.ParseForm()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !validateUrl(w, r) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: this http request is not from Wechat platform!&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: validateUrl Ok!&quot;)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &#8230; &#8230;//在此解析HTTP Request Body<br />
	}</font></p>
<p>通过验证后，我们开始解析HTTP Request的Body，Body中的数据是XML格式的，我们可以通过Golang标准库encoding/xml包中提供的函数对Body进行解 析。encoding/xml根据xml字段名与struct字段名或struct tag(struct中每个字段后面反单引号引用的内容，比如xml: &quot;xml&quot;)的对应关系将xml数据中的字段值解析到struct的字段中，因此我们需要根据这个xml包的组成定义出对应该格式的struct，这个 struct定义如下：</p>
<p><font face="Courier New">type TextRequestBody struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; XMLName&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Name `xml:&quot;xml&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CreateTime&nbsp;&nbsp; time.Duration<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgType&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Content&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgId&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int<br />
	}</font></p>
<p>其中FromUserName是发送方账号，这是一个OpenID，每个微信用户针对某个关注的公众号都有唯一OpenID。举个例 子：&quot;tonybai&quot;这个微信用户，关注了&quot;GoNuts&quot;和&quot;GoDev&quot;两个公众号，则&quot;tonybai&quot;发给GoNuts的消息中的 OpenID是&ldquo;tonybai-gonuts&rdquo;，而tonybai发给GoDev的消息中的OpenID则是&ldquo;tonybai-godev&rdquo;。</p>
<p>MsgId是一个64位整型，可用于消息排重。对于一个HTTP Post，微信服务器在五秒内如果收不到响应会断掉连接，并且针对该消息重新发起请求，总共重试三次。严谨的公众号服务端实现是应该实现消息排重功能的。</p>
<p>通过encoding/xml包中的Unmarshal函数，我们将上面的xml数据转换为一个TextRequestBody实例，具体代码如 下：</p>
<p><font face="Courier New">//recvtextmsg_unencrypt.go</font><br />
	<font face="Courier New">func parseTextRequestBody(r *http.Request) *TextRequestBody {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; body, err := ioutil.ReadAll(r.Body)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Fatal(err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(string(body))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; requestBody := &amp;TextRequestBody{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Unmarshal(body, requestBody)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return requestBody<br />
	}</font></p>
<p><font face="Courier New">func procRequest(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r.ParseForm()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !validateUrl(w, r) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: this http request is not from Wechat platform!&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if r.Method == &quot;POST&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody := parseTextRequestBody(r)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if textRequestBody != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;Wechat Service: Recv text msg [%s] from user [%s]!&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody.Content,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody.FromUserName)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>构建并执行该程序：</p>
<p><font face="Courier New">$&gt;sudo ./recvtextmsg_unencrypt<br />
	2014/12/19 08:03:27 Wechat Service: Start!</font></p>
<p>通过手机微信或公众开发平台提供的<a href="https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&amp;type=%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3%E8%B0%83%E8%AF%95&amp;form=%E9%93%BE%E6%8E%A5%E6%B6%88%E6%81%AF">页面调试工具</a>发送&quot;Hello, Wechat&quot;，我们可以看到如下输出：</p>
<p><font face="Courier New">2014/12/19 08:05:51 Wechat Service: validateUrl Ok!<br />
	Wechat Service: Recv text msg [Hello, Wechat] from user [oBQcwuAbKpiSAbbvd_DEZg7q27QI]!</font></p>
<p>上述接收&quot;Hello, Wechat&quot;文本消息的Http抓包分析文本如下(Copy from wireshark output)：</p>
<p><font face="Courier New">POST /?signature=9b8233c4ef635eaf5b9545dc196da6661ee039b0&amp;timestamp=1418976343&amp;nonce=1368270896 HTTP/1.0\r\n<br />
	User-Agent: Mozilla/4.0\r\n<br />
	Accept: */*\r\n<br />
	Host: wechat.tonybai.com\r\n<br />
	Pragma: no-cache\r\n<br />
	Content-Length: 286\r\n<br />
	Content-Type: text/xml\r\n</font></p>
<p>公众号服务器给微信服务器返回的HTTP Post Response为：</p>
<p><font face="Courier New">HTTP/1.0 200 OK\r\n<br />
	Date: Fri, 19 Dec 2014 08:05:51 GMT\r\n<br />
	Content-Length: 0\r\n<br />
	Content-Type: text/plain; charset=utf-8\r\n</font></p>
<p><b>二、响应文本消息</b></p>
<p>上面的例子中，终端用户发送&quot;Hello, Wechat&quot;，虽然公众号服务器成功接收到了这段内容，但终端用户并没有得到响应，这显然不那么友好！这里我们来给终端用户补发一个文本消息的响 应：Hello，用户OpenID。</p>
<p>这类响应消息可以通过HTTP Post Request的Response包携带，将数据放入Response包的Body中，当然也可以单独向微信公众平台发起请求（后话）。微信公众平台开发 文档中关于被动的文本消息响应的定义如下：</p>
<p><img alt="" src="http://tonybai.com/wp-content/uploads/wechat-wiki/wechat-wiki-public-reply-text-msg.png" style="width: 400px; height: 236px;" /></p>
<p>这与前面的接收消息结构极其类似，字段含义也不说自明。Golang encoding/xml中的Marshal(和MarshalIndent)函数提供了将struct编码为XML数据流的功能，它是 Unmarshal的逆过程，Golang实现回复 文本响应消息的代码如下：</p>
<p><font face="Courier New">type TextResponseBody struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; XMLName&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Name `xml:&quot;xml&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CreateTime&nbsp;&nbsp; time.Duration<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgType&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Content&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string<br />
	}</font></p>
<p><font face="Courier New">func makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody := &amp;TextResponseBody{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.FromUserName = fromUserName<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.ToUserName = toUserName<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.MsgType = &quot;text&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.Content = content<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.CreateTime = time.Duration(time.Now().Unix())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return xml.MarshalIndent(textResponseBody, &quot; &quot;, &quot;&nbsp; &quot;)<br />
	}</font></p>
<p><font face="Courier New">func procRequest(w http.ResponseWriter, r *http.Request) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; r.ParseForm()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !validateUrl(w, r) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: this http request is not from Wechat platform!&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if r.Method == &quot;POST&quot; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody := parseTextRequestBody(r)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if textRequestBody != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;Wechat Service: Recv text msg [%s] from user [%s]!&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody.Content,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody.FromUserName)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; responseTextBody, err := makeTextResponseBody(textRequestBody.ToUserName,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textRequestBody.FromUserName,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Hello, &quot;+textRequestBody.FromUserName)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; log.Println(&quot;Wechat Service: makeTextResponseBody error: &quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Fprintf(w, string(responseTextBody))<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>编译执行上面程序后，通过手机微信或网页调试工具发送一条&quot;Hello, Wechat&quot;到公众号，公众号会响应如下信息：&ldquo;<font face="Courier New">Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI&quot;，手机端微信会正确接收该响应。</font></p>
<p>上述响应的抓包分析如下。公众号服务器给微信服务器返回的HTTP Post Response为：</p>
<p><font face="Courier New">HTTP/1.0 200 OK\r\n<br />
	Date: Fri, 19 Dec 2014 09:03:55 GMT\r\n<br />
	Content-Length: 220\r\n<br />
	Content-Type: text/plain; charset=utf-8\r\n</font><br />
	<font face="Courier New">\r\n<br />
	&lt;xml&gt;&lt;ToUserName&gt;oBQcwuAbKpiSAbbvd_DEZg7q27QI&lt;/ToUserName&gt;&lt;FromUserName&gt;gh_xxxxxxxx&lt;/FromUserName&gt;&lt;CreateTime&gt;1418979835&lt;/CreateTime&gt;&lt;MsgType&gt;text&lt;/MsgType&gt;&lt;Content&gt;Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI&lt;/Content&gt;&lt;/xml&gt;</font></p>
<p><b>三、关于Content-Type设置</b></p>
<p>虽然Content-Type为：<font face="Courier New">text/plain; charset=utf-8</font>的 响应信息可以被微信平台正确解析，但通过抓取微信平台给公众号服务器发送的HTTP Post Request来看，在发送xml数据时微信服务器用的Content-Type为<font face="Courier New">Content-Type: text/xml</font>。我们的响应信息Body也是xml数据包，我们能否为响应信息重新设置Content-Type为 text/xml呢？我们可以通过如下代码设置：</p>
<p><font face="Courier New">w.Header().Set(&quot;Content-Type&quot;, &quot;text/xml&quot;)</font><br />
	<font face="Courier New">fmt.Fprintf(w, string(responseTextBody))</font></p>
<p>不过奇怪的是我通过AWS EC2上抓包得到的Content-Type始终是&ldquo;<font face="Courier New">text/plain; charset=utf-8</font>&rdquo;。但利用ngrok映射到本地端口后抓包看到的却是正确的&quot;text/xml&quot;，在AWS本地用 curl -d xxx.xxx.xxx.xxx测试公众号服务程序而抓到的包也是正确的。通过代码没看出什么端倪，因为逻辑上显式设置Header的Content- Type后，Go标准库不会在sniff内容的格式了。</p>
<p>通过ngrok映射本地80端口后，得到的HTTP Post Response抓包分析文字：</p>
<p>HTTP/1.1 200 OK\r\n<br />
	Content-Type: text/xml\r\n<br />
	Date: Sat, 20 Dec 2014 04:29:16 GMT\r\n<br />
	Content-Length: 220\r\n</p>
<p>xml数据包这里忽略。</p>
<p><b>四、CDATA的使用</b></p>
<p>从抓包可以看到，我们回复的响应中的XML数据包是不带CDATA，即便这样微信客户端接收也没有问题。但这并未严遵循协议样例。</p>
<p>XML下CDATA含义是：在标记CDATA下，所有的标记、实体引用都被忽略，而被XML处理程序一视同仁地当做字符数据看待，CDATA的形 式如下：</p>
<p><font face="Courier New">&lt;![CDATA[文本内容]]&gt;</font></p>
<p>我们尝试加上为每个文本类型的字段值上直接添加CDATA标记。</p>
<p><font face="Courier New">func value2CDATA(v string) string {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return &quot;&lt;![CDATA[" + v + "]]&gt;&quot;<br />
	}</font></p>
<p><font face="Courier New">func makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody := &amp;TextResponseBody{}<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.FromUserName = value2CDATA(fromUserName)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.ToUserName = value2CDATA(toUserName)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.MsgType = value2CDATA(&quot;text&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.Content = value2CDATA(content)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; textResponseBody.CreateTime = time.Duration(time.Now().Unix())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return xml.MarshalIndent(textResponseBody, &quot; &quot;, &quot;&nbsp; &quot;)<br />
	}</font></p>
<p>这样修改后，我们试着发一条消息给微信公众号平台，不过结果并不正确。手机微信无法收到响应信息，并显示&ldquo;该公众号暂时无法提供服务，请稍后再 试&rdquo;。通过Println输出Body可以看到：</p>
<p><font face="Courier New">&lt;xml&gt;&lt;ToUserName&gt;&amp;lt;![CDATA[oBQcwuAbKpiSAbbvd_DEZg7q27QI]]&amp;gt;<fromusername>&amp;lt;![CDATA[gh_1fd4719f81fe]]</fromusername></font><font face="Courier New"><font face="Courier New">&amp;gt;</font>&lt;/FromUserName&gt;&lt;CreateTime&gt;1419051400&lt;/CreateTime&gt;&lt;MsgType&gt;&amp;lt;![CDATA[text]]</font><font face="Courier New"><font face="Courier New">&amp;gt;</font>&lt;/MsgType&gt;&lt;Content&gt;&amp;lt;![CDATA[Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI]]</font><font face="Courier New"><font face="Courier New">&amp;gt;</font>&lt;/Content&gt;&lt;/xml&gt;</font></p>
<p>可以看到左右尖括号分别被转义为&amp;lt;和&amp;gt;了，这显然不是我们想要的结果。那如何加入CDATA标记呢。Golang并 不直接显式支持生成CDATA字段的xml流，我们只能间接实现。前面提到过struct定义时的struct tag，golang xml包规定：&quot;<font face="Courier New">a field with tag &quot;,innerxml&quot; is written verbatim, not subject to the usual marshalling procedure</font>&quot;。 大致的意思是如果一个字段的struct tag是&quot;,innerxml&quot;，则Marshal时字段值原封不动，不提交给通常的marshalling程序。我们就利用innerxml来实现 CDATA标记。</p>
<p><font face="Courier New">type TextResponseBody struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; XMLName&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; xml.Name `xml:&quot;xml&quot;`<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName&nbsp;&nbsp; CDATAText<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName CDATAText<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CreateTime&nbsp;&nbsp; time.Duration<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MsgType&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CDATAText<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Content&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CDATAText<br />
	}</font></p>
<p><font face="Courier New">type CDATAText struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Text string `xml:&quot;,innerxml&quot;`<br />
	}</font></p>
<p><font face="Courier New">func value2CDATA(v string) CDATAText {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return CDATAText{&quot;&lt;![CDATA[" + v + "]]&gt;&quot;}<br />
	}</font></p>
<p>编译程序后测试，这回CDATA标记正确了，微信客户端也收到的响应信息。</p>
<p><b>五、用ngrok在本地调试微信公众平台接口</b></p>
<p>在&ldquo;接入验证&rdquo;一文中，我们建议申请诸如AWS EC2来应对微信公众平台接口开发，但其方便程度毕竟不如本地。网上一开源工具ngrok可以帮助我们实现本地调试微信公众平台接口。</p>
<p>使用ngrok的步骤如下：</p>
<p>1、下载ngrok<br />
	&nbsp;ngrok也是使用golang实现的，因此主流平台都支持。ngrok下载后就是一个可执行的二进制文件，可直接执行（放在PATH路径 下）。</p>
<p>2、注册ngrok<br />
	到ngrok.com上注册一个账号，注册成功后，就能看到ngrok.com为你分配的auth token，把这个auth token放到~/.ngrok中：</p>
<p><font face="Courier New">auth_token:YOUR_AUTH_TOKEN</font></p>
<p>3、执行ngrok</p>
<p>$ngrok 80</p>
<p><font face="Courier New">ngrok&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (Ctrl+C to quit)</font></p>
<p><font face="Courier New">Tunnel Status&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; online<br />
	Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.7/1.6<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a class="moz-txt-link-freetext" href="http://xxxxxxxx.ngrok.com">http://xxxxxxxx.ngrok.com</a> -&gt; 127.0.0.1:80<br />
	Forwarding&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a class="moz-txt-link-freetext" href="https://xxxxxxxx.ngrok.com">https://xxxxxxxx.ngrok.com</a> -&gt; 127.0.0.1:80<br />
	Web Interface&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 127.0.0.1:4040<br />
	# Conn&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1<br />
	Avg Conn Time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1.90ms</font></p>
<p>其中&quot;xxxxxxxx.ngrok.com&quot;就是ngrok为你分配的子域名。</p>
<p>在你的微信开发者中心将这个地址配置到URL字段中，提交验证，验证消息就会顺着ngrok建立的隧道流到你的local机器的80端口上。</p>
<p>另外本地调试抓包，要用loopback网口，比如：<br />
	<font face="Courier New">$sudo tcpdump -w http.cap -i lo0 tcp port 80</font></p>
<p>本篇文章涉及的代码在<a href="https://github.com/bigwhite/experiments/tree/master/wechat_examples/public/2-recvtextmsg">这里</a>可以找到。</p>
<p style='text-align:left'>&copy; 2014 &#8211; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
	</channel>
</rss>
