restemplate使用HttpClient发送请求绑定本地端口port(二)——linux客户端发送http请求socket本地端口范围限制

上一篇已经讲过怎么绑定端口发送http请求和https请求,但是只能运行在单线程中不会出问题,然鹅,多线程发送请求时,就会出现一个socket不够用的情况,这就要求请求的端口需要设置一个范围,在范围内可以复用。

先写一个main方法模拟多线程请求的情况

public static void main(String[] args){
        try {
            final RestTemplateUtil2 restTemplateUtil2 = new RestTemplateUtil2();
            String url = "http://www.baidu.com";
//            String s = restTemplateUtil2.get(url,null,String.class);
            int j = 0;
            for (int i = 1; i < 100; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (j < 10){
                            String s = restTemplateUtil2.httpsGet(url, null, String.class);
//                            System.out.println("s=="+Thread.currentThread()+" = [" + s.length() + "]");
                        }
                    }
                }).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("err = [" + e + "]");
        }

    }

修改一下上一篇自己写的BindPortConnectSocketFactory类中,创建socket的方法

public class BindPortConnectSocketFactory implements ConnectionSocketFactory {
    public Map<Integer,Socket> map = new HashMap<>();
    private int startPort = 10000;
    private int endPort = 10005;
    int currentPort = startPort;
    public static final BindPortConnectSocketFactory INSTANCE = new BindPortConnectSocketFactory();

    public static BindPortConnectSocketFactory getSocketFactory() {
        return INSTANCE;
    }

    public BindPortConnectSocketFactory() {
        super();
    }

    @Override
    public synchronized Socket createSocket(HttpContext context) throws IOException {
        for (Map.Entry<Integer,Socket> entry : map.entrySet()){
            System.out.println("==socket="+entry.getValue().hashCode()+
                                "; port--"+entry.getValue().getLocalPort()+
                                "; close:"+entry.getValue().isClosed()+
                                "; connect-"+entry.getValue().isConnected()+
                                "; bind-"+entry.getValue().isBound()+
                                "; isInputShutdown:"+entry.getValue().isInputShutdown()+
                                "; isOutputShutdown:"+entry.getValue().isOutputShutdown());
        }

        FXSocket socket = new FXSocket();
        socket.bind(new InetSocketAddress(currentPort++));
        System.out.println("socket.getLocalPort() = ["+socket.getLocalPort()+
                            "]=socket="+socket.hashCode());
        return socket;
    }

    @Override
    public Socket connectSocket(
            int connectTimeout,
            Socket socket,
            HttpHost host,
            InetSocketAddress remoteAddress,
            InetSocketAddress localAddress,
            HttpContext context) throws IOException {
        final Socket sock = socket != null ? socket : createSocket(context);
        if (localAddress != null) {
            sock.bind(localAddress);
        }
        try {
            sock.connect(remoteAddress, connectTimeout);
        } catch (final IOException ex) {
            try {
                sock.close();
            } catch (final IOException ignore) {
            }
            throw ex;
        }
        System.out.println("connectSocket.getLocalPort = [" + socket.getLocalPort() + "]");
        map.put(socket.getLocalPort(),sock);
        return sock;
    }
}

这里添加了一个map来记录当前所有添加的socket和port情况,还有端口范围以及端口绑定情况

看一下输出日志

