网络协议栈--应用层--HTTP协议

本节重点

理解应用层的作用, 初识HTTP协议

一、应用层

程序员们写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。

二、HTTP协议

虽然说, 应用层协议是程序员自己定的.
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。

2.1 认识URL

平时我们俗称的 “网址” 其实就是说的 URL。
在这里插入图片描述

2.2 urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了。 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
在这里插入图片描述
“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程;

2.3 HTTP协议格式

HTTP请求:
在这里插入图片描述
(1)请求行: [请求方法] + [url] + [HTTP版本]。

(2)请求报头(Header): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;

(3)空行:遇到单单一个\n空行表示Header部分结束.

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

HTTP响应:
在这里插入图片描述
(1)状态行: [HTTP版本] + [状态码] + [状态码描述]
(2)响应报头(Header): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;
(3)空行:遇到单单一个\n空行表示Header部分结束。
(4)响应正文:空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中。

2.4 HTTP的方法

在这里插入图片描述
其中最常用的就是GET方法和POST方法。

2.4 HTTP的状态码

在这里插入图片描述
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)

2.5 HTTP常见的Header属性

(1)Content-Type: 数据类型(text/html等)
(2)Content-Length: Body的长度
(3)Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
(4)User-Agent: 声明用户的操作系统和浏览器版本信息;
(5)referer: 当前页面是从哪个页面跳转过来的;
(6)location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
(7)Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

三、最简单的HTTP服务器

实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;
我们以下的服务器是添加了一点前端的代码的:

3.1 HttpServer.hpp

#pragma once

#include <iostream>
#include "Socket.hpp"
#include <functional>
#include <fstream>
#include <vector>
#include <string>
#include <sstream>
#include <unordered_map>

// HTTP协议可以以"\r\n"作为行分隔符,也可以是"\n"
const std::string sep = "\r\n";

// 传说中的web根目录
const std::string wwwroot = "./wwwroot";

// 服务器主页
const std::string homepage = "index.html";

const uint16_t DEFAULT_PORT = 8080;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int sockfd, HttpServer *svr)
        : _sockfd(sockfd), _svr(svr)
    {
    }

public:
    int _sockfd;
    HttpServer *_svr;
};

class Request
{
public:
    // 反序列化
    void Deserialize(std::string req)
    {
        while (true)
        {
            // 用行分隔符"\r\n",分割请求报头中的属性信息
            auto pos = req.find(sep);
            if (pos == string::npos)
            {
                break;
            }
            std::string tmp = req.substr(0, pos);
            if (tmp.empty())
            {
                // 说明读取到了空行,即报头已经读完了
                req.erase(pos, sep.size());
                break;
            }
            _req_head.push_back(tmp);
            // 没获取一条属性信息记得在原报头中删除
            req.erase(0, pos + sep.size());
        }
        // 取出报头之后得到的剩余的就是请求正文
        _body = req;
    }
    // 解析http协议
    void Parse()
    {
        std::string req_line = _req_head[0];
        //stringstream默认是以空格作为分隔符的
        std::stringstream ss(req_line);
        //必须按顺序
        ss >> _method >> _url >> _http_version;

        // 拼接资源的路径
        _filepath = wwwroot;
        if (_url == "/" || _url == "/index.html")
        {
            _filepath += "/";
            _filepath += homepage;
        }
        else
        {
            _filepath += _url;
        }

        //从后往前找到'.',从而找出后缀
        auto pos = _filepath.rfind('.');
        if (pos == string::npos)
        {
            _suffix = ".html";
        }
        else
        {
            _suffix = _filepath.substr(pos);
        }
    }

    //用来调试的
    void DebugPrint()
    {
        for (auto &line : _req_head)
        {
            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: " << _filepath << std::endl;
        std::cout << _body << std::endl;
    }

    std::string GetFilePath()
    {
        return _filepath;
    }

    std::string GetFileSuffix()
    {
        return _suffix;
    }

private:
    std::string _method;                     //请求方法
    std::string _url;                        //请求的资源的url
    std::string _http_version;               //http协议版本号
    std::vector<std::string> _req_head;      //请求报头:包括请求行、报头属性信息
    std::string _body;                       //请求正文
    std::string _filepath;                   //文件的路径,从web根目录开始,即wwwroot/url
    std::string _suffix;                     //url的后缀
};

class HttpServer
{
public:
    HttpServer(const uint16_t &port = DEFAULT_PORT)
        : _port(port)
    {
        //.html对应的网页是文本类型
        _content_type[".html"] = "text/html";
        //  _content_type[".html"] = "application/json";
        //.png对应的网页是图片
        _content_type[".png"] = "image/png";
    }

