java.util.Scanner在hasNext()处停顿

6
我在使用Scanner类读取带有特殊EOF标记的Socket消息时遇到了一个非常奇怪的问题。如果客户端一次性写入所有请求或请求具有数据,则一切正常,但是当消息分块写入且下一个标记应为空字符串时,阻塞的hasNext()操作会使服务器挂起,并将其传递给客户端。
这是什么原因?我该如何避免这种情况?
以下是我正在尝试的简化版本,\n用于测试目的,假设定界符可以是任何字符串。
服务端代码:
ServerSocketChannel serverChannel = null;
try {
    serverChannel = ServerSocketChannel.open();

    ServerSocket serverSocket = serverChannel.socket();
    serverSocket.bind(new InetSocketAddress(9081));

    SocketChannel channel = serverChannel.accept();
    Socket socket = channel.socket();

    InputStream is = socket.getInputStream();
    Reader reader = new InputStreamReader(is);
    Scanner scanner = new Scanner(reader);
    scanner.useDelimiter("\n");

    OutputStream os = socket.getOutputStream();
    Writer writer = new OutputStreamWriter(os);

    while (scanner.hasNext()) {
        String msg = scanner.next();
        writer.write(msg);
        writer.write('\n');
        writer.flush();
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (serverChannel != null) {
        try {
            serverChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

工作客户端:

Socket socket = new Socket();
try {
    socket.connect(new InetSocketAddress("localhost", 9081));

    InputStream is = socket.getInputStream();
    Reader reader = new InputStreamReader(is);
    Scanner scanner = new Scanner(reader);
    scanner.useDelimiter("\n");

    OutputStream os = socket.getOutputStream();
    Writer writer = new OutputStreamWriter(os);

    writer.write("foo\n\nbar\n");
    writer.flush();
    System.out.println(scanner.next());
    System.out.println(scanner.next());
    System.out.println(scanner.next());

} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        socket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

悬挂客户端:
Socket socket = new Socket();
try {
    socket.connect(new InetSocketAddress("localhost", 9081));

    InputStream is = socket.getInputStream();
    Reader reader = new InputStreamReader(is);
    Scanner scanner = new Scanner(reader);
    scanner.useDelimiter("\n");

    OutputStream os = socket.getOutputStream();
    Writer writer = new OutputStreamWriter(os);

    writer.write("foo\n");
    writer.flush();
    System.out.println(scanner.next());

    writer.write("\n");
    writer.flush();
    System.out.println(scanner.next());

    writer.write("bar\n");
    writer.flush();
    System.out.println(scanner.next());

} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        socket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
2个回答

1

我花了一些时间追踪代码,问题明显是在Scanner类中存在缺陷。

public boolean hasNext() {
    ensureOpen();
    saveState();
    while (!sourceClosed) {
        if (hasTokenInBuffer())
            return revertState(true);
        readInput();
    }
    boolean result = hasTokenInBuffer();
    return revertState(result);
}

hasNext() 调用 hasTokenInBuffer()


private boolean hasTokenInBuffer() {
    matchValid = false;
    matcher.usePattern(delimPattern);
    matcher.region(position, buf.limit());

    // Skip delims first
    if (matcher.lookingAt())
        position = matcher.end();

    // If we are sitting at the end, no more tokens in buffer
    if (position == buf.limit())
        return false;

    return true;
}
hasTokenInBuffer()总是跳过第一个定界符,如果存在的话,如javadoc中所解释的。

next()和hasNext()方法及其原始类型伴侣方法(例如nextInt()和hasNextInt())首先跳过与定界符模式匹配的任何输入,然后尝试返回下一个标记。 hasNext和next方法都可能阻塞等待进一步输入。无论hasNext方法是否阻塞,它关联的next方法是否阻塞都没有关系。

首先,我们跳过仍在缓冲区中的令牌,然后我们注意到我们的缓冲区中没有任何新数据,因此我们调用readInput(),在这种情况下只是\n,然后我们循环回到hasTokenInBuffer(),它再次跳过我们的定界符!

此时,服务器正在等待更多输入,而客户端正在等待响应。死锁。

如果我们检查是否跳过了上一个令牌,就可以轻松避免这种情况...

private boolean skippedLast = false;

private boolean hasTokenInBuffer() {
    matchValid = false;
    matcher.usePattern(delimPattern);
    matcher.region(position, buf.limit());

    // Skip delims first
    if (!skippedLast && matcher.lookingAt()) {
        skippedLast = true;
        position = matcher.end();
    } else {
        skippedLast = false;
    }

    // If we are sitting at the end, no more tokens in buffer
    if (position == buf.limit())
        return false;

    return true;
}

0

您没有关闭已接受的套接字。

您不需要一个“特殊的EOF令牌”。流的结束是明确无歧义的。


在读取完所有输入后,我将关闭套接字通道,这将隐式地关闭底层套接字,无论程序多久之前需要关闭套接字。令牌用作消息分隔符,以便在单个会话期间发送多个消息,这不涉及检测流的末尾。我知道流的末尾会导致hasNext()函数返回。 - Jake Holzinger
请记住,这只是一个重现问题的示例,实际生产代码使用非阻塞套接字通道,持续接受连接,并生成工作线程来处理套接字的读取、写入和关闭。 - Jake Holzinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接