网络中的图片传输
前言
一张图片经过网络从主机 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
#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);
参数介绍:
- buffer:指向数据块的指针
- size:每个元素的大小,单位是字节
- count:写入的数据个数
- 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
目录结构:
-
将 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 个可打印字符来表示二进制数据的方法。由于
在二进制数据中,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
#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 两个可执行文件:
在两个终端下分别运行 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博客
__EOF__