網路中的圖片傳輸

MElephant發表於2023-03-26

網路中的圖片傳輸

前言

一張圖片經過網路從主機 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

目錄結構:

image-20230326122324487

  1. 將 readAndwrite.h、readAndwrite.c 和 testReadAndWrite.c 置於 ReadingAndWriting 目錄下。

  2. 在 image 目錄下存在一張圖片 wallpaper.png

  3. 編譯、執行

    image-20230326122504921

透過列印的日誌資訊可以看出,圖片讀寫都成功了,下面我們透過檔案樹看一下是否真的成功了:

image-20230326122547269

最後對比一下這兩個檔案的 md5sum 值:

image-20230326123200079

二、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 個。以下是其中一部分控制字元:

img

其餘 95 個,即 32~126 是可列印字元,包括數字、大小寫字母、常用符號等:

img

當不可見字元在網路上傳輸時,往往要經過多個路由裝置,由於不同的裝置對不可見字元的處理方式有一些不同,這樣那些不可見字元就有可能被處理錯誤,這是不利於傳輸的。

而圖片檔案中就包含大量的不可見字元,所以我們想要在網路中正確傳遞圖片,就可以考慮使用 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 字元來表示,相應的轉換過程如下圖所示:

img

前面 2.1 我們也提到了,Base64 包含 64 個可列印字元,相應的索引表如下:

img

等號=用來作為字尾用途。

2.3.2 Base64 編碼

瞭解完上述的知識,我們以編碼字串you為例,來直觀的感受一下編碼過程。

具體的編碼方式:

  1. 將每 3 個位元組作為一組,3 個位元組一共 24 個二進位制位
  2. 將這 24 個二進位制位分為 4 組,每個組有 6 個二進位制位,對應於 6 個 Base64 字元
  3. 每個 Base64 字元對應的將是一個小於 64 的數字,即為字元編號
  4. 最後根據索引表(圖 4),就得到了經 Base64 編碼後的字串

img

  • 由圖可知,you(3 位元組)編碼的結果為eW91(4位元組)
  • 很明顯經過 Base64 編碼後體積會增加 1/3

由於you這個字串的長度剛好是 3B,我們可以用 4 個 Base64 字元來表示。但如果待編碼的字串長度不是 3 的整數倍時,應該如何處理呢?

如果要編碼的位元組數不能被 3 整除,最後會多出 1 個或 2 個位元組,那麼可以使用下面的方法進行處理:先使用 0 位元組值在末尾補足,使其能夠被 3 整除,然後再進行 Base64 的編碼。

以編碼字元A為例,其所佔的位元組數為 1,不能被 3 整除,需要補 2 個 0 位元組,具體如下圖所示:

img

  • 字元A經過 Base64 編碼後的結果是 QQ==
  • 該結果後面的兩個 = 代表補足的位元組數

接著我們來看另一個示例,假設需編碼的字串為 BC,其所佔位元組數為 2,不能被 3 整除,需要補 1 個 0 位元組,具體如下圖所示:

img

  • 字串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

目錄結構:

image-20230326135527840

分別生成 server 和 client 兩個可執行檔案:

image-20230326135122718

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

image-20230326135036166

在兩個終端下分別執行 Server 和 Client:

image-20230326135647704

image-20230326135711321

檢視圖片傳輸情況:

image-20230326135749156

image-20230326135835587

最後附上原始碼:https://melephant.lanzoum.com/irwXt0r4noji

參考資料