HTTP 代理服务器技术选型之旅

背景

长期以来,贴吧开发人员多,业务耦合大,需求变化频繁,因此容易产生 bug。而我所负责的广告相关业务,和 UI 密切相关,一旦因为某种原因(甚至是被别人改了代码)产生了 bug,必然大幅度影响广告收入。

解决问题的一种方法在于频繁的测试,既然避免不了代码层面的耦合,那总是可以通过定时的检查来避免问题。所以我们维护了一组核心 case,密切关注最核心的功能。选择核心 case 实际上是在覆盖面和测试成本之间的权衡,然而多个 case 有不同的测试步骤,测试效率始终难以提高。

因此,我们的目标是建立一个代理服务器,能够在运行时把任何包(包括线上包)的数据改成我希望的样子。换句话说,这个代理服务器也可以理解为一个私服,它能够获得客户端的请求数据并作出修改,也可以获得服务端的响应数据并做修改。

代理服务器工作模型

在早期版本中,我们选择了简单的 HTTP 协议。这种选择对技术的要求最低,我们自己实现了一个代理服务器,开启 socket,监听端口,然后将客户端的请求发送给服务器,再把服务器的返回数据传回客户端。这种模式也被称为:“中间人模式”(MITM: Man In The Middle)。

虽然道理很简单,但实现起来还是有些地方要注意。首先,当 socket 接受数据后,应该新开一个进程/线程 进行处理。既然涉及到新的进程/线程,就一定要注意它的释放时机,否则会导致内存无限制增加。

其次,对于 socket 来说,它并没有等待函数,也就是说我无从得知何时有数据可读,因此这个艰巨的任务就交给了 select。我们把需要监听的 socket 对象作为参数传入其中,函数会一直阻塞,直到有可读、可写的对象,或者达到超时时间。

Keep-Alive 字段可以复用 TCP 连接,是一种常见的 HTTP 协议的优化方式,在 HTTP 1.1 中已经是默认选项。填写这个字段后,Server 返回的数据可能是分批次的,这样能够改善用户体验,但也会增加代理服务器的实现难度。所以代理服务器在作为客户端,向真正服务器请求数据时,应该删除这个字段。

由于整套流程都是自己实现,因此可以比较容易的 HOOK 住上下行数据并做修改。只有注意在接收到全部数据后再做修改即,整个流程可以用下图简单表示:

代理服务器的工作模式

技术选型

短连接

由于长连接基于 TCP,不用每次新建连接,也省略了不必要的 HTTP 报文头部,效率明显优于 HTTP。所以各大公司基本上选择了长连接作为实际生产环境下的连接方式。然而由于不熟悉 WebSocket 协议,并且我们依然支持短连接,所以代理服务器最终选择了 HTTP 协议。

要想实现这一点, 就得在应用启动时,模拟后台向客户端发送一段控制信息,强制客户端选择 HTTP 请求。这样一来,即使是线上包也可以走代理服务器。

HTTPS

由于苹果强制要求使用 HTTPS,虽然已经延期,但也是明年的趋势。考虑到后续的使用,我们决定对之前实现的代理服务器进行升级。由于 HTTPS 涉及到请求协议的解析,以及加密解密和证书管理,上述自研方案很难 hold 住。经过一番调研,最后选择了一个比较知名的开源库 mitmproxy

Mitmproxy

选择这个库最主要的理由是它直接支持 HTTPS,不过没有中文文档,国内的使用相对来说比较少,所以在接入的时候可能会略花一点时间。

这是一个 python 库, 首先要安装 virtualenv,如果本地没装的话输入:

sudo pip install virtualenv
安装好了以后,进入 mitmproxy/venv3.5/bin 文件夹输入:
source ./active
这样就可以启用 virtualenv 环境了。

Hook 脚本

这个库可以理解为命令行中可交互版本的 Charles,不过我并不打算用它的这个功能。因为我的需求主要是利用脚本来 Hook 请求, 所以我选择了 mitmdump 这个工具。使用它的时候可以指定脚本:

mitmdump -s "xxx.py"

脚本也很简单,我们可以重写 requeest 或者 receive 函数:

def request(flow):
flow.response.content = "<p>hello world</p>"

运行脚本以后,把手机的代理设为本机 ip 地址,端口号改为 8080,然后用手机浏览器打开 http://mitm.it/,如果一切配置顺利,你会看到证书的安装界面。

安装好证书后,用手机访问任何一个网站(包括 HTTPS),你应该都会看到一个小小的 hello world,至此所有的配置就完成了。

bug 修改

这个开源库有一个很严重的 bug,在解析 multipart 类型的数据时可能会发生。它使用了 splitline 方法来分割换行符,然而如果数据中有 \n 的话,就会因此丢失。很不幸的是,很多 protobuf 编码后的数据都有 \n,一旦丢失就会导致解析失败。

如果你不幸遇到了和我一样的坑,可以把相关代码改成我的版本:

for i in content.split(b"--" + boundary):
parts = i.split(b'\r\n\r\n', )
if len(parts) > and parts[][:] != b"--":
match = rx.search(parts[])
if match:
key = match.group()
value = parts[][:len(parts[])-] # Remove last \r\n
r.append((key, value))

