C++ Socket连续传输Json Base64 imencode编码的图片

  • 写在前面
  • 原理
  • 图片编码
    • 图片的几种格式
      • opencv Mat
      • FILE二进制文件
      • opencv imencode编码的文件
      • 其他文件
    • opencv 编码的方法
    • opencv 解码的方法
    • Base64
    • nlohmann::Json
      • json编码
        • 原理
      • json解码
    • 其他几种编码方式
      • Mat to Json
      • Mat To StreamFile
      • image To json
      • Mat To json完整版
    • 图片解码完整代码
  • socket传输
    • 最重要的两个函数–send和recv
    • 发送端代码原理
    • linux做发送端
    • 接收端代码原理
    • Windows做接收端
    • linux与linux之间的socket传输
    • Windows与Windows之间的socket传输
  • 参考文献

写在前面

我想实现的功能为linux与Windows之间的socket通信。本文代码都是C++。linux里将通信函数写成了线程,在下文有所讲解。Windows直接卸载了主函数里,你可以随意更改。我的opencv版本为4.4.0,Ubuntu版本为18.04 。
我并不喜欢用客户端和服务端去解释服务器。因为客户端和服务端都可以互相发送,互相监听,甚至在socket中你让哪一个去主动连接都没有问题。所以在本文中,我更多的会使用接收端与发送端来称呼。你只需要记住我是从发送端发送图片到接收端就可以了,至于你的项目中是否使用堵塞,是否需要反馈,那都是你自己需要考虑的事情了。
我是一名大三的学生,在我实习的时候,由于网络上没有一篇完整,像样,并且通俗易懂的博客,导致我在项目进行过程中浪费了很多时间,所以写下这篇博客,希望对之后进行项目的人有帮助。

原理

在我进行工业相机二次开发的时候,我想通过远程将相机获取的图片传输到我的电脑上并展示出来。在这个过程中,需要用Socket协议传输,我采用的是TCPstream进行传输。
(TCP:传输控制协议 (The Transmission Control Protocol))。同理,还有UDP(用户数据报协议 (User Datagram Protocol)),SOCK_DGRAM等等。
在完成项目的过程中,有许多的问题。最主要的问题就是传输函数的输入变量(如果你不要求懂原理的话)。我们知道,socket的发送函数为send(),接收函数为recv(),如果你不知道,请参阅我附录中的socket入门编程。
抛开传输的相关问题,让我们来讨论一下图片编码的相关格式吧。我猜测你进入这篇博客是通过搜索了opencv imencode,json,Base64传输图片等等关键词。但真正应用的时候,这三个往往需要同时使用,来保证传输的成功。

图片编码

在这一节,我会介绍图片的几种格式,程序员需要知道的部分事情,以及如何进行图片编码,使其能使用socket发送。

图片的几种格式

opencv Mat

opencv的Mat应该是视觉方面使用次数最多的。我的程序也是通过Mat格式的图像作为input输入,进行学习。

FILE二进制文件

二进制文件使用C++是FILE指针。jpg等后缀的文件都属于二进制文件的类。具体我只以二进制文件流做解释,如果其他格式文件可能需要做转换,可以去网上查阅其他资料。

opencv imencode编码的文件

这个文件相当于Mat,只不过有的时候你拿到的可能是opencv编码的buffer,在socket传输的接收端会针对编码的文件进行处理。单独列出来方便解释说明。

其他文件

除了几种主流的格式以外,我们可能遇见各种各样的图片格式。比如我的工业相机获取的图像格式就为CFrame。但其他所有的格式都肯定会有转化方式,转成BGR24,QImage等等方法。在这里不做说明。

opencv 编码的方法

opencv提供了一种编码的方法,叫做imencode。解码叫做imdecode,我使用的opencv版本为4.4.0,系统为Ubuntu18.04 。在这里我使用官方文档的说明给大家介绍这两个函数。
opencv4.4.0文档

bool cv::imencode(
const String&  ext,
InputArray  img,
std::vector<uchar>&  buf,
const std::vector<int>&  params=std::vector<int>() 
)	
Parameters
ext	定义输出格式的文件扩展名。
img	需要被编码的图片
buf	输出缓冲区调整大小以适应压缩的图像。
params	特定于格式的参数。

