当前位置 : 首页 » 文章分类 :  开发  »  Java-Net

Java-Net

Java 网络相关笔记


JVM DNS 缓存

InetAddress 类会对成功和失败的域名解析结果进行缓存,默认情况下:

  • 如果开启了 SecurityManager 安全管理器,为了避免 DNS 欺骗攻击,会永久缓存成功的域名解析结果。
  • 如果未开启 SecurityManager 安全管理器,会对解析结果进行有限时间的缓存。解析失败的结果缓存时间很短(10秒)

可通过下面两个安全参数配置域名解析缓存时间:

  • networkaddress.cache.ttl dns解析成功的缓存时间,单位秒,-1 表示永久缓存
  • networkaddress.cache.negative.ttl dns解析失败的缓存时间,单位秒,默认 10,-1 表示永久缓存

https://docs.oracle.com/javase/8/docs/api/java/net/InetAddress.html

注意 networkaddress.cache.ttlnetworkaddress.cache.negative.ttl 是安全参数,不能直接通过 -D 配置,可以通过下面两个系统参数做对应配置:

  • sun.net.inetaddr.ttl dns缓存时间(秒),未配置 networkaddress.cache.ttl 时使用此配置项
  • sun.net.inetaddr.negative.ttl 未配置 networkaddress.cache.negative.ttl 时使用此配置项

如果开启了 SecurityManager,优先从 ${java.home}/jre/lib/security/java.security 中读取参数 networkaddress.cache.ttl 单位为秒。
如果未读取到,则会读取启动参数 sun.net.inetaddr.ttl 作为缓存 ttl.
如果以上参数均未读取到,并且未开启 SecurityManager 则会使用默认值 30s.
否则使用初始值 -1,表示永久生效。

相关代码在 sun.net.InetAddressCachePolicy.class 中

JVM DNS 缓存配置
https://www.jianshu.com/p/048e8bd3ea46

How to make Java honor the DNS Caching Timeout?
https://stackoverflow.com/questions/1256556/how-to-make-java-honor-the-dns-caching-timeout


Java 常见网络异常


BindException: Address already in use

java.net.BindException: Address already in use: JVM_Bind
该异常发生在服务器端进行 new ServerSocket(port) (port 是一个 0 到 65536 间的整型值)操作时。异常的原因是与 port 一样的一个端口已经被启动,并进行监听。此时用 netstat –anp 命令,可以看到一个 Listening 状态的端口。只需要找一个没有被占用的端口就能解决这个问题。


ConnectException: Connection refused

java.net.ConnectException: Connection refused: connect
该异常发生在客户端进行 new Socket(ip, port) 操作时,该异常发生的原因是或者具有 ip 地址的机器不能找到(也就是说从当前机器不存在到指定 ip 路由),或者是该 ip 存在,但找不到指定的端口进行监听。
出现该问题,首先检查客户端的 ip 和 port 是否写错了,如果正确则从客户端 ping 一下服务器看是否能 ping 通,如果能 ping 通(服务服务器端把 ping 禁掉则需要另外的办法),则看在服务器端的监听指定端口的程序是否启动,这个肯定能解决这个问题。


SocketException: Socket is closed

java.net.SocketException: Socket is closed
该异常在客户端和服务器均可能发生。
异常的原因是己方主动关闭了连接后(调用了 Socket 的 close 方法)再对网络连接进行读写操作。


SocketException: Connection reset

SocketException: Connection reset by peer

java.net.SocketException: Connection reset
java.net.SocketException: Connection reset by peer: socket write error

该异常在客户端和服务器端均有可能发生。

引起该异常的原因有两个:
1、第一个就是如果一端的 Socket 被关闭(主动关闭,或者因为异常退出而引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常(Connection reset by peer: socket write error)。
2、另一个是一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常(Connection reset)。
简单的说就是在连接断开后的读和写操作引起的。

导致 Connection reset 的原因是服务器端因为某种原因关闭了 Connection, 而客户端依然在读写数据,此时服务器会返回复位标志 RST, 然后此时客户端就会提示 “java.net.SocketException: Connection reset”

服务器返回了 RST 时,如果此时客户端正在从 Socket 套接字的输出流中读数据则会提示 “Connection reset”
服务器返回了 RST 时,如果此时客户端正在往 Socket 套接字的输入流中写数据则会提示 “Connection reset by peer: socket write error”

java.net.SocketException: Connection reset
        at java.net.SocketInputStream.read(SocketInputStream.java:210)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)


