如何使用 Java NIO 限制从 SocketChannel InputStream 一次读取一行

发布于 2024-11-04 07:30:28 字数 669 浏览 1 评论 0原文

我正在尝试编写一个 Websockets 客户端和服务器。最初连接是 HTTP,Websockets 握手使用 HTTP 标头来指示连接需要升级到新协议。

我想从 SocketChannel 读取 HTTP 标头集,如果指示升级,则切换到不同的库来处理 Websocket,从那时起,以完全不同的方式处理 SocketChannel 流,将其作为一组帧而不是行用 \r\n 分隔。

我知道我可以将任意数量的字节读取到 ByteBuffer 中,但是 Websockets 帧可能已通过握手发送,并且我不想在这些代码部分之间传递半消耗的缓冲区。我想要的是从套接字仅读取直到并包括序列“\r\n\r\n”的数据。除此之外的任何数据我都想留在 SocketChannel 对象输入流中。

推荐的方法是什么?从 SocketChannel 获取输入流并将其包装在缓冲读取器中?这是否可以与 NIO 正确交互,特别是非阻塞使用?一旦检测到空行,我是否可以从输入流中删除缓冲读取器,并且当通道传递到 Websockets 代码时仍然拥有所有可用的帧数据?

或者也许我需要逐字节读取(或者如果某些目标“\r\n\r\n”字符出现在块的末尾,则读取具有较小缓冲区的 4 字节块)并构建我的标头字符串方式。

或者,如果直接分配缓冲区,则操作标记、限制和位置的某种组合可能允许输入流取回之前读入 ByteBuffer 的数据。

任何建议将不胜感激。

I am trying to write a Websockets client and server. Initially the connection is HTTP and the Websockets handshake uses HTTP headers to indicate that an upgrade to a new protocol is necessary on the connection.

I want to read in the set of HTTP headers from the SocketChannel and, if an upgrade is indicated, switch over to a different library for handling Websockets and from that point on handle the SocketChannel streams completely differently, as a set of frames rather than lines delimited with \r\n.

I know I can read an arbitrary number of bytes into a ByteBuffer, but a Websockets frame may have been sent with the handshake and I don't want to be passing off half-consumed buffers between these sections of code. What I want is to read from the socket only the data up to and including sequence "\r\n\r\n". Any data beyond that I want to leave in the SocketChannel object input stream.

What is the recommended way to do this? Get the input stream from the SocketChannel and wrap it in a buffered reader? Would this interact properly with NIO, particularly non-blocking uses? Could I drop the buffered reader from the input stream once the blank line was detected and still have all the frame data available when the channel is passed to the Websockets code?

Or perhaps I need to read byte-by-byte (or 4 byte chunks with smaller buffers if some of the target "\r\n\r\n" characters appear at the end of the chunk) and build up my header strings that way.

Or maybe some combination of manipulating mark, limit and position would allow the input stream to get back data it had previously read into the ByteBuffer provided the buffer was allocated directly.

Any advice would be greatly appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

念﹏祤嫣 2024-11-11 07:30:28

我建议使用 Apache Mina 或 Grizzly 之类的东西。两者都允许您封装问题的协议方面,因此您只需处理可使用的数据。

但是,如果您想要一种快速而肮脏的方式:
但基本思想是,您需要在数据传入时读取数据。如果它不容易使用,我通常会为选择器中的 SelectionKey 创建一些可附加结构(对于简单的 StringBuilder )。每次读取后,我会将数据附加到构建器,如果检测到可用的标头,则将其从缓冲区中切片并将其向上传递(最好在工作线程上)。继续这样做,上游的任何东西都应该能够做出相应的反应。希望有帮助。

所以通常你有这样的结构:

