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();
}
}