网络中的图片传输
前言
一张图片经过网络从主机 A 传输到主机 B,主机 B 在收到这张图片后将其保存在本地,对应步骤为:
- 读:主机 A 读取待传输的图片数据
- 传:主机 A 通过 Socket 将图片传输给主机 B
- 写:主机 B 在收到图片数据后,将其保存在本地
我们来思考这样几个问题:
- 图片数据要以怎样的形式在网络中进行传输?
- 对端收到数据后怎要确保是否接收完毕?
- 怎样确保图片文件可以在网络上正确传输?
为解决这些问题,我们可以从发送的数据格式入手,收发双方约定使用如下格式进行数据传输:
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 );
参数介绍:
-
fileName:文件名,可以包含路径和文件名两部分
-
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 语言不仅支持简单地顺序读写方式,还支持随机读写(即只要求读写文件中某一指定的部分)。相比于顺序读写,随机读写需要将文件指针移动到需要读写的位置再进行读写操作,这通常也被称为文件的定位。
对于文件的定位,可以通过 fseek
与 ftell
函数来完成。
1.2.1 fseek 函数介绍
函数原型:int fseek(FILE *fp, long offset, int whence);
参数介绍:
-
fp:文件指针
-
offset:偏移量,表示要移动的字节数。正数表示正向偏移,负数表示负向偏移
-
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:文件指针
返 回 值:该函数用于得到文件指针当前位置相对于文件首的偏移字节数。
通过联动 fseek
和 ftell
可以很方便的获取文件大小:
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);
参数介绍:
- buffer:读入数据的存储地址
- size:每个数据的大小,单位是字节
- count:读取的数据个数
- fp:待读取的文件指针
返 回 值:fread()
返回实际读取的元素个数
Notes:
- 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);
参数介绍:
- buffer:指向数据块的指针
- size:每个元素的大小,单位是字节
- count:写入的数据个数
- 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
目录结构:
-
将 readAndwrite.h、readAndwrite.c 和 testReadAndWrite.c 置于 ReadingAndWriting 目录下。
-
在 image 目录下存在一张图片 wallpaper.png
-
编译、运行
通过打印的日志信息可以看出,图片读写都成功了,下面我们通过文件树看一下是否真的成功了:
最后对比一下这两个文件的 md5sum 值:
二、Base64
2.1 何为 Base64
Base64 是一种基于 64 个可打印字符来表示二进制数据的方法,这 64 个可打印字符包括:
- 大写字母
A~Z
- 小写字母
a~z
- 数字
0~9
+
和/
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
为例,来直观的感受一下编码过程。
具体的编码方式:
- 将每 3 个字节作为一组,3 个字节一共 24 个二进制位
- 将这 24 个二进制位分为 4 组,每个组有 6 个二进制位,对应于 6 个 Base64 字符
- 每个 Base64 字符对应的将是一个小于 64 的数字,即为字符编号
- 最后根据索引表(图 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 两个可执行文件:
在两个终端下分别运行 Server 和 Client:
查看图片传输情况:
最后附上源码:https://melephant.lanzoum.com/irwXt0r4noji
参考资料
- (1条消息) 一文搞懂base64!干货_晓衡的成长日记的博客-CSDN博客
- Base64编码知识详解 (baidu.com)
- (1条消息) realloc函数用法解释_Luv Lines的博客-CSDN博客
- (1条消息) realloc出现invalid next size问题的原因分析_小乐杂货铺的博客-CSDN博客
- C 文件读写 | 菜鸟教程 (runoob.com)
- fseek、ftell和rewind函数,C语言fseek、ftell和rewind函数详解 (biancheng.net)
- fread函数详解 - 云端止水 - 博客园 (cnblogs.com)
- (1条消息) Linux C/C++ 实现MySQL的图片插入以及图片的读取_c++导入图片_别,爱℡的博客-CSDN博客
- (1条消息) fopen的用法_逆流而上.的博客-CSDN博客