Cómo implementar un servidor proxy usando JAVA
Los servidores proxy son muy utilizados. Por ejemplo, dentro de la red corporativa, se puede utilizar para controlar el contenido de Internet que los empleados navegan mientras trabajan y evitar que los empleados accedan a ciertos tipos de contenido o ciertos sitios web designados. En realidad, el servidor proxy desempeña el papel de intermediario entre el navegador y el servidor web. Puede realizar diversos procesamientos en las solicitudes del navegador, filtrar anuncios y cookies y extraer previamente páginas web para aumentar la velocidad de acceso del navegador a las páginas. etc.
1. Conocimientos básicos
No importa de qué manera se utilice el servidor proxy, el proceso de monitoreo de la transmisión HTTP es siempre el siguiente:
Paso 1: Interno navegación El servidor envía una solicitud al servidor proxy. La primera línea de la solicitud contiene la URL de destino.
Paso 2: El servidor proxy lee la URL y reenvía la solicitud al servidor de destino apropiado.
Paso 3: el servidor proxy recibe la respuesta de la máquina de destino de Internet y la reenvía al navegador interno apropiado.
Por ejemplo, supongamos que un empleado de una empresa intenta acceder al sitio web www.cn.ibm.com. Si no hay un servidor proxy, el Socket abierto por el navegador del empleado conduce al servidor web que ejecuta el sitio web, y los datos devueltos por el servidor web también se pasan directamente al navegador del empleado. Si el navegador está configurado para utilizar un servidor proxy, la solicitud llega primero al servidor proxy; luego, el servidor proxy extrae la URL de destino de la primera línea de la solicitud y abre un socket que conduce a www.cn.ibm.com. Cuando www.cn.ibm.com devuelve una respuesta, el servidor proxy reenvía la respuesta al navegador del empleado.
Por supuesto, los servidores proxy no sólo son adecuados para entornos corporativos. Como desarrollador, tener su propio servidor proxy es una gran cosa. Por ejemplo, podemos utilizar un servidor proxy para analizar el proceso de interacción entre el navegador y el servidor web. Esta característica es útil al probar y solucionar problemas en aplicaciones web. Incluso podemos utilizar varios servidores proxy al mismo tiempo (la mayoría de los servidores proxy permiten vincular varios servidores entre sí). Por ejemplo, podemos tener un servidor proxy empresarial, además de un servidor proxy escrito en Java para depurar aplicaciones. Pero cabe señalar que cada servidor de la cadena de servidores proxy tendrá un cierto impacto en el rendimiento.
2. Planificación del diseño
Como su nombre indica, el servidor proxy es solo un servidor especial. Como la mayoría de los servidores, los servidores proxy deberían usar subprocesos si quieren manejar múltiples solicitudes. Este es el plan básico para un servidor proxy:
Esperando solicitudes de clientes (navegadores web).
Iniciar un nuevo hilo para manejar las solicitudes de conexión de los clientes.
Lea la primera línea de la solicitud del navegador (el contenido de esta línea contiene la URL de destino de la solicitud).
Analiza la primera línea de la solicitud y obtiene el nombre y puerto del servidor de destino.
Abra un socket en el servidor de destino (o en el siguiente servidor proxy, si corresponde).
Envía la primera línea de la solicitud al Socket de salida.
Envía el resto de la solicitud al Socket de salida.
Envía los datos devueltos por el servidor web de destino al navegador solicitante.
Por supuesto, si se consideran los detalles, la situación será más complicada. De hecho, hay dos cuestiones principales a considerar aquí: primero, leer datos línea por línea desde el Socket es más adecuado para su posterior procesamiento, pero esto creará un cuello de botella en el rendimiento; segundo, la conexión entre los dos Sockets debe ser eficiente; Hay varias formas de lograr ambos objetivos, pero cada una tiene sus propios costos. Por ejemplo, si desea filtrar los datos a medida que llegan, es mejor leerlos fila por fila; sin embargo, la mayoría de las veces, es más eficiente reenviar los datos tan pronto como llegan al servidor proxy; Además, también se pueden utilizar varios subprocesos independientes para enviar y recibir datos, pero crear y eliminar una gran cantidad de subprocesos también provocará problemas de rendimiento. Por lo tanto, para cada solicitud, usaremos un hilo para manejar la recepción y el envío de datos, y cuando los datos lleguen al servidor proxy, los reenviaremos lo más rápido posible.
3. Ejemplo
En el proceso de escribir este servidor proxy en Java, es importante prestar atención a la reutilización. Porque de esta manera, podemos reutilizar fácilmente el servidor proxy cuando queramos manejar las solicitudes del navegador de manera diferente en otro proyecto. Por supuesto, debemos prestar atención al equilibrio entre flexibilidad y eficiencia.
La Figura 1 muestra la interfaz de salida de la instancia del servidor proxy (HttpProxy.java) en este artículo. Cuando el navegador accede a /, el servidor proxy envía la URL solicitada por el navegador al dispositivo de registro predeterminado (es decir. , la pantalla del dispositivo de salida estándar). La Figura 2 muestra el resultado de SubHttpProxy. SubHttpProxy es una extensión simple de HttpProxy.
Figura 1
Figura 2
Para construir un servidor proxy, derivé la clase HttpProxy de la clase base Thread (el código que aparece en el El texto del artículo es esta clase. Algunos fragmentos, descargue el código completo desde el final de este artículo). La clase HttpProxy contiene algunas propiedades utilizadas para personalizar el comportamiento del servidor proxy; consulte el Listado 1 y la Tabla 1.
Listado 1
/********************************* *** ****
* Una clase de servidor proxy básico
*********************** ** ************
*/
importar java.net.*;
importar java.io.* ;
clase pública HttpProxy extiende el hilo {
static public int CONNECT_RETRIES=5;
static public int CONNECT_PAUSE=5;
static public int TIME-OUT=50;
static public int BUFSIZ=1024;
registro booleano público estático = false;
static public OutputStream log=null;
p>// Socket para datos entrantes
socket de socket protegido;
// Servidor proxy de nivel superior, opcional
estático cadena privada padre = nulo;
static private int parentPort=-1;
static public void setParentProxy(nombre de cadena, int pport) {
parent=nombre ;
p>parentPort=pport;
}
// Crea un hilo proxy en el Socket dado.
public HttpProxy(Socket s) { socket=s; start();
public void writeLog(int c, navegador booleano) lanza IOException {
log.write(c);
}
public void writeLog(byte[] bytes,int offset,
int len, navegador booleano) lanza IOException {
for (int i=0;i } // De forma predeterminada, la información de registro se envía a // El dispositivo de salida estándar, // Las clases derivadas pueden anularlo public String ProcessHostName(String url, host de cadena, puerto int, calcetín de socket) { java.text.DateFormat cal=java.text.DateFormat.getDateTimeInstance(); System.out.println(cal formato(nuevo java.util.Date()) + " - " + url + " " + sock.getInetAddress()+" retorno. host ; } Tabla 1 Variable/Descripción del método CONNECT_RETRIES Número de intentos de conectarse al host remoto antes de darse por vencido . CONNECT_PAUSE Tiempo de pausa entre intentos de conexión. TIME-OUT Tiempo de espera para la entrada del Socket. BUFSIZ Tamaño del buffer de entrada del socket. registro Si se requiere que el servidor proxy registre todos los datos transmitidos en el registro (verdadero significa "sí"). log es un objeto OutputStream al cual la rutina de registro predeterminada generará información de registro. setParentProxy se utiliza para vincular un servidor proxy a otro servidor proxy (debe especificar el nombre y el puerto del otro servidor). Después de que el servidor proxy se conecta al servidor web, utilizo un bucle simple para pasar datos entre los dos Sockets. Puede surgir un problema aquí, es decir, si no hay datos operables, llamar al método de lectura puede hacer que el programa se bloquee y, por lo tanto, se cuelgue. Para evitar este problema, configuro el tiempo de espera del Socket usando el método setSoTimeout (ver Listado 2). De esta manera, si un Socket no está disponible, otro todavía tiene la posibilidad de procesarlo y no tengo que crear un hilo nuevo. Listado 2 // El hilo que realiza la operación public void run() { String line; Host de cadena; int port=80; Socket saliente=null; prueba { socket.setSoTimeout (TIMEOUT ); InputStream es=socket.getInputStream(); OutputStream os=null; prueba { // Obtener contenido de línea de solicitud line=""; host=""; int state=0; espacio booleano; while (true) { int c=is.read(); if (c==-1) break; if (logging) writeLog(c,true); space=Character.isWhitespace((char)c switch (estado) { caso 0: si (espacio) continúa estado=1; caso 1: si (espacio) { estado=2; continuar; } línea=línea+(char)c; break ; caso 2: if (espacio) continuar // Saltar varios caracteres de espacio en blanco state=3; case 3: if (espacio) { state=4; // Solo analiza la parte del nombre del host String host0= host; int n; n=host.indexOf("//"); if (n!=-1) host=host. subcadena(n+2); n=host.indexOf('/'); if (n!=-1) host=ho st.substring(0,n); // Analiza los posibles números de puerto n=host.indexOf(":"); if (n !=-1) { puerto=Integer.parseInt(host.substring(n+1)); host=host.substring(0,n); } host=processHostName(host0,host,puerto,socket); if (parent!=null) { host =parent ; port=parentPort; } int retry=CONNECT_RETRIES; while (retry--!=0) { intente { saliente=nuevo Socket(host,puerto); break; } catch (Excepción e) { } // Espera Thread.sleep(CONNECT_PAUSE); } if (outbound==null) break; saliente.setSoTimeout(TIMEOUT); os=outbound.getOutputStream(); os.write(line.getBytes()); os.write(' '); os.write(host0.getBytes()); os.write(' '); tubería (is,outbound.getInputStream(),os,socket.getOutputStream()); break; } host=host+(char) c; descanso; } } } captura (IOException e) { } } captura (Excepción e) { }
");
p> finalmente {
intentar { socket.close();} capturar (Excepción e1) {}
intentar { outbound.close();} capturar (Excepción e2) {}
}
}
Como todos los objetos de hilo, el trabajo principal de la clase HttpProxy se completa dentro del método de ejecución (ver Listado 2). El método de ejecución implementa una máquina de estado simple que lee caracteres del navegador web, un carácter a la vez, y continúa este proceso hasta que haya suficiente información para encontrar el servidor web de destino. Luego, ejecutar abre un Socket para el servidor web (si hay varios servidores proxy encadenados, el método de ejecución abre un Socket para el siguiente servidor proxy de la cadena). Después de abrir el Socket, ejecutar primero escribe parte de la solicitud en el Socket y luego llama al método de tubería. El método pipe realiza directamente operaciones de lectura y escritura entre dos Sockets a la velocidad más rápida.
Si el tamaño de los datos es grande, crear un hilo adicional puede ser más eficiente; sin embargo, cuando el tamaño de los datos es pequeño, la sobrecarga requerida para crear un nuevo hilo compensará los beneficios que aporta.
El Listado 3 muestra un método principal muy simple que se puede utilizar para probar la clase HttpProxy. La mayor parte del trabajo se realiza mediante un método startProxy estático (consulte el Listado 4). Este método utiliza una técnica especial que permite a un miembro estático crear una instancia de la clase HttpProxy (o una subclase de la clase HttpProxy). Su idea básica es: pasar un objeto Class a la clase startProxy; luego, el método startProxy utiliza la API Reflection y el método getDeclaredConstructor para determinar qué constructor del objeto Class acepta un parámetro Socket; finalmente, el método startProxy llama al método newInstance Create; el objeto Clase.
Listado 3
// Método principal simple para probar
static public void main(String args[]) {
System. out.println("Iniciar servidor proxy en el puerto 808\n");
HttpProxy.log=System.out;
HttpProxy.logging=false;
HttpProxy.startProxy(808,HttpProxy.class);
}
}
Listado 4
startProxy estático público vacío( int puerto, clase clobj) {
ServerSocket ssock;
Socket calcetín;
prueba {
ssock=new ServerSocket(puerto);
while (true) {
Clase [] sarg = nueva Clase[1];
Objeto [] arg= nuevo Objeto[1];
p>
sarg[0]=Socket.class;
prueba {
java.lang.reflect.Constructor cons = clobj.getDeclaredConstructor(sarg);
arg[0]=ssock.accept();
cons.newInstance(arg); // Crea una instancia de HttpProxy o su clase derivada
} catch (Exception) e) {
Socket esock = (Socket)arg[0];
prueba { esock.close() } catch (Exception ec) {}
}
}
} captura (IOException e) {
}
}
Utilice esta tecnología , podemos ampliar la clase HttpProxy sin crear una versión personalizada del método startProxy. Para obtener un objeto Class para una clase determinada, simplemente agregue .class al nombre normal (si hay una instancia de un objeto, llame al método getClass). Dado que pasamos el objeto Class al método startProxy, no tenemos que modificar startProxy al crear una clase derivada de HttpProxy. (El código de descarga incluye un servidor proxy simple derivado).
Conclusión
Hay dos formas de utilizar clases derivadas para personalizar o ajustar el comportamiento del servidor proxy: modificar el nombre del host o capturar todos los datos que pasan a través del servidor proxy. . El método ProcessHostName permite que el servidor proxy analice y modifique el nombre del host. Si el registro está habilitado, el servidor proxy llama al método writeLog para cada carácter que pasa por el servidor. Lo que hagamos con esta información depende totalmente de nosotros: podemos escribirla en un archivo de registro, enviarla a la consola o cualquier otro procesamiento que se adapte a nuestras necesidades.
Un indicador booleano en la salida de writeLog indica si los datos provienen del navegador o del servidor web.
Como muchas herramientas, los servidores proxy en sí no son buenos ni malos. La clave está en cómo los usas. Los servidores proxy se pueden utilizar para invadir la privacidad, pero también pueden bloquear miradas indiscretas y proteger la red. Incluso si el servidor proxy y el navegador no están en la misma máquina, me gusta pensar en el servidor proxy como una forma de ampliar la funcionalidad del navegador. Por ejemplo, se podría utilizar un servidor proxy para comprimir datos antes de enviarlos al navegador; futuros servidores proxy podrían incluso traducir páginas de un idioma a otro... las posibilidades son infinitas.