socket.getLocalPort() = [10000]=socket=1704182765
socket.getLocalPort() = [10001]=socket=1119031276
connectSocket.getLocalPort = [10001]
connectSocket.getLocalPort = [10000]
关闭socket连接
==socket = [1704182765; port--10000; close:false; connect-true; bind-true; isInputShutdown:false; isOutputShutdown:false
==socket = [1119031276; port--10001; close:true; connect-true; bind-true; isInputShutdown:true; isOutputShutdown:true
socket.getLocalPort() = [10002]=socket=325716189
18:00:12.015 [Thread-41] INFO  o.a.http.impl.execchain.RetryExec - I/O exception (org.apache.http.NoHttpResponseException) caught when processing request to {}->http://www.baidu.com:80: The target server failed to respond
18:00:12.022 [Thread-41] INFO  o.a.http.impl.execchain.RetryExec - Retrying request to {}->http://www.baidu.com:80
connectSocket.getLocalPort = [10002]
关闭socket连接
18:00:13.226 [Thread-66] INFO  o.a.http.impl.execchain.RetryExec - I/O exception (org.apache.http.NoHttpResponseException) caught when processing request to {}->http://www.baidu.com:80: The target server failed to respond
==socket = [1704182765; port--10000; close:true; connect-true; bind-true; isInputShutdown:true; isOutputShutdown:true
==socket = [1119031276; port--10001; close:true; connect-true; bind-true; isInputShutdown:true; isOutputShutdown:true
==socket = [325716189; port--10002; close:false; connect-true; bind-true; isInputShutdown:false; isOutputShutdown:false
18:00:13.227 [Thread-66] INFO  o.a.http.impl.execchain.RetryExec - Retrying request to {}->http://www.baidu.com:80
socket.getLocalPort() = [10003]=socket=92872500
connectSocket.getLocalPort = [10003]

Process finished with exit code 0

大量请求实验,有兴趣的同学可以自己试试,我试过以后,发现socket永远再用的只会有2个,之前创建的会关闭。但是注意,关闭之后,不会解绑,也不会关闭连接,所以这也导致了已用过的端口无法再绑定,否则会报address already in use的错误。

看Socket的源码,也会发现这个问题

   /**
     * Closes this socket.
     * <p>
     * Any thread currently blocked in an I/O operation upon this socket
     * will throw a {@link SocketException}.
     * <p>
     * Once a socket has been closed, it is not available for further networking
     * use (i.e. can't be reconnected or rebound). A new socket needs to be
     * created.
     *
     * <p> Closing this socket will also close the socket's
     * {@link java.io.InputStream InputStream} and
     * {@link java.io.OutputStream OutputStream}.
     *
     * <p> If this socket has an associated channel then the channel is closed
     * as well.
     *
     * @exception  IOException  if an I/O error occurs when closing this socket.
     * @revised 1.4
     * @spec JSR-51
     * @see #isClosed
     */
    public synchronized void close() throws IOException {
        synchronized(closeLock) {
            if (isClosed())
                return;
            if (created)
                impl.close();
            closed = true;
        }
    }

翻译一下,意思是:

当前在此套接字上的I/O操作中阻塞的任何线程都将抛出SocketException异常

一旦一个套接字被关闭,它就不能用于进一步的网络使用(即不能重新连接或重新绑定)。需要创建一个新的套接字。

下面的源码我就把解释直接写在下面了

/**
     * Returns the connection state of the socket.
     * <p>
     * Note: Closing a socket doesn't clear its connection state, which means
     * this method will return {@code true} for a closed socket
     * (see {@link #isClosed()}) if it was successfuly connected prior
     * to being closed.
     *
     *注意:关闭套接字并不会清除它的连接状态,这意味着该方法将为关闭的套接字返回{@code true}(参见
     *{@link #isClosed()}),如果它在关闭之前已经成功连接。
     *
     * @return true if the socket was successfuly connected to a server
     * @since 1.4
     */
    public boolean isConnected() {
        // Before 1.3 Sockets were always connected during creation
        return connected || oldImpl;
    }

    /**
     * Returns the binding state of the socket.
     * <p>
     * Note: Closing a socket doesn't clear its binding state, which means
     * this method will return {@code true} for a closed socket
     * (see {@link #isClosed()}) if it was successfuly bound prior
     * to being closed.
     *
     * 注意:关闭套接字不清除其绑定状态,这意味着该方法将为关闭的套接字返回{@code true}(请参阅
     * {@link #isClosed()}),如果它在关闭之前绑定成功的话。
     *
     * @return true if the socket was successfuly bound to an address
     * @since 1.4
     * @see #bind
     */
    public boolean isBound() {
        // Before 1.3 Sockets were always bound during creation
        return bound || oldImpl;
    }

也就是说,一旦端口绑定了,想要再去绑定这个端口,就会报错,在哪里报错呢,我故意让它绑定报错一下

修改一下代码

public synchronized Socket createSocket(HttpContext context) throws IOException {
        for (Map.Entry<Integer,Socket> entry : map.entrySet()){
            System.out.println("==socket = [" + entry.getValue().hashCode()+"; port--"+ entry.getValue().getLocalPort()+ "; close:"
                    +entry.getValue().isClosed()+"; connect-"+entry.getValue().isConnected()+"; bind-"+entry.getValue().isBound()
                    +"; isInputShutdown:"+entry.getValue().isInputShutdown()+"; isOutputShutdown:"+entry.getValue().isOutputShutdown());
        }

        FXSocket socket = new FXSocket();

        if (currentPort == 10003){
            socket.bind(new InetSocketAddress(10000));
        }else{
            socket.bind(new InetSocketAddress(currentPort++));
        }

        System.out.println("socket.getLocalPort() = [" + socket.getLocalPort() + "]"+"=socket="+socket.hashCode());


        return socket;
    }

为什么写10003呢,因为一直都只会用两个端口,当10003用到的时候10000肯定已经关闭了,日志表示也是真的关闭了

 下面试报错的日志

==socket = [1368876963; port--10000; close:true; connect-true; bind-true; isInputShutdown:true; isOutputShutdown:true
==socket = [2040279492; port--10001; close:true; connect-true; bind-true; isInputShutdown:true; isOutputShutdown:true
==socket = [508958732; port--10002; close:false; connect-true; bind-true; isInputShutdown:false; isOutputShutdown:false
Exception in thread "Thread-18" org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://www.baidu.com": Address already in use: JVM_Bind; nested exception is java.net.BindException: Address already in use: JVM_Bind
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:666)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
	at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:531)
	at com.fxdigital.restcustom.RestTemplateUtil2.exchangeHttps(RestTemplateUtil2.java:150)
	at com.fxdigital.restcustom.RestTemplateUtil2.httpsGet(RestTemplateUtil2.java:142)
	at com.fxdigital.restcustom.RestTemplateUtil2$1.run(RestTemplateUtil2.java:166)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.BindException: Address already in use: JVM_Bind
	at java.net.DualStackPlainSocketImpl.bind0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
	at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
	at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
	at java.net.Socket.bind(Socket.java:644)
	at com.fxdigital.restcustom.http.BindPortConnectSocketFactory.createSocket(BindPortConnectSocketFactory.java:50)
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:118)
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353)
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380)
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88)
	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
	at com.fxdigital.restcustom.http.BindPortCloseableHttpClient.doExecute(BindPortCloseableHttpClient.java:157)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:55)
	at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:89)
	at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
	at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:652)
	... 6 more
	at java.net.DualStackPlainSocketImpl.bind0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)