通俗而讲,第一个参数为文件扩展名,比如jpg,png等等。第二个参数是源图像,也就是Mat类型的图片(也可以放其他的但我没试过)。第三个参数是写入的缓存区,也就是我编码后的图片放在了那里。第四个是参数,为2个。第一个是编码的flag,一个是编码质量。详细信息请参照官方文档。返回值为是否成功。
我的编码程序:

/*************************************************
Description: opencv imencode
Input: Mat
Output: 无
Return: buffer
Others: 无
*************************************************/
string encode(Mat src)
{//jpeg compression vector<uchar> buff;//buffer for coding vector<int> param = vector<int>(2);param[0] = CV_IMWRITE_JPEG_QUALITY;param[1] = 50;//default(95) 0-100 bool issuccess = imencode(".jpg", src, buff, param);if (issuccess == true) {//cout << "coded file size(jpg)" << buff.size() << endl;//fit buff size automatically. string str_encode(buff.begin(), buff.end());return str_encode;}else { return "fail"; }
}

输入Mat图像,return编码后string类型的buffer。与下文json编码同时使用。

opencv 解码的方法

Mat cv::imdecode(
InputArray 	buf,
int  flags 
)	

十分简单不是吗?第一个参数为buffer,第二个参数为flags,返回值为Mat。并没有什么好讲的。
但很重要的一点就是:
对于彩色图像,解码后的图像具有按B G R顺序存储的通道。

Mat img_decode;
vector<uchar> data(str_tmp.begin(), str_tmp.end());
img_decode = imdecode(data, CV_LOAD_IMAGE_COLOR);

str_tmp是经过base64解码后的string类型的字符串。下文json解码有着完整代码。

Base64

Base64 在网络上有详细的介绍,应该不需要我多说了。在使用nlohmann::json的时候,图片需要先经过一次base64编码。我使用的base64是这个

链接:https://pan.baidu.com/s/199FeHe5ktjxRX1x1O-vU9w 
提取码:jtu6 

是从别人那里保存下来的,具体是谁已经找不到了,如果你发现这是你的代码,请联系我,我回注明。同时再三感谢这个人,这个代码是我认为写的最漂亮的。(美观的那种漂亮)
C++ Socket连续传输Json Base64 imencode编码的图片-编程知识网

nlohmann::Json

nlohmann/json
只需要下载json.hpp就可以。如果下载不下来:

链接:https://pan.baidu.com/s/1bFl5SelXJk1o8h-GokUWNA 
提取码:mwfu 

直观的语法:在Python等语言中,JSON感觉就像是一流的数据类型。我们使用了现代C ++的所有操作符魔术,以在您的代码中实现相同的感觉。

微不足道的整合:我们的整个代码包含一个头文件json.hpp。而已。没有库,没有子项目,没有依赖项,没有复杂的构建系统。该类用香草C ++ 11编写。总而言之,一切都不需要调整编译器标志或项目设置。

认真测试:我们的课程经过严格的单元测试,涵盖了100%的代码,包括所有异常行为。此外,我们使用Valgrind和Clang检查是否有内存泄漏。Google OSS-Fuzz还针对所有解析器24/7运行模糊测试,到目前为止,有效执行了数十亿次测试。为了保持高质量,该项目遵循核心基础设施计划(CII)的最佳做法。

以上三句话来自github上readme,可以通过查看其文章获得更多信息。

json编码

json的基本语法在这里不做介绍,让我们来看一下json
封装的方法。

/*************************************************
Description: string转化为json
Input: opencv imencode编码的buffer
Output:SendMessage、SendWinMessage储存的json对象
Return: bool
Others: 无
*************************************************/bool buffToJson(string str_encode)
{json data;const char* c = str_encode.c_str();data["mat"] = base64_encode(c, str_encode.size());SendWinMessage = data.dump();SendMessage = data.dump();return true;
}

json定义一个对象,叫做data。将str_encode转化为const char*类型,然后通过base64编码封装进data,key叫做“mat”。最后将data使用dump()函数转化成string类型,放入SendWinMessage与SendMessage。
这里SendWinMessage与SendMessage是两个string类型的全局变量,在socket发送线程中调用。当然你可以自己定义放入的东西,或直接发送,取决于你的项目

原理

为什么要这样做?在我们使用socket发送的时候,send函数要求的buffer是const char*类型。如果你只使用opencv编码一次,那么编译的结果会有很多截断符,并且在buffer迁移到缓存区的时候导致读取错误,无法正常发送。所以我们要用base64编码使其能被正常读取,并使用json使其有更清晰地表达。

json解码

//接收数据  line为recv的buffer
json o = json::parse(line);
for (json::iterator it = o.begin(); it != o.end(); ++it) {//cout << it.key() << " : " << it.value() << "\n";if (it.key() == "mat"){Mat img_decode;string str_tmp = base64_decode(it.value());vector<uchar> data(str_tmp.begin(), str_tmp.end());img_decode = imdecode(data, CV_LOAD_IMAGE_COLOR);imshow("CV Video Client", img_decode);waitKey(1);}
}

我们之前进行了三步编码,第一步是opencv imencode编码,编码成了string类型的buffer。第二步进行了base64编码,第三步封装进了json对象中。那么我们解码也需要3步。第一步进行json对象的解码,第二步进行base64的解码,第三步进行opencv imdecode的解码。
json.dump()后的类型为string,将string发送过来,接受储存到line里。将string转化回json使用parse()函数。
注意:如果你数据不完整,会使parse报错error,return false
当我们成功拿到json之后,我们需要通过遍历去寻找我们之前定好的那个key,也就是“mat”(忘记了可以去看上面的编码)。找到key之后,我们通过base64的decode进行第二次解码。拿到string,也就是opencv imencode编码后的buffer。在通过imdecode解码,就成功拿到mat图像了。

其他几种编码方式

我们之前提到有很多图像格式。在这里放几个转换流的函数。

Mat to Json

/*************************************************
Description: Mat格式转化为Json对象封装到SendMessage里
Input:Mat
Output:string SendMessage存有json字符串
Return: bool
Others: 未使用此函数。使用了临时文件,速度极慢。
*************************************************/
bool MatToJson(Mat image)
{if (image.empty()) return false;FILE* fpw = tmpfile();if (fpw == NULL){fclose(fpw);return false;}int channl = image.channels();//第一个字节  通道int rows = image.rows;     //四个字节存 行数int cols = image.cols;   //四个字节存 列数fwrite(&channl, sizeof(char), 1, fpw);fwrite(&rows, sizeof(char), 4, fpw);fwrite(&cols, sizeof(char), 4, fpw);char* dp = (char*)image.data;if (channl == 3){for (int i = 0; i < rows * cols; i++){fwrite(&dp[i * 3], sizeof(char), 1, fpw);fwrite(&dp[i * 3 + 1], sizeof(char), 1, fpw);fwrite(&dp[i * 3 + 2], sizeof(char), 1, fpw);}}else if (channl == 1){for (int i = 0; i < rows * cols; i++){fwrite(&dp[i], sizeof(char), 1, fpw);}}int nRead;char chBuf[3888888];//fread()读取成功返回值为实际读回的数据个数(单位为Byte)nRead = fread(chBuf, sizeof(char), 3888888, fpw);//读取的内容做base64编码,返回string//要编码的部分是chBuf,编码元素的个数是nReadstring imgBase64 = base64_encode(chBuf, nRead);//封装进jsonjson data;data["img"] = imgBase64;SendMessage = data.dump();
}

输入mat,输出json。通过FILE*指针打开一个临时文件,将mat保存成一个二进制文件。但由于使用了for循环,并写入文件,速度极慢。不推荐使用。在我的项目中,时间大约需要400ms。不使用临时文件的话时间甚至到达了900ms。

Mat To StreamFile

