网络基础(二)——HTTP协议

目录

1、2个简单的预备知识

2、HTTP请求和响应的格式

3、实现一个最简单的httpserver

4、HTTP的细节字段

4.1、GET和POST

4.2、HTTP的状态码

4.3、HTTP常见Header


1、2个简单的预备知识

首先我们来看一个域名:http://www.baidu.com/,很明显这是百度的域名,但是结合我们之前的知识其实这个域名做过包装,实际上可以解析成ip地址的形式。所以这里我们用ip地址220.181.38.150:80(http绑定端口号80,https绑定端口号443)也可以访问百度。

了解了这些后我们来看看域名的整体结构:

服务器地址后面的一串文件索引就是url(统一资源定位符),所有网络上的资源,都可以用唯一的一个“字符串标识”,并且可以获取到。

我们要知道。网络的行为就是两种:把别人的东西拿下来或者把自己的东西传上去。在少数情况下,提交或者获取的数据本身可能包含和url中特殊的字符冲突的字符,所以就要求CS双方要进行编码(encode)和解码(decode),这些我们之前已经实现过一遍,但http这里编码解码的方式有一些小区别,通过接下来要将的http请求和响应的格式就会观察到。


2、HTTP请求和响应的格式

Http请求

首行 : [ 方法 ] + [url] + [ 版本 ];
Header: 请求的属性 , 冒号分割的键值对 ; 每组属性之间使用\r \n 分隔 ; 遇到空行表示 Header 部分结束;

Body: 空行后面的内容都是 Body. Body 允许为空字符串 . 如果 Body 存在 , 则在 Header 中会有一个 Content-Length属性来标识 Body 的长度 ;

Http响应

首行 : [ 版本号 ] + [ 状态码 ] + [ 状态码解释 ];
Header: 请求的属性 , 冒号分割的键值对 ; 每组属性之间使用\r \n 分隔 ; 遇到空行表示 Header 部分结束;
Body: 空行后面的内容都是 Body. Body 允许为空字符串 . 如果 Body 存在 , 则在 Header 中会有一个 Content-Length属性来标识 Body 的长度 ; 如果服务器返回了一个 html 页面 , 那么 html 页面内容就是在body中。

HTTP请求和响应的结构和过程如图所示:


3、实现一个最简单的httpserver

HttpServer.hpp

#pragma once 

#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>

#include "Socket.hpp"
#include "Log.hpp"

//设置根目录和主页以及分隔符
const std::string wwwroot = "./wwwroot"; //web 根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";


static const int defaultport = 8080;

class HttpServer;

//设置多线程
class ThreadData
{
public:
    ThreadData(int fd, HttpServer *s):sockfd(fd), svr(s)
    {}
public:
    int sockfd;
    HttpServer *svr;
};

//服务器请求,实现解码
class HttpRequest
{
public:
    void Deserialize(std::string req)
    {
        while(true)
        {
            std::size_t pos = req.find(sep);
            if(pos == std::string::npos) break;
            std::string temp = req.substr(0, pos);
            if(temp.empty()) break;
            req_header.push_back(temp);
            req.erase(0, pos+sep.size());
        }
        text = req;
    }
    void Parse()
    {
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;
        file_path = wwwroot; // ./wwwroot
        if(url == "/" || url == "/index.html") 
        {
            file_path += '/';
            file_path += homepage; // ./wwwroot/index.html
        }
        else file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html

        auto pos = file_path.rfind(".");
        if(pos == std::string::npos) suffix = ".html";
        else suffix = file_path.substr(pos);
    }
    void DebugPrint()
    {
        std::cout << "------------------------------" << std::endl;
        for(auto &line : req_header)
        {
            std::cout << "------------------------------" << std::endl;
            std::cout << line << "\n\n";
        }

        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }
public:
    std::vector<std::string>req_header;
    std::string text;

    // 解析之后的结果
    std::string method;
    std::string url;
    std::string http_version;
    std::string file_path;

    std::string suffix;
};

//服务器接口
class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport):port_(port)
    {
        content_type.insert({".html", "text/html"});
        content_type.insert({".jpg", "image/jpeg"});
    }
    
    //启动服务器
    bool Start()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        for(;;)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            if (sockfd < 0) continue;
            lg(Info, "get a new connet, sockfd: %d", sockfd);
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this);
            td->sockfd = sockfd;
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }

    //读取指定路径的文件
    static std::string ReadHtmlContent(const std::string &htmlpath)
    {
        // 坑 传图片,要读二进制数据
        std::ifstream in(htmlpath, std::ios::binary);
        if(!in.is_open()) return "";

        in.seekg(0, std::ios_base::end);
        auto len = in.tellg();
        in.seekg(0, std::ios_base::beg);

        std::string content;
        content.resize(len);

        in.read((char*)content.c_str(), content.size());

        // std::string content;
        // std::string line;
        // while(std::getline(in, line))
        // {
        //     content += line;
        // }

        in.close();
        return content;
    }

    //确定文件后缀
    std::string SuffixToDesc(const std::string &suffix)
    {
        auto iter = content_type.find(suffix);
        if(iter == content_type.end()) return content_type[".html"];
        else return content_type[suffix];
    }

    //处理请求,对响应进行编码并发回
    void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd , buffer, sizeof(buffer) - 1, 0);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer;
            HttpRequest req;
            req.Deserialize(buffer);    
            req.Parse();

            //req.DebugPrint();

            // 返回响应的过程
            std::string text; 
            bool ok = true;
            text = ReadHtmlContent(req.file_path); // 失败?
            if(text.empty())
            {
                ok = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html";
                text = ReadHtmlContent("err_html");
            }

            std::string response_line;
            if(ok)
                response_line = "HTTP/1.0 200 OK\r\n";
            else 
                response_line = "HTTP/1.0 404 Not Found\r\n";
            // response_line = "HTTP/1.0 302 Found\r\n";
            std::string response_header = "Content-Length: ";
            response_header += std::to_string(text.size());
            response_header += "\r\n";
            response_header += "Content-Type: ";
            response_header += SuffixToDesc(req.suffix);
            response_header += "\r\n";
            response_header += "Set-Cookie: name=haha&&passwd=12345";
            response_header += "\r\n";
            // response_header += "Location: https://www.qq.com\r\n";
            std::string blank_line = "\r\n";

            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;
            
            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData*>(args);
        td->svr->HandlerHttp(td->sockfd);
        delete td;
        return nullptr;
    }
    ~HttpServer()
    {}
private:
    Sock listensock_;
    uint16_t port_;
    std::unordered_map<std::string, std::string> content_type;
};

HttpServer.cc

#include "HttpServer.hpp"

#include <iostream>
#include <memory>

using namespace std;

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);
    // unique<HttpServer> svr(new HttpServer());
    HttpServer *svr = new HttpServer(port);
    svr->Start();

    return 0;

}

用到的Socket接口和Log接口之前博客中已给出,这里不再给出。

实现的网页服务器主页界面如上(网页的美化等工作属于前端的内容,我们研究的是后端服务器的具体细节,所以不对前端做过多描述。)


4、HTTP的细节字段

4.1、GET和POST

HTTP中最常用的方法就是GET方法和POST方法,那么这两种方法有什么区别呢?接下来我们来研究一下。

首先来看get方法:

我们将主页文件里的表单提交改成get方法,输入用户名密码进行提交;

这了可以看到我们输入的用户名和密码提交给服务器时,是通过url提交的,那么我们改成post方法试一下:

这个时候我们发现,post方法的url里并没有用户名密码,那么它们在哪里呢?就在请求的正文部分,还记得我们说的请求和响应的结构吗?正文和报头之间是用一个空行分隔,而报头里都是键值对。

总结一下,如果我们要提交的参数给我们的服务器,我们使用get方法的时候,我们提交的参数是通过url提交的!而post方法也支持参数提交,采用请求的正文提交参数!

4.2、HTTP的状态码

类别原因短语
1XXInformational(信息性状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错
最常见的状态码 , 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向 ), 504(Bad Gateway)。

代码中的这一段就是在判断服务器状态码并返回相应的状态页面。

还有一个重定向的状态码我们单独拿出来说一下,重定向就是让服务器指导浏览器,让浏览器访问新的地址;

如果我们将这段注释的代码放开,那么浏览器就会自动的跳转访问qq的主页面。

4.3、HTTP常见Header

Content-Type: 数据类型 (text/html );
Content-Length: Body 的长度;
Host: 客户端告知服务器 , 所请求的资源是在哪个主机的哪个端口上 ;
User-Agent: 声明用户的操作系统和浏览器版本信息 ;
referer: 当前页面是从哪个页面跳转过来的 ;
location: 搭配 3xx 状态码使用 , 告诉客户端接下来要去哪里访问 ;
Cookie: 用于在客户端存储少量信息 . 通常用于实现会话 (session) 的功能 ;
这里除了上面的Header,我们还要单独说一个connection
可以看到,这里的Connection对应的值为keep-alive,是什么意思呢?我们先说结论,这是长连接也就是用的HTTP/1.1版本。
再来解释一下,我们要知道,其实一个浏览器页面是会包含很多的元素的,而每一个元素都是一个资源,而如果一次请求响应一个资源,再关闭连接,就是短连接,也就是对应HTTP/1.0版本;如果是建立一个TCP连接,发送和返回多个http的requset和response,就是长连接,也就是对应的HTTP/1.1版本。
这里的Connection:keep-alive就是长连接的意思。
最后我们再解释一下Cookie,Cookie是http对登录用户的会话保持功能。
举个例子,我们在登录网页版bilibili的时候输入账号密码登录,那么我们在下一次访问这个页面的时候,它就不会再要求我们输入账号密码而能自动登录,这就是Cookie的作用,那么我们再来看一下原理:
那么这样的方法其实是不安全的,因为cookie文件中的内容每次都是以明文传送,很容易被人拿走,所以一般服务器都会维护一个session文件,将我们的cookie信息用特殊的方法变成一个session id存在里面,在我们第一次登录时返回给浏览器我们的sessionid,之后我们的cookie信息便只发送这个id用来认证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

双葉Souyou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值