赞
踩
我想实现的功能为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应该是视觉方面使用次数最多的。我的程序也是通过Mat格式的图像作为input输入,进行学习。
二进制文件使用C++是FILE指针。jpg等后缀的文件都属于二进制文件的类。具体我只以二进制文件流做解释,如果其他格式文件可能需要做转换,可以去网上查阅其他资料。
这个文件相当于Mat,只不过有的时候你拿到的可能是opencv编码的buffer,在socket传输的接收端会针对编码的文件进行处理。单独列出来方便解释说明。
除了几种主流的格式以外,我们可能遇见各种各样的图片格式。比如我的工业相机获取的图像格式就为CFrame。但其他所有的格式都肯定会有转化方式,转成BGR24,QImage等等方法。在这里不做说明。
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编码同时使用。
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 在网络上有详细的介绍,应该不需要我多说了。在使用nlohmann::json的时候,图片需要先经过一次base64编码。我使用的base64是这个
链接:https://pan.baidu.com/s/199FeHe5ktjxRX1x1O-vU9w
提取码:jtu6
是从别人那里保存下来的,具体是谁已经找不到了,如果你发现这是你的代码,请联系我,我回注明。同时再三感谢这个人,这个代码是我认为写的最漂亮的。(美观的那种漂亮)
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
封装的方法。
/************************************************* 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使其有更清晰地表达。
//接收数据 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图像了。
我们之前提到有很多图像格式。在这里放几个转换流的函数。
/************************************************* 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,编码元素的个数是nRead string imgBase64 = base64_encode(chBuf, nRead); //封装进json json data; data["img"] = imgBase64; SendMessage = data.dump(); }
输入mat,输出json。通过FILE*指针打开一个临时文件,将mat保存成一个二进制文件。但由于使用了for循环,并写入文件,速度极慢。不推荐使用。在我的项目中,时间大约需要400ms。不使用临时文件的话时间甚至到达了900ms。
/************************************************* 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; }
将上面那个程序拆开。
/************************************************* 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,编码元素的个数是nRead string imgBase64 = base64_encode(chBuf, nRead); //封装进json json data; data["img"] = imgBase64; fclose(fIn); SendMessage = data.dump(); }
将上上面那个程序拆开
/************************************************* 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);
}
}
转自:参考文献【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都解释清楚。至于其他两个方式,照着葫芦画葫芦就可以了。
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() 就可以了,它会检查套接字是否绑定端口,如果没有,它会自己绑定一个没有使用的本地端口。
在网络连续传输图片的时候,有一点需要格外注意,也就是“连续”两个字。我采用的方法为先发送一个报文头,让对方知道你这个图片大小是多少。(因为我的项目是摄像机获取图片,每一张图片大小不尽相同)然后再按照特定的字节接受。
也就是说,我要先把一个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); } //Listening listen(localSocket, 3); cout << "Waiting for connections...\n" << "Server Port:" << port << endl; //accept connection from an incoming client int 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 image string 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();
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;//客户端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)); //---------------------------------------------------------- //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的第四个参数,存于这个头文件中。版本不同我不了解,但你可以通过查阅相关文档得到结果。
我认为这个很好用,是TCPstream传输的,封装成的类。
Github:vichargrave/tcpsockets
如果你前面全部读懂了,应该对你太简单了。
【TCP通信接收数据不完整的解决方法】
【C++ socket 循环发送,循环接收样例】
【c++中Socket编程(入门)】
【Github: nlohmann/json】
【c++实现socket以json格式传输图片】
【OpenCV与Socket实现树莓派获取摄像头视频至电脑】
【【OpenCV开发】OpenCV图像编码和解码 imencode和imdecode使用,用于网络传输图片】
我还参考了许多人的博客,也可能使用了其中一部分知识,但实在太多,我没办法全部找到。如果你发现本文引用了你的博客,请联系我删除或注明。
最后,再对所有认真写博客的人表示感谢。希望这篇博客能帮助到刚接触这方面的小白。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。