/*************************************************
Description: Mat格式转化为二进制文件
Input:Mat,文件名
Output:二进制文件
Return: bool
Others: 未使用此函数。写入了文件,速度极慢。
*************************************************/
bool imageToStreamFile(Mat image, string filename)
{if (image.empty()) return false;const char* filenamechar = filename.c_str();FILE* fpw = fopen(filenamechar, "wb");//如果没有则创建,如果存在则从头开始写if (fpw == NULL){fclose(fpw);return false;}int channl = image.channels();//第一个字节  通道int rows = image.rows;     //四个字节存 行数int cols = image.cols;   //四个字节存 列数fwrite(&channl, sizeof(char), 1, fpw);fwrite(&rows, sizeof(char), 4, fpw);fwrite(&cols, sizeof(char), 4, fpw);char* dp = (char*)image.data;if (channl == 3){for (int i = 0; i < rows * cols; i++){fwrite(&dp[i * 3], sizeof(char), 1, fpw);fwrite(&dp[i * 3 + 1], sizeof(char), 1, fpw);fwrite(&dp[i * 3 + 2], sizeof(char), 1, fpw);}}else if (channl == 1){for (int i = 0; i < rows * cols; i++){fwrite(&dp[i], sizeof(char), 1, fpw);}}fclose(fpw);return true;
}

将上面那个程序拆开。

image To json

/*************************************************
Description: 二进制文件转化为json
Input: 文件名
Output:SendMessage储存的json对象
Return: bool
Others: 未使用此函数。打开了文件,速度极慢。
*************************************************/
bool imageTojson(string filename)
{int nRead;char chBuf[3888888];const char* filenamechar = filename.c_str();FILE* fIn = fopen(filenamechar, "rb");if (fIn == NULL){fclose(fIn);return false;}//fread()读取成功返回值为实际读回的数据个数(单位为Byte)nRead = fread(chBuf, sizeof(char), 3888888, fIn);//读取的内容做base64编码,返回string//要编码的部分是chBuf,编码元素的个数是nReadstring imgBase64 = base64_encode(chBuf, nRead);//封装进jsonjson data;data["img"] = imgBase64;fclose(fIn);SendMessage = data.dump();
}

将上上面那个程序拆开

Mat To json完整版

/*************************************************
Description: string转化为json
Input: opencv imencode编码的buffer
Output:SendMessage、SendWinMessage储存的json对象
Return: bool
Others: 无
*************************************************/
bool buffToJson(string str_encode)
{json data;const char* c = str_encode.c_str();data["mat"] = base64_encode(c, str_encode.size());SendWinMessage = data.dump();SendMessage = data.dump();return true;
}/*************************************************
Description: opencv imencode
Input: Mat
Output: 无
Return: buffer
Others: 无
*************************************************/
string encode(Mat src)
{//jpeg compression vector<uchar> buff;//buffer for coding vector<int> param = vector<int>(2);param[0] = CV_IMWRITE_JPEG_QUALITY;param[1] = 50;//default(95) 0-100 bool issuccess = imencode(".jpg", src, buff, param);if (issuccess == true) {//cout << "coded file size(jpg)" << buff.size() << endl;//fit buff size automatically. string str_encode(buff.begin(), buff.end());return str_encode;}else { return "fail"; }
}

当这两个函数一起调用的时候,就可以达到我们上文所说的编码三次的效果。

图片解码完整代码

和上文json解码代码一样,这段代码放入接收端

//接收数据  
json o = json::parse(line);
for (json::iterator it = o.begin(); it != o.end(); ++it) {//cout << it.key() << " : " << it.value() << "\n";if (it.key() == "mat"){Mat img_decode;string str_tmp = base64_decode(it.value());vector<uchar> data(str_tmp.begin(), str_tmp.end());img_decode = imdecode(data, CV_LOAD_IMAGE_COLOR);imshow("CV Video Client", img_decode);waitKey(1);}
}

socket传输

转自:参考文献【1】

你经常听到人们谈论着 “socket”,或许你还不知道它的确切含义。现在让我告诉你:它是使用标准Unix 文件描述符 (file descriptor) 和其它程序通讯的方式。 什么? 你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。但是(注意后面的话),这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其它的东西。Unix 中所有的东西就是文件!所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket(),它返回套接字描述符 (socket descriptor),然后你再通过它来进行send() 和 recv()调用。

“但是…”,你可能有很大的疑惑,“如果它是个文件描述符,那么为什 么不用一般调用read()和write()来进行套接字通讯?”简单的答案是:“你可以使用!”。详细的答案是:“你可以,但是使用send()和recv()让你更好的控制数据传输。”

我们只讨论如何实现。在主流的系统上,我们可以大致分为三类传输。Windows与linux之间的传输,Windows与Windows之间的传输,linux与linux之间的传输。在这里,我详细解释Windows与linux之间的传输,因为这样可以将2个不同系统的socket都解释清楚。至于其他两个方式,照着葫芦画葫芦就可以了。

最重要的两个函数–send和recv

int send(int sockfd, const void *msg, int len, int flags);

sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。(详细的资料请看 send() 的 man page)。另外的两个flag是不应对数据进行路由发送OOB数据
send() 返回实际发送的数据的字节数–它可能小于你要求发送的数目! 注意,有时候你告诉它要发送一堆数据,可是它不能处理成功。它只是发送它可能发送的数据,然后希望你能够发送其它的数据。如果 send() 返回的已发送数据和 len 不匹配,你就应该发送剩下的数据。但是这里也有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一次发送完。如果发送失败,它在错误的时候返回-1,并设置 error。
实际上,如果你处于堵塞模式,它会自己继续发送剩下的数据,只是分了多次。但这并不是你不考虑他的理由。(不过如果你不想考虑也没问题)

int recv(int sockfd, void *buf, int len, unsigned int flags);

sockfd 是要读的套接字描述符。buf是要读的信息的缓冲。len 是缓冲的最大长度。flags可以设置为0。(请参考recv() 的 man page。)另外的3个flag为MSG_WAITALL(等待所有),处理OOB数据窥视传入的数据(不会从队列里删除) recv()返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1,同时设置error。

发送端代码原理

socket();bind();listen();/* accept() 应该在这 */

说简单点就是这样:我创建了一个socket,我将其与机器上的一定的端口(port)关联起来,我监听看看有没有远程要连接我的,我听到了,有人要连接(connect)我,我用accept接受它。

这里也有要注意的几件事情。localAddr.sin_port 是网络字节顺序,localAddr.sin_addr.s_addr 也是的。另外要注意到的事情是因系统的不同, 包含的头文件也不尽相同.

localAddr.sin_port = 0; /* 随机选择一个没有使用的端口 */

localAddr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的IP地址 */

通过将0赋给 localAddr.sin_port,你告诉 bind() 自己选择合适的端口。同样,将 localAddr.sin_addr.s_addr 设置为 INADDR_ANY,你告诉它自动填上它所运行的机器的 IP 地址。

如果你一向小心谨慎,那么你可能注意到我没有将 INADDR_ANY 转换为网络字节顺序!这是因为我知道内部的东西:INADDR_ANY 实际上就是0!即使你改变字节的顺序,0依然是0。但是完美主义者说应该处处一致,INADDR_ANY或许是1,2呢?你的代码就不能工作了,那么就看下面的代码:

localAddr.sin_port = htons(0); /* 随机选择一个没有使用的端口 */

localAddr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用自己的IP地址 */

你或许不相信,上面的代码将可以随便移植。我只是想指出,既然你所遇到的程序不会都运行使用htonl的INADDR_ANY。

在你调用 bind() 的时候,你要小心的另一件事情是:不要采用小于1024的端口号。所有小于1024的端口号都被系统保留!你可以选择从1024 到65535的端口(如果它们没有被别的程序使用的话)。
你要注意的另外一件小事是:有时候你根本不需要调用它。如果你使用 connect() 来和远程机器进行通讯,你不需要关心你的本地端口号(就像你在使用 telnet 的时候),你只要简单的调用 connect() 就可以了,它会检查套接字是否绑定端口,如果没有,它会自己绑定一个没有使用的本地端口。

linux做发送端

在网络连续传输图片的时候,有一点需要格外注意,也就是“连续”两个字。我采用的方法为先发送一个报文头,让对方知道你这个图片大小是多少。(因为我的项目是摄像机获取图片,每一张图片大小不尽相同)然后再按照特定的字节接受。
也就是说,我要先把一个int发送过去,然后接收端先接受这一个报文头。
send是无法发送int类型的。他要求缓存区是const char*类型。所以我们要先把int转化为网络字节顺序。使用htonl()函数。

用 “h” 表示 “本机 (host)”,接着是 “to”,然后用 “n” 表 示 “网络 (network)”,最后用 “s” 表示 “short”: h-to-n-s, 或者 htons() (“Host to Network Short”)。

太简单了… ,如果不是太傻的话,你一定想到了由"n",“h”,“s”,和 "l"形成的正确 组合,例如这里肯定没有stolh() (“Short to Long Host”) 函数,不仅在这里 没有,所有场合都没有。但是这里有:

htons()–“Host to Network Short”

htonl()–“Host to Network Long”

ntohs()–“Network to Host Short”

ntohl()–“Network to Host Long”

int len = SendWinMessage.size();
len = htonl(len);
send(remoteSocket, (const void*)&len, sizeof(len), 0);

这样,我们就把length先发过去了。接着再发送图片数据,就可以全部接收到啦。
让我们看下完整的代码:

void* socketToWin(void* args)
{//--------------------------------------------------------//networking stuff: socket, bind, listen//--------------------------------------------------------int localSocket, remoteSocket, port = 5000;struct sockaddr_in localAddr, remoteAddr;int addrLen = sizeof(struct sockaddr_in);localSocket = socket(AF_INET, SOCK_STREAM, 0);if (localSocket == -1) {perror("socket() call failed!!");}localAddr.sin_family = AF_INET;localAddr.sin_addr.s_addr = INADDR_ANY;localAddr.sin_port = htons(port);if (bind(localSocket, (struct sockaddr*) & localAddr, sizeof(localAddr)) < 0) {perror("Can't bind() socket");exit(1);}//Listeninglisten(localSocket, 3);cout << "Waiting for connections...\n" << "Server Port:" << port << endl;//accept connection from an incoming clientint bytes;while (1){remoteSocket = accept(localSocket, (struct sockaddr*) & remoteAddr, (socklen_t*)&addrLen);if (remoteSocket < 0){printf("accept failed!");continue;}cout << "Connection accepted" << endl;sleep(1);cout << "here" << endl;while (remoteSocket >= 0){//send processed imagestring SendWinMessage = *((string*)args);int len = SendWinMessage.size();len = htonl(len);send(remoteSocket, (const void*)&len, sizeof(len), 0);sleep(0.1);if ((bytes = send(remoteSocket, SendWinMessage.c_str(), SendWinMessage.size(), 0)) < 0)//if ((bytes = SendAll(remoteSocket, SendWinMessage.c_str(), SendWinMessage.size())) == -1){break;}cout << "bytes = " << bytes << endl;}}
}

这是一个线程,SendWinMessage通过*((string*)args)输入进来。如果你已经了解了linux里线程的知识,你会知道args是传递的变量。

接收端代码原理

socket();

connect();

Windows做接收端

WSADATA wsaData;
SOCKET sockClient;//客户端Socket
SOCKADDR_IN addrServer;//服务端地址
WSAStartup(MAKEWORD(2, 2), &wsaData);//新建客户端socket
sockClient = socket(AF_INET, SOCK_STREAM, 0);//定义要连接的服务端地址
addrServer.sin_addr.S_un.S_addr = inet_addr(SOCKET_IP); //服务端IP
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(SOCKET_PORT);//服务端连接端口//连接到服务端
connect(sockClient, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));

写的时候需要学习一些linux和Windows中socket不同的表达方式,上面就是Windows给的例子。
让我们看一下完整代码:

