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

由于厂里爬虫业务需要,我一直想复制国外的初创公司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就好了。

0

发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>