HTTP代理服务器的实现

领悟 专栏收录该内容
1 篇文章 1 订阅

接下来都是我对HTTP代理服务器的理解。

HTTP代理服务(proxy server) 器就是客户端也是服务端,是一个事务处理的中间人,就像下图所展示的一样,
这里写图片描述
图片来源于《HTTP权威指南》

代理服务器的作用有很多,例如:儿童过滤器、文档访问控制、安全防火墙、web缓存等等。

以下,我用最基础的代码写一个简易的HTTP代理服务器,最后还有相关的解说,有助于更容易理解HTTP代理服务器的机制。

package testproxy;
import java.lang.reflect.Constructor;
import java.net.*;
import java.io.*;
public class TextHttpProxy extends Thread {

    //操作实现代理服务器的类
    static public int RETRIES = 5;
    //在放弃之前尝试连接远程主机的次数
    static public int PAUSE = 5;
    //在两次连接尝试之间的暂停时间
    static public int TIMEOUT = 50;
    //等待Socket输入的等待时间
    static public int BUFSIZ = 1024;
    //输入的缓冲大小
    static public boolean logging = false;
    //是否要求代理服务器在日志中记录所有已传输的数据
    static public OutputStream log = null;
    //默认日志例程将向该OutputStream对象输出日志信息
    protected Socket socket;
    // 传入数据用的Socket
    static private String parent = null;
    //上级代理服务器
    static private int parentPort = -1;
    //  用来把一个代理服务器链接到另一个代理服务器(需要指定另一个服务器的名称和端口)。  
    static public void setParentProxy(String name, int port) {

        parent = name;
        parentPort = port;

    }
    public TextHttpProxy(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 printLog(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;

    }
    public void run() {

        // 执行操作的线程
        String line;
        String host;
        int port = 80;
        //默认端口为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 = printLog(host0, host, port, socket);
                            //获得网站域名

                            if (parent != null) {

                                host = parent;
                                port = parentPort;

                            }
                            int retry = RETRIES;
                            while (retry-- != 0) {

                                try {

                                    outbound = new Socket(host, port);
                                    //创建连接对象,通向目标服务器
                                    break;

                                } catch (Exception e) {

                                    System.out.println("无法创建连接:"+e.getMessage());

                                }
                                Thread.sleep(PAUSE);
                                //设置线程等待

                            }
                            if (outbound == null)
                                break;
                            outbound.setSoTimeout(TIMEOUT);
                            //设置超时时间,防止read方法导致的组赛
                            os = outbound.getOutputStream();
                            //获得输出流对象
                            os.write(line.getBytes());
                            //将信息写入流
                            os.write(' ');
                            os.write(host0.getBytes());
                            //将信息写入流
                            os.write(' ');
                            writeInfo(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) {

            }
        }
    }
    void writeInfo(InputStream is0, InputStream is1, OutputStream os0,
            OutputStream os1) throws IOException {

        //读取流中信息写入日志
        try {

            int ir;
            byte bytes[] = new byte[BUFSIZ];
            //创建字节数组,大小:1024
            //也是定影socket缓冲区的大小
            while (true) {

                try {

                    if ((ir = is0.read(bytes)) > 0) {

                        //判断读取输入流的信息
                        os0.write(bytes, 0, ir);
                        //将读取的数据写入输出流对象中

                        if (logging)
                            writeLog(bytes, 0, ir, true);
                        //写入日志

                    } else if (ir < 0)
                        //读取完毕
                        break;
                } catch (InterruptedIOException e) {

                    //捕获中断IO流异常

                }
                try {

                    if ((ir = is1.read(bytes)) > 0) {

                        //判断读取输入流的信息
                        os1.write(bytes, 0, ir);
                        //将读取的数据写入输出流对象中
                        if (logging)
                            writeLog(bytes, 0, ir, false);
                        //写入日志

                    } else if (ir < 0)
                        //读取完毕
                        break;

                } catch (InterruptedIOException e) {

                    //捕获中断IO流异常

                }
            }
        } catch (Exception e0) {

            //捕获异常
        }
    }


    static public void proxyStart(int port, Class<TextHttpProxy> clobj) {

        ServerSocket serverSocket;
        try {

            serverSocket = new ServerSocket(port);
            //根据端口创建服务器端Socket对象
            while (true) {

                Class[] objClass = new Class[1];
                //创建类数组,大小为1
                Object[] obj = new Object[1];
                //创建对象数组,大小为1
                objClass[0] = Socket.class;
                //添加Socket类
                try {

                    Constructor cons = clobj.getDeclaredConstructor(objClass);
                    //创建代理服务器实例
                    obj[0] = serverSocket.accept();
                    //挂起等待客户的请求
                    cons.newInstance(obj); 
                    // 创建TextHttpProxy或其派生类的实例  创建传入类

                } catch (Exception e) {

                    Socket socket = (Socket) obj[0];
                    //对象强制转换
                    try {

                        socket.close();
                        //释放资源

                    } catch (Exception ec) {

                    }

                }

            }

        } catch (IOException e) {

        }
    }
    static public void main(String args[]) {

        System.out.println("HTTP代理服务器已经成功启动!");
        TextHttpProxy.log = System.out;
        //日志信息输出到控制台
        TextHttpProxy.logging = false;
        TextHttpProxy.proxyStart(9080, TextHttpProxy.class);

        //调用方法
    }

}

代码解说:

开启一个服务器Socket 监听指定端口的请求,同时,代理服务器挂起等待客户端的请求。服务器的Socket监听到连接请求时,则开启一个新的线程处理这个连接请求,服务器的Socket 再次进入监听状态。
在连接线程时,代理服务器接受来自客户端的请求,并执行操作的线程,设置超时时间,解析客户端Host头域里面的值,获取目标web服务器地址,分析可能存在的端口号,建立socket将请求发送到远程服务器,然后将响应报文转发回原socket。最后把 socket 关闭,线程销毁。

写一个简单的main方法来进行代理服务器测试,并将日志信息显示在控制台。

代码解说图:
这里写图片描述

以下图片是测试效果:

使用firefox浏览器,手动设置代理服务器
这里写图片描述

运行代码,开启代理服务器,输入HTTP协议的URI,如图

这里写图片描述

能进入这个网站,说明你的代理成功了!

在控制台会输出以下信息
这里写图片描述

在这个实现HTTP代理服务器的过程中,socket 是非常重要的,它也叫做套接字。如下是它的作用:

socket (套接字)之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认

服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

如果不太懂HTTP和socket之间的联系,那就用传说中的比喻来说说:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

如果以后懂得更多,会及时补充~

  • 5
    点赞
  • 7
    评论
  • 39
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值