org.apache.catalina.connector.ClientAbortException: java.io.IOException: Connection reset by peer
        at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:380)
        at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:420)
        at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:345)

常见的场景举例:
1、比如 nginx 网关设有超时时间,超时时间内后端服务没有返回,网关主动断开了连接,等后端服务处理完请求发送响应时,就会报这个错。

2、比如如果发起一个 http 请求,但是这个响应很慢,然后客户端关闭了连接,服务端发送消息体的时候,发现连接被关闭了,会抛出这个异常


SocketException: Broken pipe

java.net.SocketException: Broken pipe
该异常在客户端和服务器均有可能发生。

在抛出 SocketExcepton:Connect reset by peer:Socket write error 后,如果再继续写数据则抛出该异常。前两个异常的解决方法是首先确保程序退出前关闭所有的网络连接,其次是要检测对方的关闭连接操作,发现对方关闭连接后自己也要关闭该连接。

常见的场景举例:
1、客户端由于某种原因断开了连接(比如读取超时,比如网关设置了60秒超时,超过60秒后网关直接返回超时并断开连接),而这时候服务器还在处理请求,它并不知道客户端已经断开了连接,处理完请求后再将处理结果发给客户端,就 broken pipe 了;
2、客户端读取超时关闭了连接,这时候服务器端再向客户端已经断开的连接写数据时就发生了broken pipe异常!

注意,并不是只有超时才会导致这个问题,只要是连接断开,再往这个断开的连接上去执行写操作,都会出现这个异常,客户端超时断开只是其中的一种情况

java.io.IOException: Broken pipe
https://stackoverflow.com/questions/15785175/java-io-ioexception-broken-pipe/15785439

一次SocketException:Connection reset 异常排查
https://www.cnblogs.com/shoren/p/httpclient-connectionreset.html

org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:380)
at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:420)
at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:345)


java.io.IOException: UT010029: Stream is closed

因为一个流关闭了但是你又试着使用它就会报这个异常

https://stackoverflow.com/questions/38692035/java-io-ioexception-ut010029-stream-is-closed

Failed to invoke @ExceptionHandler method: public com.nio.common.web.HttpResponse com.nio.common.web.ErrorHandler.handleAllException(java.lang.Exception,javax.servlet.http.HttpServletRequest)
java.io.IOException: UT010029: Stream is closed
at io.undertow.servlet.spec.ServletOutputStreamImpl.write(ServletOutputStreamImpl.java:137) ~[undertow-servlet-1.4.26.Final.jar!/:1.4.26.Final]
at com.fasterxml.jackson.core.json.UTF8JsonGenerator._flushBuffer(UTF8JsonGenerator.java:2039) ~[jackson-core-2.8.11.jar!/:2.8.11]
at com.fasterxml.jackson.core.json.UTF8JsonGenerator.flush(UTF8JsonGenerator.java:1051) ~[jackson-core-2.8.11.jar!/:2.8.11]
at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:953) ~[jackson-databind-2.8.11.3.jar!/:2.8.11.3]


SocketTimeoutException

问题:
springboot 开发的下载接口遇到较大的文件,比如 1GB,2GB,3GB 下载过程会中断报错:

Caused by: org.apache.catalina.connector.ClientAbortException: java.net.SocketTimeoutException
    at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:351)

Caused by: java.net.SocketTimeoutException: null
    at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:134)

Spring MVC 中并没有控制请求处理超时时间的配置,默认都是不限制处理时间。
server.tomcat.connection-timeout 是 Tomcat 的一个参数只不过是通过 spring 来设置,它并不是请求的处理超时时间,而是指创建连接后 server 等待 client 发送请求数据的超时时间。

Amount of time the connector will wait, after accepting a connection, for the request URI line to be presented.
https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html

解决:
虽然网上解释这个参数与接口处理超时无关,但是设置 server.tomcat.connection-timeout=1800000 后,确实解决了大文件(3GB)下载中断报错的问题。


Orderly Versus Abortive Connection Release in Java

Oracle 官方文档中关于正常 TCP 连接释放和异常 TCP 连接释放的说明

Orderly Versus Abortive Connection Release in Java
https://docs.oracle.com/javase/8/docs/technotes/guides/net/articles/connection_release.html


Socket

package java.net;
public class Socket implements java.io.Closeable {
}

close()