More

到了这一步,基本上已经成功实现支持 HTTPS 的代理服务器了。后续要处理的可能就是解析 protobuf,完善业务代码等等琐碎的事情,只要小心谨慎,基本上不会有问题。

HTTP 代理服务器技术选型之旅的更多相关文章

  1. 【SSM之旅】Spring+SpringMVC+MyBatis+Bootstrap整合基础篇(一)项目简介及技术选型相关介绍

    试水 一直想去搭建个自己的个人博客,苦于自己的技术有限,然后也个人也比较懒散.想动而不能动,想动而懒得动,就这么一直拖到了现在.总觉得应该把这几年来的所学总结一番,这样才能有所成长. 不知在何时,那就 ...

  2. #数据技术选型#即席查询Shib+Presto,集群任务调度HUE+Oozie

    郑昀 创建于2014/10/30 最后更新于2014/10/31   一)选型:Shib+Presto 应用场景:即席查询(Ad-hoc Query) 1.1.即席查询的目标 使用者是产品/运营/销售 ...

  3. 老王讲自制RPC框架.(一.前言与技术选型)

    (#)背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. 单一应用架构 当网站流量很小时,只 ...

  4. Atitit 开发2d游戏的技术选型attilax总结

    Atitit 开发2d游戏的技术选型attilax总结 1.1. 跨平台跨平台:一定要使用跨平台的gui技术,目前最好的就是h5(canvas,webgl,dom) +js了..1 1.2. 游戏前后 ...

  5. 《2016ThoughtWorks技术雷达峰会----js爆炸下的技术选型》

    JS爆炸下的技术选型  刘尚奇    ThoughtWorks, 高级咨询师 JS每6个星期出现一个新框架,那么如何进行JS的选型.以下从四个方面来分析. 1.工具 NPM for all the t ...

  6. 手机web站点和手机app 技术选型的困惑于思考

    今年一直在关注移动端技术的发展,自己也用博客园的rss接口玩了半年,关于技术选型的困惑和大家说说 一 趋势 随着手机硬件不断的升级,外加4g牌照的发放,不出2年时间移动端web站点和手机app一定会进 ...

  7. atitit.技术选型方法总结为什么java就是比.net有前途

    atitit.技术选型方法总结为什么java就是比.net有前途 #----按照不同的需要有不铜的法... 一般有开发效率,稳定性上的需要.. 作者 老哇的爪子 Attilax 艾龙,  EMAIL: ...

  8. 消息中间件的技术选型心得-RabbitMQ、ActiveMQ和ZeroMQ

    消息中间件的技术选型心得-RabbitMQ.ActiveMQ和ZeroMQ 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs RabbitMQ.Active ...

  9. AutoLayout技术选型和应用

    前言:这篇文章是笔者在项目中对布局技术进行技术选型和应用的相关介绍,供大家参考. && [self.buttonscount] > 0) { UIButton *button = ...

随机推荐

  1. Java线程(十一):Fork/Join-Java并行计算框架

    并行计算在处处都有大数据的今天已经不是一个新奇的词汇了.如今已经有单机多核甚至多机集群并行计算.注意,这里说的是并行,而不是并发.严格的将,并行是指系统内有多个任务同一时候运行,而并发是指系统内有多个 ...

  2. java实现读取ftp服务器上的csv文件

    定义ftp操作接口 import java.io.InputStream; import java.util.List; import org.apache.commons.net.ftp.FTPCl ...

  3. Swift3 substring几种常用用法

    举例: " let length = str.characters.count //截取前四位 )) //截取后2位(两种方法) )) )) //截取中间4位,从第2位开始(二种方法) le ...

  4. git 错误信息 6 uncommitted changes would be overwritten by merge

    表示本地库有几个文件修改没有提交,说明本地有几个文件没有跟踪

  5. ubuntu源列表(清华,阿里,官方,选一即可)

    Ubuntu的源列表在/etc/apt/sources.list中,替换即可 #清华的源deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial m ...

  6. Android之listview运用(美团美食列表)

    首先我们将listview简单实现,有图形,有文字:效果如图 之前我们完成了一个较为简单的listview视图列表,但是生活中我们往往碰到的 是更为复杂列表,有图像有评分标准,不如我们来试一试手,做一 ...

  7. oracle下导出某用户所有表的方法

    oracle下导出某用户所有表的方法 scott/tiger是用户名和密码,handson是导出的实例名 按用户方式导出数据(owner当中写的是用户名) exp scott/tiger@handso ...

  8. 《JAVA与模式》之中介者模式(转载)

    原文出处:http://blog.csdn.net/zhengzhb/article/details/7430098   定义:用一个中介者对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用 ...

  9. thinkphp C函数的实现原理

    在写一个php原生函数的时候,想起使用thinkphp的C函数读取数据库配置非常方便,于是看了看源码的实现,原理很简单,分享一下: 下面是common.php,实现了C函数: if(is_file(& ...

  10. Eclipse插件的安装与配置

    1.下载插件时注意要和Eclipse版本兼容. 2.安装Eclipse插件时注意是否要安装其他的插件,这一点很容易被忽视. 3.有时启动Eclipse未加载插件,解决方法很多,总结一下:     a ...