    ~HttpServer()
    {
    }

    void InitHttpServer()
    {
        // 1、创建套接字
        _listen_sock.Socket();

        // 2、绑定
        _listen_sock.Bind(_port);

        // 3、监听
        _listen_sock.Listen();
    }

    static string ReadIndexHtml(const std::string &path)
    {
        // 有坑?
        std::string str;
        //注意,这里一定要用二进制的方式去读取,否则类似于图片的文件就有可能会读取出
        //错从而在访问的时候看不到图片
        ifstream in(path, std::ios::binary);
        if (!in.is_open())
        {
            return "";
        }
        
        //对于seekg函数,0表示偏移量,std::ios_base::end是基准,
        //意思是:设置当前位置相对于最后一个位置的偏移量是0,
        //说明当前指针的位置就指向文件内容的最后一个位置
        in.seekg(0, std::ios_base::end);
        //对于tellg函数,是获取当前读取位置的,因为上面已经设置了
        //当前指针指向的位置是文件内容的最后一个位置,所以当前位置
        //的数值就等于文件内容的大小,即一共有len个自己的内容
        int len = in.tellg();
        //同上,即设置指针指向文件的开始位置
        in.seekg(0, std::ios_base::beg);
        std::string content;
        content.resize(len);
        //把内容读取到content中
        in.read((char *)content.c_str(), len);

        in.close();
        return content;
    }

    //url的后缀转换成对应的后缀描述,用于构建响应
    std::string SuffixToDesc(const std::string &suffix)
    {
        auto ret = _content_type.find(suffix);
        if (ret == _content_type.end())
        {
            //如果从类型中没有找到,那就统一当成是.html后缀
            //返回对应的文本类型的描述"text/html"
            return _content_type[".html"];
        }
        else
        {
            return _content_type[suffix];
        }
    }

    static void *Handler(void *args)
    {
        //线程分离
        pthread_detach(pthread_self());

        ThreadData *ptd = static_cast<ThreadData *>(args);
        int sockfd = ptd->_sockfd;

        while (true)
        {
            char buffer[10240] = {0};
            ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = '\0';
                std::cout << buffer << std::endl;

                //解析http协议请求
                Request req;
                req.Deserialize(buffer);
                req.Parse();
                req.DebugPrint();

                // 构建http响应
                std::string response;
                bool ok = true;
                //根据请求的url,读取对应的文件内容作为响应的正文
                std::string response_body = ReadIndexHtml(req.GetFilePath());
                if (response_body.empty())
                {
                    ok = false;
                    std::string err_html = wwwroot;
                    err_html += '/';
                    err_html += "err.html";
                    response_body = ReadIndexHtml(err_html);
                }

                // 状态行
                std::string state_line;
                if (ok)
                {
                    state_line = "HTTP/1.0 200 OK\r\n";
                }
                else
                {
                    state_line = "HTTP/1.0 404 Not Found\r\n";
                }
                response += state_line;

                //做重定向的状态行
                //response = "HTTP/1.0 302 Found\r\n";

                // 响应报头
                std::string response_head;
                //属性1:content-lenth
                std::string content_lenth = "Content-Lenth: ";
                content_lenth + to_string(response_body.size());
                //属性2:content-type
                std::string content_type = "Content-Type: ";
                content_type += ptd->_svr->SuffixToDesc(req.GetFileSuffix());
                response_head += content_lenth;
                response_head += "\r\n";
                response_head += content_type;
                response_head += "\r\n";
                //属性3:set cookie
                response_head += "Set-Cookie: name=kobe&&passwd=123456";
                response_head += "\r\n";
                //属性4:Location,设置重定向时需要访问的网址
                // response_head += "Location: http://www.qq.com/";
                // response_head += "\r\n";
                response += response_head;
                // 空行
                response += "\r\n";
                // 正文
                response += response_body;
                ssize_t n = send(sockfd, response.c_str(), response.size(), 0);
            }
            else if (n == 0)
            {
                log(Info, "client quit...,关闭连接:%d", sockfd);
                close(sockfd);
                return nullptr;
            }
            else if (n < 0)
            {
                std::cout << "n=" << n << std::endl;
                log(Error, "recv error,关闭连接:%d", sockfd);
                close(sockfd);

                return nullptr;
            }
        }
        log(Info, "服务器退出,关闭连接:%d", sockfd);
        close(sockfd);
        return nullptr;
    }