#include <stdio.h>
#include <string>
#include <iostream>
#include <Winsock2.h>
#include <opencv2/opencv.hpp>
#include "opencv2/imgcodecs/legacy/constants_c.h"
#include <vector>
#include "base64.h"
#include "json.hpp"
#pragma comment(lib,"ws2_32.lib")
#define SOCKET_PORT 5000
#define SOCKET_IP "your IPAddress" //自己更改
#pragma warning(disable:4996)
using namespace cv;
using namespace std;
using json = nlohmann::json;int RecvAll(SOCKET& sock, char* buffer)
{int len;recv(sock, (char*)&len, sizeof(len), 0);int size = ntohl(len);printf("%d\n", ntohl(len));int TotalRecvSize = 0;while (size > 0)//剩余部分大于0{int RecvSize = recv(sock, buffer, size, 0);if (SOCKET_ERROR == RecvSize)return -1;size = size - RecvSize;cout << "RecvSize" << RecvSize << endl;cout << "size" << size << endl;buffer += RecvSize;TotalRecvSize += RecvSize;}return TotalRecvSize;
}int main()
{namedWindow("CV Video Client");//--------------------------------------------------------//networking stuff: socket , connect//--------------------------------------------------------WSADATA wsaData;SOCKET sockClient;//客户端SocketSOCKADDR_IN addrServer;//服务端地址WSAStartup(MAKEWORD(2, 2), &wsaData);//新建客户端socketsockClient = socket(AF_INET, SOCK_STREAM, 0);//定义要连接的服务端地址addrServer.sin_addr.S_un.S_addr = inet_addr(SOCKET_IP); //服务端IPaddrServer.sin_family = AF_INET;addrServer.sin_port = htons(SOCKET_PORT);//服务端连接端口//连接到服务端connect(sockClient, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));//----------------------------------------------------------//OpenCV Code//----------------------------------------------------------while (1) {int bytes = 0;char line[88888];//if ((bytes = recv(sockClient, (char*)line, sizeof(line), 0)) != -1) {if (bytes = RecvAll(sockClient, line)) {line[bytes] = 0x00;cout << "recv successfully, received bytes = " << bytes << endl;//接收数据  json o = json::parse(line);for (json::iterator it = o.begin(); it != o.end(); ++it) {//cout << it.key() << " : " << it.value() << "\n";if (it.key() == "mat"){Mat img_decode;string str_tmp = base64_decode(it.value());vector<uchar> data(str_tmp.begin(), str_tmp.end());img_decode = imdecode(data, CV_LOAD_IMAGE_COLOR);imshow("CV Video Client", img_decode);waitKey(1);}}}//delete[] line;//line = nullptr;}closesocket(sockClient);WSACleanup();return 0;
}

有几项需要注意的事情。
1、#pragma warning(disable:4996) 关闭4996警告。因为vs编译会让你用 新函数,而我的代码使用了inet_addr是一个旧函数。
2、char line[88888]; 这里是放置图片的缓冲区。你可以自己调整大小,但在main函数中使用要小心超出堆栈的大小,可以new一段但是如何指向它需要更改。
3、int RecvAll(SOCKET& sock, char* buffer) 这是定义的一个函数,用来防止接受不完全。又回到了那个问题,堵塞状态下发送可能不需要,但接受需要。见参考文献【2】(SendAll与RecvAll函数,在这里不再贴出)
4、#include "opencv2/imgcodecs/legacy/constants_c.h" 这很重要,因为opencv编码imencode的第四个参数,存于这个头文件中。版本不同我不了解,但你可以通过查阅相关文档得到结果。

linux与linux之间的socket传输

我认为这个很好用,是TCPstream传输的,封装成的类。
Github:vichargrave/tcpsockets

Windows与Windows之间的socket传输

如果你前面全部读懂了,应该对你太简单了。

参考文献

【TCP通信接收数据不完整的解决方法】
【C++ socket 循环发送,循环接收样例】
【c++中Socket编程(入门)】
【Github: nlohmann/json】
【c++实现socket以json格式传输图片】
【OpenCV与Socket实现树莓派获取摄像头视频至电脑】
【【OpenCV开发】OpenCV图像编码和解码 imencode和imdecode使用,用于网络传输图片】
我还参考了许多人的博客,也可能使用了其中一部分知识,但实在太多,我没办法全部找到。如果你发现本文引用了你的博客,请联系我删除或注明。
最后,再对所有认真写博客的人表示感谢。希望这篇博客能帮助到刚接触这方面的小白。