logger.info("接口开始");
con.setConnectTimeout(5000);//建立连接的超时时间,单位毫秒
con.setReadTimeout(3 * 60 * 60 * 1000); //接口最长处理3小时
// 开启连接
con.connect();
//请求参数
if (params != null && params.length() > 0) {
os = con.getOutputStream();
os.write(params.getBytes(StandardCharsets.UTF_8));
os.flush();
}
//响应
logger.info("接口结果:" + con.getResponseCode()); // 这里抛出异常 java.net.SocketTimeoutException: Read timed out
读超时con.setReadTimeout(3 * 60 * 60 * 1000);已设置为3小时。
日志
2023-06-02 05:16:44.341 logback [main] INFO com.ustc.transfer.TransferService - 接口开始
2023-06-02 08:16:44.389 logback [main] ERROR com.ustc.transfer.TransferService - Read timed out
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735)
at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:678)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1607)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1512)
at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
at com.ustc.transfer.TransferService.sendPost(TransferService.java:489)
日志发现 等待了 3小时,抛出了异常
经过排查,是因为 后端防火墙,连接空闲20分钟,连接就会被丢弃。
解决办法是,使用 socket.setKeepAlive(true);
注意HttpURLConnection的connection.setRequestProperty("Connection", "keep-alive");是不能达到到效果的,这个作用是是复用连接。
注意与HTTP中请求头”Connection: keep-alive“的区别
socket.setKeepAlive(true)这个参数与TCP KeepAliveTime参数配合使用,默认2小时,空闲2小时的连接发送一次心跳防止连接被杀掉。
我这里的场景是,这个接口后台要处理3小时左右,而后端防火墙会丢弃空闲20分钟的连接。
客户端是windows系统,TCP KeepAliveTime默认是连接空闲2小时才发送一次[TCP Keep-Alive]报文。根据我的实际情况改成15分钟。
在 cmd 中输入 regedit 打开注册表,复制下面的路径回车
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
右键 Parameters > 新建 > DWORD(32位)> KeepAliveTime > 右键修改
修改后,需要重启电脑,这样连接空闲15分钟后,就会发送一次[TCP Keep-Alive]报文。
socket.setSoTimeout(3 * 60 * 60 * 1000);//读超时,单位毫秒
socket.setKeepAlive(true);
package com.study;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* Socket模拟Http客户端
* @date 2023/3/31 18:05
*/
public class SocketHttpClient {
private static final Logger logger = LoggerFactory.getLogger(SocketHttpClient.class);
public static void main(String[] args) throws Exception {
String url = "http://localhost:5033/temp?t=21";
// 请求参数:name=vaue
Map<String, String> params = new HashMap<>();
params.put("name", "张三");
params.put("age", "18");
String sendPost = sendPost(url, params);
logger.info(sendPost);
}
/**
* 模拟POST请求
* @param url 请求路径
* @param params 请求参数
* @author
* @date 2023/3/31 17:42
*/
public static String sendPost(String url, Map<String, String> params) {
Socket socket = null;
InputStream inputStream = null;
OutputStream outputStream = null;
try {
URI uri = new URI(url);
// 要连接的服务端IP地址和端口
String host = uri.getHost();
int port = uri.getPort();
// 与服务端建立连接
socket = new Socket(host, port);
// socket.setSoTimeout(5000);//读超时,单位毫秒
socket.setSoTimeout(3 * 60 * 60 * 1000);//读超时,单位毫秒
socket.setTcpNoDelay(true);//数据不作缓冲,立即发送
// 这个参数与TCP KeepAliveTime参数配合使用,默认2小时,空闲2小时的连接发送一次心跳防止连接被杀掉。
// 注意与HTTP中请求头”Connection: keep-alive“的区别
// 修改注册表”计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters“新建”KeepAliveTime“值为900000即15分钟。
socket.setKeepAlive(true);
// 建立好连接后,从socket中获取输入流
inputStream = socket.getInputStream();
// 建立好连接后,从socket中获取输出流
outputStream = socket.getOutputStream();
// 获取http模拟报文
byte[] http = getHttp(url, params);
outputStream.write(http);
outputStream.flush();
// 测试发现,HTTP报文的终止符应该是"\r\n\r\n"即连续两个换行符,这部分内容刚好是请求头中的内容,
// 假如请求头中没有"Content-Length",则请求体中的内容会被丢弃,
// 如果有"Content-Length"请求头,后端继续读取"Content-Length"请求头指定大小的内容。
// 在模拟HTTP请求时不要主动关闭连接,HTTP通过"Content-Length"请求头指定的大小来接收请求体中的数据。
// 所以模拟HTTP报文时,是不需要shutdownOutput()关闭(单向)连接的,
// 若shutdownOutput()关闭了连接,在120秒后,客户端会发送一个RST(连接重置)信号,并抛出异常java.net.SocketException: Connection reset
// 请求报文发送完成后,发送终止符
// socket.shutdownOutput();// 单向关闭输出流,发送流的终止符-1。
// 获取响应内容
byte[] buf = new byte[1024];
int len;
ByteArrayOutputStream out = new ByteArrayOutputStream();
//只有当客户端关闭它的输出流的时候,服务端才能取得结尾的-1
while ((len = inputStream.read(buf)) != -1) {
out.write(buf, 0, len);
}
// 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
String result = out.toString(StandardCharsets.UTF_8.name());
// 响应头和响应体之间有一个空行,这里只返回响应体中的内容
if (result != null && result.indexOf("\r\n\r\n") != -1) {
return result.substring(result.indexOf("\r\n\r\n") + 4);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 拼接http报文
* @param url 请求路径
* @param params 请求参数
* @author
* @date 2023/3/31 17:47
*/
private static byte[] getHttp(String url, Map<String, String> params) throws Exception {
URI uri = new URI(url);
// 要连接的服务端IP地址和端口
int port = uri.getPort();
String host = uri.getHost() + ":" + port;
// 拼接http请求体
StringBuilder bodyBuilder = new StringBuilder();
if (params != null && params.size() > 0) {
for (Map.Entry<String, String> entry : params.entrySet()) {
// 参数名
bodyBuilder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()));
bodyBuilder.append("=");
// 参数值
bodyBuilder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
bodyBuilder.append("&");
}
bodyBuilder.deleteCharAt(bodyBuilder.length() - 1);
}
byte[] body = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8);
// 定义存放 http 报文的缓冲区
ByteArrayOutputStream http = new ByteArrayOutputStream();
http.write(("POST " + url + " HTTP/1.1\r\n").getBytes(StandardCharsets.UTF_8));
http.write(("Host: " + host + "\r\n").getBytes(StandardCharsets.UTF_8));
// 请求参数为key1=value1&key2=value2形式
http.write(("Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n").getBytes(StandardCharsets.UTF_8));
// 请求参数为json格式
// http.write(("Content-Type: application/json; charset=UTF-8\r\n").getBytes(StandardCharsets.UTF_8));
http.write(("Content-Length: " + body.length + "\r\n").getBytes(StandardCharsets.UTF_8));
// http.write(("Cookie: " + cookie + "\r\n").getBytes(StandardCharsets.UTF_8));
// 请求头和请求体之间有一个空行
http.write("\r\n".getBytes(StandardCharsets.UTF_8));
http.write(body);
return http.toByteArray();
}
}
// 请求报文发送完成后,发送终止符
// socket.shutdownOutput();// 单向关闭输出流,发送流的终止符-1。
测试发现,HTTP报文的终止符应该是"\r\n\r\n"即连续两个换行符,这部分内容刚好是请求头中的内容,
假如请求头中没有"Content-Length",则请求体中的内容会被丢弃,
如果有"Content-Length"请求头,后端继续读取"Content-Length"请求头指定大小的内容。
在模拟HTTP请求时不要主动关闭连接shutdownOutput(),HTTP后端通过"Content-Length"请求头指定的大小来接收请求体中的数据。
所以模拟HTTP报文时,是不需要shutdownOutput()关闭(单向)连接的,
若shutdownOutput()关闭了连接,在120秒后,客户端会发送一个RST(连接重置)信号,并抛出异常java.net.SocketException: Connection reset
现象1:
请求报文发送完成后,如果关闭shutdownOutput单向关闭输出流,如果后台处理时间超过120秒,客户端会发送一个RST(连接重置)信号,并抛出异常Connection reset。
现象2:
请求报文发送完成后,如果不关闭shutdownOutput单向关闭输出流,会延迟20秒才能接收到后台的响应结果。
问:不关闭shutdownOutput响应结果为什么会延迟20秒?
这是因为在使用Socket发送HTTP报文时,服务器端会根据Content-Length或Transfer-Encoding字段来判断报文是否已经发送完毕。如果客户端没有主动关闭输出流,服务器端会一直等待接收数据,直到超时。因此,如果不调用shutdownOutput方法关闭输出流,可能会导致数据发送不完整,服务器端一直等待直到超时。而shutdownOutput方法会显式地告知服务器端数据已经发送完毕,服务器端不必等待超时,从而提高响应速度。
问:为什么关闭shutdownOutput单向关闭输出流,在请求时长超过120秒时会报错Connection resetd?
如果调用shutdownOutput方法关闭输出流,但请求时长超过了120秒,就会因为TCP连接超时而导致连接被重置,从而导致Connection reset错误。这是因为TCP连接有一个默认的超时时间,如果在超时时间内没有收到对方的回复,就会认为连接已经失效,从而重置连接。
问:请求报文发送完成后,到底该不该shutdownOutput()关闭连接?
1、如果请求在120秒内就会有响应结果,强烈建议关闭shutdownOutput连接。
2、如果请求不知道什么时候返回响应结果,有两种方案:
方案一:
关闭shutdownOutput连接,并修改KeepAliveTime=60000毫秒,即1分钟就会发送一次[TCP Keep-Alive]报文,连接就不会超时被重置。文章来源:https://www.toymoban.com/news/detail-731146.html
方案二:
不关闭shutdownOutput连接,这样做有个缺点,接收响应结果会晚20秒。文章来源地址https://www.toymoban.com/news/detail-731146.html
到了这里,关于java.net.SocketTimeoutException: Read timed out,tcp连接心跳[TCP Keep-Alive],socket模拟http的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!