1. 程式人生 > >libcurl簡單教程(翻譯自官方教程)

libcurl簡單教程(翻譯自官方教程)

原文用的windows平臺,我使用linux平臺,所以我把程式碼改成了linux平臺下的,而且原文用的是c++,我把其改成了c語言。有興趣可以參看原譯文:http://blog.csdn.net/JGood 。

    譯者注:這是一篇介紹如何使用libcurl的入門教程。文件不是逐字逐句按原文翻譯,而是根據筆者對libcurl的理解,參考原文寫成。文中用到的一些例子,可能不是出自原文,而是筆者在學習過程中,寫的一些示例程式(筆者使用的libcurl版本是:7.19.6)。出現在這裡主要是為了更好的說明libcurl的某些api函式的使用。許多例子都參考libcurl提供的example程式碼。

目標

    本文件介紹了在應用程式開發過程中,如何正確使用libcurl的基本方式和指導原則。文件使用C語言來呼叫libcurl的介面,當然也適用於其他與C語言接近的語言。

    文件主要針對使用libcurl來進行開發的人員。文件所摜的應用程式泛指你寫的原始碼,這些程式碼使用了libcurl進行資料傳輸。

    更多關於libcurl的功能和介面資訊,可以在相關的主頁上查閱。

編譯原始碼

    有很多種不同的方式來編譯C語言程式碼。這裡使用UNIX平臺下的編譯方式。即使你使用的是其他的作業系統,你仍然可以通過閱讀本文件來獲取許多有用的資訊。

編譯

    你的編譯器必須知道libcurl標頭檔案的位置。所以在編譯的時候,你要設定標頭檔案的包含路徑。可以使用curl-config工具來獲取這方面的資訊:

    $ curl-config --cflags

連結

    編譯完原始碼(這時的原始碼不是指libcurl的原始碼,你是你自己寫的程式程式碼)之後,你還必須把目標檔案連結成單個可執行檔案。你要連結libcurl庫,以及libcurl所依賴的其他庫,例如OpenSLL庫。當然可能還需要一些其他的作業系統庫。最後你還要設定一些編譯選項,當然可以使用curl-config工具簡化操作:

    $curl-config  --libs

說明:可以使用如下命令來進行編譯,當然也可以使用Makefile,下邊會有介紹。將curl-config括起來的符號時ESC下邊的那個按鍵。

$ gcc -o sample sample.c `curl-config --cflags --libs`

是否使用ssl

    定製編譯libcurl。與其他庫不同的是,libcurl可以定製編譯,根據實際需要是否支援某些特性,如是否支援SSL傳輸,像HTTPS和FTPS。如果決定需要支援SSL,必須在編譯時正確的設定。可以使用’curl-config’來判斷libcurl庫是否支援SSL:

    $ curl-config –feature

使用ssl時,必須在連結時加上這兩個選項,-lssl和-lcrypto,還看到網上說順序不能調換,沒有測試。

如果編譯時提示找不到<openssl/ssl.h>標頭檔案,是沒有安裝ssl庫,使用如下方式解決:
安裝openssl
sudo apt-get install openssl
再安裝以下:
sudo apt-get install libssl-dev build-essential zlibc zlib-bin libidn11-dev libidn11

autoconf巨集

    當你編寫配置指令碼來檢測libcurl及其相應設定時,你可以使用預定義巨集。文件docs/libcurl/libcurl.m4告訴你如何使用這些巨集。

跨平臺的可移植的程式碼

    libcurl的開發人員花費很大的努力,使libcurl儘可能在大多數平臺上正常執行。

全域性的初始化

    應用程式在使用libcurl之前,必須先初始化libcurl。libcurl只需初始化一次。可以使用以下語句進行初始化:

curl_global_init();

    curl_global_init()接收一個引數,告訴libcurl如何初始化。引數CURL_GLOBAL_ALL 會使libcurl初始化所有的子模組和一些預設的選項,通常這是一個比較好的預設引數值。還有兩個可選值:

CURL_GLOBAL_WIN32

    只能應用於Windows平臺。它告訴libcurl初始化winsock庫。如果winsock庫沒有正確地初始化,應用程式就不能使用socket。在應用程式中,只要初始化一次即可。

CURL_GLOBAL_SSL

    如果libcurl在編譯時被設定支援SSL,那麼該引數用於初始化相應的SSL庫。同樣,在應用程式中,只要初始化一次即可。

    libcurl有預設的保護機制,如果在呼叫curl_easy_perform時它檢測到還沒有通過curl_global_init進行初始化,libcurl會根據當前的執行時環境,自動呼叫全域性初始化函式。但必須清楚的是,讓系統自已初始化不是一個好的選擇。

    當應用程式不再使用libcurl的時候,應該呼叫curl_global_cleanup來釋放相關的資源。

    在程式中,應當避免多次呼叫curl_global_init和curl_global_cleanup。它們只能被呼叫一次。

libcurl提供的功能

    在執行時根據libcurl支援的特性來進行開發,通常比編譯時更好。可以通過呼叫curl_version_info函式返回的結構體來獲取執行時的具體資訊,從而確定當前環境下libcurl支援的一些特性。下面是在deepin linux 2014.1系統上執行的結果:


這只是簡答打印出來了返回字串版本的函式,這個函式是curl_veriosn()。

可以通過curl_version_info()函式來返回一個curl_version_info_data指標,獲取更加詳細的資訊。

這個結構體定義如下:

