Linux——http协议1

目录

URL

http协议

http请求

http响应

细节优化

makefile

 HttpServer.hpp

HttpServer.cc

Util.hpp

Usage.hpp

Sock.hpp

Log.hpp

wwwroot/index.html


 

应用层:就是程序员基于socket接口之上编写的具体逻辑,做的很多工作,都是和文本处理有关的——协议分析与处理。

http协议,一定会有大量的文版分析和协议处理。

URL

 平时我们俗称的 "网址" 其实就是说的 URL

 

 其中new.qq.com是域名,浏览器会自动把域名解析为IP,域名后的第一个/叫做web根目录

我们平时上网:1.想获取什么内容 2.我们想上传什么内容

我们想获取的内容称为资源,当这个资源没有被我们拿到的时候,资源在服务器上。

一个服务器上可能会存在很多资源,这些资源以文件的形式保存在服务端,请求资源拿到我们的本地可理解为服务进程打开要访问的文件,读取该文件,通过网络发到客户端,但要打开这个文件,首先得找到这个文件

        linux中标识一个文件,是通过路径来标识得。

URL理解:协议名称://server ip[:80]/a/b/c/d/e.html

路径加文件名就是客户要得资源。

URL通常可以用来定义互联网中唯一得一个资源,url被称作:统一资源定位符

所有得资源:在全球范围内找到它得url就能访问该资源。基于这种获取资源得方式被称作:www(万维网)

我们在百度搜索?和helloworld,第一个是?,第二个是helloworld

 我们可以看到有俩个wd,其中?被转码为%3F

 如果用户想在url中包含url本身用来作为特殊字符得字符,url在形成得时候,浏览器会自动进行编码encode,即把特殊符号用其它形式替代,一般服务端收到之后,需要进行转回特殊字符。

 转换规则是将字符的ASCII码值转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式,汉字也会被编码。

http协议

http是基于请求和响应的,这种请求响应模式叫做CS模式。

http协议是应用层的协议,底层采用的是TCP,在双方通信之前已经经历了三次握手的过程,即已经建立好了连接

 单纯在报文角度,http可以是基于行的文本协议

请求报头里会包含本次请求的相关属性,如本次请求是长连接还是短连接。下图是http的请求

http请求

http/1.1是协议版本 ,这里是客户端告诉服务器,客户端用的是哪一个版本

我们可以根据\r\n,将http请求看成线性结构

http响应

 

 这里的版本是服务器告诉客户端,服务器用的是哪一个版本。

我们暂时要关注该协议如何封装和解包,以及向上交付。

http是如何区分报头和有效载荷?http通过\r\n区分报头和有效载荷的。这样意味着我们一定能够把报头读完,空行读完接下来在读的就是正文了。

我们如何得知正文的大小呢?在报头当中,涵盖一个Cotent-Length:123 这个就是正文的长度

我们启动后在浏览器就可以进行进行http访问了,只不过我们这里没有进行请求响应

 我们当前服务端已经收到了一堆请求

 

GET请求方法,如果并未告诉服务器请求什么资源, 默认填一个/代表web根目录,并不代表要把web根目录下的所有文件发给它,而是请求默认的网页,HTTP/1.1是协议版本,告诉客户端,我的版本是1.1

 注意以上是未加响应的效果

 加一些响应

 我们访问之后会得到响应

 

细节优化

 如果我们这样访问

代表我们要请求的资源是/a/b/c目录下的d资源,第一个/是web根目录,

 web根目录我们可以自定义路径,比如我们可以把所有的网页资源放在这个路径下,我们就可以让客户端在请求资源时,请求的是wwwroot路径下的资源,wwwroot就叫做web根目录

 这个正文应该以网页的形式存在wwwroot根目录下,然后让用户去请求这个网页

 如果我们当前请求的是wwwroot目录,默认代表的是请求当前我们所对应的web服务器的默认首页

因此我们可以构建一个首页,把首页的内容返回给用户即可

 访问的时候根据,根目录去访问我们想要的资源,如果只有根目录,就默认访问/index文件,如果有对应的路径,则按照对应的路径去进行访问资源。

 当服务器读取的时候,首先得把对方发送的资源路径提取出来。我们写一个util.hpp进行路径提取

 效果