ByteBuffer reUsableBuffer = ByteBuffer.allocateDirect(5120);
Selector selector = Selector.open();
ServerSocketChannel channel = .. // wherever you get it from 
channel.register(selector, SelectionKey.OP_ACCEPT);
Executor executor = Executors.newThreadPoolExecutor();
while(selector.isOpen()) { 
 int numKey = selector.select();
 for (SelectionKey key: selector.selectedKeys()) {
    if (key.isAcceptable()) {
             /// Sort of included for completeness but you get the idea
           ServerSocketChannel server = (ServerSocketChannel)key.channel();
           SocketChannel channel = server.accept();
           channel.register(selector, SelectionKey.OP_READ | Selection.OP_WRITE, new StringBuilder());
    }    if (key.isReadable()) {
          // READ the data
          reUsableBuffer.clear();
          // You have to keep track of previous state.
          // NIO makes no guarantees of anything
          StringBuilder builder = key.attachment();
          SocketChannel socketChannel = (SocketChannel)key.channel();
          int readCount = socketChannel.read(reUsableBuffer);
          if (readCount > 0) {
             reUsableBuffer.flip();
             byte[] subStringBytes = new byte[readCount];
             reUsableBuffer.read(subStringBytes);
             // Assuming ASCII (bad assumption but simplifies the example)
             builder.append(new String(substringBytes));

             Command[] commands = removeCommands(builder);
             // Deal with your commands in some async manor defined by you
             executor.execute(new Task(commands));
          }
        }
        selector.selectedKeys().clear(); } ....

    }   

//
// Parse out the commands and return them, also remove traces of them in the
// the builder, such that for a string, "COMMAND, COMMAND, COM"
// an array of 2 should be returned with a left over buffer of "COM"
public Command[] parseCommands(StringBuilder s) { ... }

I would recommend using something like Apache Mina or Grizzly. Both allow you to encapsulated the protocol aspect of your problem so you only have to handle consumable data.

However if you want a quick and dirty way:
The basic idea though is yeah you need to read the data as it comes in. If it is not readily usable, I usually create some appendable structure (StringBuilder for something simple) to the SelectionKey that is in the selector. After each read I would append the data to the builder, and if you detect a usable header, slice it out of the buffer and pass it up stream (preferably on a worker thread). Keep doing this and whatever is upstream should be able to react accordingly. Hope that helps.

So usually you have a structure like this:

ByteBuffer reUsableBuffer = ByteBuffer.allocateDirect(5120);
Selector selector = Selector.open();
ServerSocketChannel channel = .. // wherever you get it from 
channel.register(selector, SelectionKey.OP_ACCEPT);
Executor executor = Executors.newThreadPoolExecutor();
while(selector.isOpen()) { 
 int numKey = selector.select();
 for (SelectionKey key: selector.selectedKeys()) {
    if (key.isAcceptable()) {
             /// Sort of included for completeness but you get the idea
           ServerSocketChannel server = (ServerSocketChannel)key.channel();
           SocketChannel channel = server.accept();
           channel.register(selector, SelectionKey.OP_READ | Selection.OP_WRITE, new StringBuilder());
    }    if (key.isReadable()) {
          // READ the data
          reUsableBuffer.clear();
          // You have to keep track of previous state.
          // NIO makes no guarantees of anything
          StringBuilder builder = key.attachment();
          SocketChannel socketChannel = (SocketChannel)key.channel();
          int readCount = socketChannel.read(reUsableBuffer);
          if (readCount > 0) {
             reUsableBuffer.flip();
             byte[] subStringBytes = new byte[readCount];
             reUsableBuffer.read(subStringBytes);
             // Assuming ASCII (bad assumption but simplifies the example)
             builder.append(new String(substringBytes));

             Command[] commands = removeCommands(builder);
             // Deal with your commands in some async manor defined by you
             executor.execute(new Task(commands));
          }
        }
        selector.selectedKeys().clear(); } ....

    }   

//
// Parse out the commands and return them, also remove traces of them in the
// the builder, such that for a string, "COMMAND, COMMAND, COM"
// an array of 2 should be returned with a left over buffer of "COM"
public Command[] parseCommands(StringBuilder s) { ... }
枯寂 2024-11-11 07:30:28

我将使用适当的面向行的读取器(例如 LineNumberReader)包装套接字 InputStream。在幕后,这些读者一次读取一个字节。由于您所说的原因,我不会为此使用 BufferedReader 。

I would wrap the socket InputStream with an appropriate line oriented reader, such as LineNumberReader. Under the hood these readers read a byte at a time. I would not use a BufferedReader for this, for the reason you state.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文