typedef struct {
   CURLversion age;          /* see description below */
 

  /* when 'age' is 0 or higher, the members below also exist: */
   const char *version;      /* human readable string */
   unsigned int version_num; /* numeric representation */
   const char *host;         /* human readable string */
   int features;             /* bitmask, see below */
   char *ssl_version;        /* human readable string */
   long ssl_version_num;     /* not used, always zero */
   const char *libz_version; /* human readable string */
   const char **protocols;   /* list of protocols */
 

  /* when 'age' is 1 or higher, the members below also exist: */
   const char *ares;         /* human readable string */
   int ares_num;             /* number */
 

  /* when 'age' is 2 or higher, the member below also exists: */
   const char *libidn;       /* human readable string */
 

  /* when 'age' is 3 or higher (7.16.1 or later), the members below also
      exist  */
   int iconv_ver_num;       /* '_libiconv_version' if iconv support enabled */
 

  const char *libssh_version; /* human readable string */
 

} curl_version_info_data;

使用easy interface

    首先介紹libcurl中被稱為easy interface的api函式,所有這些函式都是有相同的字首:curl_easy 。

    當前版本的libcurl也提供了multi interface,關於這些介面的詳細使用,在下面的章節中會有介紹。在使用multi interface之前,你首先應該理解如何使用easy interface。

    要使用easy interface,首先必須建立一個easy handle,easy handle用於執行每次操作。基本上,每個執行緒都應該有自己的easy handle用於資料通訊(如果需要的話)。千萬不要在多執行緒之間共享同一個easy handle。下面的函式用於獲取一個easy handle :

CURL *easy_handle = curl_easy_init();

    在easy handle上可以設定屬性和操作(action)。easy handle就像一個邏輯連線,用於接下來要進行的資料傳輸。

    使用curl_easy_setopt函式可以設定easy handle的屬性和操作,這些屬性和操作控制libcurl如何與遠端主機進行資料通訊。一旦在easy handle中設定了相應的屬性和操作,它們將一直作用該easy handle。也就是說,重複使用easy hanle向遠端主機發出請求,先前設定的屬性仍然生效。

    easy handle的許多屬性使用字串(以/0結尾的位元組陣列)來設定。通過curl_easy_setopt函式設定字串屬性時,libcurl內部會自動拷貝這些字串,所以在設定完相關屬性之後,字串可以直接被釋放掉(如果需要的話)。

    easy handle最基本、最常用的屬性是URL。你應當通過CURLOPT_URL屬性提供適當的URL:

curl_easy_setopt(easy_handle, CURLOPT_URL, "http://www.example.com");

    假設你要獲取URL所表示的遠端主機上的資源。你需要寫一段程式用來完成資料傳輸,你可能希望直接儲存接收到的資料而不是簡單的在輸出視窗中列印它們。所以,你必須首先寫一個回撥函式用來儲存接收到的資料。回撥函式的原型如下:

size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp);

    可以使用下面的語句來註冊回撥函式,回撥函式將會在接收到資料的時候被呼叫:

curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_data);

    可以給回撥函式提供一個自定義引數,libcurl不處理該引數,只是簡單的傳遞:

curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, &internal_struct);

    如果你沒有通過CURLOPT_WRITEFUNCTION屬性給easy handle設定回撥函式,libcurl會提供一個預設的回撥函式,它只是簡單的將接收到的資料列印到標準輸出。你也可以通過CURLOPT_WRITEDATA屬性給預設回撥函式傳遞一個已經開啟的檔案指標,用於將資料輸出到檔案裡。

    下面是一些平臺相關的注意點。在一些平臺上,libcurl不能直接操作由應用程式開啟的檔案。所以,如果使用預設的回撥函式,同時通過CURLOPT_WRITEDATA屬性給easy handle傳遞一個檔案指標,應用程式可能會執行失敗。如果你希望自己的程式能跑在任何系統上,你必須避免出現這種情況。

    如果以win32動態連線庫的形式來使用libcurl,在設定CURLOPT_WRITEDATA屬性時,你必須同時 使用CURLOPT_WRITEFUNCTION來註冊回撥函式。否則程式會執行失敗(筆者嘗試只傳遞一個開啟的檔案指標而不顯式設定回撥函式,程式並沒有崩潰。可能是我使用的方式不正確。)。

    當然,libcurl還支援許多其他的屬性,在接下來的篇幅裡,你將會逐步地接觸到它們。呼叫下面的函式,將執行真正的資料通訊:

success = curl_easy_perform(easy_handle);

    curl_easy_perfrom將連線到遠端主機,執行必要的命令,並接收資料。當接收到資料時,先前設定的回撥函式將被呼叫。libcurl可能一次只接收到1位元組的資料,也可能接收到好幾K的資料,libcurl會盡可能多、及時的將資料傳遞給回撥函式。回撥函式返回接收的資料長度。如果回撥函式返回的資料長度與傳遞給它的長度不一致(即返回長度 != size * nmemb),libcurl將會終止操作,並返回一個錯誤程式碼。

    當資料傳遞結束的時候,curl_easy_perform將返回一個程式碼表示操作成功或失敗。如果需要獲取更多有關通訊細節的資訊,你可以設定CURLOPT_ERRORBUFFER屬性,讓libcurl快取許多可讀的錯誤資訊。

    easy handle在完成一次資料通訊之後可以被重用。這裡非常建議你重用一個已經存在的easy handle。如果在完成資料傳輸之後,你建立另一個easy handle來執行其他的資料通訊,libcurl在內部會嘗試著重用上一次建立的連線。

    對於有些協議,下載檔案可能包括許多複雜的子過程:日誌記錄、設定傳輸模式、選擇當前資料夾,最後下載檔案資料。使用libcurl,你不需要關心這一切,你只需簡單地提供一個URL,libcurl會給你做剩餘所有的工作。

    下面的這個例子演示瞭如何獲取網頁原始碼,將其儲存到本地檔案,並同時將獲取的原始碼輸出到控制檯上。

/*************************************************************************
	> File Name: get_data.c
	> Author: gwq
	> Mail: [email protected] 
	> Created Time: 2014年12月17日 星期三 22時08分13秒
 ************************************************************************/

/*
 * 將接收到的資料儲存到本地檔案中,並輸出到控制檯。
 */
#include <stdio.h>
#include <stdlib.h>
#include <curl/curl.h>