public synchronized void close() throws IOException {
    synchronized(closeLock) {
        if (isClosed())
            return;
        if (created)
            impl.close();
        closed = true;
    }
}

setSoLinger()

在 Java Socket 中,当我们调用 Socket 的 close() 方法时,默认的行为是当底层网卡所有数据都发送完毕后,关闭连接
通过 setSoLinger 方法,我们可以修改 close 方法的行为。

1、on = false
这是默认行为,当 on 为 false 时,linger 对应的设置就没有意义,当 socket 主动 close, 调用的线程会马上返回,不会阻塞,残留在缓冲区中的数据将继续发送给对端,并且与对端进行 FIN-ACK 协议交换,最后进入 TIME_WAIT 状态。

2、on = true, linger > 0
当网卡收到关闭连接请求后,等待 linger 时间
如果在 linger 过程中数据发送完毕,正常四次挥手关闭连接
如果在 delay_time 过程中数据没有发送完毕,发送 RST 包关闭连接

调用 close 的线程将阻塞,发生两种可能的情况:
1是剩余的数据继续接收,进行关闭协议交换;
2就是超时过期,剩余的数据将被删除,进行 FIN-ACK 交换。

3、on = true, linger = 0
这种方式就是所谓 hard-close, 这个方式是讨论或者争论最多的用法,当网卡收到关闭连接请求后,无论数据是否发送完毕,立即发送RST包关闭连接,任何剩余的数据都被立即丢弃,并且 FIN-ACK 交换也不会发生,替代产生 RST ,让对端抛出 Connection reset by peer 的 SocketException 。

public void setSoLinger(boolean on, int linger) throws SocketException {
    if (isClosed())
        throw new SocketException("Socket is closed");
    if (!on) {
        getImpl().setOption(SocketOptions.SO_LINGER, new Boolean(on));
    } else {
        if (linger < 0) {
            throw new IllegalArgumentException("invalid value for SO_LINGER");
        }
        if (linger > 65535)
            linger = 65535;
        getImpl().setOption(SocketOptions.SO_LINGER, new Integer(linger));
    }
}

tcp 参数 so_linger 说明及测试
http://www.4e00.com/blog/linux/2019/03/27/tcp-option-so-linger.html


InetAddress

Java InetAddress 获取本机IP和端口号

IP
String IP = InetAddress.getLocalHost().getHostAddress();

端口号一般指的是 spring 端口号,直接读取配置项即可。

public class MyService {
    @Value("${server.port}")
    private int port;
}

InetAddress.getLocalHost().getHostAddress()获取的ip为127.0.0.1

在 test 环境可以通过 InetAddress.getLocalHost().getHostAddress() 顺利获取本机 ip,但换了一台新的服务器后每次获取的都是 127.0.0.1 了。

原因:
InetAddress.getLocalHost().getHostAddress() 是通过本机名去获取本机ip的
默认情况下本机名是 localhost, 在host文件中对应的ip是127.0.0.1,所以通过这个函数获取到的ip就是127.0.0.1了

解决方法:
1、hostname newhostname 修改主机名(重启后失效)
2、在 /etc/hosts 里加一行
本机IP newhostname

获取本机IP列表

若本机有多个网卡,下面方法可以获取所有的 ip

public static Set<InetAddress> resolveLocalAddresses() {
    Set<InetAddress> addrs = new HashSet<InetAddress>();
    Enumeration<NetworkInterface> ns = null;
    try {
        ns = NetworkInterface.getNetworkInterfaces();
    } catch (SocketException e) {
        // ignored...
    }
    while (ns != null && ns.hasMoreElements()) {
        NetworkInterface n = ns.nextElement();
        Enumeration<InetAddress> is = n.getInetAddresses();
        while (is.hasMoreElements()) {
            InetAddress i = is.nextElement();
            if (!i.isLoopbackAddress() && !i.isLinkLocalAddress() && !i.isMulticastAddress()
                    && !isSpecialIp(i.getHostAddress())) addrs.add(i);
        }
    }
    return addrs;
}

getHostName() 获取hostname

1、用 getLocalHost() 方法创建的 InetAddress 对象
此时 getHostName 返回的是本机名。

2、用域名创建 InetAddress 对象
用域名作为 getByName 和 getAllByName 方法的参数得到的 InetAddress 对象,该对象会记录此域名,当调用 getHostName 时,就无需再访问DNS服务器,而是直接将这个域名返回。

