服务器 - 客户端中的异步读取:单独的客户端消息

时间:2014-08-21 11:34:05

标签: java asynchronous nio2

我正在使用AsynchronousServerSocketChannel实现一个简单的服务器。出于测试目的,我创建了一个小型客户端原型,它发送两条消息"hi""stackoverflow",然后断开连接。在服务器端,我读取到达的消息并将它们打印到标准输出。当客户端执行时,我期待收到:

message [hi], bytecount 2
message [stackoverflow], bytecount 13

问题是,当服务器调用读回调时,有时两条消息都已到达,所以我得到了

message [histackoverflow], bytecount 15

代替。

问题是,如果可以在服务器端确保消息单独到达,如果是,那该怎么办?

这是处理客户端连接的CompletionHandler原型:

class CommunicationHandler implements CompletionHandler<AsynchronousSocketChannel, Void> {

    private final AsynchronousServerSocketChannel server;

    public CommunicationHandler(final AsynchronousServerSocketChannel server) {
        this.server = server;
    }

    @Override
    public void failed(Throwable ex, Void attachment) {}

    @Override
    public void completed(final AsynchronousSocketChannel client, Void attachment) {

        // handle client messages
        final ByteBuffer buffer = ByteBuffer.allocateDirect(Server.BUFFER_SIZE);
        final Session session = new Session();
        try {
            client.read(buffer, session, new CompletionHandler<Integer, Session>() {

                @Override
                public void completed(Integer byteCount, final Session currSession) {
                    if (byteCount == -1) {
                        return;
                    }
                    buffer.flip();
                    // TODO forward buffer to message handler (probably protocol?)
                    System.out.println("message [" + convertToString(buffer) + "], byteCount " + byteCount);
                    buffer.clear();
                    // read next message
                    client.read(buffer, currSession, this);
            }

            @Override
            public void failed(Throwable ex, final Session currSession) {}
        });
    }
    // accept the next connection
    server.accept(null, this);
}

ByteBuffer转换为String

public static String convertToString(ByteBuffer bb) {
    final byte[] bytes = new byte[bb.remaining()];
    bb.duplicate().get(bytes);
    return new String(bytes);
}

这是一个测试客户端原型:

public class Client {

public final void start() {
    try (AsynchronousSocketChannel client = AsynchronousSocketChannel.open();) {

        Future<Void> connCall = client.connect(InetAddress.getByName("127.0.0.1"), 8060));
        connCall.get();
        // client is now connected
        // send greeting message
        Future<Integer> writeCall = client.write(Charset.forName("utf-8").encode(CharBuffer.wrap("hi")));
        writeCall.get();
//        Thread.sleep(5000L);
        writeCall = client.write(Charset.forName("utf-8").encode(CharBuffer.wrap("stackoverflow")));
        writeCall.get();
        client.close();
    } catch (IOException e) {
    } catch (InterruptedException ex) {
    } catch (ExecutionException ex) {
    }
}

1 个答案:

答案 0 :(得分:2)

除了在一次读取中获得两次(或甚至更多次)写入的可能性之外,对于较大的消息(通常大约3k或更多),您可以通过多次读取获得一次写入分割。 TCP是一种流协议,并不保留记录边界,除非是偶然的:What is a message boundary?有两种解决方案通常可以工作,虽然使用异步通道我认为你需要进行自己的缓冲管理,这可能会令人困惑并且难以测试:

  • 在每条记录

  • 之前添加显式长度字段
  • 在没有使用字节的情况下,在每条记录之后添加分隔符,或者可以使用转义来区分数据和分隔符

和其他一些已经尝试过的人:

  • 正如您的评论所示,请等待足够长的时间,以便在发送第二个请求之前始终读取第一个请求。在开发人员使用的本地网络和测试系统上,这通常是几毫秒甚至更短;在真正的互联网上,它通常是几秒钟,有时是几分钟,理论上可能是几小时甚至几天。

  • 如果记录永远不会超过几个片段(可能是10k左右),请使用UDP(在Java中以DatagramSocket提供,但不能作为NIO通道AFAICS)并实现自己的协议来处理消息丢失,重复和重新排序(这很难做到,并且经常在30年前在TCP中发现并避免或修复的一些模糊案例中失败)

  • 使用SCTP(在所有AFAICS中都不提供Java,也没有太多其他系统)

除此之外:您的测试客户端以UTF-8发送数据,但new String (byte[])使用默认编码,该编码取决于平台,而不一定是UTF-8。我不确定它是否有保证,但实际上所有可用的编码都包括ASCII作为子集,您的示例数据是ASCII。但是如果你想为它支持实际的UTF-8数据代码。