/*
 * 回撥函式
 * buffer,接收到的資料所在緩衝區
 * size,資料長度
 * nmenb,資料片數量
 * user_p,使用者自定義指標
 */
size_t process_data(void *buffer, size_t size, size_t nmenb, void *userp)
{
	FILE *fp = (FILE *)userp;
	size_t ret = fwrite(buffer, size, nmenb, fp);
	printf("%s\n", (char *)buffer);
	return ret;
}

int main(int argc, char *argv[])
{
	if (argc != 3) {
		printf("用法:./get_data.c 檔名 網址\n");
		return 0;
	}

	CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
	if (ret != CURLE_OK) {
		fprintf(stderr, "curl初始化失敗!\n");
		exit(EXIT_FAILURE);
	}
	CURL *easy_handle = curl_easy_init();
	if (easy_handle == NULL) {
		fprintf(stderr, "獲取easy_handle錯誤!\n");
		curl_global_cleanup();
		exit(EXIT_FAILURE);
	}
	FILE *fp = fopen(argv[1], "w+");
	// 設定網址
	curl_easy_setopt(easy_handle, CURLOPT_URL, argv[2]);
	// 設定回撥函式
	curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, process_data);
	// 回撥函式引數
	curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, fp);
	// 輸出通訊過程中的一些細節,這可以用來除錯
	// 如果使用的是http協議,請求頭/響應頭也會被輸出
	//curl_easy_setopt(easy_handle, CURLOPT_VERBOSE, 1);
	// 將訊息頭設定為出現在內容中
	//curl_easy_setopt(easy_handle, CURLOPT_HEADER, 1);

	// 執行資料請求
	curl_easy_perform(easy_handle);

	// 釋放資源
	fclose(fp);
	curl_easy_cleanup(easy_handle);
	curl_global_cleanup();

	return 0;
}
多執行緒問題

    首先一個基本原則就是:絕對不應該線上程之間共享同一個libcurl handle,不管是easy handle還是multi handle(將在下文中介紹)。一個執行緒每次只能使用一個handle。

    libcurl是執行緒安全的,但有兩點例外:訊號(signals)和SSL/TLS handler。 訊號用於超時失效名字解析(timing out name resolves)。libcurl依賴其他的庫來支援SSL/STL,所以用多執行緒的方式訪問HTTPS或FTPS的URL時,應該滿足這些庫對多執行緒操作的一些要求。詳細可以參考:

    NSS: 宣稱是多執行緒安全的。

什麼時候libcurl無法正常工作

    傳輸失敗總是有原因的。你可能錯誤的設定了一些libcurl的屬性或者沒有正確的理解某些屬性的含義,或者是遠端主機返回一些無法被正確解析的內容。

    這裡有一個黃金法則來處理這些問題:將CURLOPT_VERBOSE屬性設定為1,libcurl會輸出通訊過程中的一些細節。如果使用的是http協議,請求頭/響應頭也會被輸出。將CURLOPT_HEADER設為1,這些頭資訊將出現在訊息的內容中。

    當然不可否認的是,libcurl還存在bug。當你在使用libcurl的過程中發現bug時,希望能夠提交給我們,好讓我們能夠修復這些bug。你在提交bug時,請同時提供詳細的資訊:通過CURLOPT_VERBOSE屬性跟蹤到的協議資訊、libcurl版本、libcurl的客戶程式碼、作業系統名稱、版本、編譯器名稱、版本等等。

    如果你對相關的協議瞭解越多,在使用libcurl時,就越不容易犯錯。

上傳資料到遠端站點

    libcurl提供協議無關的方式進行資料傳輸。所以上傳一個檔案到FTP伺服器,跟向HTTP伺服器提交一個PUT請求的操作方式是類似的:

1. 建立easy handle或者重用先前建立的easy handle。

2. 設定CURLOPT_URL屬性。

3. 編寫回調函式。在執行上傳的時候,libcurl通過回撥函式讀取要上傳的資料。(如果要從遠端伺服器下載資料,可以通過回撥來儲存接收到的資料。)回撥函式的原型如下:

size_t function(char *bufptr, size_t size, size_t nitems, void *userp);

    bufptr指標表示緩衝區,用於儲存要上傳的資料,size * nitems是緩衝區資料的長度,userp是一個使用者自定義指標,libcurl不對該指標作任何操作,它只是簡單的傳遞該指標。可以使用該指標在應用程式與libcurl之間傳遞資訊。

4. 註冊回撥函式,設定自定義指標。語法如下:

// 註冊回撥函式
curl_easy_setopt(easy_handle, CURLOPT_READFUNCTION, read_function);
// 設定自定義指標
curl_easy_setopt(easy_handle, CURLOPT_READDATA, &filedata);

5. 告訴libcurl,執行的是上傳操作。

curl_easy_setopt(easy_handle, CURLOPT_UPLOAD, 1L);

    有些協議在沒有預先知道上傳檔案大小的情況下,可能無法正確判斷上傳是否結束,所以最好預先使用CURLOPT_INFILESIZE_LARGE屬性:告訴它要上傳檔案的大小:

/* in this example, file_size must be an curl_off_t variable */
curl_easy_setopt(easy_handle, CURLOPT_INFILESIZE_LARGE, file_size);

另:在linux下獲取檔案大小有兩種方法,一種是

unsigned long get_file_size(const char *path)
{
    unsigned long filesize = -1;
    FILE *fp;
    fp = fopen(path, "r");
    if(fp == NULL)
        return filesize;
    fseek(fp, 0L, SEEK_END);
    filesize = ftell(fp);
    fclose(fp);
    return filesize;
}

此種以開啟檔案的方法取得檔案的大小,不適合大檔案,並且可能會出現訪問衝突(比如正在下載的檔案),效率也比較低

另一種方法是使用系統呼叫stat:

#include <sys/stat.h>
  