3、用 IP 地址创建 InetAddress 对象
使用 IP 地址创建的 InetAddress 对象(getByName,getAllByName,getByAddress 方法都可以通过 IP 地址创建 InetAddress 对象),当调用 getHostName 时需要通过DNS服务器逆向查找域名,如果IP地址不存在或DNS服务器不允许进行IP地址和域名映射,就返回这个IP地址。

getLocalHost() 最终会调用 Inet4AddressImpl 里的

public native String getLocalHostName() throws UnknownHostException;

在 linux 上,最终是通过调用 gethostname 内核函数实现的。
linux gethostname 先查找 /etc/hosts 文件的内容,然后查询 DNS 服务器。如果 /etc/hosts 文件没有配置,返回的主机名就是 localhost 也就是 127.0.0.1

getCanonicalHostName

下面两种获取 hostname 方式哪个好?

Runtime.getRuntime().exec("hostname")
InetAddress.getLocalHost().getHostName()

Recommended way to get hostname in Java
https://stackoverflow.com/questions/7348711/recommended-way-to-get-hostname-in-java


URL

package java.net;
public final class URL implements java.io.Serializable {}

openConnection() 创建连接

返回一个 URLConnection 实例,代表到远程资源对象的一个连接。

每次调用此 URL 的 handler 的 URLStreamHandler.openConnection(URL) 方法都会创建一个新的 URLConnection 实例。

注意:创建 URLConnection 实例时并没有建立真正的网络连接,只有在 URLConnection.connect() 时才会建立网络连接

如果在 java.lang, java.io, java.util, java.net 包或这些包的子包中有 URLConnection 的具体子类,则 openConnection() 返回的就是这些具体子类,例如 HTTP 协议返回的是 HttpURLConnection 类,JAR 协议返回的是 JarURLConnection 类。

public URLConnection openConnection() throws java.io.IOException {
    return handler.openConnection(this);
}

URL.openConnection() 和 URLConnection.connect()

1、URL.openConnection() 时并没有真正打开连接,openConnection() 只是检查协议并创建 URLConnection 对象。
URLConnection.connect() 时才会建立网络连接

2、可以直接调用 URLConnection.getContentLength(), URLConnection.getResponseCode() 方法,会自动打开连接。

openStream()

url.openStream() 等价于 openConnection().getInputStream()
openConnection() 返回一个 java.net.URLConnection 如果连接的是 Http 的 url, 返回的就是一个 HttpURLConnection 对象,通过其 getInputStream() 方法直接就可以读取 http 资源。

HttpURLConnection 内部会初始化一个 TCP 连接到指定 http 地址,并发送一个 http get 请求,如果返回 200 则可以通过 InputStream 读取 response 内容。


URLConnection

package java.net;
public abstract class URLConnection {
}

connect()

setRequestProperty() 设置请求参数

setRequestProperty(String key, String value)
key-value 形式的请求参数,例如设置 Accept Header "Accept", "*/*"
httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT, ContentType.WILDCARD.getMimeType());

setConnectTimeout() 设置连接超时时间

设置连接超时时间,单位毫秒。
设置的时间到达后若还没成功建立连接,抛出 java.net.SocketTimeoutException 异常。
设置为 0 表示不限制超时时间,默认为0。
建议一定要设置连接和读取超时时间,避免调用的url hang住,导致整个应用挂掉
httpURLConnection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(1));

setReadTimeout() 设置读取超时时间

设置读取超时时间,单位毫秒,表示从 url 输入流读取数据的超时时间。
设置的时间到达后若还没读取到数据,抛出 java.net.SocketTimeoutException
设置为 0 表示不限制超时时间,默认为0。
httpURLConnection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(1));

setDoOutput() 设置可写入body数据

URLConnection 可用于输入流和输出流。
doOutput 设为 true 表示将 URLConnection 用作输出流,这样才能写入 body 数据。默认为false。

// doOutput=true表示URLConnection可作为输出流,即可以发送数据
urlConnection.setDoOutput(true);
// 写入post body到URLConnection的输出流中
IOUtils.write("{}", urlConnection.getOutputStream(), StandardCharsets.UTF_8);

使用示例

Java HttpURLConnection 发送 GET 请求

