HTTP協定工作方式首先用戶端發送一個請求(request)給伺服器,伺服器在接收到這個請求後将生成一個響應(response)傳回給用戶端。
在這個通信的過程中HTTP協定在以下4個方面做了規定:
1. Request和Response的格式
Request格式:
HTTP請求行
(請求)頭
空行
可選的消息體
注:請求行和标題必須以<CR><LF> 作為結尾(也就是,回車然後換行)。空行内必須隻有<CR><LF>而無其他空格。在HTTP/1.1 協定中,所有的請求頭,除Host外,都是可選的。
執行個體:
GET / HTTP/1.1
Host: gpcuster.cnblogs.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
If-Modified-Since: Mon, 25 May 2009 03:19:18 GMT
Response格式:
HTTP狀态行
(應答)頭
可選的消息體
HTTP/1.1 200 OK
Cache-Control: private, max-age=30
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Expires: Mon, 25 May 2009 03:20:33 GMT
Last-Modified: Mon, 25 May 2009 03:20:03 GMT
Vary: Accept-Encoding
Server: Microsoft-IIS/7.0
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
Date: Mon, 25 May 2009 03:20:02 GMT
Content-Length: 12173
消息體的内容(略)詳細的資訊請參考:RFC 2616。關于HTTP headers的簡要介紹,請檢視:Quick reference to HTTP headers
2.建立連接配接的方式
HTTP支援2中建立連接配接的方式:非持久連接配接和持久連接配接(HTTP1.1預設的連接配接方式為持久連接配接)。
1)非持久連接配接
讓我們檢視一下非持久連接配接情況下從伺服器到客戶傳送一個Web頁面的步驟。假設該貝面由1個基本HTML檔案和10個JPEG圖像構成,而且所有這些對象都存放在同一台伺服器主機中。再假設該基本HTML檔案的URL為:gpcuster.cnblogs.com/index.html。
下面是具體步騾:
1.HTTP客戶初始化一個與伺服器主機gpcuster.cnblogs.com中的HTTP伺服器的TCP連接配接。HTTP伺服器使用預設端口号80監聽來自HTTP客戶的連接配接建立請求。
2.HTTP客戶經由與TCP連接配接相關聯的本地套接字發出—個HTTP請求消息。這個消息中包含路徑名/somepath/index.html。
3.HTTP伺服器經由與TCP連接配接相關聯的本地套接字接收這個請求消息,再從伺服器主機的記憶體或硬碟中取出對象/somepath/index.html,經由同一個套接字發出包含該對象的響應消息。
4.HTTP伺服器告知TCP關閉這個TCP連接配接(不過TCP要到客戶收到剛才這個響應消息之後才會真正終止這個連接配接)。
5.HTTP客戶經由同一個套接字接收這個響應消息。TCP連接配接随後終止。該消息标明所封裝的對象是一個HTML檔案。客戶從中取出這個檔案,加以分析後發現其中有10個JPEG對象的引用。
6.給每一個引用到的JPEG對象重複步騾1-4。
上述步驟之是以稱為使用非持久連接配接,原因是每次伺服器發出一個對象後,相應的TCP連接配接就被關閉,也就是說每個連接配接都沒有持續到可用于傳送其他對象。每個TCP連接配接隻用于傳輸一個請求消息和一個響應消息。就上述例子而言,使用者每請求一次那個web頁面,就産生11個TCP連接配接。
2)持久連接配接
非持久連接配接有些缺點。首先,客戶得為每個待請求的對象建立并維護一個新的連接配接。對于每個這樣的連接配接,TCP得在用戶端和伺服器端配置設定TCP緩沖區,并維持TCP變量。對于有可能同時為來自數百個不同客戶的請求提供服務的web伺服器來說,這會嚴重增加其負擔。其次,如前所述,每個對象都有2個RTT的響應延長——一個RTT用于建立TCP連接配接,另—個RTT用于請求和接收對象。最後,每個對象都遭受TCP緩啟動,因為每個TCP連接配接都起始于緩啟動階段。不過并行TCP連接配接的使用能夠部分減輕RTT延遲和緩啟動延遲的影響。
在持久連接配接情況下,伺服器在發出響應後讓TCP連接配接繼續打開着。同一對客戶/伺服器之間的後續請求和響應可以通過這個連接配接發送。整個Web頁面(上例中為包含一個基本HTMLL檔案和10個圖像的頁面)自不用說可以通過單個持久TCP連接配接發送:甚至存放在同一個伺服器中的多個web頁面也可以通過單個持久TCP連接配接發送。通常,HTTP伺服器在某個連接配接閑置一段特定時間後關閉它,而這段時間通常是可以配置的。持久連接配接分為不帶流水線(without pipelining)和帶流水線(with pipelining)兩個版本。如果是不帶流水線的版本,那麼客戶隻在收到前一個請求的響應後才發出新的請求。這種情況下,web頁面所引用的每個對象(上例中的10個圖像)都經曆1個RTT的延遲,用于請求和接收該對象。與非持久連接配接2個RTT的延遲相比,不帶流水線的持久連接配接已有所改善,不過帶流水線的持久連接配接還能進一步降低響應延遲。不帶流水線版本的另一個缺點是,伺服器送出一個對象後開始等待下一個請求,而這個新請求卻不能馬上到達。這段時間伺服器資源便閑置了。
HTTP/1.1的預設模式使用帶流水線的持久連接配接。這種情況下,HTTP客戶每碰到一個引用就立即發出一個請求,因而HTTP客戶可以一個接一個緊挨着發出各個引用對象的請求。伺服器收到這些請求後,也可以一個接一個緊挨着發出各個對象。如果所有的請求和響應都是緊挨着發送的,那麼所有引用到的對象一共隻經曆1個RTT的延遲(而不是像不帶流水線的版本那樣,每個引用到的對象都各有1個RTT的延遲)。另外,帶流水線的持久連接配接中伺服器空等請求的時間比較少。與非持久連接配接相比,持久連接配接(不論是否帶流水線)除降低了1個RTT的響應延遲外,緩啟動延遲也比較小。其原因在于既然各個對象使用同一個TCP連接配接,伺服器發出第一個對象後就不必再以一開始的緩慢速率發送後續對象。相反,伺服器可以按照第一個對象發送完畢時的速率開始發送下一個對象。在http1.0協定中每次請求和響應都會建立一個新的tcp連接配接,http1.1之後才開始支援可以重用第一次請求的http連接配接, 預設支援長連接配接形式。 如果client或server端不想支援長連接配接,則需要在htt的header加上connection:close,如果支援,則設定header為connection:keep-alive。
以上主要是簡要闡述了http請求的流程,需要實作一個簡易的httpclient,需要注意:
1)短連接配接or長連接配接。
2)header的解析與建構。
3)body的解析與建構。
4)chunk與content-length的不同解析方式。
5)http method的不同。
....
如下主要是根據執行個體化的httpRequest生成header,支援POST,GET,OPTIONS三種method。
string TC_HttpRequest::encode()
{
// assert(_requestType == REQUEST_GET || _requestType == REQUEST_POST || !_originRequest.empty());
ostringstream os;
if(_requestType == REQUEST_GET)
{
encode(REQUEST_GET, os);
}
else if(_requestType == REQUEST_POST)
setContentLength(_content.length());
encode(REQUEST_POST, os);
os << _content;
else if(_requestType == REQUEST_OPTIONS)
encode(REQUEST_OPTIONS, os);
return os.str();
}
header與body之間是兩個\r\n\。
void TC_HttpRequest::encode(int iRequestType, ostream &os)
os << requestType2str(iRequestType) << " " << _httpURL.getRequest() << " HTTP/1.1\r\n";
os << genHeader();
os << "\r\n";
便利所有的header key,用\r\n進行換行分隔。
string TC_Http::genHeader() const
ostringstream sHttpHeader;
for(http_header_type::const_iterator it = _headers.begin(); it != _headers.end(); ++it)
if(it->second != "")
{
sHttpHeader << it->first << ": " << it->second << "\r\n";
}
return sHttpHeader.str();
如下是一個HttpRequest的解析請求。通過TCP socket将構造的http request header發送至伺服器http server端口。建構緩沖區循環接收傳回資料,直到用戶端完整的response包接收完畢或者伺服器異常關閉。
int TC_HttpRequest::doRequest(TC_HttpResponse &stHttpRsp, int iTimeout)
//隻支援短連接配接模式
setConnection("close");
string sSendBuffer = encode();
string sHost;
uint32_t iPort;
getHostPort(sHost, iPort);
TC_TCPClient tcpClient;
tcpClient.init(sHost, iPort, iTimeout);
int iRet = tcpClient.send(sSendBuffer.c_str(), sSendBuffer.length());
if(iRet != TC_ClientSocket::EM_SUCCESS)
return iRet;
stHttpRsp.reset();
string sBuffer;
char *sTmpBuffer = new char[10240];
size_t iRecvLen = 10240;
while(true)
iRecvLen = 10240;
iRet = tcpClient.recv(sTmpBuffer, iRecvLen);
if(iRet == TC_ClientSocket::EM_SUCCESS)
sBuffer.append(sTmpBuffer, iRecvLen);
switch(iRet)
case TC_ClientSocket::EM_SUCCESS:
if(stHttpRsp.incrementDecode(sBuffer))
{
delete []sTmpBuffer;
return TC_ClientSocket::EM_SUCCESS;
}
continue;
case TC_ClientSocket::EM_CLOSE:
delete []sTmpBuffer;
stHttpRsp.incrementDecode(sBuffer);
return TC_ClientSocket::EM_SUCCESS;
default:
return iRet;
assert(true);
return 0;
資料接收分為兩部分,第一部分是header,通過header頭的解讀,進一步接收與解析body content,如果解析傳回false,表示http response并未接收完成,繼續接收。
case TC_ClientSocket::EM_SUCCESS:
if(stHttpRsp.incrementDecode(sBuffer))
{
delete []sTmpBuffer;
return TC_ClientSocket::EM_SUCCESS;
}
continue;
當資料接收成功,将接收的buffer放入resp中進行解析:
bool TC_HttpResponse::incrementDecode(string &sBuffer)
//解析頭部
if(_headLength == 0)
string::size_type pos = sBuffer.find("\r\n\r\n");
if(pos == string::npos)
return false;
parseResponseHeader(sBuffer.c_str());
if(_status == 204)
http_header_type::const_iterator it = _headers.find("Content-Length");
if(it != _headers.end())
_iTmpContentLength = getContentLength();
else
//沒有指明ContentLength, 接收到伺服器關閉連接配接
_iTmpContentLength = -1;
_headLength = pos + 4;
sBuffer = sBuffer.substr(_headLength);
//重定向就認為成功了
if((_status == 301 || _status == 302) && !getHeader("Location").empty())
return true;
//是否是chunk編碼
_bIsChunked = (getHeader("Transfer-Encoding") == "chunked");
//删除頭部裡面
eraseHeader("Transfer-Encoding");
if(_bIsChunked)
while(true)
string::size_type pos = sBuffer.find("\r\n");
if(pos == string::npos)
return false;
//查找目前chunk的大小
string sChunkSize = sBuffer.substr(0, pos);
int iChunkSize = strtol(sChunkSize.c_str(), NULL, 16);
if(iChunkSize <= 0) break; //所有chunk都接收完畢
if(sBuffer.length() >= pos + 2 + (size_t)iChunkSize + 2) //接收到一個完整的chunk了
//擷取一個chunk的内容
_content += sBuffer.substr(pos + 2, iChunkSize);
//删除一個chunk
sBuffer = sBuffer.substr(pos + 2 + iChunkSize + 2);
else
//沒有接收完整的chunk
setContentLength(getContent().length());
sBuffer = "";
if(_iTmpContentLength == 0 || _iTmpContentLength == (size_t)-1)
return true;
else
if(_iTmpContentLength == 0)
_content += sBuffer;
sBuffer = "";
//自動填寫content-length
else if(_iTmpContentLength == (size_t)-1)
//短連接配接模式, 接收到長度大于頭部為止
size_t iNowLength = getContent().length();
//頭部的長度小于接收的内容, 還需要繼續增加解析後續的buffer
if(_iTmpContentLength > iNowLength)
return true;
該解析if(_headLength == 0)判斷是否header已經開始接收,知道遇見
string::size_type pos = sBuffer.find("\r\n\r\n");
if(pos == string::npos)
return false;
則表示header接收完成,并解析完整的header,其中HTTP status 204HTTP狀态碼2XX 都表示成功。HTTP的204(no content)響應,表示執行成功,但沒有資料傳回,浏覽器不用重新整理頁面,也不用導向新的頁面。
parseResponseHeader(sBuffer.c_str());
if(_status == 204)
{
return false;
}
void TC_HttpResponse::parseResponseHeader(const char* szBuffer)
const char **ppChar = &szBuffer;
_headerLine = TC_Common::trim(getLine(ppChar));
string::size_type pos = _headerLine.find(' ');
if(pos != string::npos)
_version = _headerLine.substr(0, pos);
string left = TC_Common::trim(_headerLine.substr(pos));
string::size_type pos1 = left.find(' ');
if(pos1 != string::npos)
_status = TC_Common::strto<int>(left.substr(0, pos));
_about = TC_Common::trim(left.substr(pos1 + 1));
_status = TC_Common::strto<int>(left);
_about = "";
parseHeader(*ppChar, _headers);
return;
_version = _headerLine;
_status = 0;
_about = "";
// throw TC_HttpResponse_Exception("[TC_HttpResponse_Exception::parseResponeHeader] http response format error : " + _headerLine);
接下來判斷http response的content-length,如果明确傳回則body字段确定,如果沒有,則需要接收伺服器直至關閉。
http_header_type::const_iterator it = _headers.find("Content-Length");
if(it != _headers.end())
_iTmpContentLength = getContentLength();
else
//沒有指明ContentLength, 接收到伺服器關閉連接配接
_iTmpContentLength = -1;
_headLength = pos + 4;
_headLength = pos + 4;
sBuffer = sBuffer.substr(_headLength); //把body提取出來
//重定向就認為成功了
if((_status == 301 || _status == 302) && !getHeader("Location").empty())
//是否是chunk編碼
_bIsChunked = (getHeader("Transfer-Encoding") == "chunked");
//删除頭部裡面
eraseHeader("Transfer-Encoding");
接下來将開始循環接收資料,如果非chunk,分為三種情況0(沒有body,即可就可以停止接收),-1(一直接收,知道伺服器關閉),确定長度(接收完成即可以停止):
_content += sBuffer; //将整體内容儲存下來
sBuffer = ""; //清空接收緩存
//有明确的content-length長度
return false; //如果接收的位元組大于content-length就可以停止了,否則繼續接收下去
如果是chunk分片,則更加複雜一些:
while(true)
string::size_type pos = sBuffer.find("\r\n");
if(pos == string::npos)
return false;
//查找目前chunk的大小
string sChunkSize = sBuffer.substr(0, pos);
int iChunkSize = strtol(sChunkSize.c_str(), NULL, 16);
if(iChunkSize <= 0) break; //所有chunk都接收完畢
if(sBuffer.length() >= pos + 2 + (size_t)iChunkSize + 2) //接收到一個完整的chunk了
//擷取一個chunk的内容
_content += sBuffer.substr(pos + 2, iChunkSize);
//删除一個chunk
sBuffer = sBuffer.substr(pos + 2 + iChunkSize + 2);
//沒有接收完整的chunk
setContentLength(getContent().length());
sBuffer = "";
if(_iTmpContentLength == 0 || _iTmpContentLength == (size_t)-1)
return true;
每一個chunk前兩個字段為chunk的zise,用16進制辨別,如果chunk size為0,表示沒有chunk了,否則則接收完成一個chunk,如果目前chunk沒有完成,則繼續接收完成這個chunk後處理,
如果目前chunk完成了,則将這個chunk在buffer裡面删除,放到content内容中來。