API使用部分的介紹在末尾,文章前面會有比較長的基礎(chǔ)簡介,視自己的技術(shù)水平可看可不看,豐儉由人~
HTTP協(xié)議與TCP/IP協(xié)議族的關(guān)系
當(dāng)我們談到TCP/IP時(shí),會有兩個含義,一個是TCP協(xié)議和IP協(xié)議本身,另一個含義是指TCP/IP協(xié)議族。 TCP/IP協(xié)議族是互聯(lián)網(wǎng)相關(guān)的各種協(xié)議集合的總稱。TCP/IP協(xié)議族將協(xié)議層次分為四層,而OSI標(biāo)準(zhǔn)則分為七層。一般來說,TCP/IP協(xié)議是實(shí)際的標(biāo)準(zhǔn),所以一般理解TCP/IP的四層協(xié)議模型即可。從上到下,TCP/IP協(xié)議族分層具體為:
- 應(yīng)用層。應(yīng)用層決定了向用戶提供服務(wù)時(shí)的通信的活動,是最接近用戶的層次。這個層次包括用于文件傳輸?shù)腇TP(File Transfer Protocol,文件傳輸協(xié)議),用于解析域名,獲取IP地址的DNS(Domain Name System,域名系統(tǒng))等,以及包括本文主題HTTP協(xié)議。
- 傳輸層。傳輸層提供處于網(wǎng)絡(luò)連接中的兩臺計(jì)算機(jī)之間的數(shù)據(jù)傳輸,為應(yīng)用層提供服務(wù)。具體包括TCP(Transmission Control,傳輸控制協(xié)議)和UDP(User Data Protocol,用戶數(shù)據(jù)報(bào)協(xié)議)。HTTP是基于TCP協(xié)議的應(yīng)用層協(xié)議。
- 網(wǎng)絡(luò)層(網(wǎng)絡(luò)互聯(lián)層)。網(wǎng)絡(luò)層協(xié)議一般特指IP協(xié)議(Internet Protocol),主要作用是規(guī)定如何把傳輸層的數(shù)據(jù)包傳送到目標(biāo)機(jī)器上。
- 鏈路層。規(guī)定處理網(wǎng)絡(luò)硬件,網(wǎng)絡(luò)相關(guān)的設(shè)備驅(qū)動,操作系統(tǒng)相關(guān)的內(nèi)容。
在HTTP協(xié)議通信過程中,HTTP依賴的底層通信協(xié)議會加上各種首部,這些首部包含了當(dāng)前協(xié)議的內(nèi)容和信息,而目標(biāo)機(jī)器則會解析這些首部,達(dá)到互相通信的過程。

TCP協(xié)議和HTTP協(xié)議關(guān)系密切,是HTTP協(xié)議使用的傳輸層協(xié)議。TCP協(xié)議提供字節(jié)流服務(wù)(Byte Stream Service),即為了方便傳輸,將大塊數(shù)據(jù)分割為以報(bào)文段(segment)為單位的數(shù)據(jù)包進(jìn)行管理。另一方面,TCP協(xié)議使用了三次握手和四次揮手的機(jī)制,并且把數(shù)據(jù)包發(fā)送出去之后,服務(wù)端和客戶端都會向?qū)Ψ酱_認(rèn)是否成功送達(dá),這保證了TCP提供可靠的傳輸服務(wù)。由于HTTP協(xié)議基于TCP協(xié)議的基礎(chǔ)上構(gòu)建,因此也集成了TCP的一部分特性,包括傳輸服務(wù)可靠,但傳輸性能相對基于UDP協(xié)議的應(yīng)用層協(xié)議可能要差一些,等。另外,和TCP協(xié)議一致,HTTP協(xié)議的通信雙方必定有一方是客戶端,另一方是服務(wù)器。
HTTP協(xié)議通信的基本過程
客戶端向服務(wù)器發(fā)送請求報(bào)文,發(fā)起HTTP請求;服務(wù)端向客戶端發(fā)送響應(yīng)報(bào)文,進(jìn)行HTTP響應(yīng)。請求報(bào)文由請求方法、請求URI、協(xié)議版本、可選的請求首部字段和內(nèi)容實(shí)體構(gòu)成;響應(yīng)報(bào)文由協(xié)議版本、狀態(tài)碼(表示請求成功或失敗的數(shù)字代碼)、用以解釋狀態(tài)碼的原因短語、可選的響應(yīng)首部字段以及實(shí)體主體構(gòu)成。