这里我们看到,绑定最后走到了native方法,这我就想不开了,这要我怎么搞,总不至于还让公司的人去改底层吧

但是控制台的10000端口也确实没有进程使用

前面是在执行请求的时候,后面是绑定的端口socket已经关闭了,但是还是不能绑定

突发奇想一下,那要是系统自己自动随机的端口呢

去掉自己的代码,重新运行,还好可以去map中直接拿到已经关闭的socket曾经绑定的端口

int port = 0;

 public synchronized Socket createSocket(HttpContext context) throws IOException {
        for (Map.Entry<Integer,Socket> entry : map.entrySet()){
            System.out.println("==socket = [" + entry.getValue().hashCode()+"; port--"+ entry.getValue().getLocalPort()+ "; close:"
                    +entry.getValue().isClosed()+"; connect-"+entry.getValue().isConnected()+"; bind-"+entry.getValue().isBound()
                    +"; isInputShutdown:"+entry.getValue().isInputShutdown()+"; isOutputShutdown:"+entry.getValue().isOutputShutdown());
            if (entry.getValue().isClosed()){
                port = entry.getKey();
            }

        }

        FXSocket socket = new FXSocket();
        if (port != 0){
            socket.bind(new InetSocketAddress(port));
        }

        System.out.println("socket.getLocalPort() = [" + socket.getLocalPort() + "]"+"=socket="+socket.hashCode());


        return socket;
    }