我们可以看到字符串提取成功

 

 对程序稍作修改,只提取第一行内容

 若输入IP:端口号/a/b/c/d/e/d.html,其中请求的资源路径就是a/b/c/d/e.html,若只输入IP:端口号,则代表请求根目录

我们创建一个首页

 

 当用户想访问某个路径时,我们要把文件给用户打开,此时拿到了用户想要的资源

 

 如果不带任何路径,此时其实是客户只请求了一个/,我们返回默认首页,只要输入的是空路径,其实就是输入了/,我们就默认返回首页,这里我们设置默认首页是"index.html"

makefile

HttpServer:HttpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f HttpServer

 HttpServer.hpp

#include<iostream>
#include<signal.h>
#include"Sock.hpp"
#include<functional>
class HttpServer
{
    public:
        using func_t=std::function<void(int)>;//返回值为void,参数为int类型的函数
    private:
        int listensock_;//监听套接字
        uint16_t port_;//端口号
        Sock sock;
        func_t func_;
    public:
        HttpServer(const uint16_t &port,func_t func)
            :port_(port),func_(func)
        {
            //Sock sock;
            listensock_=sock.Socket();
            sock.Bind(listensock_,port_);
            sock.Listen(listensock_);
        }
        void Start()
        {
            signal(SIGCHLD,SIG_IGN);//对子进程的信号做忽略,主进程不必阻塞等待
            for(;;)
            {
                std::string clientIp;//客户端IP
                uint16_t clientPort=0;
                //客户端端口号,默认端口号设为0
                int sockfd=sock.Accept(listensock_,&clientIp,&clientPort);
                if(sockfd<0) continue;//如果没连上,就继续连
                if(fork()==0)//如果连上了
                {
                    close(listensock_);//关闭格子不需要的文件描述符
                    func_(sockfd);//对http请求做处理
                    close(sockfd);//处理完之后关闭sockfd
                    exit(0);
                }
                close(sockfd);
            }
        }
        ~HttpServer()
        {
            if(listensock_>=0)
                close(listensock_);
        }
};

HttpServer.cc

#include<iostream>
#include"HttpServer.hpp"
#include<fstream>
#include<memory>
#include<cassert>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include"Usage.hpp"
#include"Util.hpp"
#define ROOT "./wwwroot"//一般http都要有自己的web根目录
//如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE"index.html"//默认首页
void HandlerHttpRequest(int sockfd)
{
    //1.读取请求 
    char buffer[10240];
    ssize_t s=recv(sockfd,buffer,sizeof(buffer)-1,0);
    if(s>0)
    {
        buffer[s]=0;
        //std::cout<<buffer<<"---------------------------------\n"<<std::endl;
    }
    std::vector<std::string> vline;
    Util::cutString(buffer,"\n",&vline);//字符串切割,一行一行切
    /*for(auto &iter:v)//遍历一下v,看是否切割成功
    {
        std::cout<<iter<<"\n"<<std::endl;
    }*/
    std::vector<std::string> vblock;
    Util::cutString(vline[0]," ",&vblock);//截取第一行,以空格为分隔符,把第一行切成三部分
    std::string file=vblock[1];//拿到请求的资源
    std::string target=ROOT;
    if(file=="/")
        file="/index.html";
    target+=file;
    std::cout<<target<<std::endl;
    //我们让从/wwwroot下开始查找,而不是根目录,这就是web根目录的作用
    std::string content;
    std::ifstream in(target);//打开资源文件
    //文件如果存在,就读取文件的内容,如果不存在
    if(in.is_open())//未被成功打开
    {
        std::string line;
        while(std::getline(in,line))
        {
            content+=line;
        }
        in.close();
    }
    std::string HttpResponse;
    if(content.empty())HttpResponse="Http/1.1 404 NotFound\r\n";
    //当前资源不存在
    else HttpResponse="Http/1.1 200 OK\r\n";//当前资源存在
    HttpResponse+="\r\n";//这是响应报头,我们暂时不填
    HttpResponse+=content;//正文
    //2.试着构建一个http的响应
    send(sockfd,HttpResponse.c-str(),HttpResponse.size(),0);//发送
}
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<HttpServer> httpserver(new Httpserver(atoi(argv[1]),HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

Util.hpp

#include<vector>
#include<iostream>
class Util
{
    public:
        //我们先按照报文当中的内容提取一行内容
        static void cutString(const std::string s,const std::string &sep,std::vector<std::string>*out)
        //该函数是用来切割子串的
        {
            std::size_t start=0;
            while(start<s.size())
            {
            auto pos=s.find(sep,start);//sep是分隔符,先找到分隔符
            if(pos==std::string::npos) break;
             std::string sub = s.substr(start, pos - start);
                //没找到
            out->push_back(sub);//截取子串,放到vector中
            //std::cout<<"----"<<sub<<std::endl;
            start+=sub.size();
            start+=sep.size();//走到分隔符的下一处
            }
            if(start<s.size())
            {
                out->push_back(s.substr(start));
            }
        }
};

Usage.hpp

#pragma once
#include<iostream>
#include<string>
void Usage(std::string proc)
{
    std::cout<<"\nUsage:"<<proc<<" port\n"<<std::end;
}

Sock.hpp

#pragma once
#include<iostream>
#include<cassert>
#include<pthread.h>
#include<sys/wait.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<netinet/in.h>
#include<string>
#include<signal.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<memory>
#include<cerrno>
#include<cstring>
#include"Log.hpp"
class Sock
{
    private:
        const static int gbacklog=20;//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
    public:
        Sock()
        {}
        int Socket()//创建套接字
        {
           int  listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
            //参数含义第一个:网络通信 第二个:流式通信
            if(listensock<0)//创建套接字失败
            {
                logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
                exit(2);
            }
            logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
            return listensock;
        }
        void Bind(int sock,uint16_t port,std::string ip="0.0.0.0")
        {
            //bind目的是让IP和端口进行绑定
            //我们需要套接字,和sockaddr(这个里面包含家族等名称)
            //绑定——文件和网络
            struct sockaddr_in local;
            memset(&local,0,sizeof local);//初始化local
            local.sin_family=AF_INET;
            local.sin_port=htons(port);//端口号
             inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
            //IP地址,由于我们构造的时候是IP是个空的字符串
            //所以我们可以绑定任意IP
            //我们一般推荐绑定0号地址或特殊IP
            //填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
            if(bind(sock,(struct sockaddr*)&local,sizeof local)<0)
            {
                //走到这就绑定失败了,我们打印错误信息
                logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
                exit(3);
            }
        }
        void Listen(int sock)//将套接字设置为listen状态
        {
            //因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
            if(listen(sock,gbacklog)<0)
            {
                logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
                exit(4);
            }
            logMessage(NORMAL,"init server success");
        }
        //一般经验
        //const std;;string &::输入型参数
        //std::string *:输出型参数
        //std::string &:输入输出型参数
        int Accept(int listensock,std::string *ip,uint16_t *port)//获取连接
        {
            //从套接字中获取到客户端相关的信息
                struct sockaddr_in src;
                socklen_t len=sizeof(src);
                int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
                if(servicesock<0)
                {
                    //获取连接失败
                    logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
                    return -1;
                }
                if(port) *port=ntohs(src.sin_port);//如果port被设置,获取新的port
                //客户端端口号在src
                //由于是网络发送过来得套接字信息
                //所以要把信息进行网络转主机
                if(ip) *ip=inet_ntoa(src.sin_addr);//如果Ip被设置,获取新的Ip
                //我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
                //到这里我们拿到了IP和端口号
                return servicesock;
        }
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)//进行连接
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }
        ~Sock(){}
};

Log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    // va_list ap;
    // va_start(ap, format);
    // while()
    // int x = va_arg(ap, int);
    // va_end(ap); //ap=nullptr
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    //FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    //fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    //fclose(fp);
}

wwwroot/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Linux学习</title>
</head>
<body>
    <h3>这个一个Linux课程</h3>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
</body>
</html>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

头发没有代码多

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

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

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

打赏作者

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

抵扣说明:

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

余额充值