为什么 PNG 图像的标准输出有时会在 printf 中刷新到图像的一半?

Why does the stdout of a PNG image get flushed halfway the image sometimes in printf?

我正在尝试通过标准输出从 C++ 将 PNG 文件发送到 Nodejs。但是,当我发送它时,当我在 NodeJS 中读取它时,它有时似乎会被截断,而我只在用 C++ 发送整个 PNG 后才刷新。是什么导致了这种行为?

我发送图片的代码:

void SendImage(Mat image)
{   //from: 
    std::vector<uchar> buffer;
    #define MB image_size.width*image_size.height
    buffer.resize(200 * MB);
    cv::imencode(".png", image, buffer);

    printf("image ");
    for(int i = 0; i < buffer.size(); i++)
        printf("%c", buffer[i]);

    fflush(stdout);
}

然后,我在 Nodejs 中接收它并测试我接收到的内容:

this.puckTracker.stdout.on('data', (data) => {
  console.log("DATA");
  var str = data.toString();
  console.log(str);
  //first check if its an image being sent. C++ prints "image 'imageData'". So try to see if the first characters are 'image'.
  const possibleImage = str.slice(0, 5);
  console.log("POSSIBLEIMAGE: " + possibleImage);
}

我在 C++ 中尝试了以下命令来尝试删除自动刷新:

    //disable sync between libraries. This makes the stdout much faster, but you must either use cout or printf, no mixes. Since printf is faster, use printf everywhere.
    std::ios_base::sync_with_stdio(false);
    //make sure C++ ONLY flushes when I say so, so no data gets broken in half.
    std::setvbuf(stdout, nullptr, _IOFBF, BUFSIZ);

当我运行 C++ 程序有一个可见的终端时,似乎没问题。 我希望 NodeJS 控制台打印的是:

DATA
image ëPNG

IHDR ... etc, all the image data.
POSSIBLEIMAGE: image

这是我发送的每张图片。

相反,我得到:

DATA
image �PNG

IHDT ...
POSSIBLEIMAGE: image
DATA
-m5VciVWjՖҬvXjvXm9kV[d嬭v
POSSIBLEIMAGE: -m5V
DATA
image �PNG
etc.

据我所知,似乎每个图像都被剪切了一次。 这是一个 pastebin,以防有人需要完整的日志。 (打印一些额外的东西,但这不重要。)https://pastebin.com/VJEbm6V5

for(int i = 0; i < buffer.size(); i++)
    printf("%c", buffer[i]);

fflush(stdout);

无法保证只有最后一个 fflush 会在一个块中发送所有数据。

你从来没有,将来也不会保证只有当你明确想要它时,stdout 才会被刷新。 stdout 或其 C++ 等价物的典型实现使用固定大小的缓冲区,当缓冲区满时自动刷新,无论您是否需要。当每个角色出门时,它都会被添加到这个固定大小的缓冲区中。当缓冲区已满时,缓冲区将刷新到输出。 fflush 做的唯一一件事就是显式地进行,清除部分填充的缓冲区。

那么,这还不是全部。

当您从网络连接中读取时,您也无法保证您将读取所有写入的内容,在一个块中,即使它是在一个块中刷新的。套接字和管道不是这样工作的。数据之间的任何地方都可以分解成中间块,并一次一个块地传递给您的阅读过程。

//make sure C++ ONLY flushes when I say so, so no data gets broken in half.
std::setvbuf(stdout, nullptr, _IOFBF, BUFSIZ);

这不会关闭缓冲,有效地使缓冲无限。来自 Linux 空缓冲区指针发生的情况的文档:

If the argument buf is NULL, only the mode is affected; a new buffer will be allocated on the next read or write operation.

所有这一切只是给你一个默认缓冲区,默认大小。 stdout 已经有了。

现在,您当然可以创建一个与图像一样大的自定义缓冲区,以便预先缓冲所有内容。但是,正如我所解释的那样,这不会完成任何有用的事情。数据仍然可能在传输过程中被分解,你将在 nodejs 中一次读取它一个块。

这整个方法是完全错误的。您需要预先单独发送字节数,先读取它,然后您知道期望的字节数,然后读取给定的字节数。

   printf("image ");

后面放字节数,这里,在nodejs中读取,解析,然后就知道要继续读取多少字节,直到全部搞定。

当然,请记住,出于我上面解释的原因,您的 nodejs 代码首先可以读取(不太可能,但它可能会发生,一个好的程序员会编写正确的代码来正确处理所有可能性):

image 123

在下一个块中读取“40”部分,表示后面有 12340 个字节。或者,它同样可以只读:

ima

其余如下。

结论:您无法保证您以任何方式读取的内容始终与写入内容的字节数完全匹配,无论它在写入端如何缓冲或何时刷新.套接字和管道从来没有给你这种保证(有一些轻微的 read/write 语义记录,用于管道,但那是无关紧要的)。您将需要相应地对读取端的所有内容进行编码:无论读取的大小,您的代码都需要逻辑解析 "image ### ",一次一个字符,确定解析 [=49] 时何时停止=] 在一个数字之后。解析它会得到字节数,然后您的代码将需要从逻辑上读取要遵循的确切字节数。这和第一个数据块可能是您首先阅读的内容。您首先想到的可能就是 "i"。你永远不知道会发生什么。这就像玩彩票。你没有任何保证,但这就是事情的运作方式。不,这并不容易,要做到正确。

我已修复它,现在可以使用了。我将我的代码放在这里,以防功能中有人需要它。

发送方C++ 为了能够连接我的缓冲区并正确解析它,我在发送的消息周围添加了 "stArt" 和 "eNd"。示例:stArtimage‰PNG..IHDR..二进制数据..eNd。 您也可以通过仅使用 PNG 本身的默认启动和停止,或者甚至仅使用启动并在下一次启动之前获取所有内容来执行此操作。但是,我也需要发送自定义数据。 C++ 代码现在是:

void SendImage(Mat image)
{
    std::vector<uchar> buffer;
    cv::imencode(".png", image, buffer);

    //StArt (that caps) is the word to split the data chunks on in nodejs.
    cout << "stArtimage";
    fwrite(buffer.data(), 1, buffer.size(), stdout);
    cout << "eNd";
    fflush(stdout);
}

非常重要:在程序开始时添加它,否则图像变得不可读:

#include <io.h>
#include <fcntl.h>

//sets the stdout to binary. If this is not done, it replaces \n by \r\n, which gives issues when sending PNG images.
_setmode(_fileno(stdout), O_BINARY);

接收端NodeJS 当新数据进来时,我将与以前未使用的数据连接起来。如果我能同时找到 startArt 和 eNd,那么数据就完整了,我可以使用中间的部分。然后我将所有字节存储在 eNd 之后,以便下次获取数据时可以使用它们。在我的代码中,它被放置在 class 中,所以如果它没有编译,那么就这样做 :)。我还使用 SocketIO 将数据从 NodeJS 发送到浏览器,所以这就是您看到的 eventdispatcher.emit。