@Test
public void testGet() {
    try {
        URL getUrl = new URI("https://echo.apifox.com/get?key1=value1").toURL();
        HttpURLConnection urlConnection = (HttpURLConnection) getUrl.openConnection();
        // 获取HTTP响应码
        int code = urlConnection.getResponseCode();
        // 从URLConnection的输入流中读取响应数据
        String response = IOUtils.toString(urlConnection.getInputStream(), StandardCharsets.UTF_8);
        log.info("code: {}, response: {}", code, response);
        urlConnection.disconnect();
    } catch (Exception e) {
        log.error("testGet error", e);
    }
}

Java HttpURLConnection 发送 POST 请求

@Test
public void get() {
    try {
        URL postUrl = new URI("https://echo.apifox.com/post").toURL();
        HttpURLConnection urlConnection = (HttpURLConnection) postUrl.openConnection();
        // 设置请求方法为 POST
        urlConnection.setRequestMethod("POST");
        // doOutput=true表示URLConnection可作为输出流,即可以发送数据
        urlConnection.setDoOutput(true);
        // 写入post body到URLConnection的输出流中
        IOUtils.write("{}", urlConnection.getOutputStream(), StandardCharsets.UTF_8);
        // 获取HTTP响应码
        int code = urlConnection.getResponseCode();
        // 从URLConnection的输入流中读取响应数据
        String response = IOUtils.toString(urlConnection.getInputStream(), StandardCharsets.UTF_8);
        log.info("code: {}, response: {}", code, response);
        urlConnection.disconnect();
    } catch (Exception e) {
        log.error("testPost error", e);
    }
}

java java.net.URLConnection 实现http get,post
https://www.cnblogs.com/ooo0/p/9846073.html

Java: how to use UrlConnection to post request with authorization?
https://stackoverflow.com/questions/2026260/java-how-to-use-urlconnection-to-post-request-with-authorization


Java HttpURLConnection 测试 POST 接口连通性

@Test
public void batchTestPost() {
    List.of(
            "abcd",
            "localhost",
            "127.0.0.1",
            "http://localhost",
            "http://127.0.0.1",
            "localhost:8500",
            "127.0.0.1:8500",
            "http://localhost:8500",
            "http://127.0.0.1:8500",
            "echo.apifox.com",
            "http://echo.apifox.com",
            "http://echo.apifox.com/",
            "https://echo.apifox.com",
            "https://echo.apifox.com/",
            "https://echo.apifox.com/post",
            "http://echo.apifox.com/post",
            "http://echo.apifox.com/get",
            "https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/406",
            "https://www.sina.com/"
    ).forEach(this::testPost);
}

/**
 * post url 格式及连通性测试
 */
public void testPost(String url) {
    try {
        URL postUrl = new URL(url);
        HttpURLConnection httpURLConnection = (HttpURLConnection) postUrl.openConnection();
        httpURLConnection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(3));
        httpURLConnection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(3));
        httpURLConnection.setRequestMethod(HttpMethod.POST.name());
        // "Accept", "*/*"
        httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT, ContentType.WILDCARD.getMimeType());
        httpURLConnection.setRequestProperty(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
        httpURLConnection.setDoOutput(true);
        IOUtils.write("{}", httpURLConnection.getOutputStream(), StandardCharsets.UTF_8);
        HttpStatus httpStatus = HttpStatus.valueOf(httpURLConnection.getResponseCode());
        String response = IOUtils.toString(httpURLConnection.getInputStream(), StandardCharsets.UTF_8);
        if (httpStatus.is2xxSuccessful()) {
            log.info("URL({}) 请求成功,返回码 {}, 内容 {}", url, httpStatus, StringUtils.remove(response, "\n"));
        } else {
            log.error("URL({}) 请求失败,返回码 {},内容 {}", url, httpStatus, StringUtils.remove(response, "\n"));
        }
    } catch (MalformedURLException e) {
        log.error("URL({}) 格式异常 {}", url, e.toString());
    } catch (IOException e) {
        log.error("URL({}) 连接异常 {}", url, e.toString());
    } catch (Exception e) {
        log.error("URL({}) 请求异常 {}", url, e.toString());
    }
}

Java下载图片最简单的方式

下载图片为 byte[]

byte[] image = IOUtils.toByteArray(new URL(imageUrl));

使用Java URL下载图片

比如可以直接读取图片:

