網路中的圖片傳輸
前言
一張圖片經過網路從主機 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可能包含不可見字元及'\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
目錄結構:
分別生成 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部落格