从零开始一个http服务器(二)【实战系列,持续更新】

首先先来了解一下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,下节我们来接着写~~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值