基于 Java 的 NIO 的 luminati.io 代理方案客户端填坑记录

2017-03-24 15:46:04 +08:00
 gouchaoer

由于厂里爬虫业务需要,我一直想复制国外的初创公司 luminati.io 的代理方案,魔改一下可以应用到厂里的一些业务上。这玩意儿也没啥大不了的,本质上是就是个服务器端转发了 1 次+客户端反向连接转发 1 次的代理隧道之类的东东,我断断续续研究了几个月以后终于打通了。和一般的 http 代理服务器原理一样,服务器端和客户端本质上都是异步并发的 tcp 操作,它们用一个随机数字相互 tcp 握手以后爬虫(浏览器或者 httpclient )设置服务器端为代理,并且在 header 里面加上这个随机数字(为了支持浏览器+https ,这个随机数字似乎只能放在 Proxy-Authorization 中),最后通过爬虫<-->服务器端<-->客户端<-->互联网这样来访问网站。 demo 都是用 php 来实现的,虽然服务器端可以继续用 php ,但是客户端我需要用 java 重写。本来只有 70 行的 php 客户端代码,结果硬生生的花了我几个星期的时间才翻译成了 java 。也许是我 java 水平不够,也许是 NIO 太坑了,总之今天要来记录这些个坑。

由于必须同时保持几十条的 tcp 连接,所以客户端必须是异步的、单线程的和并发的,我在 github 上翻了很久终于找了个安卓的代理的 Demo : https://github.com/dawsonice/KissProxy 。看他的介绍很不错: NIO based 就可以不依赖 netty 之类的(我的业务需要尽量不依赖第三方的库)、 Single Thread 单线程(这是必然的,我肯定不接受线程池方案)、支持 HTTPS 那是必须的,总之我觉得这个 demo 不错于是就打算照着他的例子用 NIO 来写了,然后开启了漫漫的填坑之旅。

我照着这个 KissProxy 就慢慢魔改起来,结果遇到 2 个坑。第一个就是在发起 TCP 连接的时候用了同步的方式: https://github.com/dawsonice/KissProxy/blob/master/src/me/dawson/proxyserver/core/ChannelPair.java#L177 ,单线程情况下这就阻塞了,所以这个代理服务器实现是不对的。解决方法当然是把发起 tcp 请求的 SocketChannel 操作弄成异步的,可是这个 NIO 并没有办法直接对 SocketChannel 设置回调,需要通过 Selector 机制来注册 OP_CONNECT 和 OP_READ 之类的,搞起了虽然麻烦了点不过还是搞定了。

第二个就是 NIO 的 SocketChannel 在写的时候写缓存可能是满的写不进去,需要注册 OP_WRITE 事件等待写缓存可写,他没有考虑这一情况就会导致数据丢失: https://github.com/dawsonice/KissProxy/blob/master/src/me/dawson/proxyserver/core/ChannelPair.java#L235 。我在实际使用的时候就因为 SocketChannel 的写缓存经常满导致出错(因为我的代理相当于经过了 2 次转发,服务器端接收数据包缓存满了的话客户端也发不出去,导致客户端写缓存满容易触发)。总之又注册上了 OP_WRITE 事件,把缓存满的情况考虑进去,但是这个 OP_WRITE 的触发条件是“只要写缓存没满就触发”,而不是“写缓存从满的状态到可以写才触发”这样,这就导致每次 select 就立刻返回了。然后我就怒了这 NIO 居然暴露这么底层的细节给开发者就算了,这 API 设计太反人类了,搞定了之后现在代码已经成了一锅粥了。

然后问题又来了,我发现整个事件循环是吃满 CPU 的, select 如果没有事件返回不是可以阻塞么(我把 OP_WRITE 事件去掉了,因为这个事件总是触发的,然后设置一个超时时间),一看似乎是 JDK 的一个 bug : http://stackoverflow.com/questions/35858537/selector-selecttimeout-returns-0-before-timeout 。为了保险我稍微魔改了一下 select 的机制,如果 select 到的事件为空(排除 OP_WRITE )就 sleep 一小会儿,虽然比较 dirty 不过能 work 就好了。

半个月前的问题: https://www.v2ex.com/t/346155 ,我终于搞定了

原文: http://qsalg.com/?p=557

5183 次点击
所在节点    Java
13 条回复
coolcfan
2017-03-24 16:01:30 +08:00
敢于直接对着那个 Selector API 编程的人都是猛士……
话说尝试过研究 Java.NIO2 里 AIO 的部分么,好像直接提供了基于回调的机制(CompletionHandler 什么的)。
sagaxu
2017-03-24 16:13:01 +08:00
1. 几十个连接直接上多线程就行了,单机 1 万个以内线程的 IO 型应用,调度开销忽略不计。
2. 我们 Java 码农一般是不会直接用 NIO 的,我们喜欢用 mina/netty/vertx 。
sagaxu
2017-03-24 16:16:21 +08:00
70 行 PHP 翻译成功能一样的 Java ,如果超过 100 行,就要想一下是不是姿势有问题了
gouchaoer
2017-03-24 16:24:48 +08:00
@coolcfan 草,我刚看了一下 AIO ,发现这个正是我需要的,可惜 NIO 的屎已经吃下去了

@sagaxu mina 和 netty 太重了,我不能使用第三方库,这可能放到 app 里面,至于几十 tcp 连接用几十个线程池更新不可能

写了 500 行,如果早点发现 AIO 估计要好很多
coolcfan
2017-03-24 16:28:03 +08:00
@gouchaoer #4 不过 AIO 背后要有线程池,不过 whatever ,线程池配置成 1 就好了。(其实 Netty 也可以)
hiro0729
2017-03-24 16:29:28 +08:00
netty 把 nio 的坑都填了,你竟然不用而去选择把坑扒开了往里跳,何苦呢
sagaxu
2017-03-24 16:33:19 +08:00
@gouchaoer 你可以试试 Go 语言写个 lib , ios 和 android 通吃,比 AIO 还能简单不少
gouchaoer
2017-03-24 16:37:43 +08:00
@sagaxu Go 语言是垃圾,不用
SoloCompany
2017-03-25 01:02:04 +08:00
这个和你那 70 行 php 代码完全没关系好吧
NIO 本身就是个底层的玩意儿,不封装没法用的
要封装到可用,那可不是几百行能完成的事情
moyang
2017-03-25 04:27:16 +08:00
兄弟,你好!我是在群里跟你聊过的。近期我们有计划用一个新方式获取中国大陆 ip ,来突破之前的问题(ip 太少,全中国只有 40k)。有进展的时候我 qq 上通知你
gouchaoer
2017-03-25 09:40:53 +08:00
@moyang 多谢,贵厂的技术让人印象深刻,比如 chrome 插件机制根本没提供 socket 权限,还是可以通过 hack 来搞定很多东西
moyang
2017-03-25 16:22:45 +08:00
@gouchaoer 我司跟做黑产的差不多,全都是 hack
carrotuestc
2017-04-20 09:37:02 +08:00
膜拜超哥

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/350061

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX