合作流消费者的设计理念?

时间:2013-01-04 02:26:12

标签: java design-patterns stream

假设以下情形:

我们有一个来自某个库的Java类,它使用一个字节流,比如一些XML解析器XmlParser1,它公开了一个方法xmlParser1.parse(inputStream);这种方法通常会在一次调用中占用所有字节,最终会阻塞。 我们还有一个来自其他库的类,它使用不同的实现来执行类似的操作:XmlParser2xmlParser2.parse(inputStream)。 现在,我们想要用两个解析器解析单个流

我的第一个答案是:我们被搞砸了。由于我们无法控制每个类如何使用流,我们所能做的就是缓冲内存中的所有字节或临时文件(如果可能,打开/重新打开它)。这些消费者的API本质上是非合作的。

现在,假设我们控制XmlParser1(实现和签名),我们希望以更灵活和合作的方式对其进行编码,以便调用者能够以合理有效的方式实现上述行为。你会建议什么?

我正在考虑的一些替代方案:

1)使XmlParser1实现FilterInputStream,这样当某个类(XmlParser1)试图从中读取一些字节时,它会在内部解析它所拥有的内容(迭代地,也许是一些合理的缓冲)并返回原始字节。 (这与FilterInputStream概念并不完全一致,我会说。通过这种方式,客户端代码可以简单地链接解析器:

   public class XmlParser1 extends FilterInputStream {
       public XmlParser1(InputStream rawInputStream) { ... } 
       public int read(byte[] b, int off, int l) throws IOException {
           // this would invoke the underlying stream read, parse internall the read bytes,
           // and leave them in the buffer
       }
   }

   XmlParser1 parser1 = new XmlParser1(inputstream);
   XmlParser2 parser2 = new XmlParser2(parse); 
   parser2.parse(); // parser2 consumes all the input stream, which causes parser1 to read an parse it too

2)不要将XmlParser1视为消费者字节,而是将其视为接收器:我们不会让它自己吃掉字节,我们会用勺子喂它。所以,我们可以拥有xmlParser1.parse(inputStream)而不是xmlParser1.write(byte[]) InputStream ...也就是说,我们不会将OutputStream传递给XmlParser2。这将允许客户端创建一个TeeInputStream,将字节透明地传递给XmlParser1.write()类,同时调用{{1}}

请注意,我们在任何情况下都不需要单独的线程。

我不确定哪一个(如果有的话)在概念上更可取,如果有更好的选择。这听起来像是一个应该已经讨论过的设计问题,但我找不到太多 - 不一定限于Java。欢迎提出意见和参考。

5 个答案:

答案 0 :(得分:1)

假设两个解析器在两个独立的线程中运行,它可能是这样的(不是工作代码)

public class Test extends FilterInputStream {
    byte[] buf = new byte[8192];
    int len;
    Thread thread = null;

    @Override
    public synchronized int read(byte[] b, int off, int l) throws IOException {
        while (thread == Thread.currentThread() && len > 0) {
            thread.wait();
        }
        if (len > 0) {
            System.arraycopy(buf, 0, b, off, l);
            len = 0;
            return l;
        }
        len = super.read(b, off, l);
        System.arraycopy(b, off, buf, 0, len);
        thread = Thread.currentThread();
        notify();
        return len;
    }

也就是说,#1读取字节并将它们保存在buf中,#1的下一次尝试被阻止,直到#2从缓冲区中读取所有内容

答案 1 :(得分:1)

如果您的线程在同一台服务器上,那么拆分InputStreams的想法将毫无意义。因为您仍然只使用一个InputStream和一个BufferedInputStream来获取数据,所以从InputStream中创建对象,然后在两个不同的运行线程中使用这些对象。结论:在Java中任何时候都不需要阻止任何InputStream。我甚至认为它有害,因为如果你阻塞,如果你的缓冲区 - 或管道 - 流过会发生什么?队列溢出!

编辑:如果你想停止一个流,你需要告诉发件人不再发送任何数据。或者你像youtube一样,他们将视频分成几部分(即1部分1分钟)并且只能一次预加载这些部分,因此停止视频根本不会影响预加载,因为只有在时间线中达到某个位置时才会预先加载(例如45秒,1分45秒,2分45秒,aso)。好吧,这实际上只是一种预加载技术而且没有真正的流媒体,这就是为什么Youtube不需要丢弃丢包的原因。)

但是,我仍然有几行伪代码,客户端:

BufferedOutputStream bos = new BufferedOutputStream(/*yourBasicInputStream*/);
ObjectOutputStream oos = new ObjectOutputStream(bos);  //Or use another wrapper
oos.writeObject(yourObjectToSend);      //Or use another parser: Look into the API: ObjectInputStream

主线程控制器(又称服务器)中的类变量:

Thread thread1;  //e.g. a GUI controller
Thread thread2;  //e.g. a DB controller

服务器(或服务器启动的另一个服务器线程,两个线程都作为参数):

