Socket 基礎之超時時間

平時經常會聽到“連接超時”、“Socket 超時”,那麼到底是什麼超時呢。以我們目前內部使用的調度任務爲例,有時候會收到這樣的調度異常郵件:

在 xxxx,Exception to request execution plan:java.net.SocketTimeoutException:connect timed out

有時候會收到這樣的:

在 xxxx,Exception to request execution plan:java.net.SocketTimeoutException:Read timed out

即雖然是 time out,但也是分情況的。

TCP

Socket 類有多個構造方法:
在這裏插入圖片描述

我們都知道 TCP 是面向連接的,在上面的構造方法中,除了無參構造,其他的構造方法都會試圖與服務端建立連接。

Socket 類有一個 connect 方法:

//timeout 單位是毫秒,默認爲 0,即無限等待
public void connect(SocketAddress endpoint, int timeout) throws IOException;

如果在指定時間內出現了其他異常,如 ConnectException,那麼會拋出這個異常;如果在指定時間內沒有連接成功,切也沒有拋出其他異常,那麼會拋出 SocketTimeoutException(後面會有相關的 Demo)。

看一個例子:

public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("github.com", 443), 10);
        System.out.println("connect...");
    }

我這裏超時時間設置的很短,很快就出現了連接超時異常:

Exception in thread "main" java.net.SocketTimeoutException: connect timed out
	at java.net.PlainSocketImpl.socketConnect(Native Method)
	at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
	at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
	at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
	at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
	at java.net.Socket.connect(Socket.java:589)
	at com.dongguabai.socket.demo.tcp.TcpClient.main(TcpClient.java:16)

要注意的是這裏拋出的是 SocketTimeoutException,還有一個異常是 ConnectExceptionSocketTimeoutException 是當套接字讀取或者連接的時候超時就會拋出。而 ConnectException 是當套接字嘗試連接的地址和端口沒有被服務器進程所監聽或者服務器拒絕連接就會拋出。一個簡單的例子:

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), 8080), 10000);
        System.out.println("connect...");
    }

拋出異常:

Exception in thread "main" java.net.ConnectException: Connection refused (Connection refused)
	at java.net.PlainSocketImpl.socketConnect(Native Method)
	at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
	at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
	at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
	at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
	at java.net.Socket.connect(Socket.java:589)
	at com.dongguabai.socket.demo.tcp.TcpClient.main(TcpClient.java:17)

上面演示的是出現 connect timed out 的情況,再看一個 Read timed out 的例子。

這是一個 TCP 服務端:

public class TcpServer {

    private static final ExecutorService POOL_EXECUTOR = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws IOException {
        //服務端端口爲8881
        ServerSocket serverSocket = new ServerSocket(8881, 3);
        System.out.println("服務器啓動:" + serverSocket.getLocalSocketAddress());
        while (true) {
            Socket socket = serverSocket.accept();
            POOL_EXECUTOR.submit(new Handle(socket));
        }
    }

    static class Handle implements Runnable {

        private Socket socket;

