HTTP代理服务器的实现

2023-05-16

接下来都是我对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是发动机,提供了网络通信的能力。

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HTTP代理服务器的实现 的相关文章

随机推荐

  • APM_ArduCopter源码解析学习(四)——IMU

    APM ArduCopter源码解析学习 xff08 四 xff09 IMU 前言一 system cpp 1 1 无人机内部初始化1 2 Copter init ardupilot 1 3 Copter startup INS groun
  • 查看当前系统的glibc版本

    有时我们经常需要查看当前系统的glibc版本 xff0c 可以这样查看 lib libc so 6 有时 lib x86 64 linux libc so 6 把这个文件当命令执行一下 为什么这个库可以直接run呢 xff1f 原来在lib
  • Java多线程学习三:有哪几种实现生产者消费者模式的方法

    我们先来看看什么是生产者消费者模式 xff0c 生产者消费者模式是程序设计中非常常见的一种设计模式 xff0c 被广泛运用在解耦 消息队列等场景 在现实世界中 xff0c 我们把生产商品的一方称为生产者 xff0c 把消费商品的一方称为消费
  • 如何在 Ubuntu 中管理和使用逻辑卷管理LVM

    在我们之前的文章中 xff0c 我们介绍了什么是 LVM 以及能用 LVM 做什么 xff0c 今天我们会给你介绍一些 LVM 的主要管理工具 xff0c 使得你在设置和扩展安装时更游刃有余 正如之前所述 xff0c LVM 是介于你的操作
  • 如何获取本地和远程主机的IP及MAC地址

    这篇文章 xff0c 我们不准备大规模的讨论技术问题 只是向大家介绍一下我们将如何获得一台主机的IP地址 在Win32 API中我们可以使用NetWork API完成这项工作 xff0c 但是在 Net平台下我们应当如何做呢 xff1f 其
  • 聊聊java中一些减少if-else 的编码方式!

    01 前言 前段时间在阅读别人所写的代码的时候 发现其中一些业务相关的方法体内 出现了比较多的if else语句多层嵌套的情况 首先我个人不是不提倡写if else语句 不得不说 很多时候 在写某些逻辑 使用if else 去做判断 代码看
  • 如何用简单方法推导正弦函数的和角公式: sin(α+β)=sinαcosβ+cosαsinβ ?

    问题 xff1a 看2014年湖北省高考理科数学题 xff0c 选择题第6题 xff1a 这道题目答案是C xff0c 组是正交函数 xff0c 组不是正交函数 可以用数形结合方式 xff0c 快速做出判断 详细解析如下 分析 xff1a
  • Http头部参数:Authorization

    项目uu约优中 xff0c 用到了头部Authorization 当时传递的参数也是后端返回的20位字符 项目sxaik中 xff0c http请求的头部传递Authorization xff0c 值为32位小写字符 xff0c 不确定是m
  • 高性能计算

    信息时代的硬件芯片和存储器价格以摩尔定律的形式下降 xff0c 可是现在处理的数据量也越来越大 我们先以cocoa编程为例 xff0c 然后再结合网格计算 云计算 xff0c 综合对最新的高性能计算技术作介绍 使用 runloop 在coc
  • @Documented注解的作用

    目录 在哪里用到了 96 64 Documented 96 注解 xff1f 那么 64 Documented的作用是什么 xff1f 在哪里用到了 64 Documented注解 xff1f 64 Documented是元注解 xff0c
  • 球的表面积公式是怎么推导出来的?

    球的体积公式的推导 球的表面积公式是 xff1a 证明方式一 xff1a 体积求导 基本思路 xff1a 可以把半径为R的球 xff0c 从球心到球表面分成n层 xff0c 每层厚为 r n xff0c 像洋葱一样 半径获得增量是 r xf
  • ViewBinding简单使用

    官方文档 xff1a https developer android google cn topic libraries view binding hl 61 zh cn java 在app module下的build gradle文件中
  • Android广播实现进程间通信,很简单

    应用A发送广播 xff1a span class token keyword public span span class token keyword class span span class token class name MainA
  • 下载JDK8 JVM源码

    性子急的可以直接看快速下载步骤 xff1a 目录 详细步骤快速下载步骤 详细步骤 打开openJDK官网 xff1a https openjdk org 找到左侧的Mercurial xff0c 点击进入新界面 选择jdk8 xff0c 点
  • Git查看分支的创建人

    开发小组人多的时候 xff0c 仓库里会有跟多分支 xff0c 需要看下某个分支具体是谁创建的 命令 xff1a git for each ref format 61 39 committerdate 09 authorname 09 re
  • kotlin的this关键字几种用法

    与java不同的是 xff0c 原先MainActivity this这种写法在kotlin中会报错 如下 正确的写法有许多 xff0c 直接就写this也可以识别到 xff0c 如下 xff1a span class token clas
  • kotlin中匿名内部类的写法

    原本java开发安卓常用的setOnClickListener xff0c 用kotlin写 xff0c 也变得五花八门了 span class token keyword var span view span class token op
  • Spring与SpringMVC的区别和联系是啥?

    Spring Spring是一个开源容器框架 xff0c 可以接管web层 xff0c 业务层 xff0c dao层 xff0c 持久层的组件 xff0c 并且可以配置各种bean 和维护bean与bean之间的关系 其核心就是控制反转 I
  • “在XML文件中给代码加注释”请注意注释的位置

    先科普一下eclipse加注释的快捷键 xff1a eclipse中编辑Java文件时 xff0c 注释和取消注释的快捷键都是 xff1a 34 CTRL 43 34 编辑xml文件时 xff0c 注释 xff1a CTRL 43 SHIF
  • HTTP代理服务器的实现

    接下来都是我对HTTP代理服务器的理解 HTTP代理服务 xff08 proxy server xff09 器就是客户端也是服务端 xff0c 是一个事务处理的中间人 xff0c 就像下图所展示的一样 xff0c 图片来源于 HTTP权威指