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的本地端口限制了,就留在下一篇啦。

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