public static byte[] getImageBytesFromUrl(String imageUrl) {
    DataInputStream dataInputStream = null;
    ByteArrayOutputStream output = null;
    try {
        URL url = new URL(imageUrl);
        dataInputStream = new DataInputStream(url.openStream());

        output = new ByteArrayOutputStream();

        byte[] buffer = new byte[1024];
        int length;

        while ((length = dataInputStream.read(buffer)) > 0) {
            output.write(buffer, 0, length);
        }

        return output.toByteArray();
    } catch (IOException e) {
        logger.error("getImageBytesFromUrl error. imageUrl : " + imageUrl, e);
    } finally {
        IOUtils.closeQuietly(output);
        IOUtils.closeQuietly(dataInputStream);
    }
    return null;
}

HttpURLConnection用法详解
https://www.cnblogs.com/guodongli/archive/2011/04/05/2005930.html


使用SafeHttpURLConnection代替

使用 HttpURLConnection/URLConnection 请求网络资源时,必须使用安全 SDK 的 SafeHttpURLConnection 类替代其完成相关功能

规则说明:
禁止在代码中使用 HttpURLConnection 类或 URLConnection 类请求网络资源。

应使用 Java Web 安全基础库 中的 SafeHttpURLConnection 类完成相关功能。

详细说明:
HttpURLConnection 和 URLConnection 未对参数进行安全校验,极易导致 SSRF 漏洞。

攻击者可通过提交恶意构造的参数对服务器所在内网进行攻击,窃取内网敏感信息,甚至以此服务器为跳板对内网其他脆弱Web应用进行攻击,造成巨大的安全影响和损失。

Java Web 安全基础库 的 SafeHttpURLConnection 类针对 SSRF 漏洞在底层提供了较为完善的保护机制,可防御此类攻击。

正例01:使用安全基础库请求网络资源

//SafeHttpURLConnection类对URLConnection/HttpURLConnection类进行了全面封装,直接替换即可。
URL url = new URL(targetUrl);
SafeHttpURLConnection connection = new SafeHttpURLConnection(url);
//一些set*操作
connection.connect();

反例01:使用HttpURLConnection造成SSRF漏洞

URL url = new URL(targetUrl);
//由HttpURLConnection直接对可控地址targetUrl发起请求,未对内网资源进行拦截,造成SSRF漏洞
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//一些set*操作
connection.connect();

反例02:使用URLConnection造成SSRF漏洞

try {
    URL url = new URL(targetUrl);
    //URLConnection直接对可控地址targetUrl发起请求,未对内网资源进行拦截,造成SSRF漏洞
    URLConnection connection = url.openConnection();
    System.out.println("Content-Type: " + connection.getContentType());
} catch (IOException e) {
    e.printStackTrace();
}

异常

IllegalStateException: connect in progress

如果在 URLConnection.connect() 之后继续设置 URLConnection 的属性,就会提示 IllegalStateException: connect in progress, 因为此时已经连接了,无法再修改属性。


HttpURLConnection

java.net 包里有个的抽象类 java.net.HttpURLConnection 实现了 URLConnection

package java.net;
abstract public class HttpURLConnection extends URLConnection {
}

sun.net.www.protocol.http 包里有个 HttpURLConnection 继续实现了 java.net.HttpURLConnection

package sun.net.www.protocol.http;
public class HttpURLConnection extends java.net.HttpURLConnection {
}

setRequestMethod() 设置http方法

setRequestMethod(String method) 设置请求方法,例如 GET, POST 等,默认是 GET。
可取下面的值:
GET
POST
HEAD
OPTIONS
PUT
DELETE
TRACE

getResponseCode() 获取http响应码

int code = urlConnection.getResponseCode();
例如:

HttpStatus httpStatus = HttpStatus.valueOf(httpURLConnection.getResponseCode());
if (httpStatus.is2xxSuccessful()) {
    log.info("URL返回码: {}", httpStatus);
} else {
    log.error("target url call fail {}", httpStatus);
}

HttpURLConnection 长连接/持久连接/保活(keep-alive)

HttpURLConnection 没有连接池,但是对每个地址会保持一个长连接,即利用 HTTP 的 persistence connection。

JDK 自带的 HttpURLConnection 默认启用 keepAlive,支持 HTTP/1.1 和 HTTP/1.0 持久连接。使用后的 HttpURLConnection 会放入缓存中供以后的同 host:port 的请求重用,底层的 socket 在 keepAlive 超时之前不会关闭。

HttpURLConnection 受以下 system properties 控制:
http.keepAlive=<boolean> 默认值:true,是否启用keepAlive,如果设置为false,则 HttpURLConnection 不会缓存,使用完后会关闭 socket 连接。
http.maxConnections=<int> 默认值:5,每个目标host缓存socket连接的最大数。