請求報(bào)文中提到的方法,代表了客戶端需要從服務(wù)端獲得的服務(wù)類型。常用方法包括:
- GET方法用來請求訪問已被URI識別的資源。指定的資源經(jīng)服務(wù)器端解析后返回響應(yīng)內(nèi)容。
- POST方法用于向服務(wù)端傳輸數(shù)據(jù),并且可以獲取服務(wù)端的響應(yīng)及資源。雖然GET方法可以向URI中加入?yún)?shù)的方式傳輸簡單數(shù)據(jù),但傳輸文件等大塊數(shù)據(jù),還是需要使用POST方法。
- HEAD方法和GET方法一樣,只是不返回報(bào)文主體部分。用于確認(rèn)URI的有效性及資源更新的日期時(shí)間等。
- OPTIONS方法來查詢針對請求URI指定的資源支持的方法。
- CONNECT方法要求在與代理服務(wù)器通信時(shí)建立隧道,實(shí)現(xiàn)用隧道協(xié)議進(jìn)行TCP通信。主要使用SSL(Secure Sockets Layer,安全套接層)和TLS(Transport Layer Security,傳輸層安全)協(xié)議把通信內(nèi)容加密后經(jīng)網(wǎng)絡(luò)隧道傳輸。
HTTP首部字段是構(gòu)成HTTP報(bào)文的要素之一。在客戶端與服務(wù)器之間以HTTP協(xié)議進(jìn)行通信的過程中,無論是請求還是響應(yīng)都會使用首部字段,它能起到傳遞額外重要信息的作用。使用首部字段是為了給瀏覽器和服務(wù)器提供報(bào)文主體大小、所使用的語言、認(rèn)證信息等內(nèi)容。
需要注意,HTTP是無狀態(tài)(stateless)協(xié)議,即不會對請求方和響應(yīng)方的通信狀態(tài)進(jìn)行保存。由于一些web應(yīng)用需要保留穩(wěn)定的通信狀態(tài)信息(比如登陸狀態(tài)),于是有了一些基于HTTP的擴(kuò)展機(jī)制,包括Cookie和Session機(jī)制等。Cookie會根據(jù)從服務(wù)器端發(fā)送的響應(yīng)報(bào)文內(nèi)的一個叫做Set-Cookie的首部字段信息,通知客戶端保存Cookie。當(dāng)下次客戶端再往該服務(wù)器發(fā)送請求時(shí),客戶端會自動在請求報(bào)文中加入Cookie值后發(fā)送出去;而Session機(jī)制則是由服務(wù)端維護(hù)的一個對話狀態(tài),客戶端需要在Cookie中發(fā)送SessionId來使用Session機(jī)制。
HTTP相關(guān)的Web技術(shù)
HTTP/1.1規(guī)范允許HTTP服務(wù)器搭建多個Web站點(diǎn),使用虛擬主機(jī)(Virtual Host)的功能即可實(shí)現(xiàn)。服務(wù)器可以托管多個域名,客戶端請求服務(wù)器托管的多個域名都會通過DNS服務(wù)映射到服務(wù)器的IP地址。
HTTP通信時(shí),除了客戶端和服務(wù)器外,還有一些用于通信數(shù)據(jù)轉(zhuǎn)發(fā)的應(yīng)用程序,包括代理,網(wǎng)關(guān)和隧道等。
- 代理服務(wù)器的基本行為就是接收客戶端發(fā)送的請求后轉(zhuǎn)發(fā)給其他服務(wù)器。持有資源實(shí)體的服務(wù)器被稱為源服務(wù)器。從源服務(wù)器返回的響應(yīng)經(jīng)過代理服務(wù)器后再傳給客戶端。使用代理服務(wù)器的理由有:利用緩存技術(shù)減少網(wǎng)絡(luò)帶寬的流量,組織內(nèi)部針對特定網(wǎng)站的訪問控制,以獲取訪問日志為主要目的等。
- 網(wǎng)關(guān)的工作機(jī)制和代理十分相似。而網(wǎng)關(guān)能使通信線路上的服務(wù)器提供非HTTP協(xié)議服務(wù)。利用網(wǎng)關(guān)能提高通信的安全性,因?yàn)榭梢栽诳蛻舳伺c網(wǎng)關(guān)之間的通信線路上加密以確保連接的安全。
- 隧道可按要求建立起一條與其他服務(wù)器的通信線路,屆時(shí)使用SSL等加密手段進(jìn)行通信。隧道的目的是確??蛻舳四芘c服務(wù)器進(jìn)行安全的通信。
HTTPS與HTTP相關(guān)的安全技術(shù)
在安全層面上,HTTP的不足包括:通信使用明文(不加密) 內(nèi)容可能會被竊聽;不驗(yàn)證通信方的身份,因此有可能遭遇偽裝;無法證明報(bào)文的完整性,所以有可能已遭篡改。
HTTP協(xié)議中沒有加密機(jī)制,但可以通過和SSL(Secure Socket Layer,安全套接層)或TLS(Transport Layer Security,安全層傳輸協(xié)議)的組合使用,加密通信內(nèi)容。與SSL組合使用的HTTP被稱為HTTPS(HTTP Secure,超文本傳輸安全協(xié)議)或HTTP over SSL。SSL采用一種叫做公開密鑰加密(Public-key cryptography)的加密處理方式。公開密鑰加密使用一對非對稱的密鑰。一把叫做私有密鑰(private key),另一把叫做公開密鑰(public key)。顧名思義,私有密鑰不能讓其他任何人知道,而公開密鑰則可以隨意發(fā)布,任何人都可以獲得。使用公開密鑰加密方式,發(fā)送密文的一方使用對方的公開密鑰進(jìn)行加密處理,對方收到被加密的信息后,再使用自己的私有密鑰進(jìn)行解密。利用這種方式,不需要發(fā)送用來解密的私有密鑰,也不必?fù)?dān)心密鑰被攻擊者竊聽而盜走。另外,為了保證公開密鑰不會被在傳輸過程中被篡改,可以使用由數(shù)字證書認(rèn)證機(jī)構(gòu)(CA,Certificate Authority)和其相關(guān)機(jī)關(guān)頒發(fā)的公開密鑰證書。客戶端使用對應(yīng)的算法去驗(yàn)證公開密鑰證書上的數(shù)字簽名,驗(yàn)證通過后,即可確認(rèn)服務(wù)器發(fā)送的公開密鑰是可信賴的。

