为什么PNG图像的stdout有时会在printf中刷新图像的一半



我正在尝试将PNG文件从C++通过stdout发送到Nodejs。但是,当我发送它时,当我在 NodeJS 中阅读它时,它似乎有时会被剪切一半,而我只有在C++发送整个 PNG 后才会刷新。是什么原因导致这种行为?

我发送图像的代码:

void SendImage(Mat image)
{   //from: https://stackoverflow.com/questions/41637438/opencv-imencode-buffer-exception
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 文档中的空缓冲区指针发生的情况:

如果参数 buf 为 NULL,则仅模式受到影响;新缓冲区 将在下一次读取或写入操作时分配。

所有这些功能都是为您提供一个具有默认大小的默认缓冲区。反正stdout已经有了。

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

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

printf("image ");

把要跟在后面的字节数放在这里,在nodejs中读取它,解析它,然后你知道要继续读取多少字节,直到你得到一切。

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

image 123

在下一个块中读取"40"部分,表示后面有 12340 字节。或者,它同样可以很好地阅读:

ima

其余的紧随其后。

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

我已经修复了它,它现在可以工作了。我把我的代码放在这里,以防功能中的某个人需要它。

发送端C++为了能够连接我的缓冲区并正确解析它,我在发送的消息周围添加了"stArt"和"eNd"。示例:stArtimage‰PNG。IHDR..二进制数据..结束。 您可能也可以通过使用 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 rn, which gives issues when sending PNG images.
_setmode(_fileno(stdout), O_BINARY);

接收端 NodeJS当新数据进来时,我会与以前未使用的数据连接起来。如果我能同时找到 stArt 和 eNd,则数据是完整的,我使用介于两者之间的部分。然后,我将所有字节存储在 eNd 之后,以便下次获取数据时可以使用它们。在我的代码中,它被放置在一个类中,所以如果它不编译,请:)执行。我还使用 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 中准备好画布。

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,否则您将溢出标准输出和与数据的连接,并且它将比浏览器或服务器处理它的速度更快地打印出来。

最新更新