1、如果在 HttpURLConnection 的 header 中加入 Connection: close,则此连接不会启用 keepAlive
2、想要启用 keepAlive,程序请求完毕后,必须调用 HttpURLConnection.getInputStream().close()(表示归还长连接给缓存,以供下次同host:port的请求重用底层socket连接),而不能调用 HttpURLConnection.disconnect()(表示关闭底层socket连接,不会启用keepAlive)
3、keepAliveTimeout 首先从 http response keep-alive header 中获取,如果没有取到,则默认为 5 秒,sun.net.www.http.KeepAliveCache.java 中有一个线程,每 5 秒执行一次,检查缓存的连接的空闲时间是否超过 keepAliveTimeout,如果超过则关闭连接。从 KeepAliveCache 中获取缓存的连接时也会检查获取到的连接的空闲时间是否超过 keepAliveTimeout,如果超过则关闭连接,并且获取下一个连接,再执行以上检查,直达获取到空闲时间在 keepAliveTimeout 以内的缓存连接为此。

HttpURLConnection 连接加入缓存过程

读取 http 响应数据时,会使用 sun.net.www.protocol.http.HttpURLConnection 的 内部类 HttpInputStream 的 int read(byte[] b) 方法,最终到
ChunkedInputStream 类的 processRaw() 方法,里面处理原始输出字节流,关闭连接时调用 closeUnderlying() 方法。

sun.net.www.http.ChunkedInputStream.closeUnderlying() 方法的注释可以看出,这个方法里将连接放入 KeepAliveCache 或是 关闭连接:

/**
    * Close the underlying input stream by either returning it to the
    * keep alive cache or closing the stream.
    * <p>
    * As a chunked stream is inheritly persistent (see HTTP 1.1 RFC) the
    * underlying stream can be returned to the keep alive cache if the
    * stream can be completely read without error.
    */
private void closeUnderlying() throws IOException {
    if (in == null) {
        return;
    }

    if (!error && state == STATE_DONE) {
        hc.finished();
    } else {
        if (!hurry()) {
            hc.closeServer();
        }
    }

    in = null;
}

sun.net.www.http.HttpClient.finished() 方法中,判断是 keep-alive 连接且剩余连接数大于0且未超时,调用 putInKeepAliveCache() 方法加入 keep-alive 缓存。

protected static KeepAliveCache kac = new KeepAliveCache();

/* return it to the cache as still usable, if:
    * 1) It's keeping alive, AND
    * 2) It still has some connections left, AND
    * 3) It hasn't had a error (PrintStream.checkError())
    * 4) It hasn't timed out
    *
    * If this client is not keepingAlive, it should have been
    * removed from the cache in the parseHeaders() method.
    */

public void finished() {
    if (reuse) /* will be reused */
        return;
    keepAliveConnections--;
    poster = null;
    if (keepAliveConnections > 0 && isKeepingAlive() &&
            !(serverOutput.checkError())) {
        /* This connection is keepingAlive && still valid.
            * Return it to the cache.
            */
        putInKeepAliveCache();
    } else {
        closeServer();
    }
}

protected synchronized void putInKeepAliveCache() {
    if (inCache) {
        assert false : "Duplicate put to keep alive cache";
        return;
    }
    inCache = true;
    kac.put(url, null, this);
}

最终放入缓存的操作在 sun.net.www.http.KeepAliveCache 类的 put() 方法中

HttpURLConnection 从缓存获取连接过程

java.net.HttpURLConnection.connect() 方法最终调用 plainConnect0() 获取连接,最终调用 getNewHttpClient:

protected HttpClient getNewHttpClient(URL url, Proxy p, int connectTimeout) throws IOException {
    return HttpClient.New(url, p, connectTimeout, this);
}

sun.net.www.http.HttpClient.New() 方法中从 KeepAliveCache 缓存获取连接过程如下:

public class HttpClient extends NetworkClient {
    protected static KeepAliveCache kac = new KeepAliveCache();

