Http請求工具實例編寫
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:
- delete []sTmpBuffer;
- 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)
- {
- return false;
- }
- 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
- return false;
- }
- setContentLength(getContent().length());
- }
- sBuffer = "";
- if(_iTmpContentLength == 0 || _iTmpContentLength == (size_t)-1)
- {
- setContentLength(getContent().length());
- }
- return true;
- }
- else
- {
- if(_iTmpContentLength == 0)
- {
- _content += sBuffer;
- sBuffer = "";
- //自動填寫content-length
- setContentLength(getContent().length());
- return true;
- }
- else if(_iTmpContentLength == (size_t)-1)
- {
- _content += sBuffer;
- sBuffer = "";
- //自動填寫content-length
- setContentLength(getContent().length());
- return false;
- }
- else
- {
- //短連接模式, 接收到長度大于頭部為止
- _content += sBuffer;
- sBuffer = "";
- size_t iNowLength = getContent().length();
- //頭部的長度小于接收的內容, 還需要繼續增加解析后續的buffer
- if(_iTmpContentLength > iNowLength)
- return false;
- return true;
- }
- }
- 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));
- }
- else
- {
- _status = TC_Common::strto<int>(left);
- _about = "";
- }
- parseHeader(*ppChar, _headers);
- return;
- }
- else
- {
- _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())
- {
- return true;
- }
- //是否是chunk編碼
- _bIsChunked = (getHeader("Transfer-Encoding") == "chunked");
- //刪除頭部里面
- eraseHeader("Transfer-Encoding");
- 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())
- {
- return true;
- }
- //是否是chunk編碼
- _bIsChunked = (getHeader("Transfer-Encoding") == "chunked");
- //刪除頭部里面
- eraseHeader("Transfer-Encoding");
接下來將開始循環接收數據,如果非chunk,分為三種情況0(沒有body,即可就可以停止接收),-1(一直接收,知道服務器關閉),確定長度(接收完成即可以停止):
- {
- if(_iTmpContentLength == 0)
- {
- _content += sBuffer; //將整體內容保存下來
- sBuffer = ""; //清空接收緩存
- //自動填寫content-length
- setContentLength(getContent().length());
- return true;
- }
- else if(_iTmpContentLength == (size_t)-1)
- {
- _content += sBuffer;
- sBuffer = "";
- //自動填寫content-length
- setContentLength(getContent().length());
- return false;
- }
- //有明確的content-length長度
- else
- {
- //短連接模式, 接收到長度大于頭部為止
- _content += sBuffer;
- sBuffer = "";
- size_t iNowLength = getContent().length();
- //頭部的長度小于接收的內容, 還需要繼續增加解析后續的buffer
- if(_iTmpContentLength > iNowLength)
- return false; //如果接收的字節大于content-length就可以停止了,否則繼續接收下去
- return true;
- }
- }
如果是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);
- }
- else
- {
- //沒有接收完整的chunk
- return false;
- }
- setContentLength(getContent().length());
- }
- sBuffer = "";
- if(_iTmpContentLength == 0 || _iTmpContentLength == (size_t)-1)
- {
- setContentLength(getContent().length());
- }
- return true;
每一個chunk前兩個字段為chunk的zise,用16進制標識,如果chunk size為0,表示沒有chunk了,否則則接收完成一個chunk,如果當前chunk沒有完成,則繼續接收完成這個chunk后處理,
如果當前chunk完成了,則將這個chunk在buffer里面刪除,放到content內容中來。