使用Java实现HTTP和HTTPS代理服务详解

 更新时间:2024年04月30日 10:28:29   作者:cloudy491  
这篇文章主要为大家详细介绍了如何使用Java实现HTTP和HTTPS代理服务,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

HTTP代理和HTTPS代理介绍

简单来说就是其他的服务器代替帮你访问HTTP或者HTTPS,当你的网络受限无法访问某些网站时,但是你的代理服务器可以访问某些网站,这时候你就设置代理,通过代理服务器去访问某些网站。例如:你使用某个ip频繁去爬网站信息时,可能你的ip就会被封,这时候你可能就需要代理去切换ip了。

HTTP代理

首先我们简单用Java搭一个Socket服务端,打印一下HTTP代理请求报文。

public static void main(String[] args) {
    try {
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("Server started, waiting for client...");
        while (true) {
            Socket socket = serverSocket.accept();
            System.out.println("Client connected: " + socket.getInetAddress().getHostAddress());
            new Thread(()->{
                try(InputStream inputStream = socket.getInputStream()) {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                    while (reader.ready()){
                        System.out.println(reader.readLine());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

使用curl -x 127.0.0.1:8888 http://www.baidu.com 设置本地代理8888端口,代理访问百度。

然后看一下Java的控制台,可以看到代理请求过来的HTTP报文。

GET http://www.baidu.com/ HTTP/1.1
Host: www.baidu.com
User-Agent: curl/8.4.0
Accept: */*
Proxy-Connection: Keep-Alive

下面是通火狐浏览器拿到的请求头原始报文,可以发现代理HTTP报文和正常HTTP报文不同处:

第一行中间一个是http://www.baidu.com/,另一个是/

一个是Proxy-Connection,另一个是Connection

GET / HTTP/1.1
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: BIDUPSID=52C8D6333DE11AAB7F6B193EE8925F3E; PSTM=1700558835; H_PS_PSSID=40300_40446_40080_60138_40463_60175; BAIDUID=52C8D6333DE11AAB7F6B193EE8925F3E:FG=1; BD_UPN=13314752; COOKIE_SESSION=8189645_0_5_4_2_3_1_0_4_3_31_1_41_0_56_0_1713792557_0_1713792501%7C5%230_0_1713792501%7C1; baikeVisitId=3cf47ba3-c45a-4e5c-bc50-1cef890ac1f1; BA_HECTOR=a02g018h000l2l250k2k8k043iu9l41j2u8gd1t; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598
Upgrade-Insecure-Requests: 1

根据上面的不同,我们只需要将代理的报文转成正常的HTTP报文,在Java中添加下面两个方法替换一下内容。

//替换域名部分,将http://www.baidu.com 变成 /
private static String replaceDomain(String line) {
    String reqUrl = line.split("\s+")[1];
    //http://www.baidu.com/
    // 提取域名部分
    int endIndex = reqUrl.indexOf("/", 8); // 从第 8 个字符开始搜索斜杠
    String domain = endIndex != -1 ? reqUrl.substring(0, endIndex + 1) : reqUrl;
    // 替换
    return line.replaceFirst(domain, "/");
}
//Connection部分,将Proxy-Connection 变成 Connection
private static String replaceConnection(String line) {
    String prefix = "Proxy-Connection: ";
    return line.replaceFirst(prefix, "Connection: ");
}

报文修改后,还得知道这个报文往那个地方发送,可以通Host字段提出域名和端口,代码如下可以拿到Host。

private static Object[] getHeader(String str) {
    String hostPrefix = "Host: ";
    String contentLengthPrefix = "Content-Length: ";
    String host = null;
    Integer contentLength = null;
    String[] line = str.split("\r\n");
    for (String l : line) {
        if (l.startsWith(hostPrefix)) {
            host = l.substring(hostPrefix.length());
        } else if (l.startsWith(contentLengthPrefix)) {
            contentLength = Integer.parseInt(l.substring(contentLengthPrefix.length()));
        }
    }
    return new Object[]{host, contentLength};
}

有了Host我可以从里面拿到域名和端口,然后进行Socket连接,然后把修改好的报文写入Socket中。

//获取host
String address = (String) getHeader(sb.toString())[0];
//分割host和端口
String[] split = address.split(":");
String host = split[0];
int port = split.length > 1 ? Integer.parseInt(split[1]) : 80;
System.out.println(sb.toString());
//初始化socket连接
socket = new Socket(host, port);
//写入第一次请求的数据
socket.getOutputStream().write(sb.toString().getBytes());

之后创建一个线程去读Socket返回的数据,也就是百度那边返回的数据,拿到数据后再写回给代理的clientSocket

//创建新线程执行socket读取
Socket finalSocket = socket;
new Thread(() -> {
    try {
        InputStream socketInputStream = finalSocket.getInputStream();

        byte[] socketBt = new byte[1024];
        int socketlen = -1;
        while ((socketlen = socketInputStream.read(socketBt)) != -1) {
            System.out.println(new String(socketBt, 0, socketlen));
            clientSocket.getOutputStream().write(socketBt, 0, socketlen);
        }
        socketInputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            finalSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();

这样就完成了一半,比较麻烦的是在每次连接读取的第一个代理报文,你得判断结束,GET请求可以通过\r\n\r\n,POST就得通过Content-Length去判断了,总数据长度 = Content-Length+ 数据中的\r\n\r\n位置后。下面代码可能长些,就是判断每次连接的第一个代理报文结束,结束标志要么用Content-Length或者\r\n\r\n,拿到第一个代理报文就可以创建Socket连接,之后的数据写入这个Socket就行了。

InputStream inputStream = clientSocket.getInputStream();
StringBuilder sb = new StringBuilder();
byte[] bt = new byte[1024];
int len = -1;
boolean isFirstLine = true;
int contentLength = -1;
while ((len = inputStream.read(bt)) != -1) {
    
    //第一次请求
    if (socket == null) {

        if (isFirstLine) {
            isFirstLine = false;

            String replace = replaceDomain(bt, len);
            
            //跳过HTTPS连接
            if (replace.startsWith("CONNECT")) {
                len = -1;
                break;
            }
            sb.append(replace);
        } else {
            sb.append(new String(bt, 0, len));
        }
        if (contentLength == -1) {
            Integer length = (Integer) getHeader(new String(bt, 0, len))[1];
            if (length != null) {
                contentLength = length;
            }
            int crlfcrlf = findCRLFCRLF(bt, len) + 4;
            contentLength -= (len - crlfcrlf);
        }
        //长度到达结束
        if (contentLength == 0) {
            break;
        }
        if (sb.toString().endsWith("\r\n\r\n")) {
            break;
        }
    } else {
        //首行需要修改
        if (isFirstLine) {
            String replace = replaceDomain(bt, len);
            socket.getOutputStream().write(replace.getBytes());
        } else {
            socket.getOutputStream().write(bt, 0, len);
        }
    }
}

使用curl -x 127.0.0.1:8888 http://www.baidu.com 测试一下,发现正常响应。

上面是简单的测试,还得用浏览器去测试,话说找一个http的网站,也太难,我这里就用python的文件下载管理页面。

if __name__ == '__main__':
    # 定义服务器的端口
    PORT = 8000
    # 创建请求处理程序
    Handler = http.server.SimpleHTTPRequestHandler
    # 设置工作目录
    os.chdir("C:\Windows\Media")
    # 创建服务器
    with socketserver.TCPServer(("", PORT), Handler) as httpd:
        print(f"服务启动在端口 {PORT}")
        httpd.serve_forever()

使用火狐浏览设置代理。

搞定,浏览器和控制台显示正常,注意浏览器的地址不要写127.0.0.1,他是不走代理的。

HTTPS代理

HTTPS代理比HTTP代理简单多了,也是通过Socket服务端,使用curl -x 127.0.0.1:8888 https://www.baidu.com 控制台可以拿到下面HTTPS的代理报文。

CONNECT www.baidu.com:443 HTTP/1.1
Host: www.baidu.com:443
User-Agent: curl/8.4.0
Proxy-Connection: Keep-Alive

我简单画一下大致流程,浏览器通过代理把上面CONNECT报文发送Java应用,Java应用拿到报文,从报文中获取到域名和端口,通过域名和端口连接百度服务器,连接成功后,回应浏览器HTTP/1.1 200 Connection estabished告诉他这边已经连接好了,可以发送数据了,最后浏览器和百度服务器双方交换数据。

浏览器Java应用百度服务器CONNECT报文连接HTTP/1.1 200 Connection estabished发送数据写入数据浏览器Java应用百度服务器

逻辑和HTTP一样的,只是他不用把CONNECT报文发送到百度服务器上,就不用修改报文,而且结束只用通过\r\n\r\n判断,和百度服务器连接上就回应HTTP/1.1 200 Connection estabished,简单多了。

以上就是使用Java实现HTTP和HTTPS代理服务详解的详细内容,更多关于Java实现HTTP和HTTPS代理的资料请关注脚本之家其它相关文章!

相关文章

  • java线程组构造方法源码解析

    java线程组构造方法源码解析

    这篇文章主要为大家介绍了java线程组构造方法源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • 如何用Springboot Admin监控你的微服务应用

    如何用Springboot Admin监控你的微服务应用

    这篇文章主要介绍了如何用Springboot Admin监控你的微服务应用,帮助大家更好的理解和使用springboot框架,感兴趣的朋友可以了解下。
    2021-01-01
  • 教你使用springboot配置多数据源

    教你使用springboot配置多数据源

    发现有很多小伙伴还不会用springboot配置多数据源,今天特地给大家整理了本篇文章,文中有非常详细的图文介绍及代码示例,对正在学习java的小伙伴很有帮助,需要的朋友可以参考下
    2021-05-05
  • SpringBoot项目设置断点debug调试无效忽略web.xml问题的解决

    SpringBoot项目设置断点debug调试无效忽略web.xml问题的解决

    这篇文章主要介绍了SpringBoot项目设置断点debug调试无效忽略web.xml问题的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • 关于Java中Bean的作用域详解

    关于Java中Bean的作用域详解

    这篇文章主要介绍了关于Java中Bean的作用域详解,限定程序中变量的可⽤范围叫做作⽤域,或者说在源代码中定义变量的某个区域就叫做作⽤域,需要的朋友可以参考下
    2023-08-08
  • springboot工程jar包部署到云服务器的方法

    springboot工程jar包部署到云服务器的方法

    这篇文章主要介绍了springboot工程jar包部署到云服务器的方法,本文通过实例介绍的非常详细,具有一定的参考借鉴价值,需要的朋友参考下吧
    2018-05-05
  • Maven的porfile与SpringBoot的profile结合使用案例详解

    Maven的porfile与SpringBoot的profile结合使用案例详解

    这篇文章主要介绍了Maven的porfile与SpringBoot的profile结合使用,通过maven的profile功能,在打包的时候,通过-P指定maven激活某个pofile,这个profile里面配置了一个参数activatedProperties,不同的profile里面的这个参数的值不同,需要的朋友可以参考下吧
    2021-12-12
  • mybatis中大批量数据插入解析

    mybatis中大批量数据插入解析

    这篇文章主要介绍了mybatis中大批量数据插入解析,使用Mybatis框架批量插入的3种方法,分别是多次调用insert方法、foreach标签、batch模式,本文来详细说明一下,需要的朋友可以参考下
    2024-01-01
  • java中的接口能够被实例化吗

    java中的接口能够被实例化吗

    这篇文章主要介绍了java中的接口能够被实例化吗,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • JpaRepository 实现简单条件查询

    JpaRepository 实现简单条件查询

    这篇文章主要介绍了JpaRepository 实现简单条件查询,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11

最新评论