雖然使用HTTP協(xié)議無法確定通信方,但如果使用SSL則可以。SSL不僅提供加密處理,而且還使用了一種被稱為證書的手段,可用于確定方。證書由值得信任的第三方機(jī)構(gòu)頒發(fā),用以證明服務(wù)器和客戶端是實(shí)際存在的。另外,偽造證書從技術(shù)角度來說是異常困難的一件事。所以只要能夠確認(rèn)通信方(服務(wù)器或客戶端)持有的證書,即可判斷通信方的真實(shí)意圖。
由于HTTP協(xié)議無法證通信的報(bào)文的完整性,因此,在請求或響應(yīng)送出之后直到對方接收之前的這段時(shí)間 ,即請求或響應(yīng)的內(nèi)容遭到篡改,也沒有辦法獲悉。請求或響應(yīng)在傳輸途中,遭攻擊者攔截并篡改內(nèi)容的攻擊稱為中間人攻擊。HTTPS通信過程中,發(fā)送數(shù)據(jù)時(shí)會附加一種叫做MAC(Message Authentication Code)的報(bào)文摘要。MAC能夠查知報(bào)文是否遭到篡改,從而保護(hù)報(bào)文的完整性。
HTTPS也存在一些問題,那就是當(dāng)使用SSL時(shí),它的處理速度會變慢。SSL的慢分兩點(diǎn)。一種是指通信慢。和使用HTTP相比,網(wǎng)絡(luò)負(fù)載可能會變慢2到100倍。除去和TCP連接、發(fā)送HTTP請求,響應(yīng)以外,還必須進(jìn)行SSL通信,因此整體上處理通信量不可避免會增加。另一種是指由于大量消耗CPU及內(nèi)存等資源,導(dǎo)致處理速度變慢。使用SSL讓服務(wù)器和客戶端都需要進(jìn)行加密和解密的運(yùn)算處理。因此從結(jié)果上講,比起HTTP會更多地消耗服務(wù)器和客戶端的硬件資源,導(dǎo)致負(fù)載增強(qiáng)。此外,要進(jìn)行HTTPS通信,證書是必不可少的。而使用的證書必須向認(rèn)證機(jī)構(gòu)(CA)購買。因此,依舊有部分安全性要求不高的個人網(wǎng)站,使用HTTP的通信方式。
HTTP/HTTPS的身份認(rèn)證機(jī)制
HTTP的認(rèn)證方式主要有:BASIC認(rèn)證(基本認(rèn)證)、DIGEST認(rèn)證(摘要認(rèn)證)、SSL客戶端認(rèn)證,F(xiàn)ormBase認(rèn)證(基于表單認(rèn)證)等。BASIC認(rèn)證和DIGEST認(rèn)證的安全性不高,一般不會在Web應(yīng)用中被采用;SSL客戶端認(rèn)證需要第三方機(jī)構(gòu)發(fā)放數(shù)字證書,有一定成本,使用也比較少?,F(xiàn)有的Web應(yīng)用大部分使用的是各自實(shí)現(xiàn)的表單認(rèn)證。
基于表單的認(rèn)證方法并不是在HTTP協(xié)議中定義的??蛻舳藭蚍?wù)器上的Web應(yīng)用程序發(fā)送登錄信息(Credential),按登錄信息的驗(yàn)證結(jié)果認(rèn)證。所以表單認(rèn)證的安全性主要看服務(wù)端開發(fā)者做的是否足夠完善?;诒韱握J(rèn)證的標(biāo)準(zhǔn)規(guī)范尚未有定論,一般會使用Cookie來管理Session(會話)的方式來實(shí)現(xiàn)。Session相關(guān)的用戶認(rèn)證信息是保存在服務(wù)端的,所以通過客戶端攻擊的風(fēng)險(xiǎn)大大降低。另外,為減輕跨站腳本攻擊(XSS)造成的損失,一般會在Cookie內(nèi)加上httponly屬性,禁止JS腳本訪問Cookie。至于密碼的傳輸和保存過程如何保證其安全性,一般是通過加鹽(salt)的方式實(shí)現(xiàn)的。