运行一下,看一下日志

==socket = [1219446347; port--4082; close:true; connect-true; bind-true; isInputShutdown:true; isOutputShutdown:true
==socket = [696481194; port--4075; close:false; connect-true; bind-true; isInputShutdown:false; isOutputShutdown:false
Exception in thread "Thread-1" org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://www.baidu.com": Address already in use: JVM_Bind; nested exception is java.net.BindException: Address already in use: JVM_Bind
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:666)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
	at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:531)
	at com.fxdigital.restcustom.RestTemplateUtil2.exchangeHttps(RestTemplateUtil2.java:150)
	at com.fxdigital.restcustom.RestTemplateUtil2.httpsGet(RestTemplateUtil2.java:142)
	at com.fxdigital.restcustom.RestTemplateUtil2$1.run(RestTemplateUtil2.java:166)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.BindException: Address already in use: JVM_Bind
	at java.net.DualStackPlainSocketImpl.bind0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
	at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
	at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
	at java.net.Socket.bind(Socket.java:644)
	at com.fxdigital.restcustom.http.BindPortConnectSocketFactory.createSocket(BindPortConnectSocketFactory.java:55)
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:118)
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353)
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380)
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88)
	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
	at com.fxdigital.restcustom.http.BindPortCloseableHttpClient.doExecute(BindPortCloseableHttpClient.java:157)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:55)
	at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:89)
	at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
	at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:652)
	... 6 more
Exception in thread "Thread-49" org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://www.baidu.com": Address already in use: JVM_Bind; nested exception is java.net.BindException: Address already in use: JVM_Bind
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:666)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
	at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:531)
	at com.fxdigital.restcustom.RestTemplateUtil2.exchangeHttps(RestTemplateUtil2.java:150)
	at com.fxdigital.restcustom.RestTemplateUtil2.httpsGet(RestTemplateUtil2.java:142)
	at com.fxdigital.restcustom.RestTemplateUtil2$1.run(RestTemplateUtil2.java:166)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.BindException: Address already in use: JVM_Bind
	at java.net.DualStackPlainSocketImpl.bind0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
	at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
	at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
	at java.net.Socket.bind(Socket.java:644)
	at com.fxdigital.restcustom.http.BindPortConnectSocketFactory.createSocket(BindPortConnectSocketFactory.java:55)
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:118)
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353)
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380)

依旧还是报错,说明,就算是不修改,也是端口一旦绑定,也是一样的没有释放,说明系统自己管理的时候,也没有及时释放端口

难道要手动释放,百度有说要2分钟时长等待的,写了一个5分钟的等待,试了一下,依旧不行

这下我就懵逼了,这怎么搞,要是这样一直加下去,范围我根本控制不了啊

百度的时候,偶然发现了这样一个人写的博客

https://www.cnblogs.com/yunnotes/archive/2013/04/19/3032301.html

在linux系统中,直接修改/proc/sys/net/ipv4/ip_local_port_range文件中的值,就可以限制socket建立的tcp连接端口范围了。感觉自己辛苦了那么久,好像走错了方向,求内心的阴影面积

开心的我拿到了公司的linux服务器,在上面迅速跑了一把,其中限制端口为10000-10200

多次测试之后,端口一直都是在10000-10200之间,当改回原来的配置32768-61000之后,端口也成了40000多,it worked

但是proc目录下的文件,都是运行时生成的,所以,重启之后,就没有用了,我们要配置好,就得在另外一个地方去配置了

在/etc/system.config文件中这样写

# System default settings live in /usr/lib/sysctl.d/00-system.conf.
# To override those settings, enter new settings here, or in an /etc/sysctl.d/<name>.conf file
#
# For more information, see sysctl.conf(5) and sysctl.d(5).
net.ipv4.ip_local_port_range=20000 210000

接下来,就是windows的本地端口限制了,就留在下一篇啦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章