首先,Http 本身也是基于 TCP 协议实现的
代码实现
先声明,下面代码是临时写的,没经过测试,只是为了表示http的实现过程,等需要的时候,再来详细调试测试;
void HttpRequest(std::string strUrl, bool bPost, const char* pPostData, std::map<std::string, std::string>* pmapHeader = nullptr) {
static std::pair<std::string, std::string> prConn = std::make_pair(std::string("Connection"), std::string("close"));
static std::pair<std::string, std::string> prAcceptCharset = std::make_pair(std::string("Accept-Charset"), std::string("*"));
static std::pair<std::string, std::string> prAcceptEncoding = std::make_pair(std::string("Accept-Encoding"), std::string("gzip"));
std::map<std::string, std::string> mapHeader;
if (pmapHeader) {
mapHeader = *pmapHeader;
}
//先准备协议头
mapHeader.insert(prConn);
mapHeader.insert(prAcceptCharset);
mapHeader.insert(prAcceptEncoding);
if (bPost) {
int nPostDataLen = strlen(pPostData);
char aBuf[64]{ 0 };
sprintf_s(aBuf, sizeof(aBuf), "%d", nPostDataLen);
mapHeader["Content-Length"] = aBuf;
}
std::string strUri;
std::string strServ;
//对目标url进行分析和提取
size_t nPos = strUrl.find("://");
if (nPos == std::string::npos) {
//没找到"http://"形式的字符串
nPos = strUrl.find("/");
if (nPos == std::string::npos) {
//没有找到"wwww.baidu.com/"形式的字符串
mapHeader["Host"] = strServ = strUrl;
strUri = "/";
nPos = strUrl.find(":");
if (nPos != std::string::npos) {
strServ = strUrl.substr(0, nPos);
}
} else {
mapHeader["Host"] = strServ = strUrl.substr(0, nPos);
strUri = strUrl.substr(nPos);
nPos = strServ.find(":");
if (nPos != std::string::npos) {
strServ = strServ.substr(0, nPos);
}
}
} else {
std::string sHttp = strUrl.substr(0, 4);
std::transform(sHttp.begin(), sHttp.end(), sHttp.begin(), tolower);
if (sHttp.compare("http")) {
return;
}
nPos = strUrl.find('/', 7);//跳过"http://"
if (nPos == std::string::npos)
{
mapHeader["Host"] = strServ = strUrl.substr(7);
strUri = "/";
nPos = strServ.find(':');
if (nPos != std::string::npos){
strServ = strServ.substr(0, nPos);
}
} else {
strUri = strUrl.substr(nPos);
mapHeader["Host"] = strServ = strUrl.substr(7, nPos - 7);
nPos = strServ.find(':');
if (nPos != std::string::npos){
strServ = strServ.substr(0, nPos);
}
}
}
if (strServ.empty())
return;
//创建tcp套接字
SOCKET hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hSocket == INVALID_SOCKET)
return;
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
//绑定套接字
if (bind(hSocket, (PSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) {
closesocket(hSocket);
return;
}
//域名解析
PADDRINFOA pAddrInfo = nullptr;
if (GetAddrInfoA(strServ.c_str(), "http", nullptr, &pAddrInfo)) {
closesocket(hSocket);
return;
}
//与目标建立连接
int nRet = connect(hSocket, pAddrInfo->ai_addr, pAddrInfo->ai_addrlen);
FreeAddrInfoA(pAddrInfo);
if (nRet == SOCKET_ERROR) {
closesocket(hSocket);
return;
}
//开始组装http请求头字符串
std::string strHeader = "GET " + strUri + " HTTP/1.1";
if (bPost)
strHeader = "POST " + strUri + " HTTP/1.1";
for (auto it : mapHeader) {
strHeader += "\r\n";
strHeader += it.first.c_str();
strHeader += ": ";
strHeader += it.second.c_str();
}
strHeader += "\r\n\r\n";//http请求头结束标志
//发送请求头
const char* pData = strHeader.c_str();
size_t nData = strHeader.length();
while (nData > 0) {
size_t nSend = send(hSocket, pData, nData, 0);
if (nSend < 0) {
break;
}
nData -= nSend;
pData += nSend;
}
//发送body数据
if (bPost) {
pData = pPostData;
nData = strlen(pPostData);
while (nData) {
size_t nSend = send(hSocket, pData, nData, 0);
if (nSend < 0) {
break;
}
nData -= nSend;
pData += nSend;
}
}
std::string sRecvData;
std::string sContent;
std::map<std::string, std::string> mapRepHeader;
bool bChunked = false;
int nContentLen = 0;
bool bGzip = false;
//接收应答数据
char aRecvBuff[1024]{ 0 };
while (true) {
size_t nRecv = recv(hSocket, aRecvBuff, sizeof(aRecvBuff), 0);
if (nRecv < 0) {
break;
}
sRecvData.append(aRecvBuff, nRecv);
//解析应答头
if (mapRepHeader.empty()) {
//需要响应消息头
nPos = sRecvData.find("\r\n\r\n");
if (nPos == std::string::npos) {
continue;
}
std::string sRepHeader = sRecvData.substr(0, nPos);
sRecvData = sRecvData.substr(nPos + 4);// 跳过响应头,包括结束标志"\r\n\r\n"
{
//开始分析响应头
/*//以下是一个完整的响应头,可以作为参考,对照代码,帮助分析
HTTP/1.1 200 OK
Date: Wed, 21 Apr 2021 03:30:16 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/7.1.7
Content-Encoding: gzip
*/
//分解应答头,并提取有用信息
nPos = sRepHeader.find(" ");
if (nPos == std::string::npos) {
break;
}
nPos = sRepHeader.find(" ", nPos + 1);
if (nPos == std::string::npos) {
break;
}
int nCode = atoi(sRepHeader.c_str() + nPos + 1);//结果码
nPos = sRepHeader.find("\r\n");//一行结束
if (nPos == std::string::npos)
break;
sRepHeader = sRepHeader.substr(nPos + 2);
while (!sRepHeader.empty()) {
nPos = sRepHeader.find("\r\n");
if (nPos == std::string::npos) {
break;
}
std::string sItem = sRepHeader.substr(0, nPos);
sRepHeader = sRepHeader.substr(nPos + 2);
nPos = sItem.find(":");
if (nPos != std::string::npos) {
std::string sKey = sItem.substr(0, nPos);
std::string sValue = sItem.substr(nPos + 1, sItem.length() - (nPos + 1 + 2));
//接下来需要对sKey sValue进行去头尾的空格,需要自己补充
//比如sKye=Date, sValue=Wed, 21 Apr 2021 03:30:16 GMT
mapRepHeader[sKey] = sValue;
}
}
auto it = mapRepHeader.find("Cookie");//有cookie
if (it != mapRepHeader.end()) {
//cookie字符串形式:key1=value1;key2=value2;key3=value3;
//对it->second按照";"进行切割,再对"="进行切割成键值对数组
//std::map<std::string,std::string> mapCookies { {key1,value2}, {key2,value2}, {key3,value3} };
}
it = mapRepHeader.find("Content-Encoding");
if (it != mapRepHeader.end()) {
if (it->second == "gzip") {
//是否gzip压缩格式
bGzip = true;
}
}
it = mapRepHeader.find("Transfer-Encoding");//分块方式
if (it != mapRepHeader.end() && it->second.length()) {
bChunked = true;
nContentLen = UINT_MAX;
} else {
it = mapRepHeader.find("Content-Length");
if (it != mapRepHeader.end()) {
nContentLen = atoi(it->second.c_str());
}
}
}
//sContent = sRecvData;
}
bool bEnd = false;
while (nContentLen) {
if (bChunked) {
//分块
nPos = sRecvData.find("\r\n");
if (nPos == std::string::npos)
break;
int nChunkSize = atoi(sRecvData.substr(0, nPos).c_str());
if (sRecvData.length() < nPos + 2 + nChunkSize + 2)
break;
std::string sTmp = sRecvData.substr(nPos + 2 + nChunkSize, 2);
if (sTmp.compare("\r\n"))
return;
if (nChunkSize == 0) {
bEnd = true;
break;
}
sRecvData = sRecvData.substr(nPos + 2);
nPos = sRecvData.find("\r\n");
sContent.append(sRecvData.c_str(), nPos);
sRecvData = sRecvData.substr(nPos + 2);
} else {
if (sRecvData.length() >= nContentLen) {
sContent = sRecvData.substr(0, nContentLen);
bEnd = true;
break;
}
}
}
if (bEnd)
break;
}
if (bGzip && sContent.length()) {
//解压缩过程,需要添加zlib.lib库
z_stream streamBuffer{ 0 };
if (inflateInit2(&streamBuffer, 16 + MAX_WBITS) != Z_OK) {
return;
}
streamBuffer.avail_in = sContent.length();
streamBuffer.next_in = reinterpret_cast<Byte*>(const_cast<char*>(sContent.c_str()));
static _declspec(thread) char aBuff[4096];
std::string sRetData;
while (1) {
streamBuffer.avail_out = sizeof(aBuff);
streamBuffer.next_out = reinterpret_cast<Byte*>(aBuff);
int nErr = inflate(&streamBuffer, Z_NO_FLUSH);
if (nErr != Z_OK && nErr != Z_STREAM_END) {
inflateEnd(&streamBuffer);
return;
}
sRetData.append(aBuff, aBuff + sizeof(aBuff) - streamBuffer.avail_out);
if (nErr == Z_STREAM_END) {
inflateEnd(&streamBuffer);
break;//解压结束
}
}
}
//最终sRetData就是我们需要的数据
closesocket(hSocket);
}
http相对简单,基于tcp协议,建立一次连接,获得一次应答,完事立即断开,绝不拖泥带水;
以下摘抄网上文章中的内容:
请求消息格式如下所示:
请求行
通用信息头|请求头|实体头
CRLF(回车换行)
实体内容
其中“请求行”为:请求行 = 方法 [空格] 请求URI [空格] 版本号 [回车换行]
请求行实例:
Eg1:
GET /index.html HTTP/1.1
Eg2:
POST http://192.168.2.217:8080/index.jsp HTTP/1.1
HTTP请求消息实例:
GET /hello.htm HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
If-Modified-Since: Wed, 17 Oct 2007 02:15:55 GMT
If-None-Match: W/"158-1192587355000"
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
Host: 192.168.2.162:8080
Connection: Keep-Alive
HTTP响应消息的格式如下所示:
状态行
通用信息头|响应头|实体头
CRLF
实体内容
其中:状态行 = 版本号 [空格] 状态码 [空格] 原因 [回车换行]
状态行举例:
Eg1:
HTTP/1.0 200 OK
Eg2:
HTTP/1.1 400 Bad Request
HTTP响应消息实例如下所示:
HTTP/1.1 200 OK
ETag: W/"158-1192590101000"
Last-Modified: Wed, 17 Oct 2007 03:01:41 GMT
Content-Type: text/html
Content-Length: 158
Date: Wed, 17 Oct 2007 03:01:59 GMT
Server: Apache-Coyote/1.1