salt其實(shí)就是由服務(wù)器隨機(jī)生成的一個字符串,但是要保證長度足夠長,并且是真正隨機(jī)生成的。然后把它和密碼字符串相連接(前后都可以)生成散列值。當(dāng)兩個用戶使用了同一個密碼時(shí),由于隨機(jī)生成的salt值不同,對應(yīng)的散列值也將是不同的。這樣一來,很大程度上減少了密碼特征,攻擊者也就很難利用自己手中的密碼特征庫進(jìn)行破解。
WinHTTP的簡單使用
HTTP/HTTPS相關(guān)的C++網(wǎng)絡(luò)庫有很多,可以使用libcurl、Boost、Qt等提供了HTTP/S通信機(jī)制的第三方庫(似乎C++新標(biāo)準(zhǔn)中即將加入網(wǎng)絡(luò)設(shè)施,敬請期待)。但對于沒有跨平臺需求的Windows C++應(yīng)用程序來說,WinHTTP不管是打包后的可執(zhí)行文件大小,API獲取和使用上的便捷性是最好的。尤其是追求可執(zhí)行文件大小盡量少的場景下,個人測試引入WinHTTP基本只會增加幾十kb的可執(zhí)行文件大小,相比libcurl增加幾百kb大小優(yōu)勢很足。Qt就更不提了,打包大小一直是被詬病的。
以下是WinHTTP API的基本使用流程。至于里面的接口,結(jié)合文章前面的基礎(chǔ)知識一一簡單介紹下。

