SOCKS5标准在《RFC1928》中进行了概要的说明。SOCKS5位于应用层和传输层之间,因此不能支持网络层的协议(如ICMP);与SOCKS4相比,SOCKS5:
1、同时支持TCP和UDP;
2、支持强身份验证;
3、支持域名寻址;
4、支持IPv6;
Netty原生支持SOCKS5,下面通过curl观察SOCKS5协议的通信。使用形如:
curl --socks5 socks-server-ip:socks-server-port http://www.meilongkui.com
的命令可以让curl使用SOCKS5代理进行访问:

curl命令使用SOCKS5代理
此时,可以在Netty端看到如下的日志:

Netty的SOCKS5服务器日志
这里面比较让人迷茫的地方有几点:
1、CONNECT命令成功后,SOCKS客户端是否后续就直接向SOCKS服务器发送裸数据数据了?从Netty输出的日志看,是的。
2、curl每次执行完HTTP请求后就立即关闭了到SOCKS服务器的连接,从而使得SOCKS服务器也立即关闭了到远程服务器的连接。那么,如果SOCKS客户端与SOCKS服务器每次都要建立连接、SOCKS服务器每次都要和远程服务器建立连接是否性能方面会有比较大的开销?
我们用Apache HttpClient进行一下验证,首先不用代理判断一下连接池能够启用:

Apache HttpClient不用代理使用连接池
第一次请求时,从连接池中取出连接后,可以看到与远程服务器之间建立了连接:

Apache HttpClient不用代理使用连接池首次连接
第二次请求则没有再建立连接:

Apache HttpClient不用代理使用连接池第二次连接使用了连接池
如果超过了Keep-alive的时间,那么则会重新建立连接(例如在最后一次请求前休眠10秒再请求):

Apache HttpClient不用代理使用连接池Keep-alive超时后会重建连接
那么,当启用SOCKS5代理后连接池仍然有效么?假设我们使用如下的典型代码:

Apache HttpClient使用SOCKS5代理及连接池
动态调试会发现SocksConnectionSocketFactory中的connectSocket执行前并不会有任何的实际连接动作,直到connectSocket执行以后才会自动按SOCKS5协议连接到SOCKS5服务器并发出CONNECT命令。应该注意,这个sock是java.net.Socket类型的,connect方法的逻辑在JDK里面。因此,具体如何执行的SOCKS的具体执行细节无需太关注(java.net.Socket里面有个SocksSocketImpl类型的成员变量):

java.net.Socket里面SocksSocketImpl类型的成员变量
通过HttpClient的日志和SOCKS5服务器的日志我们可以确定上述的判断(即connectSocket执行前并不会有任何的实际连接动作):

connectSocket执行前并不会有任何的实际连接动作
直到connectSocket执行以后才会自动按SOCKS5协议连接到SOCKS5服务器并发出CONNECT命令:

connectSocket执行以后才会自动按SOCKS5协议连接到SOCKS5服务器并发出CONNECT命令
此时HttpClient才认为ConnectionEstablished:

connectSocket中的socket连接成功后Httpclient才认为ConnectionEstablished
此时,如果连续进行十次HTTP请求,会发现正常用到了连接池:

使用SOCKS5代理时依然可以用到连接池
在SOCKS5服务器的日志中也能发现SOCKS5客户端到SOCKS5服务器之间的连接和SOCKS5服务器到远程服务器之间的连接都是被复用的:

启用连接池后SOCKS5客户端连接的关闭情况
从上图我们可以看出,3秒IDLE之后SOCKS5客户端到SOCKS5服务器的连接被断开,随后SOCKS5服务器到远程服务器之间的连接被断开。我们应该注意,HttpClient的日志并没有反映出使用了SOCKS代理(很明显,在前面的sock.connect方法是JDK自己完成,不是HttpClient的逻辑;当然,如果HttpClient要打日志的话也能打出来,只是现在没有打)。
这里,由于SOCKS5服务器与SOCKS5客户端之间存在IDLE的问题,我们会有一种怀疑,即如果Keep-alive的时间(5000ms)长于SOCKS5服务器与SOCKS5客户端之间的IDLE时间(3000ms),那么此时SOCKS5服务器与SOCKS5客户端之间的连接已经被中断,会发生什么呢?让我们在执行最后一个HTTP请求前休眠4秒,让SOCKS5服务器与SOCKS5客户端之间的连接因为IDLE而关闭:

Keep-alive未超时,但SOCKS客户端与SOCKS服务器之间IDLE超时
我们从HttpClient的日志中可以看出,尽管Keep-alive时间尚未到达,但HttpClient感知到了SOCKS5服务器与SOCKS5客户端之间的连接断开(考虑应该通过类似于远程主机主动关闭了一个连接的那种异常捕获到的),并在后续发出HTTP请求前与第一次请求一样通过SOCKS5的CONNECT命令重新建立了连接:

重新建立连接
除此以外,还有一种情况是访问多个不同Host的:

HttpClient同时访问多个Host
实测也能正确处理。
这样,我们就对SOCKS5基本的逻辑和几个未在RFC上明确说明的问题建立了一定的理解,也为后续进行性能优化等等相关的工作奠定了基础。
转载时请保留出处,违法转载追究到底:进城务工人员小梅 » HttpClient/Curl使用SOCKS5代理数据包分析