网络中的图片传输

网络中的图片传输

前言

一张图片经过网络从主机 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 <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

目录结构:

网络中的图片传输

  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=2^{6}),所以一个 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 <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可能包含不可见字符及'',所以参数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 = '';     *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[''] = 64,该函数的作用其实等价于 strlen(encoded)     {         bufin++;     }     *len = bufin - encoded; // 获取encoded的字符长度(包含'=')     return (*len / 4) * 3; }  /********************************************************  * 函数功能:base64解码  * 参数说明:base64Encoded 入参,表示经base64编码后的字符串  *          base64Decoded 出参,用于保存解码后的字符串  * 返 回 值:返回经base64解码后的字符串的实际长度  * 备    注:1. 考虑'='的影响  *           2. 由于经base64解码后的字符串可能包含不可见字符及'',所以是有必要返回解码后的字符串长度的  *******************************************************/ 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.1rn Host: %srn Content-Length: %drn Content-Type: imagern rn %srn rn"  /* 定义响应 HTTP 报文格式 */ #define SERVER_HTTP_BUF " HTTP/1.1 200 OKrn Content-Length: %drn Content-Type: text/plainrn rn %srn rn"  #define HTTP_HDR_TAIL_STR "rnrn"    // 报文头结束标志 #define HTTP_HDR_LINE_TAIL_STR "rn"   // 行结束标志 #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 指的是报文头的结束后的换行 rnrn      // 找到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

目录结构:

网络中的图片传输

分别生成 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

参考资料

发表评论

相关文章