用Java开发代理服务器

一、简介

    代理服务器的应用非常广泛。比如,在企业网内部,它可以用来控制员工在工作时浏览的Internet内容,阻止员工访问某些类型的内容或某些指定的网站。代理服务器实际上扮演着浏览器和Web服务器之间的中间人的角色,能够对浏览器请求进行各种各样的处理,能够过滤广告和Cookie,能够预先提取Web页面,使得浏览器访问页面的速度更快,等等。

二、基础知识

    不管以哪种方式应用代理服务器,其监控HTTP传输的过程总是如下:

    · 步骤一:内部的浏览器发送请求给代理服务器。请求的第一行包含了目标URL

    · 步骤二:代理服务器读取该URL,并把请求转发给合适的目标服务器。

    · 步骤三:代理服务器接收来自Internet目标机器的应答,把应答转发给合适的内部浏览器。

    例如,假设有一个企业的雇员试图访问www.cn.ibm.com网站。如果没有代理服务器,雇员的浏览器打开的Socket通向运行这个网站的Web服务器,从Web服务器返回的数据也直接传递给雇员的浏览器。如果浏览器被配置成使用代理服务器,则请求首先到达代理服务器;随后,代理服务器从请求的第一行提取目标URL,打开一个通向www.cn.ibm.comSocket。当www.cn.ibm.com返回应答时,代理服务器把应答转发给雇员的浏览器。

三、设计规划

    正如其名字所示,代理服务器只不过是一种特殊的服务器。和大多数服务器一样,如果要处理多个请求,代理服务器应该使用线程。下面是一个代理服务器的基本规划:

    1 、等待来自客户(Web浏览器)的请求。

    2 、启动一个新的线程,以处理客户连接请求。

    3 、读取浏览器请求的第一行(该行内容包含了请求的目标URL)。

    4 、分析请求的第一行内容,得到目标服务器的名字和端口。

    5 、打开一个通向目标服务器(或下一个代理服务器,如合适的话)的Socket

    6 、把请求的第一行发送到输出Socket

    7 、把请求的剩余部分发送到输出Socket

    8 、把目标Web服务器返回的数据发送给发出请求的浏览器。

    当然,如果考虑细节的话,情况会更复杂一些。实际上,这里主要有两个问题要考虑:

    第一,从Socket按行读取数据最适合进一步处理,但这会产生性能瓶颈

    第二,两个Socket之间的连接必需高效。

    有几种方法可以实现这两个目标,但每一种方法都有各自的代价。例如,如果要在数据进入的时候进行过滤,这些数据最好按行读取;然而,大多数时候,当数据到达代理服务器时,立即把它转发出去更适合高效这一要求。另外,数据的发送和接收也可以使用多个独立的线程,但大量地创建和拆除线程也会带来性能问题。因此,对于每一个请求,我们将用一个线程处理数据的接收和发送,同时在数据到达代理服务器时,尽可能快速地把它转发出去。

四、实例

     图一显示了本文代理服务器实例(HttpProxy.java)的输出界面,当浏览器访问http://www-900.ibm.com/cn/时,代理服务器向默认日志设备(即标准输出设备屏幕)输出浏览器请求的URL。图二显示了SubHttpProxy的输出。SubHttpProxyHttpProxy的一个简单扩展。
图一


 图二


    为了构造代理服务器,我从Thread基类派生出了HttpProxy类(文章正文中出现的代码是该类的一些片断,完整的代码请从本文最后下载)。HttpProxy类包含了一些用来定制代理服务器行为的属性,参见Listing 1和表一。

【Listing 1】
/*************************************
 * 一个基础的代理服务器类
 *************************************
 */
import java.net.*;
import java.io.*;

public class HttpProxy extends Thread {
	static public int CONNECT_RETRIES = 5;
	static public int CONNECT_PAUSE = 5;
	static public int TIMEOUT = 50;
	static public int BUFSIZ = 1024;
	static public boolean logging = false;
	static public OutputStream log = null;
	// 传入数据用的Socket
	protected Socket socket;
	// 上级代理服务器,可选
	static private String parent = null;
	static private int parentPort = -1;

	static public void setParentProxy(String name, int pport) {
		parent = name;
		parentPort = pport;
	}

	// 在给定Socket上创建一个代理线程。
	public HttpProxy(Socket s) {
		socket = s;
		start();
	}

	public void writeLog(int c, boolean browser) throws IOException {
		log.write(c);
	}

	public void writeLog(byte[] bytes, int offset, int len, boolean browser)
			throws IOException {
		for (int i = 0; i < len; i++)
			writeLog((int) bytes[offset + i], browser);
	}

