最近帮别人做一个代理服务器,只是个小demo,代码发上来,共享一下。
代理服务器的原理就是接受客户端的请求,从HTTP头中提取出目标服务器的地址,然后构建客户端和目标服务器的通信管道,就可以实现代理的功能。当然真正的代理服务器还有很多工作要做,但是原理大抵如此。
本文实现的代理服务器采用多线程的方式,每个请求用单独地线程进行处理,并附带了dns缓存的功能,进一步加速代理服务器的处理效率。
代码目录结构如下:
其中,proxyd.java是服务的主程序,为每个request产生一个线程来单独处理。启动服务要设置服务的端口,如:
#java proxyd -p 8888
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import edu.npu.proxy.HttpProxyMainThread; public class proxyd { public static void main(String[] args) { if (args.length > 0 && args[0].equals("-p")) { int port = Integer.parseInt(args[1]); ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port); System.out.println("The proxy have start on port:" + port + "\n"); while (true) { Socket socket = null; try { socket = serverSocket.accept(); new HttpProxyMainThread(socket).start(); } catch (Exception e) { System.out.println("Thread start fail"); } } } catch (IOException e1) { System.out.println("proxyd start fail\n"); }finally{ try { serverSocket.close(); } catch (IOException e) { //e.printStackTrace(); } } }else{ System.out.println("parameter error"); } } }
HttpProxyMainThread是程序的主线程,这里完成主要工作。首先获取HTTP请求头中的第一行,从中提取出目标url,从url中提取出目标服务器的域名,端口信息等,将域名解析成IP,并缓存下来,然后通过提取出来的IP和端口,向目标主机建立连接,然后构建客户端和目标服务器的通信管道,其中一个线程负责从将客户端请求写到目标服务器,另一个将目标服务器的响应返回给客户端。
package edu.npu.proxy; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import edu.npu.utils.URL; public class HttpProxyMainThread extends Thread { static public int CONNECT_RETRIES = 5; // 尝试与目标主机连接次数 static public int CONNECT_PAUSE = 5; // 每次建立连接的间隔时间 static public int TIMEOUT = 50; // 每次尝试连接的最大时间 protected Socket csocket;// 与客户端连接的Socket public HttpProxyMainThread(Socket cs) { this.csocket = cs; } public void run() { String firstLine = ""; // http请求头第一行 String urlStr = ""; // 请求的url Socket ssocket = null;//与目标服务器连接的socket // cis为客户端输入流,sis为目标主机输入流 InputStream cis = null, sis = null; // cos为客户端输出流,sos为目标主机输出流 OutputStream cos = null, sos = null; try { csocket.setSoTimeout(TIMEOUT); cis = csocket.getInputStream(); cos = csocket.getOutputStream(); while (true) { int c = cis.read(); if (c == -1) break; // -1为结尾标志 if (c == '\r' || c == '\n') break;// 读入第一行数据,从中获取目标主机url firstLine = firstLine + (char) c; } urlStr = extractUrl(firstLine); System.out.println(urlStr); URL url = new URL(urlStr);//将url封装成对象,完成一系列转换工作,并在getIP中实现了dns缓存 firstLine = firstLine.replace(url.getScheme()+"://"+url.getHost(), "");//这一步很重要,把请求头的绝对路径换成相对路径 int retry = CONNECT_RETRIES; while (retry-- != 0) { try { ssocket = new Socket(url.getIP(), url.getPort()); // 尝试建立与目标主机的连接 System.out.println("+++++successfully connect to ("+url.getIP()+":"+url.getPort()+")(host:"+url.getHost()+")+++++,get resource("+url.getResource()+")"); break; } catch (Exception e) { System.out.println("-----fail connect to ("+url.getIP()+":"+url.getPort()+")(host:"+url.getHost()+")-----"); } // 等待 Thread.sleep(CONNECT_PAUSE); } if (ssocket != null) { ssocket.setSoTimeout(TIMEOUT); sis = ssocket.getInputStream(); sos = ssocket.getOutputStream(); sos.write(firstLine.getBytes()); // 将请求头写入 pipe(cis, sis, sos, cos); // 建立通信管道 } } catch (Exception e) { //e.printStackTrace(); } finally { try { csocket.close(); cis.close(); cos.close(); } catch (Exception e1) { } try { ssocket.close(); sis.close(); sos.close(); } catch (Exception e2) { } } } /** * 从http请求头的第一行提取请求的url * @param firstLine http请求头第一行 * @return url */ public String extractUrl(String firstLine) { String[] tokens = firstLine.split(" "); String URL = ""; for (int index = 0; index < tokens.length; index++) { if (tokens[index].startsWith("http://")) { URL = tokens[index]; break; } } return URL; } /** * 为客户机与目标服务器建立通信管道 * @param cis 客户端输入流 * @param sis 目标主机输入流 * @param sos 目标主机输出流 * @param cos 客户端输出流 */ public void pipe(InputStream cis, InputStream sis, OutputStream sos, OutputStream cos) { Client2ServerThread c2s = new Client2ServerThread(cis, sos); Server2ClientThread s2c = new Server2ClientThread(sis, cos); c2s.start(); s2c.start(); try { c2s.join(); s2c.join(); } catch (InterruptedException e1) { } } }
Client2ServerThread.java和Server2ClientThread.java就是通信管道的两个子线程。
package edu.npu.proxy import java.io.InputStream; import java.io.OutputStream; public class Client2ServerThread extends Thread{ private InputStream cis; private OutputStream sos; public Client2ServerThread(InputStream cis, OutputStream sos) { this.cis = cis; this.sos = sos; } public void run() { int length; byte bytes[] = new byte[1024]; while(true){ try { if ((length = cis.read(bytes)) > 0) { sos.write(bytes, 0, length);//将http请求头写到目标主机 sos.flush(); } else if (length < 0) break; } catch (Exception e) { //System.out.println("\nRequest Exception:"); //e.printStackTrace(); } } } }
package edu.npu.proxy; import java.io.InputStream; import java.io.OutputStream; public class Server2ClientThread extends Thread{ private InputStream sis; private OutputStream cos; public Server2ClientThread(InputStream sis, OutputStream cos) { this.sis = sis; this.cos = cos; } public void run() { int length; byte bytes[] = new byte[1024]; while(true){ try { if ((length = sis.read(bytes)) > 0) { cos.write(bytes, 0, length);//将http请求头写到目标主机 cos.flush(); } else if (length < 0) break; } catch (Exception e) { //System.out.println("\nRequest Exception:"); } } } }
URL.java是封装的一个工具类,可以将URL分成不同的部分,构造函数传一个url,自动将url转换成包含scheme, host, ip, port, resource等部分。其中getIP中实现了dns解析,并且利用了java提供的类实现了dns的缓存,并每30s更新一次dns缓存。
package edu.npu.utils; import java.net.InetAddress; import java.net.UnknownHostException; public class URL { private String scheme; private String host; private String IP; private String resource; private int port; public String getScheme() { return scheme; } public void setScheme(String schemes) { this.scheme = schemes; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getIP() { java.security.Security.setProperty("networkaddress.cache.ttl", "30"); try { this.IP = InetAddress.getByName(this.host).getHostAddress(); } catch (UnknownHostException e) { return ""; } return this.IP; } public String getResource() { return resource; } public String toString() { return "scheme:" + this.getScheme() + "\nhost:" + this.getHost() + "\nport:" + this.getPort() + "\nIP:"+this.getIP()+"\nResource:"+this.getResource(); } public URL(String url) { String scheme = "http"; String host = ""; String port = "80"; int index; // 抽取host index = url.indexOf("//"); if (index != -1) scheme = url.substring(0, index - 1); host = url.substring(index + 2); index = host.indexOf('/'); if (index != -1){ this.resource = host.substring(index); host = host.substring(0, index); } index = host.indexOf(':'); if (index != -1) { port = host.substring(index + 1); host = host.substring(0, index); } this.scheme = scheme; this.host = host; this.port = Integer.parseInt(port); } }
完整代码详见:https://github.com/sean-npu/JavaHttpProxy
转载请注明出处fullstackdevel.com:SEAN是一只程序猿 » Java实现多线程代理服务器-JavaHttpProxy
运行的截图是啥样的
为什么执行proxyd的第八行会挂起,运行结果直接是parameter error
你没有配置端口吧?
麻烦问下着端口怎么配置,因为初学比较水,谢谢啊
#java proxyd -p 8888大神,这个在哪里设置
在命令行下执行
命令行下?体会不来,能否具体点啊
你编的这个如何可以实现代理访问,也就是如何掩饰?比如说不启动代理的话,客户端访问不了目标服务器,启动代理的话就可以访问。
先在命令行下把服务跑起来,然后在浏览器里设置你的代理服务器是本地的地址就行了