首先先来了解一下HTTP的基本知识:HTTP下午茶
本节我们来解析一下浏览器的请求,重点分为以下几步:
解析http request
- 观察收到的http数据
- 解析 request 请求行 的 method url version
- 解析 header
- 解析 body
观察收到的http请求
上一节我们完成了一个简单的基于TCP/IP的socket server 程序。那么接下来底层的东西我们就先不用考虑了,直接用socket就行。我们只要考虑应用层的HTTP协议,一旦我们的服务器程序能读懂HTTP请求,并做出符合HTTP协议的响应,那么也就完成了HTTP的通信。
上一节最后测试的时候我们用telnet成功连接了我们的服务器,但只是向它传送了一些没有意义的字符。如果是浏览器,会传送什么呢?
我们继续打开我们的服务器,然后试着在浏览器地址栏输入我们的服务器地址: 127.0.0.1:10000 后访问,发现浏览器:
那是说我们返回给浏览器的数据浏览器读不懂,因为现代的浏览器默认用http协议请求访问我们的服务器,而我们的返回的数据只是"helloworld"字符串,并不符合http协议的返回格式。虽然如此,但浏览器却是很有诚意的给我们的服务器发标准的http请求,不信我们看下我们的服务器收到的信息:
先观察一会儿,看起来第一行是http请求的类型,第二行开始是一些":"号分割的键值对。的确如此,第一行告诉我们是用的GET请求,请求的url是"/",用的是1.1的HTTP版本。第二行开始是HTTP的请求头部。 除了GET请求外,另一种常用的请求是POST。用浏览器发POST请求稍麻烦,我们就借用curl工具来发送个HTTP POST请求给服务器看下数据又会是怎们样的:
curl -d "message=nice to meet you" 127.0.0.1:9734/hello
, 服务器收到的信息:
POST /hello HTTP/1.1
Host: 127.0.0.1:10000
User-Agent: curl/7.54.0
Accept: */*
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
message=nice to meet you
可以看到头部信息之后多了一空行和之后的POST的body数据信息。还要注意的是Content-Length头,代表POST的body数据的大小。
解析 request 的 method url version
先来解析最简单的第一行: "POST /hello HTTP/1.1", 只需要用空格split出三个字符串就好了。
//request.h
#ifndef __REQUEST__
#define __REQUEST__
#include <string>
#include <unordered_map>
using namespace std;
struct http_request{
string method;
string url;
string version;
unordered_map<string,string> headers;//这个Map用来存header
string body;//用来存消息体
};
void process_request(struct http_request * request,char* http_data);
#endif
//request.cpp
#include "request.h"
#include "StringUtil.h"
#include <sstream>
#include <string>
#include <iostream>
using namespace std;
void process_request(struct http_request * request,char* http_data){
string inbuf = http_data;
//以\r\n分割每一行
std::vector<string> lines;
StringUtil::Split(inbuf, lines, "\r\n");
if (lines.size() < 1 || lines[0].empty())
{
return;
}
//解析请求行
std::vector<string> chunk;
StringUtil::Split(lines[0],chunk," ");
//chunk中至少有三个字符串:GET+url+HTTP版本号
if (chunk.size() < 3)
{
return;
}
request->method = chunk[0];
request->url = chunk[1];
request->version = chunk[2];
}
这里我们直接写了一个Split类来分割字符串:
//StringUtil.h
#ifndef __STRING_UTIL_H__
#define __STRING_UTIL_H__
#include <string>
#include <vector>
class StringUtil
{
private:
StringUtil() = delete;
~StringUtil() = delete;
StringUtil(const StringUtil& rhs) = delete;
StringUtil& operator=(const StringUtil& rhs) = delete;
public:
static void Split(const std::string& str, std::vector<std::string>& v, const char* delimiter = "|");
};
#endif //!__STRING_UTIL_H__
//StringUtil.cpp
#include "StringUtil.h"
#include <string.h>
void StringUtil::Split(const std::string& str, std::vector<std::string>& v, const char* delimiter/* = "|"*/)//默认参数是 "|"
{
if (delimiter == NULL || str.empty())
return;
std::string buf = str;
size_t pos = std::string::npos;
std::string substr;
int delimiterlength = strlen(delimiter);
while (true)
{
pos = buf.find(delimiter);
if (pos != std::string::npos)
{
substr = buf.substr(0, pos);
if (!substr.empty())
v.push_back(substr);
else
v.push_back("\r\n");
buf = buf.substr(pos + delimiterlength);//更新buf,从当前分割字符往后构造新的字串
}
else//处理最后一个分隔字符串
{
if (!buf.empty())
v.push_back(buf);
break;
}
}
}
编写测试用例:
//requestTest.cpp
#include <iostream>
#include "request.h"
#include <unordered_map>
using namespace std;
int main(){
struct http_request request;
char data[] = "GET / HTTP/1.1\r\nHost:127.0.0.1:9734\r\nUser-Agent:curl/7.54.0\r\nConnection: keep-alive\r\n\r\nmessage=nice to meet you\r\nhhhhh";
process_request(&request,data);
cout<<request.method<<endl;
cout<<request.url<<endl;
cout<<request.version<<endl;
}
在t目录下执行 :
g++ request.cpp StringUtil.cpp requestTest.c && ./a.out
可以看到我们解析的方法正确。
解析 header
header的解析看起来比较复杂,每一行很容易看出是用":"分割的key-value对,所以我们可以用unordered_map来存储。注意不能用map,因为会把首部重新排序。
具体就是把我们之前分好的行直接再按 “ :”来分割一次,然后存到unordered_map里。直到遇到空行为止。
//request.cpp
//逐行读出headers,并存储
int i = 1;
std::vector<string> header_content;
for(i = 1;lines[i] != "\r\n";++i){
StringUtil::Split(lines[i],header_content,":");
string content;
size_t pos = std::string::npos;
pos = lines[i].find(":");
content = lines[i].substr(pos + 1,lines[i].size()-pos-1);
request->headers.insert(pair<string,string>(header_content[0],content));
header_content.clear();
}
解析body
解析body很简单,空行最后的就是body数据的:
//request.cpp
//解析body
for(i = i+1; i < lines.size(); ++i){
request->body += lines[i];
if( i != lines.size()-1)
request->body += "\r\n";
}
最后打印我们的成果
//requestTest.cpp
#include <iostream>
#include "request.h"
#include <unordered_map>
using namespace std;
int main(){
struct http_request request;
char data[] = "GET / HTTP/1.1\r\nHost:127.0.0.1:9734\r\nUser-Agent:curl/7.54.0\r\nConnection: keep-alive\r\n\r\nmessage=nice to meet you\r\nhhhhh";
process_request(&request,data);
cout<<request.method<<endl;
cout<<request.url<<endl;
cout<<request.version<<endl;
//打印headers
unordered_map<string,string> headers_map;
unordered_map<string,string>::iterator m_iter;
headers_map = request.headers;
for(m_iter =headers_map.begin();m_iter != headers_map.end() ;m_iter++){
//第一个元素iter->first 第二个元素 iter->second
cout << m_iter->first <<":"<< m_iter -> second << endl;
}
cout<<request.body<<endl;
}
测试发现没问题,然后我们加上我们上一节的代码来跑一下:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sstream>
#include <iostream>
#include "request.h"
using namespace std;
#define BUFFER_SIZE 512
void getRequest(int client_sockfd){
char buffer[BUFFER_SIZE];
struct http_request request;
memset(buffer,0,BUFFER_SIZE);
int message_len = 0;
if((message_len = recv(client_sockfd,buffer,BUFFER_SIZE,0)) == -1){
cout<<"error handling incoming request!";
return;
}
process_request(&request,buffer);//将接收到的请求发给process_request()函数进行逐行处理
cout<<"Method:"<<request.method<<endl;
cout<<"Url:"<<request.url<<endl;
cout<<"Version:"<<request.version<<endl;
//打印headers
unordered_map<string,string> headers_map;
unordered_map<string,string>::iterator m_iter;
headers_map = request.headers;
for(m_iter =headers_map.begin();m_iter != headers_map.end() ;m_iter++){
//第一个元素iter->first 第二个元素 iter->second
cout << m_iter->first <<":"<< m_iter -> second << endl;
}
cout<<request.body<<endl;
}
void handleAccept(int server_sockfd){
struct sockaddr_in client_address;
int client_sockfd = 0;
socklen_t client_len=sizeof(client_address);
//accept的第一个参数是监听套接字,相当于门卫的功能;后两个都是客户端的地址信息
client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address, &client_len);//会新创建一个套接字传给client_sockfd
getRequest(client_sockfd);
close(client_sockfd);
}
int main() {
int server_sockfd = 0;
int server_len;
struct sockaddr_in server_address;
//创建套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0); //协议族信息,套接字数据传输方式为面向消息的TCP
//初始化服务器地址信息 sockaddr_in
server_address.sin_family = AF_INET;//地址族
server_address.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有IP
server_address.sin_port = htons(10000); // 设置端口号,注意这里的 htons 方法(主机序转网络序,s指的是short)
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);//设置同时监听最大个数,其实就是队列长度
while(true) {
handleAccept(server_sockfd);
}
}
在目录下,我们:
g++ request.cpp main.cpp StringUtil.cpp -o Server
然后执行:
./server
打开一个新的终端,用curl模拟一下浏览器:
然后看到我们服务端的实现:
可以看到我们并没有给浏览器发response,下节我们来接着写~~