	// 默认情况下,日志信息输出到
	// 标准输出设备
	// 派生类可以覆盖它
	public String processHostName(String url, String host, int port, Socket sock) {
		java.text.DateFormat cal = java.text.DateFormat.getDateTimeInstance();
		System.out.println(cal.format(new java.util.Date()) + " - " + url + " "
				+ sock.getInetAddress() + "\n");
		return host;
	}

表一

变量/方法

说明

CONNECT_RETRIES

在放弃之前尝试连接远程主机的次数。

CONNECT_PAUSE

在两次连接尝试之间的暂停时间。

TIME-OUT

等待Socket输入的等待时间。

BUFSIZ

Socket输入的缓冲大小。

logging

是否要求代理服务器在日志中记录所有已传输的数据(true表示)。

log

一个OutputStream对象,默认日志例程将向该OutputStream对象输出日志信息。

setParentProxy

用来把一个代理服务器链接到另一个代理服务器(需要指定另一个服务器的名称和端口)。

    当代理服务器连接到Web服务器之后,我用一个简单的循环在两个Socket之间传递数据。这里可能出现一个问题,即如果没有可操作的数据,调用read方法可能导致程序阻塞,从而挂起程序。为防止出现这个问题,我用setSoTimeout方法设置了Socket的超时时间(参见Listing 2)。这样,如果某个Socket不可用,另一个仍旧有机会进行处理,我不必创建一个新的线程。

【Listing 2】 
   // 执行操作的线程
	public void run() {
		String line;
		String host;
		int port = 80;
		Socket outbound = null;
		try {
			socket.setSoTimeout(TIMEOUT);
			InputStream is = socket.getInputStream();
			OutputStream os = null;
			try {
				// 获取请求行的内容
				line = "";
				host = "";
				int state = 0;
				boolean space;
				while (true) {
					int c = is.read();
					if (c == -1)
						break;
					if (logging)
						writeLog(c, true);
					space = Character.isWhitespace((char) c);
					switch (state) {
					case 0:
						if (space)
							continue;
						state = 1;
					case 1:
						if (space) {
							state = 2;
							continue;
						}
						line = line + (char) c;
						break;
					case 2:
						if (space)
							continue; // 跳过多个空白字符
						state = 3;
					case 3:
						if (space) {
							state = 4;
							// 只取出主机名称部分
							String host0 = host;
							int n;
							n = host.indexOf("//");
							if (n != -1)
								host = host.substring(n + 2);
							n = host.indexOf('/');
							if (n != -1)
								host = host.substring(0, n);
							// 分析可能存在的端口号
							n = host.indexOf(":");
							if (n != -1) {
								port = Integer.parseInt(host.substring(n + 1));
								host = host.substring(0, n);
							}
							host = processHostName(host0, host, port, socket);
							if (parent != null) {
								host = parent;
								port = parentPort;
							}
							int retry = CONNECT_RETRIES;
							while (retry-- != 0) {
								try {
									outbound = new Socket(host, port);
									break;
								} catch (Exception e) {
								}
								// 等待
								Thread.sleep(CONNECT_PAUSE);
							}
							if (outbound == null)
								break;
							outbound.setSoTimeout(TIMEOUT);
							os = outbound.getOutputStream();
							os.write(line.getBytes());
							os.write(' ');
							os.write(host0.getBytes());
							os.write(' ');
							pipe(is, outbound.getInputStream(), os, socket
									.getOutputStream());
							break;
						}
						host = host + (char) c;
						break;
					}
				}
			} catch (IOException e) {
			}

		} catch (Exception e) {
		} finally {
			try {
				socket.close();
			} catch (Exception e1) {
			}
			try {
				outbound.close();
			} catch (Exception e2) {
			}
		}
	}
    和所有线程对象一样, HttpProxy 类的主要工作在 run 方法内完成(见 Listing 2 )。 run 方法实现了一个简单的状态机,从 Web 浏览器每次一个读取字符,持续这个过程直至有足够的信息找出目标 Web 服务器。然后, run 打开一个通向该 Web 服务器的 Socket (如果有多个代理服务器被链接在一起,则 run 方法打开一个通向链里面下一个代理服务器的 Socket )。打开 Socket 之后, run 先把部分的请求写入 Socket ,然后调用 pipe 方法。 pipe 方法直接在两个 Socket 之间以最快的速度执行读写操作。

    如果数据规模很大,另外创建一个线程可能具有更高的效率;然而,当数据规模较小时,创建新线程所需要的开销会抵消它带来的好处。

