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

欢迎关注公众号
​​​​​​在这里插入图片描述

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