BufferedInputStream bis = new BufferedInputStream(/*yourBasicInputStream*/);
ObjectInputStream ois = new ObjectInputStream(bis);   //Or use another wrapper
//now we use an interface MyNetObject implementing the method getTarget(), but
//also an abstract class would be possible (with a more complex getTarget-method):
MyNetObject netObject = (MyNetObject) ois.readObject();   //Or use another parser...
if(netObject.getTarget()=="Thread1ClassANDThread2Class"){
    thread1.activateSync(netObject);        //notify...  
    thread2.activateSync(netObject);        //...both threads!
}
else if(netObject.getTarget()=="Thread1Class"){
    thread1.activate(netObject);        //give thread1 a notification
}
else if(netObject.getTarget()=="Thread2Class"){
    thread2.activate(netObject);        //give thread2 a notification
}
else {//do something else...}

不要忘记同步“activateSync(netObject)” - 方法,但仅当您想对对象进行任何更改时(您不需要同步读数,只需要写作):

public void activateSync(MyNetObject netObject){
    synchronize(netObject){
        //do whatever you wanna do with the object...the other thread will always get the actual object!
    }
}

这很简单,快速,一致......并且完全面向对象。希望你能得到这个想法。 ;)

<强>更新

了解流或阅读器实际上也是“解析器”非常重要。有一个重要的区别:Streams是(通常)网络驱动的类,用于编写和读取任何类型的数据 - 除了字符。读者可以用来阅读任何类型的文本/字符。因此,您的正确实现将是:使用某些流读取传入的数据包,然后将数据存储到适当的对象中。然后你有一个通用的对象,你可以在任何类型的阅读器中使用。如果您只想要阅读图片,可以尝试使用类readUTF()http://docs.oracle.com/javase/1.4.2/docs/api/java/io/ObjectInputStream.html)中的解析器ObjectInputStream来生成字符串:

BufferedInputStream bis = new BufferedInputStream(/*yourBasicInputStream*/);
ObjectInputStream ois = new ObjectInputStream(bis);
String string = ois.readUTF();   //Or another usable parser/method
XmlParser1.read(string);      //for reads there is...
XmlParser2.read(string);      //...no synchronisation needed!

现在,唯一剩下的就是教解析器如何读取该字符串。虽然对象字符串本身可以被视为“接收器”。如果这对您不起作用,只需找到另一个解析器/方法来创建“接收器”对象。

请注意,这里讨论的解决方案 - 使用带有适当解析器的ObjectInputStream类 - 在很多情况下都可以工作,也可以使用大数据(然后在发送之前只需将1GB的文件切成几个字符串/对象“数据包”)网,就像洪流一样)。但它不适用于视频/音频流,你可能会丢包,无论如何都需要完全不同的解决方案(这本身就是一门科学:http://www.google.ch/search?q=video+stream+packet+drop)。

答案 2 :(得分:0)

我尝试将otiginal输入Stream拉入Apache Commons TeeInputStream以创建OutputStream。 http://commons.apache.org/io/api-release/org/apache/commons/io/input/TeeInputStream.html

作为要由IeeInputStream写入的OutputStream,我使用了一个java PipedOutputStream。

我将此PipedOutputStream连接到java PipedInputStream。

这允许我读取TeeInputStream和PipedInputStream。不确定它是否适合您,或者它是否至少会提供下一步。

我创建了一个ReaderThread类来检查我是否可以并行读取它们:

  private static class ReaderThread extends Thread
  {
    InputStream inStream;
    int threadId;
    public ReaderThread(int threadId, InputStream inStream)
    {
      this.inStream = inStream;
      this.threadId = threadId;
    }

    @Override
    public void run()
    {
      try
      {
        int c = inStream.read();
        while (c != -1)
        {
          System.out.println("From ("+threadId+ ") "+c);
          c = inStream.read();
        }
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
    }
  }

然后由以下代码驱动:

InputStream inStream = new FileInputStream(fileName);
PipedInputStream pipedInStream = new PipedInputStream();
OutputStream pipedOutStream = new PipedOutputStream(pipedInStream);
TeeInputStream tin = new TeeInputStream(inStream,
  pipedOutStream);

ReaderThread firstThread = new ReaderThread(1,tin);
ReaderThread secondThread = new ReaderThread(2,pipedInStream);

firstThread.start();
secondThread.start();

答案 3 :(得分:0)

有些不清楚XmlParse1和XmlParser2内部发生了什么,但假设它们实际上关心的是最终的XML数据而不是InputStream字节,我会切换到StAX XMLEvent api。你可以使两个解析器都实现XMLEventConsumer。然后,您只需要一个外部循环来解析实际流并将事件传递给消费者:

public void parseXml(InputStream stream) {
  XMLEventReader reader = ...; // convert stream into XMLEventReader

  XMLConsumer[] consumers = new XMLConsumer[]{new XmlParser1(), new XmlParser2()};

  while(reader.hasNext()) {
    XMLEvent event = reader.nextEvent();
    for(XMLConsumer consumer : consumers) {
      consumer.add(event));
  }
}

答案 4 :(得分:0)

你说过“这些消费者的API本质上是非合作的。”因此,不要试图制作它们,保持孤立并给予他们想要的东西。单独的输入流。

让线程读取实际输入流并写入两个输出流。

然后从这些输出流中创建输入流,您可以使用Piped Streams

执行此操作
  

pipedInputStream1 = new PipedInputStream(pipedOutputStream1);

  

ByteArrayInputStream(((ByteArrayOutputStream)byteOutputStream1).toByteArray());