当前位置: 首页 > article >正文

socket实现HTTP请求,参考HttpURLConnection源码解析

背景

有台服务器,网卡绑定有2个ip地址,分别为:
A:192.168.111.201
B:192.168.111.202
在这台服务器请求目标地址
C:192.168.111.203
时必须使用B作为源地址才能访问目标地址C,在这台服务器默认又是使用A地址作为源地址。

1、curl解决办法

#指定源ip
curl -X POST -H "Content-Type:application/json"  --interface 192.168.111.202 http://192.168.111.203:8080/v1 -d '{"model":"x"}'

2、使用nginx解决办法 

        #转发接口
        location ^~ /v1 {
            root html;
            limit_rate 2048k;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            client_max_body_size 100m;
            client_body_buffer_size 128m;
            proxy_connect_timeout 120s;
            proxy_send_timeout 120s;
            proxy_read_timeout 120s;
            proxy_bind 192.168.111.202;  # 指定源IP
            proxy_pass http://192.168.111.203:8080;
        }

3、使用socket实现HTTP请求

解决过程如下:

HttpURLConnection 示例 

 /**
     * 发送POST请求
     * @param url         请求地址
     * @param params      请求参数
     * @param contentType ContentType请求头类型
     * @param timeout     读超时,单位:秒
     * @author lhs
     * @date 2024/12/2 15:35
     */
    public static String sendPost(String url, String params, String contentType, Integer timeout) {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        HttpURLConnection connection = null;
        int responseCode = 0;
        try {
            connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("POST");
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setUseCaches(false);
            connection.setConnectTimeout(10000);// 连接超时(单位:毫秒)
            if (timeout == null || timeout == 0) {
                connection.setReadTimeout(15000);// 读超时(单位:毫秒)
            } else {
                connection.setReadTimeout(timeout * 1000);// 读超时(单位:毫秒)
            }
            if (contentType == null || contentType.length() == 0) {
                connection.setRequestProperty("Content-Type", APPLICATION_FORM_URLENCODED);
            } else {
                connection.setRequestProperty("Content-Type", contentType);
            }
            if (params != null && params.length() > 0) {
                outputStream = connection.getOutputStream();
                outputStream.write(params.getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
            }
            int len;
            byte[] buf = new byte[4096];
            responseCode = connection.getResponseCode();
            inputStream = connection.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = inputStream.read(buf)) != -1) {
                baos.write(buf, 0, len);
                baos.flush();
            }
            String result = baos.toString("UTF-8");
            baos.close();
            return result;
        } catch (Exception e) {
            String cause = e.getCause() == null ? "" : e.getCause().getMessage();
            return "Exception:" + responseCode + ":" + cause + e.getMessage();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
                if (connection != null) {
                    connection.disconnect();
                }
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        }
    }

 

HttpURLConnection源码分析过程

入口:connection.getInputStream()
 

情况一:

当不能预先确定报文体的长度时,不可能在头中包含Content-Length域来指明报文体长度,此时就需要通过Transfer-Encoding域来确定报文体长度。
 

情况二: 

响应头有 Content-Length

 

 

 socket实现HTTP请求

socket实现http请求很简单,抓包看下报文就知道了,比较麻烦的是解析响应报文。
根据分析HttpURLConnection 源码可以看出响应报文解析需要区分响应头有Transfer-Encoding和响应头有 Content-Length 两种情况。

若需要指定源IP,打开“指定源IP方式”后面的注释代码,注释“不需要指定源IP方式”后面两行代码。

