尝试使用非阻塞IO打包TCP很难!难道我做错了什么?

时间:2010-04-11 23:13:54

标签: java tcp network-programming

哦,我希望TCP是基于数据包的,就像UDP一样! [见评论]但是,唉,情况并非如此,所以我正在尝试实现自己的数据包层。这是迄今为止的事件链(忽略编写数据包)

哦,我的数据包结构非常简单:两个无符号字节的长度,然后是byte [length]数据。 (我无法想象如果它们更复杂,我会在if陈述中听到我的声音!)

  • Server处于无限循环中,接受连接并将其添加到Connection列表中。
  • PacketGatherer(另一个主题)使用Selector来确定哪些Connection.SocketChannel已准备好阅读。
  • 它循环显示结果,并告诉每个Connection read()
  • 每个Connection都有一个部分IncomingPacket和一个Packet列表,这些列表已经完全阅读并等待处理。
  • read()上:
    • 告诉部分IncomingPacket阅读更多数据。 (IncomingPacket.readData以下)
    • 如果已完成阅读(IncomingPacket.complete()),请从中Packet创建Packet并将IncomingPacket粘贴到等待处理的列表中,然后将其替换为新的IncomingPacket }。

这有几个问题。首先,一次只读取一个数据包。如果IncomingPacket只需要一个字节,则此次传递只读取一个字节。这当然可以通过循环来修复,但它开始变得复杂,我想知道是否有更好的整体方式。

其次,int readBytes; // number of total bytes read so far byte length1, length2; // each byte in an unsigned short int (see getLength()) public int getLength() { // will be inaccurate if readBytes < 2 return (int)(length1 << 8 | length2); } public void readData(SocketChannel c) { if (readBytes < 2) { // we don't yet know the length of the actual data ByteBuffer lengthBuffer = ByteBuffer.allocate(2 - readBytes); numBytesRead = c.read(lengthBuffer); if(readBytes == 0) { if(numBytesRead >= 1) length1 = lengthBuffer.get(); if(numBytesRead == 2) length2 = lengthBuffer.get(); } else if(readBytes == 1) { if(numBytesRead == 1) length2 = lengthBuffer.get(); } readBytes += numBytesRead; } if(readBytes >= 2) { // then we know we have the entire length variable // lazily-instantiate data buffers based on getLength() // read into data buffers, increment readBytes // (does not read more than the amount of this packet, so it does not // need to handle overflow into the next packet's data) } } public boolean complete() { return (readBytes > 2 && readBytes == getLength()+2); } 中的逻辑有点疯狂,能够读取长度的两个字节,然后读取实际数据。这是代码,快速简化&amp; amp;易读:

public void fillWriteBuffer() {
    while(!writePackets.isEmpty() && writeBuf.remaining() >= writePackets.peek().size()) {
        Packet p = writePackets.poll();
        assert p != null;
        p.writeTo(writeBuf);
    }
}

public void fillReadPackets() {
    do {
        if(readBuf.position() < 1+2) {
            // haven't yet received the length
            break;
        }

        short packetLength = readBuf.getShort(1);

        if(readBuf.limit() >= 1+2 + packetLength) {
            // we have a complete packet!

            readBuf.flip();

            byte packetType = readBuf.get();

            packetLength = readBuf.getShort();

            byte[] packetData = new byte[packetLength];
            readBuf.get(packetData);

            Packet p = new Packet(packetType, packetData);
            readPackets.add(p);
            readBuf.compact();
        } else {
            // not a complete packet
            break;
        }

    } while(true);
}

基本上我需要有关我的代码和整个过程的反馈。请提出任何改进建议。如果你有关于如何更好地实现整个系统的建议,即使对整个系统进行检修也是可以的。也欢迎预订建议;我喜欢书。我只是觉得事情不太正确。


这是我想出的一般解决方案,感谢Juliano的回答:(如果您有任何问题,请随时发表评论)

{{1}}