WinHttpOpen、WinHttpConnect:初始化 WinHTTP 函數(shù)的使用并返回 WinHTTP 會話句柄。這里的會話和前文說到的服務(wù)端維護(hù)的會話不是一回事,個人理解是類似于Socket編程中返回的一個套接字描述符,后續(xù)代碼利用這個描述符來進(jìn)行網(wǎng)絡(luò)編程。在與服務(wù)器交互之前,必須通過調(diào)用WinHttpOpen來初始化 WinHTTP 。WinHttpOpen創(chuàng)建一個會話上下文來維護(hù)有關(guān) HTTP 會話的詳細(xì)信息,并返回一個會話句柄。使用此句柄,WinHttpConnect函數(shù)可以指定目標(biāo) HTTP 或安全超文本傳輸協(xié)議 (HTTPS) 服務(wù)器。
WinHttpOpenRequest、WinHttpSendRequest:WinHttpOpenRequest函數(shù)打開一個特定資源的 HTTP 請求并返回一個HINTERNET句柄,其他 HTTP 函數(shù)可以使用該句柄。WinHttpOpenRequest在調(diào)用時(shí)不會向服務(wù)器發(fā)送請求。WinHttpSendRequest函數(shù)實(shí)際上通過網(wǎng)絡(luò)建立連接并發(fā)送請求。
WinHttpSetCredentials:用于設(shè)置身份驗(yàn)證相關(guān)的信息,可選。
WinHttpAddRequestHeaders:添加HTTP請求首部信息。不過WinHttpSendRequest有一個參數(shù)可以指定首部,此函數(shù)看情況可以不用。
WinHttpWriteData:用于向服務(wù)器發(fā)布數(shù)據(jù),只用于POST方法和PUT方法,GET方法不需要調(diào)用。
WinHttpReceiveResponse:函數(shù)等待接收對WinHttpSendRequest發(fā)起的 HTTP 請求的響應(yīng)。如果HTTP設(shè)置為異步,則不需要調(diào)用。
WinHttpSetStatusCallback:若異步,則安裝回調(diào)函數(shù),否則調(diào)用WinHttpReceiveResponse,阻塞等待網(wǎng)絡(luò)響應(yīng)。
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
LPSTR pszOutBuffer;
BOOL bResults = FALSE;
HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;
// Use WinHttpOpen to obtain a session handle.
hSession = WinHttpOpen( L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0 );
// Specify an HTTP server.
if( hSession )
hConnect = WinHttpConnect( hSession, L"www.microsoft.com",
INTERNET_DEFAULT_HTTPS_PORT, 0 );
// Create an HTTP request handle.
if( hConnect )
hRequest = WinHttpOpenRequest( hConnect, L"GET", NULL,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE );
// Send a request.
if( hRequest )
bResults = WinHttpSendRequest( hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0,
0, 0 );
// End the request.
if( bResults )
bResults = WinHttpReceiveResponse( hRequest, NULL );
// Keep checking for data until there is nothing left.
if( bResults )
{
do
{
// Check for available data.
dwSize = 0;
if( !WinHttpQueryDataAvailable( hRequest, &dwSize ) )
printf( "Error %u in WinHttpQueryDataAvailable.\n",
GetLastError( ) );
// Allocate space for the buffer.
pszOutBuffer = new char[dwSize+1];
if( !pszOutBuffer )
{
printf( "Out of memory\n" );
dwSize=0;
}
else
{
// Read the data.
ZeroMemory( pszOutBuffer, dwSize+1 );
if( !WinHttpReadData( hRequest, (LPVOID)pszOutBuffer,
dwSize, &dwDownloaded ) )
printf( "Error %u in WinHttpReadData.\n", GetLastError( ) );
else
printf( "%s", pszOutBuffer );
// Free the memory allocated to the buffer.
delete [] pszOutBuffer;
}
} while( dwSize > 0 );
}
// Report any errors.
if( !bResults )
printf( "Error %d has occurred.\n", GetLastError( ) );
// Close any open handles.
if( hRequest ) WinHttpCloseHandle( hRequest );
if( hConnect ) WinHttpCloseHandle( hConnect );
if( hSession ) WinHttpCloseHandle( hSession );
以上是微軟提供的同步HTTP請求的例程。若要使用異步HTTP請求,則必須調(diào)用WinHttpSetStatusCallback函數(shù)。
參考:
https://learn.microsoft.com/en-us/windows/win32/winhttp/using-winhttp
《圖解HTTP》