    void Start()
    {
        while (true)
        {
            std::string client_ip;
            uint16_t client_port;
            int sockfd = _listen_sock.Accept(client_ip, client_port);
            if (sockfd < 0)
            {
                continue;
            }
            pthread_t tid;
            ThreadData td(sockfd, this);

            // 创建线程
            pthread_create(&tid, nullptr, Handler, (void *)(&td));
        }
    }

private:
    Sock _listen_sock;
    uint16_t _port;
    std::unordered_map<std::string, std::string> _content_type;
};

3.2 HttpServer.cc

#include "HttpServer.hpp"
#include <memory>

void Usage(const std::string& proc)
{
    std::cout<<"\t\n"<<std::endl;
    std::cout<<"Usage: "<<proc<<" server_port[>=1024]"<<std::endl<<std::endl;
}

int main(int argc, char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t server_port=(uint16_t)stoi(argv[1]);
    std::unique_ptr<HttpServer> svr(new HttpServer(server_port));
    svr->InitHttpServer();
    svr->Start();

    return 0;
}

3.3 HttpClient.cc

提示一下:如果直接用HttpClient.cc访问服务器,那么需要按照HTTP协议的标准格式构建报文才能正确地返回,否则服务器会发生段错误的。

#include "HttpServer.hpp"
#include "Socket.hpp"

int main()
{
    Sock sock;
    sock.Socket();
    sock.Connect("43.138.156.240",8081);

    string buffer;
    while(true)
    {
        std::cout<<"Please Enter:";
        std::getline(std::cin,buffer);
        send(sock.Sockfd(),buffer.c_str(),buffer.size(),0);
        sleep(1);

        char tmp[10240]={0};
        recv(sock.Sockfd(),tmp,sizeof(tmp),0);
        std::cout<<tmp<<std::endl;
    }

    return 0;
}

3.4 log.hpp

#pragma once

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <time.h>
#include <stdarg.h>

// 日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define OneFile 2
//向多个文件打印
#define Classfile 3
#define SIZE 1024

#define LogFile "log.txt"



class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int mothod)
    {
        printMethod = mothod;
    }

    string LevelToString(int level)
    {
        switch (level)
        {
        case Info:
        {
            return "Info";
        }
        case Debug:
        {
            return "Debug";
        }
        case Warning:
        {
            return "Warning";
        }
        case Error:
        {
            return "Error";
        }
        case Fatal:
        {
            return "Fatal";
        }
        default:
        {
            return "None";
        }
        }
    }

    void printlog(int level,const string& logtxt)
    {
        switch(printMethod)
        {
        case Screen:
        {
            cout<<logtxt<<endl;
            break;
        }
        case OneFile:
        {
            PrintOneFile(LogFile,logtxt);
            break;
        }
        case Classfile:
        {
            PrintClassfile(level,logtxt);
            break;
        }
        default:
        {
            break;
        }
        }
    }

    void PrintOneFile(const string& logname,const string& logtxt)
    {
        string _logname=path+logname;
        int fd=open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd<0)
        {
            perror("open fail");
            return;
        }

        write(fd,logtxt.c_str(),logtxt.size());

        close(fd);

    }

    void PrintClassfile(int level,const string& logtxt)
    {
        string filename=LogFile;
        filename+='.';
        filename+=LevelToString(level);
        PrintOneFile(filename,logtxt);
    }

    void operator()(int level,const char* format,...)
    {
        time_t t=time(nullptr);
        struct tm* ctime=localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer,SIZE,"[%s][%d-%d-%d %d:%d:%d]",LevelToString(level).c_str(),
        ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,
        ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        va_list s;
        va_start(s,format);
        char rightbuffer[SIZE]={0};
        vsnprintf(rightbuffer,SIZE,format,s);
        va_end(s);


        char logtxt[SIZE*2];
        snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer);

        printlog(level,logtxt);
    }

    ~Log()
    {
    }