unsigned long get_file_size(const char *path)
{
    unsigned long filesize = -1;
    struct stat statbuff;
    if(stat(path, &statbuff) < 0){
        return filesize;
    }else{
        filesize = statbuff.st_size;
    }
    return filesize;
}
此種使用讀取檔案屬性的方法得到檔案的大小,效率較高,也較穩定

其中stat結構體的定義如下:

struct stat {
	dev_t     st_dev;     /* ID of device containing file */
	ino_t     st_ino;     /* inode number */
	mode_t    st_mode;    /* protection */
	nlink_t   st_nlink;   /* number of hard links */
	uid_t     st_uid;     /* user ID of owner */
	gid_t     st_gid;     /* group ID of owner */
	dev_t     st_rdev;    /* device ID (if special file) */
	off_t     st_size;    /* total size, in bytes */
	blksize_t st_blksize; /* blocksize for filesystem I/O */
	blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
	time_t    st_atime;   /* time of last access */
	time_t    st_mtime;   /* time of last modification */
	time_t    st_ctime;   /* time of last status change */
};
stat函式執行成功返回0,否則返回-1,errno被設定。

6. 呼叫curl_easy_perform。

    接下來,libcurl將會完成剩下的所有工作。在上傳檔案過程中,libcurl會不斷呼叫先前設定的回撥函式,用於將要上傳的資料讀入到緩衝區,並執行上傳。

    下面的例子演示如何將檔案上傳到FTP伺服器。使用了vsftdp搭建的ftp伺服器,匿名使用者沒有配置好,總是上傳失敗。所以,使用了本地的賬號密碼登陸。

/*************************************************************************
	> File Name: curl_upload.c
	> Author: gwq
	> Mail: [email protected] 
	> Created Time: 2014年12月18日 星期四 16時56分49秒
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <curl/curl.h>

size_t readfile(char *buffer, size_t size, size_t nmemb, void *userp)
{
	return fread(buffer, size, nmemb, (FILE *)userp);
}

int main(int argc, char *argv[])
{
	if (argc != 3) {
		printf("用法:./curl_upload 網址 檔名\n");
		return 0;
	}
	const char *url = argv[1];
	const char *filename = argv[2];
	CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
	if (ret != CURLE_OK) {
		fprintf(stderr, "初始化curl失敗!\n");
		exit(EXIT_FAILURE);
	}

	FILE *fp = fopen(filename, "r");
	if (fp == NULL) {
		fprintf(stderr, "開啟檔案%s失敗!\n", filename);<pre name="code" class="cpp" style="color: rgb(51, 51, 51); font-size: 14px; line-height: 26px;"><pre name="code" class="cpp" style="color: rgb(51, 51, 51); font-size: 14px; line-height: 26px;"><span style="white-space:pre">		</span>curl_global_cleanup();
exit(EXIT_FAILURE);}// 獲取檔案大小fseek(fp, 0L, SEEK_END);int filesize = ftell(fp);rewind(fp);// 獲取easy handleCURL *easy_handle = curl_easy_init();if (easy_handle == NULL) {fprintf(stderr, "獲取easy_handle失敗!\n");fclose(fp);curl_global_cleanup();exit(EXIT_FAILURE);}// 設定的url代表遠端儲存檔案的名字curl_easy_setopt(easy_handle, CURLOPT_URL, url);// 上傳curl_easy_setopt(easy_handle, CURLOPT_UPLOAD, 1L);// 設定讀取回調函式curl_easy_setopt(easy_handle, CURLOPT_READFUNCTION, readfile);curl_easy_setopt(easy_handle, CURLOPT_READDATA, fp);// 設定檔案大小,確保協議知道檔案已經傳輸成功curl_easy_setopt(easy_handle, CURLOPT_INFILESIZE_LARGE, filesize);// 設定ftp的密碼,不設定也可以使用匿名使用者登陸,匿名使用者要求有寫許可權curl_easy_setopt(easy_handle, CURLOPT_USERPWD, "gwq5210:pwd");// 訪問代理伺服器的賬號密碼//curl_easy_setopt(easy_handle, CURLOPT_PROXYUSERPWD, "gwq5210:pwd");// 使用ssl時,提供一個私鑰用於資料安全通道//curl_easy_setopt(easy_handle, CURLOPT_KEYPASSWD, "keypassword");// 除錯//curl_easy_setopt(easy_handle, CURLOPT_VERBOSE, 1);//curl_easy_setopt(easy_handle, CURLOPT_HEADER, 1);printf("網址為:%s\n", url);printf("檔名為:%s\n", filename);printf("檔案大小為:%d\n", filesize);ret = curl_easy_perform(easy_handle);if (ret == CURLE_OK) {printf("上傳成功!\n");} else {printf("上傳失敗!\n");}fclose(fp);curl_easy_cleanup(easy_handle);curl_global_cleanup();return 0;}

   關於密碼

 客戶端向伺服器傳送請求時,許多協議都要求提供使用者名稱與密碼。libcurl提供了多種方式來設定它們。

    一些協議支援在URL中直接指定使用者名稱和密碼,類似於: protocol://user:[email protected]/path/。libcurl能正確的識別這種URL中的使用者名稱與密碼並執行相應的操作。如果你提供的使用者名稱和密碼中有特殊字元,首先應該對其進行URL編碼,類似%XX,其中XX是兩個十六進位制位。

    也可以通過CURLOPT_USERPWD屬性來設定使用者名稱與密碼。引數是格式如 “user:password ”的字串:

curl_easy_setopt(easy_handle, CURLOPT_USERPWD, "user_name:password");

    (下面這幾段文字我理解地模模糊糊)有時候在訪問代理伺服器的時候,可能時時要求提供使用者名稱和密碼進行使用者身份驗證。這種情況下,libcurl提供了另一個屬性CURLOPT_PROXYUSERPWD:

curl_easy_setopt(easy_handle, CURLOPT_PROXYUSERPWD, "user_name:password");

    在UNIX平臺下,訪問FTP的使用者名稱和密碼可能會被儲存在$HOME/.netrc檔案中。libcurl支援直接從這個檔案中獲取使用者名稱與密碼:

curl_easy_setopt(easy_handle, CURLOPT_NETRC, 1L);

    在使用SSL時,可能需要提供一個私鑰用於資料安全傳輸,通過CURLOPT_KEYPASSWD來設定私鑰:

curl_easy_setopt(easy_handle, CURLOPT_KEYPASSWD, "keypassword");
HTTP驗證

    上一章介紹瞭如何在libcurl中,對需要身份驗證的URL設定使用者名稱與密碼。在使用HTTP協議時,客戶端有很多種方式向伺服器提供驗證資訊。預設的HTTP驗證方法是"Basic”,它將使用者名稱與密碼以明文的方式、經Base64編碼後儲存在HTTP請求頭中,發往伺服器。當然這不太安全。

    當前版本的libcurl支援的驗證方法有:basic, Digest, NTLM, Negotiate, GSS-Negotiate and SPNEGO。(譯者感嘆:搞Web這麼多年,盡然不知道這些Http的驗證方式,實在慚愧。)可以通過CURLOPT_HTTPAUTH屬性來設定具體的驗證方式:

curl_easy_setopt(easy_handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);

    向代理伺服器傳送驗證資訊時,可以通過CURLOPT_PROXYAUTH設定驗證方式:

curl_easy_setopt(easy_handle, CURLOPT_PROXYAUTH, CURLAUTH_NTLM);

    也可以同時設定多種驗證方式(通過按位與), 使用‘CURLAUTH_ANY‘將允許libcurl可以選擇任何它所支援的驗證方式。通過CURLOPT_HTTPAUTH或CURLOPT_PROXYAUTH屬性設定的多種驗證方式,libcurl會在執行時選擇一種它認為是最好的方式與伺服器通訊:

curl_easy_setopt(easy_handle, CURLOPT_HTTPAUTH,  CURLAUTH_DIGEST|CURLAUTH_BASIC); 
// curl_easy_setopt(easy_handle, CURLOPT_HTTPAUTH,  CURLAUTH_ANY);

HTTP post

    這一章介紹如何使用libcurl以Post方式向HTTP伺服器提交資料。

    方法一,也是最簡單的方式,就像html中使用<form>標籤提交資料一樣,只需向libcurl提供一個包含資料的字串即可。下面是筆者學習過程中的一個demo程式:

/*************************************************************************
	> File Name: curl_post.c
	> Author: gwq
	> Mail: [email protected] 
	> Created Time: 2014年12月18日 星期四 19時44分36秒
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <curl/curl.h>

/*
 * http表單提交
 */