    Listing 3显示了一个很简单的main方法,可以用来测试HttpProxy类。大部分的工作由一个静态的startProxy方法完成(见Listing 4)。这个方法用到了一种特殊的技术,允许一个静态成员创建HttpProxy类(或HttpProxy类的子类)的实例。它的基本思想是:把一个Class对象传递给startProxy类;然后,startProxy方法利用映像APIReflection API)和getDeclaredConstructor方法确定该Class对象的哪一个构造函数接受一个Socket参数;最后,startProxy方法调用newInstance方法创建该Class对象。

【Listing 3】
// 测试用的简单main方法
	static public void main(String args[]) {
		System.out.println("在端口808启动代理服务器\n");
		HttpProxy.log = System.out;
		HttpProxy.logging = true;
		HttpProxy.startProxy(808, HttpProxy.class);
	}

【Listing 4】
	static public void startProxy(int port, Class clobj) {
		ServerSocket ssock;
		Socket sock;
		try {
			ssock = new ServerSocket(port);
			while (true) {
				Class[] sarg = new Class[1];
				Object[] arg = new Object[1];
				sarg[0] = Socket.class;
				try {
					java.lang.reflect.Constructor cons = clobj
							.getDeclaredConstructor(sarg);
					arg[0] = ssock.accept();
					cons.newInstance(arg); // 创建HttpProxy或其派生类的实例
				} catch (Exception e) {
					Socket esock = (Socket) arg[0];
					try {
						esock.close();
					} catch (Exception ec) {
					}
				}
			}
		} catch (IOException e) {
		}
	}
    利用这种技术,我们可以在不创建 startProxy 方法定制版本的情况下,扩展 HttpProxy 类。要得到给定类的 Class 对象,只需在正常的名字后面加上 .class (如果有某个对象的一个实例,则代之以调用 getClass 方法)。由于我们把 Class 对象传递给了 startProxy 方法,所以创建 HttpProxy 的派生类时,就不必再特意去修改 startProxy 。(下载代码中包含了一个派生得到的简单代理服务器)。

五、结束语

    利用派生类定制或调整代理服务器的行为有两种途径:修改主机的名字,或者捕获所有通过代理服务器的数据。processHostName方法允许代理服务器分析和修改主机名字。如果启用了日志记录,代理服务器为每一个通过服务器的字符调用writeLog方法。如何处理这些信息完全由我们自己决定――可以把它写入日志文件,可以把它输出到控制台,或进行任何其他满足我们要求的处理。writeLog输出中的一个Boolean标记指示出数据是来自浏览器还是Web主机。

    和许多工具一样,代理服务器本身并不存在好或者坏的问题,关键在于如何使用它们。代理服务器可能被用于侵犯隐私,但也可以阻隔偷窥者和保护网络。即使代理服务器和浏览器不在同一台机器上,我也乐意把代理服务器看成是一种扩展浏览器功能的途径。例如,在把数据发送给浏览器之前,可以用代理服务器压缩数据;未来的代理服务器甚至还可能把页面从一种语言翻译成另一种语言……可能性永无止境。

    请从这里下载本文代码: JavaProxyServer_code.zip

     转载自:http://www.ibm.com/developerworks/cn/java/l-javaproxy/ 


基本原理: 代理服务器打开一个端口接收浏览器发来的访问某个站点的请求,从请求的字符串中解析出用户想访问哪个网页,让后通过URL对象建立输入流读取相应的网页内容,最后按照web服务器的工作方式将网页内容发送给用户浏览器 源程序: import java.net.*; import java.io.*; public class MyProxyServer { public static void main(String args[]) { try { ServerSocket ss=new ServerSocket(8080); System.out.println("proxy server OK"); while (true) { Socket s=ss.accept(); process p=new process(s); Thread t=new Thread(p); t.start(); } } catch (Exception e) { System.out.println(e); } } }; class process implements Runnable { Socket s; public process(Socket s1) { s=s1; } public void run() { String content=" "; try { PrintStream out=new PrintStream(s.getOutputStream()); BufferedReader in=new BufferedReader(new InputStreamReader(s.getInputStream())); String info=in.readLine(); System.out.println("now got "+info); int sp1=info.indexOf(' '); int sp2=info.indexOf(' ',sp1+1); String gotourl=info.substring(sp1,sp2); System.out.println("now connecting "+gotourl); URL con=new URL(gotourl); InputStream gotoin=con.openStream(); int n=gotoin.available(); byte buf[]=new byte[1024]; out.println("HTTP/1.0 200 OK"); out.println("MIME_version:1.0"); out.println("Content_Type:text/html"); out.println("Content_Length:"+n); out.println(" "); while ((n=gotoin.read(buf))>=0) { out.write(buf,0,n); } out.close(); s.close(); } catch (IOException e) { System.out.println("Exception:"+e); } } };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值