• 网络中的图片传输


    网络中的图片传输

    前言

    一张图片经过网络从主机 A 传输到主机 B,主机 B 在收到这张图片后将其保存在本地,对应步骤为:

    1. 读:主机 A 读取待传输的图片数据
    2. 传:主机 A 通过 Socket 将图片传输给主机 B
    3. 写:主机 B 在收到图片数据后,将其保存在本地

    我们来思考这样几个问题:

    1. 图片数据要以怎样的形式在网络中进行传输?
    2. 对端收到数据后怎要确保是否接收完毕?
    3. 怎样确保图片文件可以在网络上正确传输?

    为解决这些问题,我们可以从发送的数据格式入手,收发双方约定使用如下格式进行数据传输:

    POST /Picture HTTP/1.1
    Host: IP:端口号
    Content-Length: 数据长度
    
    数据内容

    而对于数据内容,可以考虑使用 JSON 格式:

    {
        "imageName" : "test.png",
        "imageSize" : 4,
        "imageData" : "abcd"
    }

    这样就构成了一条数据,以主机 A(192.168.3.60) 向主机 B(192.168.3.66) 的 5073 端口发送数据为例,其完整格式为:

    POST /Picture HTTP/1.1
    Host: 192.168.3.66::5073
    Content-Length: 83
    
    {
        "imageName" : "test.png",
        "imageSize" : 4,
        "imageData" : "abcd"
    }
    

    主机 B 在收到主机 A 数据后,根据报文头部的长度 + Content-Length 对应的值,便可以轻松得到此次接收的数据总长度。全部接收完毕后将 imageData 值解析出来保存在本地即可,而对于 JSON 字符的解析操作,可以考虑使用轻量级的 cJSON 解析器。

    但是还有一个问题,我们知道,在一张图片数据中存在大量的不可见字符,当不可见字符在网络上传输时,往往要经过多个路由设备,由于不同的设备对不可见字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。

    那么怎样确保图片数据被正确传输了呢?答案就是使用 Base64。

    接下来我们就「图片读写操作、Base64、cJSON 和 Socket 编程」来完成网络中图片的传输。

    一、图片读写操作

    在正式开始图片读写之前,我们先来看下与文件读写相关的一些函数。

    1.1 fopen 和 fclose函数

    1.1.1 fopen 函数介绍

    函数原型:FILE *fopen( const char *fileName, const char *mode );

    参数介绍:

    1. fileName:文件名,可以包含路径和文件名两部分

    2. mode:表示打开文件的类型,关于文件类型的规定参见下表:

      访问模式 描述
      r 打开一个已有的文本文件,允许读取文件
      w 打开一个文本文件,允许写入文件
      如果文件存在,则该文件会被截断为零长度,重新写入
      如果文件不存在,则会创建一个新文件
      a 打开一个文本文件,以追加模式写入文件
      如果文件不存在,则会创建一个新文件
      r+ 打开一个文本文件,允许读写文件
      w+ 打开一个文本文件,允许读写文件
      如果文件已存在,则文件会被截断为零长度,重新写入
      如果文件不存在,则会创建一个新文件
      a+ 打开一个文本文件,允许读写文件
      如果文件不存在,则会创建一个新文件
      读取会从文件的开头开始,写入则只能是追加模式。

      如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

      • "rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

    返 回 值:如果成功的打开一个文件,返回文件指针;否则返回空指针。

    1.1.2 fclose 函数介绍

    函数原型:int fclose(FILE *fp);

    fclose 函数用来关闭一个由 fopen 函数打开的文件。该函数返回一个整型数:

    • 当文件关闭成功时,返回0
    • 否则返回一个非零值
    FILE *fp = fopen(fileName, "r");
    fclose(fp);

    1.2 fseek 和 ftell 函数

    对于文件的读写方式,C 语言不仅支持简单地顺序读写方式,还支持随机读写(即只要求读写文件中某一指定的部分)。相比于顺序读写,随机读写需要将文件指针移动到需要读写的位置再进行读写操作,这通常也被称为文件的定位。

    对于文件的定位,可以通过 fseekftell 函数来完成。

    1.2.1 fseek 函数介绍

    函数原型:int fseek(FILE *fp, long offset, int whence);

    参数介绍:

    1. fp:文件指针

    2. offset:偏移量,表示要移动的字节数。正数表示正向偏移,负数表示负向偏移

    3. whence:表示设定从文件的哪里开始偏移,取值范围如下表所示

      起始点
      文件首 SEEK_SET 0
      当前位置 SEEK_CUR 1
      文件末尾 SEEK_END 2

    返 回 值:

    • 如果该函数执行成功则返回 0,并将 fp 指向以 whence 为基准,偏移 offset 个字节的位置
    • 如果该函数执行失败则返回 -1,并设置 errno 的值,但并不改变 fp 指向的位置

    通过 offset 和 whence 参数,可精准调节文件指针的位置。

    /*将读写位置正向偏移至离文件开头 100 字节处*/
    fseek(fp, 100L, SEEK_SET);
    
    /*将读写位置正向偏移至离文件当前位置 100 字节处*/
    fseek(fp, 100L, SEEK_CUR);
    
    /*将读写位置负向偏移至离文件结尾 100 字节处*/
    fseek(fp, -100L, SEEK_END);
    
    /*将读写位置移动到文件的起始位置*/
    fseek(fp, 0L, SEEK_SET);
    
    /*将读写位置移动到文件尾*/
    fseek(fp, 0L, SEEK_END);

    1.2.2 ftell 函数介绍

    函数原型:long ftell(FILE *fp);

    参数介绍:fp:文件指针

    返 回 值:该函数用于得到文件指针当前位置相对于文件首的偏移字节数。

    通过联动 fseekftell 可以很方便的获取文件大小:

    long GetFileLength(FILE *fp)
    {
        long curpos = 0L;
        long length = 0L;
    
        curpos = ftell(fp);             // 保存fp相对于文件首的偏移量
        fseek(fp, 0L, SEEK_END);        // 将fp移动到文件尾
        length = ftell(fp);             // 统计文件大小
        fseek(fp, curpos, SEEK_SET);    // 将fp归位
        
        return length;
    }

    1.3 fread 和 fwrite 函数

    1.3.1 fread 函数介绍

    函数原型:size_t fread(void *buffer, size_t size, size_t count, FILE *fp);

    参数介绍:

    1. buffer:读入数据的存储地址
    2. size:每个数据的大小,单位是字节
    3. count:读取的数据个数
    4. fp:待读取的文件指针

    返 回 值:fread() 返回实际读取的元素个数

    Notes:

    1. fread 可以读二进制文件
    2. 可通过比较实际读取的元素个数和预想的个数,来判断文件是否被正确读取。
    #include 
    #include 
    #include 
    
    long GetFileLength(FILE *fp)
    {
        long curpos = 0L;
        long length = 0L;
    
        curpos = ftell(fp);          // 保存fp相对于文件首的偏移量
        fseek(fp, 0L, SEEK_END);     // 将fp移动到文件尾
        length = ftell(fp);          // 统计文件大小
        fseek(fp, curpos, SEEK_SET); // 将fp归位
    
        return length;
    }
    int main()
    {
        FILE *fp = fopen("test.txt", "rb+"); // test.txt中的文件内容为:0123456789
    
        // 获取文件大小
        int length = GetFileLength(fp); // length = 10
    
        // 申请一块能装下整个文件的空间
        char *buffer = (char *)malloc(sizeof(char) * length);
        int size = sizeof(char);   // 每次读取1个字节
        int count = length / size; // 读取10次
    
        int readLen = fread(buffer, size, count, fp); // 如果readLen=count=10,则读取成功
        if (readLen != count) // 判断实际读取的元素个数readLen和预想的个数count是否相等
        {
            printf("fread error.\n");
        }
    
        printf("[%s](%d)\n", buffer, readLen);
        fclose(fp);
        return 0;
    }

    1.3.2 fwirte 函数介绍

    函数原型:size_t fwrite(const void *buffer, size_t size, size_t count, FILE *fp);

    参数介绍:

    1. buffer:指向数据块的指针
    2. size:每个元素的大小,单位是字节
    3. count:写入的数据个数
    4. fp:待写入的文件指针

    返 回 值:成功写入则返回实际写入的数据个数,fwrite 的返回值随着调用格式的不同而不同。

    • 调用格式一:

      #include 
      #include 
      #include 
      
      int main()
      {
          FILE *fp = fopen("test.txt", "wb+");
      
          char buffer[] = "0123456789";
          int bufLen = strlen(buffer);    // bufLen = 10
      
          int size = sizeof(char);        // 每次写入1个字节
          int count = bufLen / size;      // 写入10次
          int writeLen = fwrite(buffer, size, count, fp); // writeLen = count = 10
      
          fclose(fp);
          return 0;
      }
    • 调用格式二:

      #include 
      #include 
      #include 
      
      int main()
      {
          FILE *fp = fopen("test.txt", "wb+");
      
          char buffer[] = "0123456789";
          int bufLen = strlen(buffer);    // bufLen = 10
      
          int size = bufLen;              // 每次写入bufLen个字节,即将buffer一次性写入
          int count = bufLen / size;      // 写入1次
          int writeLen = fwrite(buffer, size, count, fp); // writeLen = count = 1
      
          fclose(fp);
          return 0;
      }

    1.4 图片读写

    1.4.1 readAndwrite.h

    #ifndef __READANDWRITE_H__
    #define __READANDWRITE_H__
    
    int Read(const char *fileName, char **buffer);
    int Write(const char *fileName, char *buffer, int length);
    
    #endif 

    1.4.2 readAndwrite.c

    #include 
    #include 
    #include "readAndwrite.h"
    
    /********************************************************
     * 函数功能:获取文件大小
     * 参数说明:fp 入参,表示文件指针
     * 返 回 值:返回fp所指向的文件大小
     *******************************************************/
    static int GetFileLength(FILE *fp)
    {
        long curpos = 0L;
        long length = 0L;
    
        curpos = ftell(fp);             // 保存fp相对于文件首的偏移量
        fseek(fp, 0L, SEEK_END);        // 将fp移动到文件尾
        length = ftell(fp);             // 统计文件大小
        fseek(fp, curpos, SEEK_SET);    // 将fp归位
        
        return (int)length;
    }
    
    /********************************************************
     * 函数功能:以二进制形式读文件
     * 参数说明:fileName 入参,表示待读取的文件
     *          buffer   出参,将读取的文件保存在buffer中
     * 返 回 值:读取成功则返回读取的文件大小,失败返回 0
     *******************************************************/
    int Read(const char *fileName, char **buffer)
    {
        if (fileName == NULL || buffer == NULL)
        {
            printf("[%s][%s-%lu] Invalid param.\n", __FILE__, __FUNCTION__, __LINE__);
            return 0;
        }
    
        FILE *fp = fopen(fileName, "rb");
        if (fp == NULL)
        {
            printf("[%s][%s-%lu] Open fail(%s) error.\n", __FILE__, __FUNCTION__, __LINE__, fileName);
            return 0;
        }
    
        int length = GetFileLength(fp);
    
        // 申请一块能装下整个文件的空间
        (*buffer) = (char *)malloc(sizeof(char) * (length + 1));
        int size = fread(*buffer, sizeof(char), length, fp);
        if (size != length) // 通过比较实际读取长度size和预期长度length,来判断是否读取成功
        {
            printf("[%s][%s-%lu] Fail to call fread.\n", __FILE__, __FUNCTION__, __LINE__);
            fclose(fp);
            return 0;
        }
    
        fclose(fp);
        return size;
    }
    
    /********************************************************
     * 函数功能:以二进制形式写文件
     * 参数说明:fileName 入参,表示文件写入的路径
     *          buffer   入参,表示待写入的文件
     *          len      入参,表示buffer的大小
     * 返 回 值:写入成功则返回实际写入的长度,失败返回 0
     *******************************************************/
    int Write(const char *fileName, char *buffer, int length)
    {
        if (fileName == NULL || buffer == NULL || length <= 0)
        {
            printf("[%s][%s-%lu] Invalid param.\n", __FILE__, __FUNCTION__, __LINE__);
            return 0;
        }
        FILE *fp = fopen(fileName, "wb+");
        if (fp == NULL)
        {
            printf("[%s][%s-%lu] Open fail(%s) error.\n", __FILE__, __FUNCTION__, __LINE__, fileName);
            return 0;
        }
    
        int size = fwrite(buffer, sizeof(char), length, fp);
        if (size != length) // 通过比较实际写入长度size和预期长度length,来判断是否写入成功
        {
            printf("[%s][%s-%lu] Fail to call fwrite.\n", __FILE__, __FUNCTION__, __LINE__);
            fclose(fp);
            return 0;
        }
        fclose(fp);
    
        return size;
    }

    1.4.3 testReadAndWrite.c

    #include 
    #include 
    #include "readAndwrite.h"
    
    #define FILE_READ_NAME "./image/wallpaper.png"
    #define FILE_WRITE_NAME "./image/wallpaper_copy.png"
    
    int main()
    {
        char *buffer;
        int readLen = Read(FILE_READ_NAME, &buffer);
        if (readLen == 0)
        {
            printf("[%s][%s-%lu] Read error.\n", __FILE__, __FUNCTION__, __LINE__);
            exit(0);
        }
        else
        {
            printf("[%s][%s-%lu] Read succeed.\n", __FILE__, __FUNCTION__, __LINE__);
        }
    
        int writeLen = Write(FILE_WRITE_NAME, buffer, readLen);
        if (writeLen == 0)
        {
            printf("[%s][%s-%lu] Write error.\n", __FILE__, __FUNCTION__, __LINE__);
            exit(0);
        }
        else
        {
            printf("[%s][%s-%lu] Write succeed.\n", __FILE__, __FUNCTION__, __LINE__);
        }
    
        return 0;
    }

    1.4.4 Tutorial

    目录结构:

    1. 将 readAndwrite.h、readAndwrite.c 和 testReadAndWrite.c 置于 ReadingAndWriting 目录下。

    2. 在 image 目录下存在一张图片 wallpaper.png

    3. 编译、运行

    通过打印的日志信息可以看出,图片读写都成功了,下面我们通过文件树看一下是否真的成功了:

    最后对比一下这两个文件的 md5sum 值:

    二、Base64

    2.1 何为 Base64

    Base64 是一种基于 64 个可打印字符来表示二进制数据的方法,这 64 个可打印字符包括:

    1. 大写字母 A~Z
    2. 小写字母 a~z
    3. 数字 0~9
    4. +/

    2.2 为什么要使用 Base64

    我们知道一个字节(1B = 8b)可表示的范围是 0~255, 其中 ASCII 值的范围为 0~127(十六进制:0x00~0x7F),而超过 ASCII 范围的 128~255 之间的值是不可见字符。

    ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,它主要用于显示现代英语。

    在 ASCII 码中 0~31 和 127 是控制字符,共 33 个。以下是其中一部分控制字符:

    其余 95 个,即 32~126 是可打印字符,包括数字、大小写字母、常用符号等:

    当不可见字符在网络上传输时,往往要经过多个路由设备,由于不同的设备对不可见字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。

    而图片文件中就包含大量的不可见字符,所以我们想要在网络中正确传递图片,就可以考虑使用 Base64:

    • 对于待传输的图片数据,可通过 Base64 将其编码为可见字符在网络中传输
    • 对端收到经 Base64 编码的数据后,通过 Base64 编码的逆过程,将其解码为原图片

    2.3 Base64 详解

    2.3.1 前置知识

    通过 2.1 我们知道,Base64 是一种基于 64 个可打印字符来表示二进制数据的方法。由于 64=26" role="presentation">64=26,所以一个 Base64 字符实际上代表着 6 个二进制位(bit,比特)。

    在二进制数据中,1 个字节对应的是8比特(1B = 8b),而 3 个字节有 24 个比特,正好对应于 4 个 Base64 字符,即 3 个字节可由 4 个 Base64 字符来表示,相应的转换过程如下图所示:

    前面 2.1 我们也提到了,Base64 包含 64 个可打印字符,相应的索引表如下:

    等号=用来作为后缀用途。

    2.3.2 Base64 编码

    了解完上述的知识,我们以编码字符串you为例,来直观的感受一下编码过程。

    具体的编码方式:

    1. 将每 3 个字节作为一组,3 个字节一共 24 个二进制位
    2. 将这 24 个二进制位分为 4 组,每个组有 6 个二进制位,对应于 6 个 Base64 字符
    3. 每个 Base64 字符对应的将是一个小于 64 的数字,即为字符编号
    4. 最后根据索引表(图 4),就得到了经 Base64 编码后的字符串

    • 由图可知,you(3 字节)编码的结果为eW91(4字节)
    • 很明显经过 Base64 编码后体积会增加 1/3

    由于you这个字符串的长度刚好是 3B,我们可以用 4 个 Base64 字符来表示。但如果待编码的字符串长度不是 3 的整数倍时,应该如何处理呢?

    如果要编码的字节数不能被 3 整除,最后会多出 1 个或 2 个字节,那么可以使用下面的方法进行处理:先使用 0 字节值在末尾补足,使其能够被 3 整除,然后再进行 Base64 的编码。

    以编码字符A为例,其所占的字节数为 1,不能被 3 整除,需要补 2 个 0 字节,具体如下图所示:

    • 字符A经过 Base64 编码后的结果是 QQ==
    • 该结果后面的两个 = 代表补足的字节数

    接着我们来看另一个示例,假设需编码的字符串为 BC,其所占字节数为 2,不能被 3 整除,需要补 1 个 0 字节,具体如下图所示:

    • 字符串BC经过 Base64 编码后的结果是QkM=
    • 该结果后面的 1 个=代表补足的字节数

    2.4 Base64 编解码

    2.4.1 base64.h

    #ifndef __BASE64_H__
    #define __BASE64_H__
    
    char *Base64Encode(const char *str, int len, int *encodedLen);
    int Base64Decode(const char *base64Encoded, char **base64Decoded);
    
    #endif

    2.4.2 base64.c

    #include 
    #include 
    #include 
    #include "base64.h"
    
    // 定义base64编码表
    static const char base64EncodeTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    
    /********************************************************
     * 函数功能:计算经过base64编码后的新字符串的长度
     * 参数说明:len 入参,表示待编码的字符串的长度
     * 返 回 值:返回经base64编码后的新字符串的长度
     *******************************************************/
    static int Base64EncodeLen(int len)
    {
        return (((len + 2) / 3) * 4);
    }
    
    /********************************************************
     * 函数功能:base64编码,返回经base64编码后的字符串
     * 参数说明:str 入参,表示待编码的字符串
     *          len 入参,表示待编码的字符串的长度
     *          encodedLen 出参,保存编码后的字符串的长度
     * 备   注:因str可能包含不可见字符及'\0',所以参数len是必须的
     * 返 回 值:返回经base64编码后的字符串
     *******************************************************/
    char *Base64Encode(const char *str, const int len, int *encodedLen)
    {
        char *encoded = (char *)malloc(Base64EncodeLen(len) + 1);
        char *p = encoded;
    
        // str中,每3位为一组,经过base64后变成4位
        int i;
        for (i = 0; i < len - 2; i += 3)
        {
            // 取出第一个字符的前6位并找出对应的结果字符
            *p++ = base64EncodeTable[(str[i] >> 2) & 0x3F];
    
            // 将第一个字符的后2位与第二个字符的前4位进行组合并找到对应的结果字符
            *p++ = base64EncodeTable[((str[i] & 0x3) << 4) | ((str[i + 1] & 0xF0) >> 4)];
    
            // 将第二个字符的后4位与第三个字符的前2位组合并找出对应的结果字符
            *p++ = base64EncodeTable[((str[i + 1] & 0xF) << 2) | ((str[i + 2] & 0xC0) >> 6)];
    
            // 取出第三个字符的后6位并找出结果字符
            *p++ = base64EncodeTable[str[i + 2] & 0x3F];
        }
        if (i < len) // 如果 i < len,说明 i % 3 != 0,需要额外补充 '='
        {
            *p++ = base64EncodeTable[(str[i] >> 2) & 0x3F];
            if (i == (len - 1)) // 剩余一个字符
            {
                *p++ = base64EncodeTable[((str[i] & 0x3) << 4)];
                *p++ = '=';
            }
            else if (i == len - 2) // 剩余两个字符
            {
                *p++ = base64EncodeTable[((str[i] & 0x3) << 4) | ((int)(str[i + 1] & 0xF0) >> 4)];
                *p++ = base64EncodeTable[((str[i + 1] & 0xF) << 2)];
            }
            *p++ = '=';
        }
        *p = '\0';
        *encodedLen = p - encoded;
    
        return encoded;
    }
    
    // 定义base64解码表,并将base64DecodeTable['=']置为0,便于统一处理编码后存在'='号的情况
    //根据 base64 编码表,以字符找到对应的十进制数据 
    static const unsigned char base64DecodeTable[] = 
    {
        64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
        64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
        64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
        64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
        64, 64, 64, 62, 64, 64, 64, 63, 52, 53, 
        54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 
        64,  0, 64, 64, 64,  0,  1,  2,  3,  4, 
         5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 
        25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 
        29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 
        39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 
        49, 50, 51, 64, 64, 64, 64, 64, 64, 64
    };
    
    /********************************************************
     * 函数功能:计算经base64解码后的字符串的最大长度
     * 参数说明:encoded 入参,表示经base64编码后的字符串
     *          len 出参,用于保存encoded的长度
     * 返 回 值:返回经base64解码后的新字符串的最大长度
     * 备    注:忽略'='的影响
     *******************************************************/
    static int Base64DecodeLen(const char *encoded, int *len)
    {
        register const char *bufin = encoded;    // 声明寄存器变量:直接存储在CPU中的寄存器中的变量,频繁调用时提高运行效率
        for (; base64DecodeTable[*bufin] <= 63;) // base64DecodeTable['\0'] = 64,该函数的作用其实等价于 strlen(encoded)
        {
            bufin++;
        }
        *len = bufin - encoded; // 获取encoded的字符长度(包含'=')
        return (*len / 4) * 3;
    }
    
    /********************************************************
     * 函数功能:base64解码
     * 参数说明:base64Encoded 入参,表示经base64编码后的字符串
     *          base64Decoded 出参,用于保存解码后的字符串
     * 返 回 值:返回经base64解码后的字符串的实际长度
     * 备    注:1. 考虑'='的影响
     *           2. 由于经base64解码后的字符串可能包含不可见字符及'\0',所以是有必要返回解码后的字符串长度的
     *******************************************************/
    int Base64Decode(const char *base64Encoded, char **base64Decoded)
    {
        int len;
        int decodedLen = Base64DecodeLen(base64Encoded, &len);
        if (len <= 0 || len % 4 != 0) // base64Encoded必须非空且长度为4的整倍数,才能进行后续的解码操作
        {
            printf("[%s][%s-%lu] Invalid param.\n", __FILE__, __FUNCTION__, __LINE__);
            return 0;
        }
    
        char *decoded = (char *)malloc(decodedLen + 1);
        decoded[decodedLen] = 0;
    
        int i;
        char *bufout = decoded;
        for (i = 0; i + 3 < len; i += 4) // 以4个字符为一组进行解码
        {
            // 取出当前组的「第1个字符对应base64解码表的十进制数的后六位」与「第2个字符对应base64解码表的十进制数的前两位」进行组合
            *bufout++ = (char)(base64DecodeTable[base64Encoded[i]] << 2 | base64DecodeTable[base64Encoded[i + 1]] >> 4);
    
            // 取出当前组的「第2个字符对应base64解码表的十进制数的后四位」与「第3个字符对应bas464解码表的十进制数的前四位」进行组合
            *bufout++ = (char)(base64DecodeTable[base64Encoded[i + 1]] << 4 | base64DecodeTable[base64Encoded[i + 2]] >> 2);
    
            // 取出当前组的「第3个字符对应base64解码表的十进制数的后两位」与「第4个字符对应bas464解码表的十进制数的前六位」进行组合
            *bufout++ = (char)(base64DecodeTable[base64Encoded[i + 2]] << 6 | base64DecodeTable[base64Encoded[i + 3]]);
        }
    
        *base64Decoded = decoded;
    
        if (base64Encoded[len - 2] == '=')
            decodedLen -= 2; // 存在两个'=',则实际长度 -2
        else if (base64Encoded[len - 1] == '=')
            decodedLen -= 1; // 存在一个'=',则实际长度 -1
    
        return decodedLen; // 返回解码后的实际长度
    }

    三、cJSON

    对于 cJSON 的介绍,详见我的这篇博客:cJson 学习笔记 - MElephant - 博客园 (cnblogs.com)

    四、Socket

    有关 Socket 的介绍,详见我的这篇博客:Socket 编程 - MElephant - 博客园 (cnblogs.com)

    4.1 socket.h

    #ifndef __SOCKET_H__
    #define __SOCKET_H__
    
    #define BIT0 (0x1 << 0)
    #define BIT1 (0x1 << 1)
    #define BIT2 (0x1 << 2)
    
    typedef unsigned int BOOL;
    #define TRUE    1
    #define FALSE   0
    
    #define E_SUCCEED   0
    #define E_ERROR     112
    
    #define BACKLOG 10 // 设置Socket最大监听个数
    
    /* 定义发送 HTTP 报文格式 */
    #define CLIENT_HTTP_BUF    "\
    POST /Picture HTTP/1.1\r\n\
    Host: %s\r\n\
    Content-Length: %d\r\n\
    Content-Type: image\r\n\
    \r\n\
    %s\r\n\
    \r\n"
    
    /* 定义响应 HTTP 报文格式 */
    #define SERVER_HTTP_BUF "\
    HTTP/1.1 200 OK\r\n\
    Content-Length: %d\r\n\
    Content-Type: text/plain\r\n\
    \r\n\
    %s\r\n\
    \r\n"
    
    #define HTTP_HDR_TAIL_STR "\r\n\r\n"    // 报文头结束标志
    #define HTTP_HDR_LINE_TAIL_STR "\r\n"   // 行结束标志
    #define HTTP_CONTENT_LENGTH_STR "Content-Length: "
    #define HTTP_HDR_LEN      256           // 发送HTTP报文格式中的头部长度,多多益善
    
    typedef enum tagSocketOpt
    {
        SOCKET_OPT_BIND     = BIT0,
        SOCKET_OPT_LISTEN   = BIT1,
        SOCKET_OPT_CONNECT  = BIT2
    } SOCKET_OPT_E;
    
    typedef struct tagIpAddr
    {
        char *ip;               // IP 地址,点分十进制
        unsigned short port;    // 端口号
    } IPADDR_S;
    
    int SocketCreate(int *fd, int createOpt, const IPADDR_S stIpAddr); // 创建 Socket,IPv4 & TCP
    int SocketSend(int hSocket, const char *sendBuf, const int bufLen);
    int SocketRecv(int hSocket, char **recvBuf, int *recvBufLen);
    
    #endif

    4.2 socket.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "socket.h"
    
    /********************************************************
     * 函数功能:创建基于IPv4的TCP socket
     * 参数说明:fd          出参,用于保存sockfd
     *          createOpt   入参,表示创建socket后的操作
     *          stIpAddr    入参,表示ip地址和端口号
     * 返 回 值:创建成功则返回 E_SUCCEED,否则返回 E_ERROR
     *******************************************************/
    int SocketCreate(int *fd, int createOpt, const IPADDR_S stIpAddr)
    {
        int hSocket = socket(AF_INET, SOCK_STREAM, 0);
        if (-1 == hSocket)
        {
            printf("[%s][%s-%lu] Fail to call socket.\n", __FILE__, __FUNCTION__, __LINE__);
            return E_ERROR;
        }
    
        if (SOCKET_OPT_BIND & createOpt)
        {
            struct sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            addr.sin_family = AF_INET;
            addr.sin_port = htons(stIpAddr.port);   // 将本地端口号转化为网络字节序
            inet_aton(stIpAddr.ip, &addr.sin_addr); // 将点分十进制的IP地址转换为网络字节序
    
            int iReuse = 1;
            setsockopt(hSocket, SOL_SOCKET, SO_REUSEADDR, &iReuse, sizeof(iReuse)); // 设置复用socket地址
            int iBind = bind(hSocket, (struct sockaddr *)&addr, sizeof(addr));
            if (-1 == iBind)
            {
                printf("[%s][%s-%lu] Fail to call bind.\n", __FILE__, __FUNCTION__, __LINE__);
                close(hSocket);
                return E_ERROR;
            }
            printf("[%s][%s-%lu] Socket bind succeed[%s:%u].\n", __FILE__, __FUNCTION__, __LINE__, stIpAddr.ip, stIpAddr.port);
        }
    
        if (SOCKET_OPT_LISTEN & createOpt)
        {
            int iListen = listen(hSocket, BACKLOG);
            if (-1 == iListen)
            {
                printf("[%s][%s-%lu] Fail to call listen.\n", __FILE__, __FUNCTION__, __LINE__);
                close(hSocket);
                return E_ERROR;
            }
            printf("[%s][%s-%lu] Socket listen succeed.\n", __FILE__, __FUNCTION__, __LINE__);
        }
    
        if (SOCKET_OPT_CONNECT & createOpt)
        {
            struct sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            addr.sin_family = AF_INET;
            addr.sin_port = htons(stIpAddr.port);   // 将本地端口号转化为网络字节序
            inet_aton(stIpAddr.ip, &addr.sin_addr); // 将点分十进制的IP地址转换为网络字节序
            int iConn = connect(hSocket, (struct sockaddr *)&addr, sizeof(addr));
            if (-1 == iConn)
            {
                printf("[%s][%s-%lu] Fail to call connect.\n", __FILE__, __FUNCTION__, __LINE__);
                close(hSocket);
                return E_ERROR;
            }
            printf("[%s][%s-%lu] Socket connect succeed[%s:%u].\n", __FILE__, __FUNCTION__, __LINE__, stIpAddr.ip, stIpAddr.port);
        }
    
        *fd = hSocket;
        return E_SUCCEED;
    }
    
    /********************************************************
     * 函数功能:发送TCP字节流
     * 参数说明:hSocket 入参,表示sockfd
     *          sendBuf 入参,表示待发送的字节流
     *          bufLen  入参,表示待发送的字节流的长度
     * 返 回 值:发送成功则返回 E_SUCCEED,否则返回 E_ERROR
     *******************************************************/
    int SocketSend(int hSocket, const char *sendBuf, const int bufLen)
    {
        int iSendLen = 0; // 已发送的字符个数
    
        while (iSendLen < bufLen)
        {
            int iRet = send(hSocket, sendBuf + iSendLen, bufLen - iSendLen, 0);
            if (iRet < 0)
            {
                if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
                    continue;
                else
                {
                    printf("[%s][%s-%lu] Socket Send Error.\n", __FILE__, __FUNCTION__, __LINE__);
                    return E_ERROR;
                }
            }
            iSendLen += iRet;
        }
    
        return E_SUCCEED;
    }
    
    /********************************************************
     * 函数功能:报文头预解
     * 参数说明:buf 入参,表示当前已接收的字节流
     * 返 回 值:跟据解析buf中的Content-Length字段,返回本次需要接收的字符串总长度
     *******************************************************/
    static int PreParseRecvedBuf(const char *buf)
    {
        char *pcTmp = NULL;
        char *pcStart = NULL;
        char *pcEnd = NULL;
        char szContentLen[16];  // 保存Content-Length的值的字符串形式
        int iContentLen = 0;    // 保存Content-Length的值的整数形式
        int bufHeadLen = 0;
    
        pcTmp = strstr(buf, HTTP_HDR_TAIL_STR);
        bufHeadLen = pcTmp - buf + 4; // 本次接收的报文头部总长度,+ 4 指的是报文头的结束后的换行 \r\n\r\n
    
        // 找到Content-Length对应的值
        pcStart = strstr(buf, HTTP_CONTENT_LENGTH_STR);
        pcStart += strlen(HTTP_CONTENT_LENGTH_STR);
        pcEnd = strstr(pcStart, HTTP_HDR_LINE_TAIL_STR);
    
        strncpy(szContentLen, pcStart, pcEnd - pcStart); // 将Content-Length值复制到szContentLen中
        iContentLen = atoi(szContentLen); // 本次接收的报文的内容总长度
    
        return bufHeadLen + iContentLen; // 本次需要接收的报文总长度 = 头部总长度 + 内容总长度
    }
    
    /********************************************************
     * 函数功能:接收TCP字节流
     * 参数说明:hSocket 入参,表示sockfd
     *          recvBuf 出参,用于保存接收后的字节流
     *          recvBufLen 出参,用于保存接收的字节流的总长度
     * 备    注:recvBuf需要在调用该函数前开辟空间,否则在调用realloc时会报invalid next size
     * 返 回 值:接收成功则返回 E_SUCCEED,否则返回 E_ERROR
     *******************************************************/
    int SocketRecv(int hSocket, char **recvBuf, int *recvBufLen)
    {
        int iRecvedLen = 0;     // 已接收的字符长度
        BOOL bPreParse = FALSE; // 判断是否处理了第一次接收的128个字符
    
        memset(*recvBuf, 0, *recvBufLen);
    
        while(TRUE)
        {
            int iRet = recv(hSocket, *recvBuf + iRecvedLen, *recvBufLen - iRecvedLen, 0);
            if (iRet < 0)
            {
                if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
                    continue;
                else
                {
                    printf("[%s][%s-%lu] Socket Recv Error, errno(%d/%s).\n", __FILE__, __FUNCTION__, __LINE__, errno, strerror(errno));
                    return E_ERROR;
                }
            }
            else if (iRet == 0)
            {
                if (iRecvedLen >= *recvBufLen) // 已接收的字符长度 ≥ 对端发送的字符总长度,说明接收完成
                {
                    break;
                }
                else
                {
                    printf("[%s][%s-%lu] Socket Recv Error, errno(%d/%s).\n", __FILE__, __FUNCTION__, __LINE__,  errno, strerror(errno));
                    return E_ERROR;
                }
            }
            else 
            {
                iRecvedLen += iRet;
    
                if (bPreParse == FALSE)
                {
                    // 预处理第一次接收的字符,并根据Content-Length确认此次需要接收的字符总长度
                    *recvBufLen = PreParseRecvedBuf(*recvBuf);   // 从接收的HTTP头部中获取本次需要接收的报文总长度
                    *recvBuf = (char *)realloc(*recvBuf, *recvBufLen + 1); // 根据需要接收的报文总长度重新为buf开辟所需长度的空间
                    memset(*recvBuf + iRecvedLen, 0, *recvBufLen + 1 - iRecvedLen);
    
                    bPreParse = TRUE;
                }
                else if (iRecvedLen < *recvBufLen)
                {
                    continue;
                }
    
                if (iRecvedLen >= *recvBufLen)
                {
                    break;
                }
            }
        }
    
        return E_SUCCEED;
    }

    五、在网络上中传输图片

    5.1 common.h

    #ifndef __COMMON_H__
    #define __COMMON_H__
    
    #define SAFE_FREE(ptr) \
        if (ptr) \
        { \
            free(ptr); \
            ptr = NULL; \
        }
    
    #define FILENAME_READ "./image/wallpaper.png"
    #define FILENAME_WRITE "./image/wallpaper_copy.png"
    
    typedef struct tagImage
    {
        char imageName[64]; // 图片名
        int imageSize;      // 图片大小
        char *data;         // 图片
    } IMAGE_S;
    
    #endif

    5.2 Server.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "common.h"
    #include "../Base64/base64.h"
    #include "../CJSON/cJSON.h"
    #include "../Socket/socket.h"
    #include "../ReadingAndWriting/readAndwrite.h"
    
    IPADDR_S ipAddr = {"192.168.204.128", 5073};
    
    int Process(const char *buf, IMAGE_S *pstImage)
    {
        char *tmp = strstr(buf ,"{");
        cJSON *pstRoot = cJSON_Parse(tmp);
    
        cJSON *pName = cJSON_GetObjectItem(pstRoot, "imageName");
        cJSON *pSize = cJSON_GetObjectItem(pstRoot, "imageSize");
        cJSON *pDataEncoded = cJSON_GetObjectItem(pstRoot, "dataEncoded");
    
        char *encoded = pDataEncoded->valuestring;
        char *decoded;
        int decodedLen = Base64Decode(encoded, &decoded);
        
        if (decodedLen != pSize->valueint)
        {
            printf("[%s][%s-%lu] Process error.\n", __FILE__, __FUNCTION__, __LINE__);
            return E_ERROR;
        }
    
        strcpy(pstImage->imageName, pName->valuestring);
        pstImage->imageSize = decodedLen;
        pstImage->data = decoded;
        
        cJSON_Delete(pstRoot);
    
        return E_SUCCEED;
    }
    int main()
    {
        int iRet = E_SUCCEED;
        int hSocket;
        int opt = SOCKET_OPT_BIND | SOCKET_OPT_LISTEN;
        iRet = SocketCreate(&hSocket, opt, ipAddr);
        if (iRet != E_SUCCEED)
        {
            printf("[%s][%s-%lu] Socket create error.\n", __FILE__, __FUNCTION__, __LINE__);
            exit(0);
        }
        
        int connfd = accept(hSocket, NULL, NULL);
    
        int recvLen = 128;
        char *recvBuf = (char *)malloc(recvLen);
        iRet = SocketRecv(connfd, &recvBuf, &recvLen);
        if (iRet != E_SUCCEED)
        {
            printf("[%s][%s-%lu] Socket recv error.\n", __FILE__, __FUNCTION__, __LINE__);
            close(hSocket);
            exit(0);
        }
    
        close(hSocket);
    
        IMAGE_S image;
        iRet = Process(recvBuf, &image);
        if (iRet != E_SUCCEED)
        {
            printf("[%s][%s-%lu] Fail to call process.\n", __FILE__, __FUNCTION__, __LINE__);
        }
        else
        {
            printf("[%s][%s-%lu] Process succeed, [%s](%d).\n", __FILE__, __FUNCTION__, __LINE__, image.imageName, image.imageSize);
            Write(FILENAME_WRITE, image.data, image.imageSize);
        }
        return 0;
    }

    5.3 Client.c

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "common.h"
    #include "../Base64/base64.h"
    #include "../CJSON/cJSON.h"
    #include "../Socket/socket.h"
    #include "../ReadingAndWriting/readAndwrite.h"
    
    IPADDR_S ipAddr = {"192.168.204.128", 5073};
    
    // 获取忽略掉路径信息的文件名,如 /image/image.png ==> image.png
    void GetFileName(const char *filename, char *name)
    {
        char *tmp = strstr(filename, "/");
        while (strstr(tmp, "/") != NULL)
        {
            tmp = strstr(tmp, "/");
            tmp++;
        }
        strcpy(name, tmp);
    }
    char *GetSendBuf(const char *filename)
    {
        char *imageData;
        int readLen = Read(filename, &imageData); // 获取原图片及其大小
        if (readLen == 0)
        {
            printf("[%s][%s-%lu] Read error.\n", __FILE__, __FUNCTION__, __LINE__);
            return NULL;
        }
    
        IMAGE_S stImage;
        GetFileName(filename, stImage.imageName);
        stImage.imageSize = readLen;
        stImage.data = imageData;
    
        int encodedLen = 0;
        char *encoded = Base64Encode(stImage.data, stImage.imageSize, &encodedLen);
    
        cJSON *pstRoot = cJSON_CreateObject();
        cJSON_AddStringToObject(pstRoot, "imageName", stImage.imageName);
        cJSON_AddNumberToObject(pstRoot, "imageSize", stImage.imageSize);
        cJSON_AddStringToObject(pstRoot, "dataEncoded", encoded);
    
        char *pcJson = cJSON_PrintUnformatted(pstRoot);
        int jsonLen = strlen(pcJson);
    
        int bufLen = jsonLen + HTTP_HDR_LEN;
        char *buf = (char *)malloc(bufLen);
        snprintf(buf, bufLen, CLIENT_HTTP_BUF, ipAddr.ip, jsonLen + 4, pcJson);
    
        cJSON_Delete(pstRoot);
        SAFE_FREE(stImage.data);
        
        return buf;
    }
    
    int main()
    {
        int iRet = E_SUCCEED;
        int hSocket;
        int opt = SOCKET_OPT_CONNECT;
        iRet = SocketCreate(&hSocket, opt, ipAddr);
        if (iRet != E_SUCCEED)
        {
            printf("[%s][%s-%lu] Socket create error.\n", __FILE__, __FUNCTION__, __LINE__);
            exit(0);
        }
        
        char *buf = GetSendBuf(FILENAME_READ);
        if (buf == NULL)
        {
            printf("[%s][%s-%lu] Get send buf error.\n", __FILE__, __FUNCTION__, __LINE__);
            close(hSocket);
            exit(0);
        }
        iRet = SocketSend(hSocket, buf, strlen(buf));
        if (iRet == E_SUCCEED)
        {
            printf("[%s][%s-%lu] Socket Send Succeed, SendLen[%d].\n", __FILE__, __FUNCTION__, __LINE__, strlen(buf));
        }
        else if (iRet == E_ERROR)
        {
            printf("[%s][%s-%lu] Socket Send Error.\n", __FILE__, __FUNCTION__, __LINE__);
        }
    
        close(hSocket);
        return 0;
    }

    5.4 Tutorial

    目录结构:

    分别生成 server 和 client 两个可执行文件:

    gcc ./Base64/base64.c ./CJSON/cJSON.c ./ReadingAndWriting/readAndwrite.c ./Socket/socket.c ./Main/Server.c -lm -o ./exeFile/Server
    gcc ./Base64/base64.c ./CJSON/cJSON.c ./ReadingAndWriting/readAndwrite.c ./Socket/socket.c ./Main/Client.c -lm -o ./exeFile/Client

    在两个终端下分别运行 Server 和 Client:

    查看图片传输情况:

    最后附上源码:https://melephant.lanzoum.com/irwXt0r4noji

    参考资料


    __EOF__

  • 本文作者: MElephant
  • 本文链接: https://www.cnblogs.com/hyacinthLJP/p/17258612.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    IntelliJ IDEA 2023.2更新了
    11.14-11.21
    大前端 - UniAPP
    Jenkins-SpringBoot-实现自动化构建
    eclipse启动一个Springboot项目
    国密国际SSL双证书解决方案,满足企事业单位国产国密SSL证书要求
    编译一日一练(DIY系列之汇编优化)
    C# CAD交互界面-自定义面板集-查找定位(六)
    LuatOS-SOC接口文档(air780E)-- gmssl - 国密算法
    MySQL--视图、存储过程、触发器
  • 原文地址:https://www.cnblogs.com/hyacinthLJP/p/17258612.html