int main(int argc, char *argv[])
{
	CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
	if (ret != CURLE_OK) {
		fprintf(stderr, "初始化curl失敗!\n");
		exit(EXIT_FAILURE);
	}

	CURL *easy_handle = curl_easy_init();
	if (easy_handle == NULL) {
		fprintf(stderr, "獲取easy_handle失敗!\n");
		curl_global_cleanup();
		exit(EXIT_FAILURE);
	}

	// 設定網址,這個url是form表單action指向的網址
	curl_easy_setopt(easy_handle, CURLOPT_URL, "http://gwq:8080/Demo/checklogin.jsp");
	// 設定post的引數
	curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, "username=gwq5210&passwd=123456789");

	// 執行post
	ret = curl_easy_perform(easy_handle);
	if (ret != CURLE_OK) {
		printf("提交資料失敗!\n");
	} else {
		printf("提交資料成功!\n");
	}

	// 釋放資源
	curl_easy_cleanup(easy_handle);
	curl_global_cleanup();

	return 0;
}

    上面的程式碼夠簡單吧~_~ 有時候,我們需要提交一些二進位制資料到HTTP伺服器,使用方法一就不行了,因為方法一中實際提交的是一個字串,字串遇到/0就表示結束了。所以在上傳二進位制資料的時候,必須明確的告訴libcurl要提交的資料的長度。在上傳二進位制資料的時候,還應該設定提交的Content-Type頭資訊。下面的示例程式碼:

/*************************************************************************
	> File Name: curl_upload_binary.c
	> Author: gwq
	> Mail: [email protected] 
	> Created Time: 2014年12月19日 星期五 10時29分22秒
 ************************************************************************/

#include <stdio.h>
#include <error.h>
#include <stdlib.h>
#include <curl/curl.h>

int main(int argc, char **argv)
{
	CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
	if (ret != CURLE_OK) {
		error(EXIT_FAILURE, 0, "初始化curl失敗!\n");
	}
	CURL *easy_handle = curl_easy_init();
	if (easy_handle == NULL) {
		curl_global_cleanup();
		error(EXIT_FAILURE, 0, "獲取easy_handle失敗!\n");
	}

	// 上傳二進位制資料
	char data[] = {1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0};

	// 設定訊息頭
	struct curl_slist *http_headers = NULL;
	http_headers = curl_slist_append(http_headers, "Content-Type: text/xml");

	curl_easy_setopt(easy_handle, CURLOPT_HTTPHEADER, http_headers);
	curl_easy_setopt(easy_handle, CURLOPT_URL, "http://localhost:8080/Demo/upload.jsp");
	curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, data);
	// 設定上傳引數大小
	curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDSIZE, sizeof(data));
	// 除錯
	curl_easy_setopt(easy_handle, CURLOPT_VERBOSE, sizeof(data));

	ret = curl_easy_perform(easy_handle);
	if (ret != CURLE_OK) {
		printf("上傳失敗!\n");
	} else {
		printf("上傳成功!\n");
	}

	// 釋放資源
	curl_slist_free_all(http_headers);
	curl_easy_cleanup(easy_handle);
	curl_global_cleanup();
	return 0;
}

     上面介紹的兩種方式,可以完成大部分的HTTP POST操作。但上面的兩種方式都不支援multi-part formposts。Multi-part formposts被認為是提交二進位制資料(或大量資料)的更好方法,可以在RFC1867, RFC2388中找到他們的定義。何為Multi-part?其實,就我理解,就是在Post提交的時候,有不同的資料單元,每個單元有自己的名稱與內容,內容可以是文字的,也可以是二進位制的。同時,每個資料單元都可以有自己的訊息頭,MIME型別,這些資料單元組成一個連結串列,提交到HTTP伺服器。libcurl提供了方便的api用於支援multi-part formposts。使用curl_formadd函式,可以新增不同的資料資料單元,然後提交到伺服器。下面是一個multi-part formposts的例子(請參考:http://curl.haxx.se/libcurl/c/curl_formadd.html ):

/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) 1998 - 2011, Daniel Stenberg, <[email protected]>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at http://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/
/* Example code that uploads a file name 'foo' to a remote script that accepts
 * "HTML form based" (as described in RFC1738) uploads using HTTP POST.
 *
 * The imaginary form we'll fill in looks like:
 *
 * <form method="post" enctype="multipart/form-data" action="examplepost.cgi">
 * Enter file: <input type="file" name="sendfile" size="40">
 * Enter file name: <input type="text" name="filename" size="30">
 * <input type="submit" value="send" name="submit">
 * </form>
 *
 * This exact source code has not been verified to work.
 */

