HTTP中继(代理)、隧道相关介绍及简单Java实现

来源:互联网 发布:变电话号码的软件 编辑:程序博客网 时间:2024/04/30 11:43

本文主要是对Http代理及隧道的相关原理进行简单介绍,如果想要“一把梭大笑”的好像记得Apache有个框架就是直接用来作为Http代理的,可以搜一下。


简单概念介绍:

Http代理:Web Server和client的集合体,用于从真正的Client端接收Http请求,然后转发到Web Server,并从Web Server接收Http响应并发送到真正的Client端;

        Http中继:“傻瓜式”Http代理,就是对流量进行盲目的转发,不进行任何逻辑处理(如:缓存、访问控制...),但是实现很简单,所以附加的例子也是一个盲中继;

        隧道:在http连接上传输其他流量,概念说不明白,最简单的可能应用场景就是防火墙限制只能通过web流量,而你偏偏想能绕过这个限制,想让其他流量也能走出去怎么办,隧道就可以帮你实现。

Http CONNECT方法(核心):用来建立隧道用,方法规范直接查看RFC协议规范文档.


以上简单的几个概念的应用场景介绍:

最常见得莫过于翻墙工具了,常见的方式:在本地装个代理(其实就是个Http代理),然后设置浏览器的代理服务器为本地代理,紧接着浏览器会将所有的http请求发送到代理之中,代理再通过无论什么方式都行(简单理解就是Socek编程)将数据发送到能够访问外网的服务器上,然后外网服务器去向真正的Web Server请求数据,并原路返回到代理,代理再发送给你,整个过程就完事了.以上纯属废话,自己打脸不用你们动手生气


原理介绍:

网络分层概念:http协议在TCP/IP编程中属于应用层协议,这个概念一定要清晰,下面的部分会用到,而我们通常的Socket编程就是针对在传输层,SSL是在传输层和应用层之间的,这也就为我们能直接Socket编程直接进行实现奠定基础;

 HTTP代理“既是client,又是server”:那我们就可以理解为我们只要写一个程序,既能实现Web server的功能又能实现Client的功能不就可以了?所以实现盲中继的第一步就是使用Socket编程编写一个简单的Web Server,刚才说了Socket编程在传输层,所以只要对Socket中传输过来的得数据进行按照HTTP协议规范处理,就实现了一个简单的Web Server;

HTTP协议CONNECT方法:此处主要说一下Conect方法,别的方法大家都常用就不啰嗦了,Connet方法是用来建立隧道的,就是client会向代理发送一个Connect请求,代理在对这个Connect请求作出200响应之后,Client会直接在建立好的这个TCP连接上发送流量,代理不会对数据进行任何修改,直接将数据原封不动的发送到目的地址。


简单介绍一个使用代理时HTTPS的一个传输过程:

1、浏览器与代理建立一个Socket连接(传输层),并向这个Socket发送数据(就是一个CONNECT方法的HTTP请求,此时没有使用SSL层,不然代理根本解析不出来,就是一个明文的HTTP消息);

2、代理从建立的Socket中读取一段数据,并按照HTTP协议规范解析出收到的是一个CONNECT请求(就是Web Server的解析数据部分的功能,怎么把一段明文数据按照HTTP协议规范解析成HTTP消息);

3、代理收到一个CONNECT请求之后(中间认证什么的直接省略),一路顺风的话,解析出CONNECT请求中的的原始Web Server的URL和方法;

4、代理使用Socket建立一个与Web Server的套接字(传输层);

5、代理向浏览器返回一个200状态的HTTP响应,告诉浏览器到原始Web Server的Socket已经建立好了;

6、浏览器收到200响应之后,可以立即(都是CONNECT方法规范的一部分)向与代理第1步建立的Socket中发送SSL(传输层和应用层之间)连接建立需要的一系列握手数据;

7、中间就是无限循环的数据发送阶段:代理直接从与浏览器建立的Socket中读数据(不关心也不知道是啥数据),然后又发送到与Web Server建立的Socket中;Web Server发过来是数据也是直接转发到浏览器的Socket中,就是一个基本的管道功能,将浏览器到代理的Socket和代理到Web Server的Sock管道连接起来;

8、剩下的就是浏览器和Web Server如何从SSL加密数据中解析出明文数据,然后按照HTTP协议的一套规范(如:第一行是请求行,接下来是请求头,再加一个空白换行之后是消息体......)


直接粘贴代码,并通过代码解释(说话比较啰嗦尴尬):

另外:代码没做异常流的处理考虑,主要是辅助讲述原理的

