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

写在前面

我想实现的功能为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 

是从别人那里保存下来的,具体是谁已经找不到了,如果你发现这是你的代码,请联系我,我回注明。同时再三感谢这个人,这个代码是我认为写的最漂亮的。(美观的那种漂亮)
Base64

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,编码元素的个数是nRead
    string imgBase64 = base64_encode(chBuf, nRead);

    //封装进json
    json 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,编码元素的个数是nRead
    string imgBase64 = base64_encode(chBuf, nRead);

    //封装进json
    json 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);
    }
    //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();

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;//客户端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的第四个参数,存于这个头文件中。版本不同我不了解,但你可以通过查阅相关文档得到结果。

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使用,用于网络传输图片
我还参考了许多人的博客,也可能使用了其中一部分知识,但实在太多,我没办法全部找到。如果你发现本文引用了你的博客,请联系我删除或注明。
最后,再对所有认真写博客的人表示感谢。希望这篇博客能帮助到刚接触这方面的小白。

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值