package com.study;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.net.www.MeteredStream;
import sun.net.www.http.ChunkedInputStream;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class HttpClientUtil {
    private static final Logger log = LoggerFactory.getLogger(HttpClientUtil.class);
    /*ContentType请求头类型*/
    public final static String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded;charset=utf-8";
    public final static String APPLICATION_JSON = "application/json;charset=utf-8";
    public final static String APPLICATION_SOAP_XML = "application/soap+xml;charset=utf-8";
    public final static String MULTIPART_FORM_DATA = "multipart/form-data;charset=utf-8";
    public final static String APPLICATION_XML = "application/xml;charset=utf-8";
    public final static String TEXT_HTML = "text/html;charset=utf-8";
    public final static String TEXT_XML = "text/xml;charset=utf-8";

    public static void main(String[] args) throws Exception {
        String url = "http://www.7timer.info/bin/astro.php";
        String params = "lon=104.06&lat=30.65&ac=0&lang=en&unit=metric&output=json&tzshift=0";
        String result = sendPost(url, params, HttpClientUtil.APPLICATION_FORM_URLENCODED, 20);
        log.info("响应报文:" + result);
    }

    /**
     * 发送POST请求
     * @param url         请求地址
     * @param params      请求参数
     * @param contentType ContentType请求头类型
     * @param soTimeout   读超时,单位:秒
     * @author lhs
     * @date 2024/12/2 15:35
     */
    public static String sendPost(String url, String params, String contentType, Integer soTimeout) throws Exception {
        URL u = new URL(url);
        String path = u.getFile();
        if (path != null && !path.isEmpty()) {
            if (path.charAt(0) == '?') {
                path = "/" + path;
            }
        } else {
            path = "/";
        }
        // 要连接的服务端IP地址和端口
        int port = u.getPort();
        String host = u.getHost();
        String authority = host;
        if (port != -1 && port != u.getDefaultPort()) {
            authority = host + ":" + port;
        }
        if (port == -1) {
            port = u.getDefaultPort();
        }
        // 设置连接超时时间
        int connectTimeout = 10 * 1000;

        // 不需要指定源IP方式
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress(host, port), connectTimeout);

        // 指定源IP方式
        // SocketAddress localAddress = new InetSocketAddress("192.168.111.202", 0);// 0表示让系统自动选择一个端口
        // socket.bind(localAddress); // 绑定本地 IP 地址和端口
        // SocketAddress remoteAddress = new InetSocketAddress(host, port);
        // socket.connect(remoteAddress, connectTimeout); // 连接到远程服务器

        OutputStream outputStream = socket.getOutputStream();
        PrintStream serverOutput = new PrintStream(new BufferedOutputStream(outputStream), false, "UTF-8");
        socket.setTcpNoDelay(true);
        socket.setSoTimeout(soTimeout * 1000);

        // 请求参数body部分
        byte[] body = params.getBytes(StandardCharsets.UTF_8);
        // // 请求参数header部分
        String header = getHttpHeader(path, authority, contentType, body.length);
        log.info("请求报文:" + header + params);
        serverOutput.print(header);//请求参数header部分
        serverOutput.flush();
        serverOutput.write(body);//请求参数body部分
        serverOutput.flush();

        InputStream inputStream = new BufferedInputStream(socket.getInputStream());

        int len = 0;
        byte[] buf = new byte[8];
        // readlimit被设置为10,意味着从标记位置开始,你可以读取最多10个字节的数据,然后仍然可以通过调用reset()方法回到这个标记位置。
        inputStream.mark(10);
        while (len < 8) {
            int read = inputStream.read(buf, len, 8 - len);
            if (read < 0) {
                break;
            }
            len += read;
        }
        String scheme = new String(buf, StandardCharsets.UTF_8);
        inputStream.reset();
        if ("HTTP/1.1".equals(scheme)) {
            Map<String, String> headerMap = parseHeader(inputStream);
            try {
                //第一行响应内容
                String firstLineHeader = headerMap.get(null);
                int index;
                for (index = firstLineHeader.indexOf(32); firstLineHeader.charAt(index) == ' '; ++index) {
                }
                //响应码
                int responseCode = Integer.parseInt(firstLineHeader.substring(index, index + 3));
                log.info("响应码:" + responseCode);

                // 当不能预先确定报文体的长度时,不可能在头中包含Content-Length域来指明报文体长度,此时就需要通过Transfer-Encoding域来确定报文体长度。
                String transferEncoding = headerMap.get("Transfer-Encoding");
                if ("chunked".equalsIgnoreCase(transferEncoding)) {
                    inputStream = new ChunkedInputStream(inputStream, sun.net.www.http.HttpClient.New(u), null);
                }

                //响应body长度
                String contentLength = headerMap.get("Content-Length");
                if (contentLength != null) {
                    long bodyLength = Long.parseLong(contentLength);
                    inputStream = new MeteredStream(inputStream, null, bodyLength);
                }

                buf = new byte[4096];
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                //只有当客户端关闭它的输出流的时候,服务端才能取得结尾的-1
                while ((len = inputStream.read(buf)) != -1) {
                    baos.write(buf, 0, len);
                }
                String result = baos.toString("UTF-8");
                return result;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 该方法参考:sun.net.www.MessageHeader#mergeHeader(java.io.InputStream)源码
     * @author lhs
     * @date 2025/1/11 10:53
     */
    private static Map<String, String> parseHeader(InputStream var1) throws IOException {
        Map<String, String> headerMap = new HashMap<>();
        if (var1 != null) {
            char[] var2 = new char[10];

            String var9;
            String var10;
            for (int var3 = var1.read(); var3 != 10 && var3 != 13 && var3 >= 0; headerMap.put(var10, var9)) {
                int var4 = 0;
                int var5 = -1;
                boolean var7 = var3 > 32;
                var2[var4++] = (char) var3;

                label104:
                while (true) {
                    int var6;
                    if ((var6 = var1.read()) < 0) {
                        var3 = -1;
                        break;
                    }

                    switch (var6) {
                        case 9:
                            var6 = 32;
                        case 32:
                            var7 = false;
                            break;
                        case 10:
                        case 13:
                            var3 = var1.read();
                            if (var6 == 13 && var3 == 10) {
                                var3 = var1.read();
                                if (var3 == 13) {
                                    var3 = var1.read();
                                }
                            }

                            if (var3 == 10 || var3 == 13 || var3 > 32) {
                                break label104;
                            }

                            var6 = 32;
                            break;
                        case 58:
                            if (var7 && var4 > 0) {
                                var5 = var4;
                            }

                            var7 = false;
                    }

                    if (var4 >= var2.length) {
                        char[] var8 = new char[var2.length * 2];
                        System.arraycopy(var2, 0, var8, 0, var4);
                        var2 = var8;
                    }

                    var2[var4++] = (char) var6;
                }

                while (var4 > 0 && var2[var4 - 1] <= ' ') {
                    --var4;
                }

                if (var5 <= 0) {
                    var10 = null;
                    var5 = 0;
                } else {
                    var10 = String.copyValueOf(var2, 0, var5);
                    if (var5 < var4 && var2[var5] == ':') {
                        ++var5;
                    }

                    while (var5 < var4 && var2[var5] <= ' ') {
                        ++var5;
                    }
                }

                if (var5 >= var4) {
                    var9 = new String();
                } else {
                    var9 = String.copyValueOf(var2, var5, var4 - var5);
                }
            }

        }
        return headerMap;
    }

    /**
     * 拼接http请求头报文
     * @author lhs
     * @date 2023/3/31 17:47
     */
    private static String getHttpHeader(String path, String authority, String contentType, int length) throws Exception {
        StringBuilder header = new StringBuilder();
        header.append("POST " + path + " HTTP/1.1\r\n");
        // header.append("Content-Type: application/json;charset=UTF-8\r\n");
        header.append("Content-Type: " + contentType + "\r\n");
        header.append("Host: " + authority + "\r\n");
        header.append("Content-Length: " + length + "\r\n");
        header.append("\r\n");
        return header.toString();
    }


}


http://www.kler.cn/a/525528.html

相关文章:

  • 7.抽象工厂(Abstract Factory)
  • Leetcode:350
  • 白嫖DeepSeek:一分钟完成本地部署AI
  • 解决Oracle SQL语句性能问题(10.5)——常用Hint及语法(7)(其他Hint)
  • 乌兰巴托的夜---音乐里的故事
  • Excel - Binary和Text两种Compare方法
  • 【开源免费】基于SpringBoot+Vue.JS景区民宿预约系统(JAVA毕业设计)
  • NVIDIA GPU介绍:概念、序列、核心、A100、H100
  • OpenEuler学习笔记(十四):在OpenEuler上搭建.NET运行环境
  • 芯片AI深度实战:实战篇之vim chat
  • 数据结构与算法之栈: LeetCode 739. 每日温度 (Ts版)
  • 企业知识管理在推动组织变革与适应性发展中的关键性作用分析
  • NPM 使用介绍
  • 在业务高峰期更新 PostgreSQL 表结构(DDL)导致性能问题
  • Java线程认识和Object的一些方法
  • 分库分表 相关问题
  • 3.目录操作
  • 软件工程概论试题二
  • “深入浅出”系列之算法篇:(5)AIGC
  • 面试经典150题——图的广度优先搜索
  • 保姆级讲解 python之zip()方法实现矩阵行列转置
  • 【Leetcode 热题 100】32. 最长有效括号
  • 深入探讨:服务器如何响应前端请求及后端如何查看前端提交的数据
  • 大模型知识蒸馏技术(2)——蒸馏技术发展简史
  • vscode软件操作界面UI布局@各个功能区域划分及其名称称呼
  • 留学生scratch计算机haskell函数ocaml编程ruby语言prolog作业VB