网络中的图片传输

前言

一张图片经过网络从主机 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:表示打开文件的类型,关于文件类型的规定参见下表:

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

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:表示设定从文件的哪里开始偏移,取值范围如下表所示

返 回 值:

  • 如果该函数执行成功则返回 0,并将 fp 指向以 whence 为基准,偏移 offset 个字节的位置
  • 如果该函数执行失败则返回 -1,并设置 errno 的值,但并不改变 fp 指向的位置
/*将读写位置正向偏移至离文件开头 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() 返回实际读取的元素个数

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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 <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    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 <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    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 <stdio.h>
#include <stdlib.h>
#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 <stdio.h>
#include <stdlib.h>
#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

目录结构:

网络中的图片传输-LMLPHP

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

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

  3. 编译、运行

    网络中的图片传输-LMLPHP

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

网络中的图片传输-LMLPHP

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

网络中的图片传输-LMLPHP

二、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 码中 0~31 和 127 是控制字符,共 33 个。以下是其中一部分控制字符:

网络中的图片传输-LMLPHP

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

网络中的图片传输-LMLPHP

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

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

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

2.3 Base64 详解

2.3.1 前置知识

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

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

网络中的图片传输-LMLPHP

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

网络中的图片传输-LMLPHP

2.3.2 Base64 编码

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

具体的编码方式:

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

网络中的图片传输-LMLPHP

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

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

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

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

网络中的图片传输-LMLPHP

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

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

网络中的图片传输-LMLPHP

  • 字符串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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#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

目录结构:

网络中的图片传输-LMLPHP

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

网络中的图片传输-LMLPHP

网络中的图片传输-LMLPHP

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

网络中的图片传输-LMLPHP

网络中的图片传输-LMLPHP

查看图片传输情况:

网络中的图片传输-LMLPHP

网络中的图片传输-LMLPHP

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

参考资料

03-27 00:14