package com.anyone.http;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;/** * 声明以下HTTP协议规范,均指1.1版本. *  * @author AnyOne * */public class HttpBlindProxy {public static void main(String[] args) throws IOException {@SuppressWarnings("resource")ServerSocket server = new ServerSocket(8008);while (true) {
//对应Https连接建立过程第一步,接收浏览器向代理建立的Socket连接Socket client = server.accept();new Worker(client).start();}}}//真正的逻辑处理class Worker extends Thread {
//http协议内容中的CRLFprivate static final String CRLF = "\r\n";private final Socket client;Worker(Socket client) {this.client = client;}@Overridepublic void run() {//Web Server地址和端口号.String serverAddr = null;// 查看是否存在端口号,如果不存在使用默认端口号80int serverPort = 80;try {// java1.7特性之TWR(TWR参考文档:http://m.blog.csdn.net/article/details?id=50901195 )try (InputStream clientIs = client.getInputStream();OutputStream clientOs = client.getOutputStream();) {//默认只读取前100K,假定所有的头部都不超过100K就可以解析到Host头部.这块也是不好的地方byte[] data = new byte[102400];int length = -1;length = clientIs.read(data);String input = new String(data, 0, length);// 获取请求行,根据HTTP协议规范,第一行为请求行.String reqLine = input.substring(0, input.indexOf(CRLF));if(reqLine == null) {client.close();}System.out.println(reqLine);// 开始解析Web Server URL 和 端口号// 根据HTTP协议规范,请求行中包含: Method Request-URL// HTTP-Version,字段间以空格符隔开,并且末尾添加换行String[] params = reqLine.split(" ");String reqMethod = params[0];String reqUrl = params[1];String serverUrl = null;//解析远端主机.if (reqUrl.startsWith("/")) {int location = 0;while((location = input.indexOf(CRLF)) > 0) {String header = input.substring(0, location);// 根据HTTP协议规范,如果请求行中使用相对路径,则Web Server主机由Host头部决定.// 需要注意HTTP协议规范中是大小写不敏感的,所以需要使用equalsIgnoreCaseif (header.length() > 5 && header.substring(0, 5).equalsIgnoreCase("Host:")) {String host = header.substring("Host:".length(), header.length());serverUrl = host;break;}input = input.substring(location + CRLF.length());}} else {// 根据HTTP协议规范,如果请求行中使用绝对路径,则Web// Server主机是URL的一部分,且必须忽略Host头部.serverUrl = reqUrl;}// url: http://www.baidu.com/index.htmlif (serverUrl.indexOf("//") > 0) {serverUrl = serverUrl.substring(serverUrl.indexOf("//") + 2);}// 去除URL中的路径只保留域名: www.baidu.com/index.htmlif (serverUrl.indexOf("/") > 0) {serverUrl = serverUrl.substring(0, serverUrl.indexOf("/"));}// 开始获取Web Server地址和端口号.serverAddr = serverUrl;if (serverUrl.indexOf(":") > 0) {serverAddr = serverUrl.substring(0, serverUrl.indexOf(":"));serverPort = Integer.parseInt(serverUrl.substring(serverUrl.indexOf(":") + 1));}// 接下来开始真正的代理转发逻辑.try (Socket serverClient = new Socket(serverAddr, serverPort);InputStream serverIs = serverClient.getInputStream();OutputStream serverOs = serverClient.getOutputStream()) {System.out.println("local port is: " + serverClient.getLocalPort());//判断是否是CONNECT方法if(reqMethod.equalsIgnoreCase("CONNECT")) {String proxyResponse = "HTTP/1.1 200 Connection Established" + CRLF +"Proxy-agent: Anyone-BlindProxyServer/1.0" + CRLF +CRLF;clientOs.write(proxyResponse.getBytes());clientOs.flush();} else {//其他的HTTP方法,将读取到的数据原封不动的发送到Server端.serverOs.write(data, 0, length);serverOs.flush();}while (!client.isInputShutdown() || !serverClient.isInputShutdown()) {// 将客户端的输入流管道输入到server sock的输出流.//client to server.new PipeThread(client, serverClient, clientIs, serverOs).start();//server to clien.new PipeThread(serverClient, client, serverIs, clientOs).start();try {java.lang.Thread.sleep(2000);} catch (Exception e) {// TODO: handle exception}}}}} catch (Exception e) {try {client.close();} catch (Exception e2) {}System.out.println("failed to connect to server[" + serverAddr +"] : [" + serverPort + "]...");}}}class PipeThread extends Thread {private final Socket client;private final Socket server;private final InputStream is;private final OutputStream os;public PipeThread(Socket client, Socket server, InputStream is, OutputStream os) {this.client = client;this.server = server;this.is = is;this.os = os;}@Overridepublic void run() {try {byte[] data = new byte[102400];int length = -1;while (!client.isInputShutdown()) {if ((length = is.read(data)) != -1) {os.write(data,0,length);os.flush();} else {//client端已经关闭了输出流.server.shutdownOutput();client.shutdownInput();}}} catch (Exception e) {}}}

以上代码和文章标题所说的HTTP中继、隧道有什么关系?

1、中继的关系

首先以上代码就是一个简单中继实现,可以看到程序是通过建立一个套接字,然后读取数据,根据HTTP消息的格式,解析发送的HTTP请求是不是CONNECT方法,如果不是,直接通过到Server的套接字将请求原封不动的发送到HTTP Server,这不就是HTTP中继的概念吗(原封不动的将HTTP消息和响应在Client 和 Web Server之间传输);

2、那隧道呢:

以上代码会对收到的HTTP请求的方法进行判断,如果方法是CONNECT方法的话,会以代理的标准200响应返回客户端,然后在发送响应之后,通过套接字盲目的转发Socket中流入的任何数据,刚才也说了协议分层的概念:Socket(传输层),SSL(传输层和应用层之间),HTTP(应用层),所以Socket在最下层跟本不用关心接收到的是SSL握手相关数据、还是HTTP消息,因为这些数据都是在Client和Web Server端解析,中间只是个管道(代理就起到了一个管道的作用),我代理只用关心直接传数据就是了,在代理转发的层次(传输层)上层传输的数据啥格式格式啥的就不关我啥事了。

3、代理的关系

那和代理相关的内容呢:代码中也能体现啊,首先中继就是一个“傻瓜式”代理,你要不想做成傻瓜式的,你不都解析出来HTTP消息的各种头部了嘛,你可以做任何处理,偷偷缓存一份,下次Client请求时直接发给他(缓存代理),修改修改HTTP消息内容,然后再发送给Web Server(如:代理的匿名功能,把HTTP消息中能看到Client的任何数据都给删除了,如From 头部删除了,再给Web Server,这样就能简单的让Web Server就不能知道到底是谁在跟他通信,当然有其他机制还是能知道的)等等,所以所谓的代理的各种功能就是你逻辑上对这些数据的处理方式了,这种就看你想做什么逻辑了。


扩展部分:

1、知道为啥HTTPS的请求、响应代理没法给你缓存?

首先SSL层在HTTP应用层之下,传输的HTTP消息都是加密后的,所以你没法读出他的明文,然后按照HTTP协议的规范(什么第一行是请求行,接下来是头部啥的...)去解析他,甚至有可能SSL层之上传输的数据都不是HTTP消息,更别说解析了,要不怎么能称为隧道呢,这就是协议分层的好处,不用管你上层传输了什么数据;

2、为啥示例做了个盲中继,不做个啥可以匿名的代理啥的?

因为简单啊,中继的话直接把通过Socket从传输层将Client端的数据原封不动的传输到Server端就行了,这些数据解析什么的麻烦逻辑事就交给Client和Server端自己处理就好了嘛大笑,还是因为懒和逻辑处理太复杂绕不过来,所以美其名约我这个中继功能简单而又强大。。。


现实中的应用场景:

啰嗦了这么半天,我到底能干啥,就能理解个原理,我要他有个铲铲用,我也是现实中有需求所以才费劲吧唧的搞得,而且也是花了2天半时间匆匆搞出来的(所以有不对的地方,还请大侠多多指正)


1、这个局域网中只有一台主机能访问外网,大家都想访问网页咋办(当然办法多的是,现在说一个代理的方式),我写个HTTP代理放在那个人的机器上,又不耗费多少资源,然后别人将浏览器统一设置代理为那个人的HTTP代理,就OK了。


关于写代码过程中遇到的小坑:

1、Socket编程不熟,在取输入输出数据的时候使用了BufferedReaderBufferedWriter这个俩个东东,搞了一天半,这俩个东东对数据中的特殊符号"\n","\r\n"啥的有特殊处理,读出和写入的数据总会少那么一点,刚开始一直不知道为啥HTTP中继能够成功,为啥一到隧道就不行了呢?我的逻辑是完美的啊(程序员标准想法),结果在调试过程中浏览器无意中报出的一个CHUNK的编码的消息格式不对,赶紧抓了下包,发现HTTP头部回来的是正确的啊,但是没有Body,所以就猜测这俩个东东对特殊符做处理了,我写的数据他改动了,所以赶紧使用Stream直接发送Byte去查看了一番,发现果然HTTPS的也好使了,很开心(技术不熟很坑爹)。。。


另外最后补充一个小插曲:

1、之前编程过程中曾经有过使用ServletRequestgetRemoteAddr来获取client是地址,后来想了想不对啊,使用代理的话Socket是代理跟Web Server建立的啊,他是怎么获取的Client地址,后来仔细一看接口注释“Returns the fully qualified name of the client or the last proxy that sent the request.”,原来接口获取的是发送请求的那个实体的地址,估计底层就是靠Socket的获取远端地址的方式实现的,所以是谁跟Web Server建立的Socket连接,获取出来的地址就是谁的(这个就求大侠评论补充了)。


编写示例代码使用的TWR曾经参考过的博客:

http://m.blog.csdn.net/article/details?id=50901195

2 0
原创粉丝点击