private:
    // 打印方法
    int printMethod;
    string path;
};

//定义一个全局的log
Log log;

3.5

#pragma once

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <unistd.h>
#include <strings.h>
#include <cstring>
#include <string>

int backlog = 10;

enum
{
    SockErr = 2,
    BindErr,
    ListenErr,
    ConnectErr,
};

class Sock
{
public:
    Sock()
        : _sockfd(-1)
    {
    }
    ~Sock()
    {
        if(_sockfd>0)
        {
            close(_sockfd);
        }
    }

    // 创建套接字
    void Socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            log(Fatal, "socket failed,errno:%d,errstring:%s", errno, strerror(errno));
            exit(SockErr);
        }
        int opt=1;
        if(setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt))<0)
        {
            log(Warning, "setsockopt failed, sockfd:%d", _sockfd);
        }
        log(Info, "setsockopt successed, sockfd:%d", _sockfd);
        log(Info, "socket successed, sockfd:%d", _sockfd);
    }
    // 绑定
    void Bind(const uint16_t &serverPort)
    {
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(serverPort);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sockfd, (struct sockaddr *)(&local), sizeof(local)) < 0)
        {
            log(Fatal, "bind failed,errno:%d,errstring:%s", errno, strerror(errno));
            exit(BindErr);
        }
        log(Info, "bind successed...");
    }
    // 监听
    void Listen()
    {
        if (listen(_sockfd, backlog) < 0)
        {
            log(Fatal, "set listen state failed,errno:%d,errstring:%s", errno, strerror(errno));
            exit(ListenErr);
        }
        log(Info, "set listen state successed");
    }
    //获取连接
    int Accept(string& clientip,uint16_t& clientport)
    {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        bzero(&client,sizeof(client));

        int sockfd=accept(_sockfd,(struct sockaddr*)(&client),&len);
        if(sockfd<0)
        {
            log(Warning, "accept new link failed,errno:%d,errstring:%s", errno, strerror(errno));
            return -1;
        }
        log(Info,"accept a new link...,sockfd:%d",sockfd);

        clientip=inet_ntoa(client.sin_addr);
        clientport=(uint16_t)(ntohs(client.sin_port));

        return sockfd;
    }
    // 连接
    void Connect(const string &serverIp, const uint16_t &serverPort)
    {
        struct sockaddr_in server;
        bzero(&server, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(serverIp.c_str());
        server.sin_port = htons(serverPort);

        if (connect(_sockfd, (struct sockaddr *)(&server), sizeof(server)) < 0)
        {
            log(Fatal, "connect server failed,errno:%d,errstring:%s", errno, strerror(errno));
            exit(ConnectErr);
        }
        log(Info, "connect server succeeded...");
    }

    void Close()
    {
        if(_sockfd>0)
        {
            close(_sockfd);
        }
    }

    int Sockfd()
    {
        return _sockfd;
    }

private:
    int _sockfd;
};

3.6 makefile

.PHONY:all
all:http_server http_client

http_server:HttpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread 

http_client:HttpClient.cc
	g++ -o $@ $^ -std=c++11 

.PHONY:clean
clean:
	rm -f http_server http_client

3.7 wwwroot目录下的资源

自行创建wwwroot目录并把资源按要求放到目录下:
web根目录下的资源

在这里插入图片描述
备注:
此处我们使用 8081 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.使用Edge测试我们的服务器时, 可以看到服务器打出的请求中还有一个
GET /favicon.ico HTTP/1.1 这样的请求。favicon.ico是用来设置网页上的小图标的:
在这里插入图片描述

可以试试把返回的状态码改成404, 403, 504等, 看浏览器上分别会出现什么样的效果。

四、HTTP协议内容一览图

在这里插入图片描述

以上就是今天想要跟大家分享的关于HTTP协议的所有内容了,你学会了吗?如果感觉到有所收获的话,那就点点小心心,再点点关注呗,后期还会持续更新有关Linux网络编程的相关知识哦,我们下期见!!!!!

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值