#include <stdio.h>
#include <string.h>

#include <curl/curl.h>

int main(int argc, char *argv[])
{
	CURL *curl;
	CURLcode res;

	struct curl_httppost *formpost = NULL;
	struct curl_httppost *lastptr = NULL;
	struct curl_slist *headerlist = NULL;
	static const char buf[] = "Expect:";

	curl_global_init(CURL_GLOBAL_ALL);

	/* Fill in the file upload field */
	curl_formadd(&formpost,
		     &lastptr,
		     CURLFORM_COPYNAME, "sendfile",
		     CURLFORM_FILE, "postit2.c", CURLFORM_END);

	/* Fill in the filename field */
	curl_formadd(&formpost,
		     &lastptr,
		     CURLFORM_COPYNAME, "filename",
		     CURLFORM_COPYCONTENTS, "postit2.c", CURLFORM_END);

	/* Fill in the submit field too, even if this is rarely needed */
	curl_formadd(&formpost,
		     &lastptr,
		     CURLFORM_COPYNAME, "submit",
		     CURLFORM_COPYCONTENTS, "send", CURLFORM_END);

	curl = curl_easy_init();
	/* initalize custom header list (stating that Expect: 100-continue is not
	   wanted */
	headerlist = curl_slist_append(headerlist, buf);
	if (curl) {
		/* what URL that receives this POST */
		curl_easy_setopt(curl, CURLOPT_URL,
				 "http://example.com/examplepost.cgi");
		if ((argc == 2) && (!strcmp(argv[1], "noexpectheader")))
			/* only disable 100-continue header if explicitly requested */
			curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
		curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);

		/* Perform the request, res will get the return code */
		res = curl_easy_perform(curl);
		/* Check for errors */
		if (res != CURLE_OK)
			fprintf(stderr, "curl_easy_perform() failed: %s\n",
				curl_easy_strerror(res));

		/* always cleanup */
		curl_easy_cleanup(curl);

		/* then cleanup the formpost chain */
		curl_formfree(formpost);
		/* free slist */
		curl_slist_free_all(headerlist);
	}
	return 0;
}

     最後要說明的是,所有在easy handle上設定的屬性都是”sticky”的,什麼意思?就是說在easy handle上設定的屬性都將被儲存,即使執行完curl_easy_perform之後,這些屬性值仍然存在。通過將CURLOPT_HTTPGET設為1可以使easy handle回到最原始的狀態:

curl_easy_setopt(easy_handle, CURLOPT_HTTPGET, 1L);

   顯示進度

 libcurl支援通訊過程中的進度控制。通過將CURLOPT_NOPROCESS設定為0開啟進度支援。該選項預設值為1。對大多數應用程式,我們需要提供一個進度顯示回撥。libcurl會不定期的將當前傳輸的進度通過回撥函式告訴你的程式。回撥函式的原型如下:

int progress_callback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow);

    通過CURLOPT_PROGRESSFUNCTION註冊該回調函式。引數clientp是一個使用者自定義指標,應用程式通過CURLOPT_PROCESSDATA屬性將該自定義指定傳遞給libcurl。libcurl對該引數不作任何處理,只是簡單將其傳遞給回撥函式。

在c++中使用libcurl

    在C++中使用libcurl跟在C語言中沒有任何區別,只有一個地方要注意:回撥函式不能是類的非靜態成員函式。例如:

class AClass {
     static size_t write_data(void *ptr, size_t size, size_t nmemb, void *ourpointer)
     { 
       /* do what you want with the data */
     }
}
代理

    什麼是代理?Merrian-Webster的解釋是:一個通過驗證的使用者扮演另一個使用者。今天,代理已經被廣泛的使用。許多公司提供網路代理伺服器,允許員工的網路客戶端訪問、下載檔案。代理伺服器處理這些使用者的請求。

    libcurl支援SOCKS和HTTP代理。使用代理,libcurl會把使用者輸入的URL提交給代理伺服器,而不是直接根據URL去訪問遠端資源。

    當前版本的libcurl並不支援SOCKS代理的所有功能。

    對於HTTP代理來說,即使請求的URL不是一個合法的HTTP URL(比方你提供了一個ftp的url),它仍然會先被提交到HTTP代理。

代理選項

    CURLOPT_PROXY屬性用於設定libcurl使用的代理伺服器地址:

curl_easy_setopt(easy_handle, CURLOPT_PROXY, "proxy-host.com:8080");

    可以把主機名與埠號分開設定:

curl_easy_setopt(easy_handle, CURLOPT_PROXY, "proxy-host.com");
curl_easy_setopt(easy_handle, CURLOPT_PROXYPORT, 8080);

    有些代理伺服器要求使用者通過驗證之後才允許接受其請求,此時應該先提供驗證資訊:

curl_easy_setopt(easy_handle, CURLOPT_PROXYUSERPWD, "user:password");

    還要告訴libcurl使用的代理型別(如果沒有提供,libcurl會認為是HTTP代理):

curl_easy_setopt(easy_handle, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4);

    環境變數

 對於有些協議,libcurl會自動檢測並使用一些環境變數,並根據這些環境變數來確定要使用的代理伺服器。這些環境變數的名稱格式一般是"[protocol]_proxy"(注意小寫)。例如輸入一個HTTP的URL,那麼名稱為"http_proxy"的環境變數就會被檢測是否存在,如果存在,libcurl會使用該環境變數指定的代理。相同的規則也適用於FTP。

    這些環境變數的值的格式必須是這樣的:"[protocol://][user:[email protected]]machine[:port]"。libcurl會忽略掉[protocol://],如果沒有提供埠號,libcurl使用該協議的預設埠。 

    有兩個比較特殊的環境變數:'all_proxy'與'no_proxy'。如果一個URL所對應的協議,它的環境變數沒有設定,那麼'all_proxy'指定的代理將被使用。'no_proxy'則指定了一個不應被使用的代理主機的列表。例如:no_proxy的值是'192.168.1.10',即使存在http_proxy,它的值也是'192.168.1.10','192.168.1.10'也不會被作為代理。no_proxy=”*”表示不允許使用任何代理。

    顯式地將CURLOPT_PROXY屬性設定為空,可以禁止libcurl檢查並使用環境變數來使用代理。

SSL和代理

    SSL為點到點通訊提供安全保障。它包含一些強壯的加密措施和其他安全檢測,這使得上面講到的代理方式不適用於SSL。除非代理伺服器提供專用通道,對進出該代理伺服器的資料不作任何檢測或禁止。通過HTTP代理伺服器開啟SSL連線,意味著代理伺服器要直接連線到目標主機的指定埠。因為代理伺服器對在專用通道上傳輸的資料的型別毫無所知,所以它往往會使某些機制失效,如快取機制。許多組織只允許在443埠上建立這種型別的資料通道。

代理通道(Tunneling Through Proxy)

    正如上面講到的,要使SSL工作必須在代理伺服器建立專用資料通道,通常專用通道只被限制應用於HTTPS。通過HTTP代理在應用程式與目標之間建立一個專用資料通道,應該預防在該專有通道上執行非HTTP的操作,如進行FTP上傳或執行FTP命令。代理伺服器管理員應該禁止非法的操作。

    通過CURLOPT_HTTPPROXYTUNNEL屬性來告訴libcurl使用代理通道:

curl_easy_setopt(easy_handle, CURLOPT_HTTPPROXYTUNNEL, 1L);

     有時候你想通過代理通道執行平常的HTTP操作,而實際上卻可能使你不經過代理伺服器而直接與遠端主機進行互動。libcurl不會代替這種新引入的行為。

自動配置代理

    許多瀏覽器支援自動配置代理,例如NetScape。libcurl並不支援這些。

持久化的好處(Persistence Is The Way to Happiness)

    當需要傳送多次請求時,應該重複使用easy handle。

    每次執行完curl_easy_perform,licurl會繼續保持與伺服器的連線。接下來的請求可以使用這個連線而不必建立新的連線(如果目標主機是同一個的話)。這樣可以減少網路開銷。 
    即使連線被釋放了,libcurl也會快取這些連線的會話資訊,這樣下次再連線到目標主機上時,就可以使用這些資訊,從而減少重新連線所需的時間。

    FTP連線可能會被儲存較長的時間。因為客戶端要與FTP伺服器進行頻繁的命令互動。對於有訪問人數上限的FTP伺服器,保持一個長連線,可以使你不需要排除等待,就直接可以與FTP伺服器通訊。

    libcurl會快取DNS的解析結果。

    在今後的libcurl版本中,還會新增一些特性來提高資料通訊的效率。 
    每個easy handle都會儲存最近使用的幾個連線,以備重用。預設是5個。可以通過CURLOPT_MAXCONNECTS屬性來設定儲存連線的數量。

    如果你不想重用連線,將CURLOPT_FRESH_CONNECT屬性設定為1。這樣每次提交請求時,libcurl都會先關閉以前建立的連線,然後重新建立一個新的連線。也可以將CURLOPT_FORBID_REUSE設定為1,這樣每次執行完請求,連線就會馬上關閉。

libcurl使用的HTTP訊息頭

     當使用libcurl傳送http請求時,它會自動新增一些http頭。我們可以通過CURLOPT_HTTPHEADER屬性手動替換、新增或刪除相應的HTTP訊息頭。

Host

    http1.1(大部分http1.0)版本都要求客戶端請求提供這個資訊頭。

Pragma

    "no-cache"。表示不要緩衝資料。

Accept

    "*/*"。表示允許接收任何型別的資料。

Except

    以POST的方式向HTTP伺服器提交請求時,libcurl會設定該訊息頭為"100-continue",它要求伺服器在正式處理該請求之前,返回一個"OK"訊息。如果POST的資料很小,libcurl可能不會設定該訊息頭。

自定義選項

    當前越來越多的協議都構建在HTTP協議之上(如:soap),這主要歸功於HTTP的可靠性,以及被廣泛使用的代理支援(可以穿透大部分防火牆)。 這些協議的使用方式與傳統HTTP可能有很大的不同。對此,libcurl作了很好的支援。

自定義請求方式(CustomRequest)

    HTTP支援GET, HEAD或者POST提交請求。可以設定CURLOPT_CUSTOMREQUEST來設定自定義的請求方式,libcurl預設以GET方式提交請求:

curl_easy_setopt(easy_handle, CURLOPT_CUSTOMREQUEST, "MYOWNREQUEST");
修改訊息頭

    HTTP協議提供了訊息頭,請求訊息頭用於告訴伺服器如何處理請求;響應訊息頭則告訴瀏覽器如何處理接收到的資料。在libcurl中,你可以自由的新增這些訊息頭:

struct curl_slist *headers=NULL; /* init to NULL is important */
headers = curl_slist_append(headers, "Hey-server-hey: how are you?");
headers = curl_slist_append(headers, "X-silly-content: yes");
/* pass our list of custom made headers */
curl_easy_setopt(easyhandle, CURLOPT_HTTPHEADER, headers);
curl_easy_perform(easyhandle); /* transfer http */
curl_slist_free_all(headers); /* free the header list */

    對於已經存在的訊息頭,可以重新設定它的值:

headers = curl_slist_append(headers, "Accept: Agent-007");
headers = curl_slist_append(headers, "Host: munged.host.line");
刪除訊息頭

    對於一個已經存在的訊息頭,設定它的內容為空,libcurl在傳送請求時就不會同時提交該訊息頭:

headers = curl_slist_append(headers, "Accept:");
強制分塊傳輸(Enforcing chunked transfer-encoding)

    (這段文字理解可能有誤碼)以非GET的方式提交HTTP請求時,如果設定了自定義的訊息頭”Transfer-Encoding:chunked”,libcurl會分塊提交資料,即使要上傳的資料量已經知道。在上傳資料大小未知的情況下,libcurl自動採用分塊上傳資料。(譯者注:非GET方式提交請求,提交的資料量往往比較大。)

HTTP版本

    每一次http請求,都包含一個表示當前使用http版本的訊息頭。libcurl預設使用HTTP 1.1。可以通過CURLOPT_HTTP_VERSION屬性來設定具體的版本號:

curl_easy_setopt(easy_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);

FTP自定義命令

    並不是所以的協議都像HTTP那樣,通過訊息頭來告訴伺服器如何處理請求。對於FTP,你就要使用另外的方式來處理。

    傳送自定義的命令到ftp伺服器,意味著你傳送的命令必須是能被ftp伺服器理解的命令(FTP協議中定義的命令,參考rfc959)。

    下面是一個簡單的例子,在檔案傳輸操作操作之前刪除指定檔案:

headers = curl_slist_append(headers, "DELE file-to-remove");
/* pass the list of custom commands to the handle */

curl_easy_setopt(easyhandle, CURLOPT_QUOTE, headers);
// curl_easy_setopt(easyhandle, CURLOPT_POSTQUOTE, headers); // 在資料傳輸之後操行刪除操作
curl_easy_perform(easyhandle); /* transfer ftp data! */
curl_slist_free_all(headers); /* free the header list */

    FTP伺服器執行命令的順序,同這些命令被新增到列表中順序是一致的。發往伺服器的命令列表中,只要有一個命令執行失敗,ftp伺服器就會返回一個錯誤程式碼,此時libcurl將直接返回CURLE_QUOTE_ERROR,不再執行剩餘的FTP命令。

    將CURLOPT_HEADER設定為1,libcurl獲取目標檔案的資訊,並以HTTP訊息頭的樣式來輸出訊息頭。

FTP自定義CUSTOMREQUEST

    使用CURLOPT_CUSTOMREQUEST屬性,可以向FTP伺服器傳送命令。"NLST"是ftp預設的列出檔案列表的命令。 下面的程式碼用於列出FTP伺服器上的檔案列表:

/*************************************************************************
	> File Name: curl_ftp_list.c
	> Author: gwq
	> Mail: [email protected] 
	> Created Time: 2014年12月19日 星期五 12時27分00秒
 ************************************************************************/

#include <stdio.h>
#include <error.h>
#include <stdlib.h>
#include <curl/curl.h>

int main(int argc, char **argv)
{
	CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
	if (ret != CURLE_OK) {
		error(EXIT_FAILURE, 0, "初始化curl失敗!\n");
	}

	CURL *easy_handle = curl_easy_init();
	if (easy_handle == NULL) {
		curl_global_cleanup();
		error(EXIT_FAILURE, 0, "獲取easy_handle失敗!\n");
	}

	curl_easy_setopt(easy_handle, CURLOPT_URL, "ftp://localhost/");
	curl_easy_setopt(easy_handle, CURLOPT_USERPWD, "gwq5210:1234567890");
	curl_easy_setopt(easy_handle, CURLOPT_CUSTOMREQUEST, "NLST");

	ret = curl_easy_perform(easy_handle);
	if (ret != CURLE_OK) {
		curl_easy_cleanup(easy_handle);
		curl_global_cleanup();
		error(EXIT_FAILURE, 0, "初始化curl失敗!\n");
	}

	curl_easy_cleanup(easy_handle);
	curl_global_cleanup();

	return 0;
}
Cookies Without Chocolate Chips

     cookie是一個鍵值對的集合,HTTP伺服器發給客戶端的cookie,客戶端提交請求的時候,也會將cookie傳送到伺服器。伺服器可以根據cookie來跟蹤使用者的會話資訊。cookie有過期時間,超時後cookie就會失效。cookie有域名和路徑限制,cookie只能發給指定域名和路徑的HTTP伺服器。

    cookie以訊息頭”Set-Cookie”的形式從HTTP伺服器傳送到客戶端;客戶端發以訊息頭”Cookie”的形式將Cookie提交到HTTP伺服器。為了對這些東西有個直觀的概念,下圖是FireFox中,使用Firebug跟蹤到的cookie訊息頭: 
 pic1

    在libcurl中,可以通過CURLOPT_COOKIE屬性來設定發往伺服器的cookie:

curl_easy_setopt(easy_handle, CURLOPT_COOKIE, "name1=var1; name2=var2;");

    下面的例子演示瞭如何使用libcurl傳送coo