4 个答案:

答案 0 :(得分:7)

可能这不是您正在寻找的答案,但有人必须说出来:您可能会过度设计解决方案以解决一个非常简单的问题。

在完全到达之前,您没有数据包,甚至包括IncomingPacket。您只有一个没有定义含义的字节流。通常,simple解决方案是将输入数据保存在缓冲区中(它可以是一个简单的byte []数组,但如果性能有问题,建议使用适当的弹性和循环缓冲区)。每次读取后,检查缓冲区的内容以查看是否可以从那里提取整个数据包。如果可以,则构造Packet,从缓冲区的开头丢弃正确的字节数并重复。如果或者当您无法提取整个数据包时,请将这些传入的字节保留在那里,直到下次从套接字中成功读取内容为止。

当你在这里时,如果你正在通过流通道进行基于数据报的通信,我建议你在每个“数据包”的开头包含一个幻数,这样你就可以测试连接的两端仍然是同步的。如果出于某种原因(一个错误),它们之一可能会从流中读取或写入错误的字节数。

答案 1 :(得分:1)

难道你不能只读取任何准备读取的字节数,并将所有传入的字节输入数据包解析状态机吗?这意味着像任何其他传入数据流一样处理传入(TCP)数据流(通过串行线路,或USB,管道或任何其他......)

所以你会有一些Selector确定哪个连接有待读取的传入字节,以及有多少个。然后你会(对于每个连接)读取可用字节,然后将这些字节提供给(特定于连接)状态机实例(尽管可以从同一个类中读取和馈送)。然后,这个数据包解析状态机类会不时吐出已完成的数据包,然后将这些数据包交给任何将处理这些完整和解析数据包的人。

对于像

这样的数据包格式
    2 magic header bytes to mark the start
    2 bytes of payload size (n)
    n bytes of payload data
    2 bytes of checksum

状态机会有类似的状态(尝试枚举,Java现在有,我收集)

wait_for_magic_byte_0,
wait_for_magic_byte_1,
wait_for_length_byte_0,
wait_for_length_byte_1,
wait_for_payload_byte (with a payload_offset variable counting),
wait_for_chksum_byte_0,
wait_for_chksum_byte_1

并且在每个传入的字节上,您可以相应地切换状态。如果传入的字节没有正确推进状态机,则通过将状态机重置为wait_for_magic_byte_0来丢弃该字节。

答案 2 :(得分:1)

暂时忽略客户端断开连接和服务器关闭,这里或多或少是传统的套接字服务器结构:

  • 选择器,处理套接字:
    • 民意调查开放套接字
    • 如果是服务器套接字,请创建新的连接对象
    • 为每个活动客户端套接字找到连接,使用事件(读取或写入)调用
  • 连接(每个插槽一个),处理一个套接字上的I / O:
    • 通过两个队列,输入和输出
    • 协议进行通信
    • 保留两个缓冲区,一个用于读取,一个用于写入,以及各自的偏移
    • on read event:读取所有可用输入字节,查找消息边界,将整个消息放入协议输入队列,调用协议
    • on write event:写入缓冲区,或者如果它为空,将消息表单输出队列放入缓冲区,开始写入
  • 协议(每个连接一个),处理一个连接上的应用程序协议交换:
    • 从输入队列中获取消息,解析消息的应用程序部分
    • 服务器工作(这里是状态机的位置 - 某些消息适用于一种状态,而不是另一种状态),生成响应消息,将其放入输出队列

就是这样。一切都可以在一个线程中。这里的关键是责任分离。 希望这会有所帮助。

答案 3 :(得分:0)

我认为你从一个稍微错误的方向接近这个问题。考虑数据结构,而不是考虑数据包。这就是你要发送的东西。实际上,是的,它是一个应用程序层数据包,但只是将其视为一个数据对象。然后,在最低级别,编写一个将读取线路的例程,并输出数据对象。这将为您提供我认为您正在寻找的抽象层。