        public Handle(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            System.out.printf("【%s:%s】進入連接\n", socket.getInetAddress(), socket.getPort());
            try {
                PrintStream ps = new PrintStream(socket.getOutputStream());
                BufferedReader br = new BufferedReader(new InputStreamReader(
                        socket.getInputStream()));
                while (true) {
                    //todo 很重要
                    br.reset();
                    String readLine = br.readLine();
                    System.out.println("服務端收到的消息:"+readLine);
                    Thread.sleep(4000);
                    ps.println("Server 收到了消息:【"+readLine+"】");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

TCP 客戶端:

public class TcpClient {

    public static void main(String[] args) throws IOException {

        try (Socket socket = new Socket()) {
            //SocketOptions#SO_TIMEOUT
            //單位是毫秒,默認是 0,無限等待;read() Socket 的 InputStream 的時候,如果超時,會拋出 SocketTimeoutException,但是 Socket 仍然可用
            socket.setSoTimeout(1000);
            //連接超時時間
            socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), 8881), 10000);
            System.out.printf("--->已連接到【%s:%s】\n", socket.getInetAddress(), socket.getPort());
            System.out.printf("--->本地信息【%s:%s】\n", socket.getLocalAddress(), socket.getLocalPort());
            System.out.printf("--->本地信息【%s】\n", socket.getLocalSocketAddress());
            System.out.println("開始聊天....");
            //獲取鍵盤輸入
            InputStream in = System.in;
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            //獲取套接字輸出
            OutputStream socketOutputStream = socket.getOutputStream();
            //轉換爲打印流
            PrintStream socketPrintStream = new PrintStream(socketOutputStream);

            //獲取服務器端輸入
            InputStream socketInputStream = socket.getInputStream();
            BufferedReader responseReader = new BufferedReader(new InputStreamReader(socketInputStream));

            while (true) {
                try {
                    //鍵盤讀取數據
                    String readLine = br.readLine();
                    //發送到Server端
                    socketPrintStream.printf("Client:%s\n", readLine);
                    //獲取服務端返回
                    String response = responseReader.readLine();
                    System.out.println(response);
                } catch (Exception e) {
                    System.out.println("發生異常...");
                    e.printStackTrace();
                }

            }
        }

    }
}

先簡單看一下效果,啓動 TcpServer 後再啓動 TcpClient。服務端輸出:

服務器啓動:0.0.0.0/0.0.0.0:8881/192.168.2.114:55209】進入連接

客戶端輸出:

--->已連接到【Dongguabai.local/192.168.2.114:8881--->本地信息【/192.168.2.114:55209--->本地信息【/192.168.2.114:55209】
開始聊天....

這時候在客戶端使用鍵盤輸入"Hello":

--->已連接到【Dongguabai.local/192.168.2.114:8881--->本地信息【/192.168.2.114:55348--->本地信息【/192.168.2.114:55348】
開始聊天....
Hello
發生異常...
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 sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.fill(BufferedReader.java:161)
	at java.io.BufferedReader.readLine(BufferedReader.java:324)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at com.dongguabai.socket.demo.tcp.TcpClient.main(TcpClient.java:51)

因爲服務端我設置了線程阻塞 4s,而客戶端設置的超時時間是 3s,所以拋出了這個異常。而且雖然服務端 4s 後也是返回了數據,但是此時客戶端是接收不到這個數據的。

還有一個要注意的地方是這時候 Socket 仍然是可用的,比如我可以再通過客戶端向服務端傳數據:

--->已連接到【Dongguabai.local/192.168.2.114:8881--->本地信息【/192.168.2.114:55348--->本地信息【/192.168.2.114:55348】
開始聊天....
Hello
發生異常...
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 sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.fill(BufferedReader.java:161)
	at java.io.BufferedReader.readLine(BufferedReader.java:324)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at com.dongguabai.socket.demo.tcp.TcpClient.main(TcpClient.java:51)
Hello2
發生異常...
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 sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.fill(BufferedReader.java:161)
	at java.io.BufferedReader.readLine(BufferedReader.java:324)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at com.dongguabai.socket.demo.tcp.TcpClient.main(TcpClient.java:51)

在一篇博客中看到了一個很形象的描述:

兩種方式控制超時的側重點不同,就像打電話一樣,方法1是打電話10秒你不接電話我就掛了,方法2是打電話接通後,等你10秒不說話就掛,10秒後說不說話都不聽了。

其他

在 JDK 1.5 之後有這樣一個方法:

public void setPerformancePreferences(int connectionTime,
                                      int latency,
                                      int bandwidth)

參數:

connectionTime- 表達最短連接時間的相對重要性的int
latency- 表達低延遲的相對重要性的int
bandwidth- 表達高帶寬的相對重要性的int

性能偏好由三個整數描述,它們的值分別指示短連接時間、低延遲和高帶寬的相對重要性。這些整數的絕對值沒有意義;爲了選擇協議,需要簡單比較它們的值,較大的值指示更強的偏好。負值表示的優先級低於正值。例如,如果應用程序相對於低延遲和高帶寬更偏好短連接時間,則其可以使用值 (1, 0, 0) 調用此方法。如果應用程序相對於低延遲更偏好高帶寬,而相對於短連接時間更偏好低延遲,則其可以使用值 (0, 1, 2) 調用此方法。

通過賦予 int 類型的值去設定這三個指標的相對重要性。但是這個方法所做的設置僅僅爲底層網絡的實現提供參考。有些底層 Socket 的實現會忽略這個設置。我目前使用的 JDK 版本:

➜  develope openJdk 
➜  develope java -version
openjdk version "1.8.0_202"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_202-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.202-b08, mixed mode)

在這個版本中這個方法是沒有任何實現的:

    public void setPerformancePreferences(int connectionTime,
                                          int latency,
                                          int bandwidth)
    {
        /* Not implemented yet */
    }

UDP

UDP 使用的是 DatagramSocket 類,同 Socket 一樣,也有 SO_TIMEOUT 選項,默認爲 0,即無限等待。

public synchronized void setSoTimeout(int timeout) throws SocketException;

這個方法必須在 receive() 方法之前執行, receive() 方法會從網絡上獲取數據包,如果沒有數據,就會阻塞,如果等待超時就會拋出 SocketTimeoutException

看一個簡單的例子。

UDP 服務端:

public class UdpServer {

    public static void main(String[] args) throws IOException, InterruptedException {
        DatagramSocket ds = new DatagramSocket(8882);

        byte[] bytes = new byte[1024];
        //接收包
        DatagramPacket receivePacket = new DatagramPacket(bytes,bytes.length);
        //接收
        ds.receive(receivePacket);

        Thread.sleep(4000);

        System.out.println("來源信息:"+receivePacket.getSocketAddress());
       // getData() 會返回緩衝數組的原始大小,即剛開始創建緩衝數組時指定的大小,這裏爲1024,因此如果我們要獲取接收到的數據,就必須截取getData()方法返回的數組中只含接收到的數據的那一部分。
        String recevie = new String(receivePacket.getData(), 0, receivePacket.getLength());
        System.out.printf("Server 接收數據:【%s】", recevie);

        byte[] sendData = ("Server 響應數據:"+recevie).getBytes();
        //響應數據
        DatagramPacket sendPackage = new DatagramPacket(sendData,sendData.length,receivePacket.getAddress(),receivePacket.getPort());
        ds.send(sendPackage);
        //由於receivePacket在接收了數據之後,其內部消息長度值會變爲實際接收的消息的字節數,需要將內部消息長度重新置爲1024
        //receivePacket.setLength(1024);
        ds.close();
    }
}

UDP 客戶端:

public class UdpClient {
    public static void main(String[] args) throws IOException {
        //發送方不需要指定端口,系統隨機分配
        DatagramSocket sender = new DatagramSocket();


        String dataStr = "Hello.....";
        //發送數據
        byte[] sendData = dataStr.getBytes();
        DatagramPacket sendPackage = new DatagramPacket(sendData,sendData.length);
        sendPackage.setAddress(InetAddress.getLocalHost());
        sendPackage.setPort(8882);
        sender.send(sendPackage);

        System.out.println("client 已發送數據"+dataStr);

        //設置接收超時時間 receive()方法最長阻塞時間
        sender.setSoTimeout(3000);

        byte[] bytes = new byte[1024];
        //接收包
        DatagramPacket receivePacket = new DatagramPacket(bytes,bytes.length);
        //接收
        sender.receive(receivePacket);
        System.out.println("來源信息:"+receivePacket.getSocketAddress());
        // getData() 會返回緩衝數組的原始大小,即剛開始創建緩衝數組時指定的大小,這裏爲1024,因此如果我們要獲取接收到的數據,就必須截取getData()方法返回的數組中只含接收到的數據的那一部分。
        String recevie = new String(receivePacket.getData(), 0, receivePacket.getLength());
        System.out.printf("Client 接收數據:【%s】", recevie);

        //由於receivePacket在接收了數據之後,其內部消息長度值會變爲實際接收的消息的字節數,需要將內部消息長度重新置爲1024
        //receivePacket.setLength(1024);
        sender.close();
    }
}

啓動服務端再啓動客戶端,客戶端會輸出:

client 已發送數據Hello.....
Exception in thread "main" java.net.SocketTimeoutException: Receive timed out
	at java.net.PlainDatagramSocketImpl.receive0(Native Method)
	at java.net.AbstractPlainDatagramSocketImpl.receive(AbstractPlainDatagramSocketImpl.java:143)
	at java.net.DatagramSocket.receive(DatagramSocket.java:812)
	at com.dongguabai.socket.demo.udp.UdpClient.main(UdpClient.java:36)

這時候出現的是 Receive time out。

References

  • 《Java 網絡編程核心技術詳解》
  • https://www.cnblogs.com/LeeXiaoYang/p/11494691.html
  • https://blog.csdn.net/ns_code/article/details/14128987

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

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