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解决办法

bash 复制代码
#指定源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解决办法

bash 复制代码
        #转发接口
        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不支持设置源ip地址,而socket支持设置源ip地址,所以使用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方式"后面两行代码。

java 复制代码
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();
    }


}
相关推荐
无名之逆4 小时前
hyperlane:Rust HTTP 服务器开发的不二之选
服务器·开发语言·前端·后端·安全·http·rust
。puppy8 小时前
HCIA—— 31 HTTP的报文、请求响应报文、方法、URI和URL
网络·网络协议·http
无职转生真好看8 小时前
HTTP和HTTPS区别
网络协议·http·https
秋名RG10 小时前
HTTP 1.0和2.0 有什么区别?
网络·网络协议·http
罗念笙10 小时前
HTTP1.0和2.0有什么区别?
网络协议·http
YiHanXii13 小时前
Axios 相关的面试题
前端·http·vue·react
金丝猴也是猿15 小时前
手机硬件检测详解:从版本信息到相机功能的全面指南
websocket·网络协议·tcp/ip·http·网络安全·https·udp
iOS技术狂热者1 天前
wireshark开启对https密文抓包
websocket·网络协议·tcp/ip·http·网络安全·https·udp
GW_Cheng1 天前
springmvc redirect 使用https后跳转到了http://域名:443问题处理
网络协议·http·https
海外住宅ip供应商-luck1 天前
PHP如何搭建设置代理http并加密使用?
开发语言·http·php