    public static HttpClient New(URL url, Proxy p, int to, boolean useCache, HttpURLConnection httpuc) throws IOException {
        if (p == null) {
            p = Proxy.NO_PROXY;
        }
        HttpClient ret = null;
        /* see if one's already around */
        if (useCache) {
            ret = kac.get(url, null);
            if (ret != null && httpuc != null &&
                httpuc.streaming() &&
                "POST".equals(httpuc.getRequestMethod())) {
                if (!ret.available()) {
                    ret.inCache = false;
                    ret.closeServer();
                    ret = null;
                }
            }

            if (ret != null) {
                if ((ret.proxy != null && ret.proxy.equals(p)) ||
                    (ret.proxy == null && p == null)) {
                    synchronized (ret) {
                        ret.cachedHttpClient = true;
                        assert ret.inCache;
                        ret.inCache = false;
                        if (httpuc != null && ret.needsTunneling())
                            httpuc.setTunnelState(TUNNELING);
                        logFinest("KeepAlive stream retrieved from the cache, " + ret);
                    }
                } else {
                    // We cannot return this connection to the cache as it's
                    // KeepAliveTimeout will get reset. We simply close the connection.
                    // This should be fine as it is very rare that a connection
                    // to the same host will not use the same proxy.
                    synchronized(ret) {
                        ret.inCache = false;
                        ret.closeServer();
                    }
                    ret = null;
                }
            }
        }
        if (ret == null) {
            ret = new HttpClient(url, p, to);
        } else {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                if (ret.proxy == Proxy.NO_PROXY || ret.proxy == null) {
                    security.checkConnect(InetAddress.getByName(url.getHost()).getHostAddress(), url.getPort());
                } else {
                    security.checkConnect(url.getHost(), url.getPort());
                }
            }
            ret.url = url;
        }
        return ret;
    }
}

最终通过 sun.net.www.http.KeepAliveCache 类的 get() 方法获取缓存的连接

public synchronized HttpClient get(URL url, Object obj) {
    KeepAliveKey key = new KeepAliveKey(url, obj);
    ClientVector v = super.get(key);
    if (v == null) { // nothing in cache yet
        return null;
    }
    return v.get();
}

SpringBoot 内置 Tomcat 超时配置

server.tomcat.connection-timeout

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tomcat.keep-alive-timeout

server.tomcat.connection-timeout 单位毫秒,默认值 60000,创建连接后 server 等待 client 发送请求数据的超时时间。
server.tomcat.keep-alive-timeout 单位毫秒,连接关闭前等待下一个请求的超时时间,这个时间会放到调用方接收到的 response Keep-Alive header 中,未设置时自动使用 server.tomcat.connection-timeout 时间,设置为 -1 表示无超时。

直接对应到原生 Tomcat 的配置参数

Apache Tomcat 9 Configuration Reference - The HTTP Connector
https://tomcat.apache.org/tomcat-9.0-doc/config/http.html

connectionTimeout
The number of milliseconds this Connector will wait, after accepting a connection, for the request URI line to be presented. Use a value of -1 to indicate no (i.e. infinite) timeout. The default value is 60000 (i.e. 60 seconds) but note that the standard server.xml that ships with Tomcat sets this to 20000 (i.e. 20 seconds). Unless disableUploadTimeout is set to false, this timeout will also be used when reading the request body (if any).

keepAliveTimeout
The number of milliseconds this Connector will wait for another HTTP request before closing the connection. The default value is to use the value that has been set for the connectionTimeout attribute. Use a value of -1 to indicate no (i.e. infinite) timeout.


HttpClient

java.net.http 模块从 Java 11 开始提供

JEP 110,从 Java 9 开始提供一个新的 HTTP client api,用来代替 HttpURLConnection
https://openjdk.org/jeps/110
JEP 321,将 Java 9 引入的 HttpClient 标准化
https://openjdk.org/jeps/321

java.net.http.HttpClient 接口
age jdk.internal.net.http.HttpClientImpl 实现类

https://openjdk.org/groups/net/httpclient/

发送 post json 请求示例:

public CompletableFuture<Void> postJSON(URI uri, Map<String,String> map) throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    String requestBody = objectMapper
          .writerWithDefaultPrettyPrinter()
          .writeValueAsString(map);

    HttpRequest request = HttpRequest.newBuilder(uri)
          .header("Content-Type", "application/json")
          .POST(BodyPublishers.ofString(requestBody))
          .build();

    return HttpClient.newHttpClient()
          .sendAsync(request, BodyHandlers.ofString())
          .thenApply(HttpResponse::statusCode)
          .thenAccept(System.out::println);
}

上一篇 Helm

下一篇 MyCat

阅读
评论
6.8k
阅读预计31分钟
创建日期 2020-09-08
修改日期 2024-03-31
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论