由于厂里爬虫业务需要,我一直想复制国外的初创公司 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 ,我终于搞定了
1
coolcfan 2017-03-24 16:01:30 +08:00 via Android
敢于直接对着那个 Selector API 编程的人都是猛士……
话说尝试过研究 Java.NIO2 里 AIO 的部分么,好像直接提供了基于回调的机制(CompletionHandler 什么的)。 |
2
sagaxu 2017-03-24 16:13:01 +08:00
1. 几十个连接直接上多线程就行了,单机 1 万个以内线程的 IO 型应用,调度开销忽略不计。
2. 我们 Java 码农一般是不会直接用 NIO 的,我们喜欢用 mina/netty/vertx 。 |
3
sagaxu 2017-03-24 16:16:21 +08:00
70 行 PHP 翻译成功能一样的 Java ,如果超过 100 行,就要想一下是不是姿势有问题了
|
4
gouchaoer OP |
5
coolcfan 2017-03-24 16:28:03 +08:00
@gouchaoer #4 不过 AIO 背后要有线程池,不过 whatever ,线程池配置成 1 就好了。(其实 Netty 也可以)
|
6
hiro0729 2017-03-24 16:29:28 +08:00
netty 把 nio 的坑都填了,你竟然不用而去选择把坑扒开了往里跳,何苦呢
|
9
SoloCompany 2017-03-25 01:02:04 +08:00
这个和你那 70 行 php 代码完全没关系好吧
NIO 本身就是个底层的玩意儿,不封装没法用的 要封装到可用,那可不是几百行能完成的事情 |
10
moyang 2017-03-25 04:27:16 +08:00 via Android
兄弟,你好!我是在群里跟你聊过的。近期我们有计划用一个新方式获取中国大陆 ip ,来突破之前的问题(ip 太少,全中国只有 40k)。有进展的时候我 qq 上通知你
|
11
gouchaoer OP @moyang 多谢,贵厂的技术让人印象深刻,比如 chrome 插件机制根本没提供 socket 权限,还是可以通过 hack 来搞定很多东西
|
13
carrotuestc 2017-04-20 09:37:02 +08:00
膜拜超哥
|