this.puckTracker.stdout.on('data', (data) => {
    try {
    this.bufferArray.push(data);
    var buff = Buffer.concat(this.bufferArray);

    //data is sent in like: concat ["stArt"][5 letters of dataType][data itself]["eNd"]
    // dataTypes: "PData" = puck data, "image" = png image, "Track" = tracking is running
    // example image: stArtimage*binaryPNGdata*eNd
    // example:       stArtPData[]eNdStArtPData[{"ID": "0", "pos": [881.023071, 448.251221]}]eNd
    var startBuf = buff.indexOf("stArt");
    var endBuf = buff.indexOf("eNd");
    if (startBuf != -1 && endBuf != -1) {
        var dataType = buff.subarray(startBuf + 5, startBuf + 10).toString(); //extract the five letters datatype directly behind stArt.
        var realData = buff.subarray(startBuf + 10, endBuf); //extract the data behind the datatype, before the end of data.

        switch (dataType) {
            //sending custom JSON data
                //sending the PNG image.
            case "image":
                this.eventDispatcher.emit('PNG', realData);
                this.refreshBuffer(endBuf, buff);
                break;
            case "customData": //do something with your custom realData
                this.refreshBuffer(endBuf, buff);
                break;
        }
    }
    else {
        this.bufferArray.length = 0; //empty the array
        this.bufferArray.push(buff); //buff contains the full concatenated buffer of the previous bufferArray, it therefore saves all previous unused data in index 0.
    }
    } catch (error) {
        console.error(error);
        console.error(data.toString());
    }
});

    refreshBuffer(endBuf, buff) {
        //do this in all cases (but not if there is no match of dataType)
        var tail = buff.subarray(endBuf + 3); //save the unused data of the previous buffer
        this.bufferArray.length = 0; //empty the array
        this.bufferArray.push(tail); //fill the first spot of the array with the tail of the previous buffer.
    }

客户端Javascript 要使答案完整,要在浏览器中呈现 PNG,请使用以下代码,并确保在 HTML.

中准备好 canvas
socket.on('PNG', (PNG) => {
    var blob = new Blob([PNG], { type: "image/png" });
    var img = new Image();
    var c = document.getElementById("canvas");
    var ctx = c.getContext("2d");


    img.onload = function (e) {
        console.log("PNG Loaded");
        ctx.drawImage(img, 0, 0);
        window.URL.revokeObjectURL(img.src);
        img = null;
    };

    img.onerror = img.onabort = function (error) {
        console.error("ERROR!", error);
        img = null;
    };
    img.src = window.URL.createObjectURL(blob);
});

确保您不要过于频繁地使用 SendImage,否则您会溢出标准输出和数据连接,并且它会以比浏览器或服务器处理速度更快的速度打印出来。