Linux网络 | 理解Web路径 以及 实现一个简单的helloworld网页

**前言:**本节内容承接上节课的http相关的概念, 主要是实现一个简单的接收http协议请求的服务。这个程序对于我们理解后面的http协议的格式,报头以及网络上的资源的理解, 以及本节web路径等等都有着重要作用。 可以说我们就用代码来理解这些东西。 那么废话不多说, 现在开始我们的学习吧。

ps:本节内容建议先看一下上一篇文章http的相关概念哦:linux网络 | 深度学习http的相关概念-CSDN博客

目录

准备文件

makefile

HttpServer.hpp

类内成员

封装sockfd

start

ThreadRun

全部代码

运行结果

响应书写

Web路径


准备文件

首先准备文件:

这里面Httpserver.cc用来运行接收http请求的服务。 HttpServer.hpp用来定义http请求。Log.hpp就是一个打印日志的小组件, Socket.hpp同样是套接字的组件。 到使用直接调用相关接口即可。(Log.hpp和Socket.hpp如何实现不讲解, 如果想要知道, 请看博主的相关文章)

日志程序:

linux进程间通信------命名管道、 日志程序_进程间通信日志系统-CSDN博客

Socket套接字:

linux网络 | 序列化反序列化的概念 与 结合网络计算器深度理解-CSDN博客

makefile

先将mkefile准备出来:

cpp 复制代码
HttpServer:Httpserver.cc
	g++ -o $@ $^ -std=c++11 -g -lpthread
.PHONY:clean
clean:
	rm -rf HttpServer

HttpServer.hpp

类内成员

cpp 复制代码
class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport) 
        :port_(port)  
    {}

    static void* ThreadRun(void* args)
    {

    }

    void start()
    {


    }



    ~HttpServer()
    {};
private:
    Socket listensock_;
    uint16_t port_;
};

类内的成员变量就是port_端口号, 到时候启动服务, 就输入一个端口号来启动我们的服务。 然后listensock是我们要接收到哪个主机的请求。 所以我们可以在开始工作的时候再初始化的同时直接accept进行连接。 这个ThreadRun是因为博主要用线程来管理服务, 这个函数就是线程要执行的方法。

封装sockfd

封装sockfd就是对scokfd进行一下封装:

cpp 复制代码
struct ThreadData
{
    int sockfd;
};

这么做的目的是为了能够将ThreadData的指针传给线程, 让线程拿到sockfd。就是ThreadRun这个函数。 这个函数创建在类内必须是静态成员。 否则就不能作为线程的执行方法。 而变成静态成员又不能直接使用sockfd。 所以我们就使用了ThreadData*类型的对象传给线程方法。 这样线程就能使用sockfd了。

start

看一下start函数, start就是服务启动后, 就执行这个函数。 先初始化, 再绑定, 然后开启监听。 然后就接收服务就行。 当有请求发来时, 那么listensock就能与对方建立连接获得sockfd。 拿到sockfd就封装起来传给线程, 让线程去执行。

cpp 复制代码
    void start()
    {
        listensock_.InitSocket();  //初始化sockfd
        listensock_.Bind(port_);   //绑定
        listensock_.Listen();      //监听
        for (;;)   
        {
            string clientip;         //请求方ip
            uint16_t clientport;     //请求方port
            //建立连接:
            int sockfd = listensock_.Accept(&clientip, &clientport);
            
            //接收请求
            pthread_t tid;
            ThreadData* td = new ThreadData();
            td->sockfd = sockfd;
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }

ThreadRun

线程执行的过程就是创建一个缓冲区, 然后从sockfd中读取数据到缓冲区当中。

cpp 复制代码
    static void* ThreadRun(void* args)
    {
        pthread_detach(pthread_self());          //先让线程分离。
        //将args,也就是封装起来的ThreadData类型强转一下。 
        ThreadData* td = static_cast<ThreadData*>(args);
        //创建缓冲区。
        char buffer[10240];
        ssize_t n = read(td->sockfd, buffer, sizeof(buffer) - 1);  //从某个地方读取, 然后读取的数据放到buffer里面。 从哪里读取, 
        if(n > 0)
        {
            buffer[n] = 0;
            cout << buffer;
        }
        close(td->sockfd);
        delete td;
        return nullptr;
    }

全部代码

cpp 复制代码
#ifndef BE0E1813_421A_4BCD_A33B_77432A3CA8D7
#define BE0E1813_421A_4BCD_A33B_77432A3CA8D7

#include<iostream>
#include"Socket.hpp"
#include"Log.hpp"
#include<pthread.h>


//创建端口号。
static const int defaultport = 8080;


struct ThreadData
{
    int sockfd;
};



class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport) 
        :port_(port)  
    {}

    static void* ThreadRun(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);
        char buffer[10240];
        ssize_t n = read(td->sockfd, buffer, sizeof(buffer) - 1);  //从某个地方读取, 然后读取的数据放到buffer里面。 从哪里读取, 
        if(n > 0)
        {
            buffer[n] = 0;
            cout << buffer;
        }
        close(td->sockfd);
        delete td;
        return nullptr;
    }

    void start()
    {
        listensock_.InitSocket();
        listensock_.Bind(port_);
        listensock_.Listen();
        for (;;)
        {
            string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            pthread_t tid;
            ThreadData* td = new ThreadData();
            td->sockfd = sockfd;
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }



    ~HttpServer()
    {};
private:
    Socket listensock_;
    uint16_t port_;
};







#endif /* BE0E1813_421A_4BCD_A33B_77432A3CA8D7 */

运行结果

先启动服务

然后就是打开浏览器, 输入我们的服务器ip:端口号。 就能请求到这个服务了。 然后就能看到我们的服务这里有了反应:

这就说明, 我们浏览器, 确实能够访问到我们的创建的http服务。

在上面获得的这些信息中, 我们看一下这个User-Agent。 这个就是请求到服务器的机器的信息。 就是我们利用一台机器访问一个网站或者网页,笼统的说叫做资源。 然后浏览器就把我们的机器的信息传给了服务端。服务端接收的时候就能接收到了。

响应书写

我们可以将响应单独封装起来。 如下:

cpp 复制代码
    static void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);  //从某个地方读取, 然后读取的数据放到buffer里面。 从哪里读取, 
        if(n > 0)
        {
            buffer[n] = 0;
            cout << buffer;
            //
            //返回响应的过程。


            
        }

        close(sockfd);
    }

    static void* ThreadRun(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);
        
        HandlerHttp(td->sockfd);
      
        delete td;
        return nullptr;
    }

到时候线程执行方法, 就执行ThreadRun函数就行了。然后ThreadRun函数就去处理请求。

HandleHttp就是处理请求的函数。 这个函数里面是先接收请求。 然后就进行响应。

关于响应我们上节内容讲到过, 第一行就是对响应行。 然后下面的多行就是报头。 最后报头和正文部分有空行。 现在我们书写一下:

cpp 复制代码
    static void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);  //从某个地方读取, 然后读取的数据放到buffer里面。 从哪里读取, 
        if(n > 0)
        {
            buffer[n] = 0;
            cout << buffer;
            //
            //返回响应的过程。
            
            //先写正文内容
            string text = "hello world";

            //然后写一下响应行
            string response_line = "HTTP/1.0 200 OK\r\n";

            //然后再写报头, 这里我们的报头只写正文的长度, 因为我们只讲了正文长度。 剩下的属性后面再讲。
            string response_header = "Content-Length: ";
            response_header += to_string(text.size());
            response_header += "\r\n";

            //空行
            string blank_line = "\r\n";

            //然后把所有的数据加到要发送到相应里面。
            string response;
            response += response_line;
            response += response_header;
            response += blank_line;
            response += text;
            //发送
            send(sockfd, response.c_str(), response.size(), 0);
        }

        close(sockfd);
    }

然后我们运行之后, 再从浏览器进行请求就请求到了字符串资源:hello world:

这样我们就能实现一个简单的处理http请求的服务了。

Web路径

我们知道, 我们的http服务, 我们平时使用浏览器, 都是访问一个网页, 或者访问一张图片。

其实, 我们的正文这里是可以写一个网页, 作为响应发送给请求方。 那么网页怎么写, 这个是Web的知识点,本节不做讲解(其实博主也不会, 但是博主特意找了一个学过Web的朋友, 请他把他的Web大作业给了博主。 现在博主有一个html了)。

我们这里简单的使用一下Web语句, 我们把正文部分改成下面的语句:

cpp 复制代码
string text = "<html><body>hello world</body></html>";  //其中html代表html文档, body就是代表网页正文。 /就是代表正文结尾或者文档结尾。

我们也可以给hello world做成标签:

cpp 复制代码
string text = "<html><body><h3>hello world</h3></body></html>";  //h1到h6是六级标签, /的意思同样是结束标志

然后我们再运行就能看到这个world变了。

我们做出一个简单的网页之后。

但是我们知道, 我们要访问的资源, 其实是带有路径的。 而路径的根目录是web根目录。 那么这个web路径和我们上面写的正文字符串有什么联系呢?

那么我们这里还要知道另一个知识点, 就是如果我们只是访问ip + 端口号。 那么浏览器会默认给我们访问/(web根目录), 访问的是web根目录下面的资源; 如果后面加了路径, 那么就被称作web路径, 访问的是对应的web路径下面的资源。

但是我们上面是使用的字符串作为正文啊, 不是一个路径, 就是一个字符串。所以无论我们后面加什么路径, 访问得到的相应都是hello world。

所以我们上面写的字符串其实不是正统的网页写法。 正统的网页, 应该是一个文件!!!

所以, 我们现在就可以捋清楚服务器响应请求的过程:

就是我们现在可以写出自己的报头, 写出请求行。 然后我们可以把资源目录(web目录)拼接到正文。

然后用户在访问我们的服务器时, 就可以根据他想要的资源, 访问相应的路径!!!!

那么, 这个资源目录怎么拼接到正文, 知道了这个, 我们也就知道了什么是Web路径。

Web根目录, 其实是可以自己指定的, 可以是linux的根目录, 也可以是当前目录,也可以是其他路径。

现在, 我们定义一个Web根目录为当前目录下的wwwroot文件:

cpp 复制代码
const string wwwroot =  "./wwwroot"; //定义web根目录为当前目录。

所以, 未来, 我们的wwwroot文件夹就是一个web目录。 以后我们的网页, 我们的图片, 都放到这个文件下面。 用户访问网页的时候, 只能以该目录为根节点, 往下访问!!!!而往下访问到的任何一个路径, 都叫做Web路径!!!

有了Web目录之后, 我们以后写的html代码就写到Web目录里面:

为了让我们的正文不再是静态的代码, 而是一个根据我们用户的请求, 想要访问的网页, 那么我们可以使用read函数读取网页。

接下来的工作就是读取文件!

封装代码:

cpp 复制代码
   static string ReadWebContent(string path)
    {
        //注意, 这里读取文件可能读到的是不完整的!所以有坑, 但是本节内容不管。
        ifstream in(path);   //打开对应路径的文件
        if (!in.is_open()) return "404";   //文件路径没有, 直接返回404

        string content;
        string line;
        while (getline(in, line))  //一行一行的读取
        {
            content += line;
        }
        in.close();
        return content;   //读取完毕之后返回结果
    }

封装好了函数之后, 我们以后text就直接等于text = ReadWebContent(某个路径)就行了:

而这个路径, 不正是用户发来的请求里面包含的吗?

所以,我们从请求里面提取路径,而我们说路径是在url里面的, 而url又在请求行的第二个部分。只要我们从这里面得到路径, 假如是/a/b/c, 那么我们再wwwroot += /a/b/c, 不就等于我们要访问的是./wwwroot/a/b/c了吗?所以, wwwroot,就叫做Web根目录!而且, 我们还可以直接创建一个配置文件,就叫config

这样以后, 我们的Web根目录, 就根据我们的想法, 想在哪就在哪!不需要改变程序, 只需要改配置文件!!!!

所以, 接下来的工作就是在处理请求:我们需要重新定义一个请求类:

cpp 复制代码
//有了这个请求类之后, 我们以后所有的请求都放到这里对象里面。
class Request
{
public:
    void Deserialize(string req)
    {
        string tmp;
        int pos = 0;

        //切割字符串
        while (true)
        {
            pos = req.find(sep);
            if (pos == string::npos) break;
            string temp = req.substr(0, pos);
            if (temp.empty()) break;

            req_header.push_back(temp);
            req.erase(0, pos + sep.size());
        }



        //剩下的都是正文
        text = req;

        DebugPrint();
    }

    //对请求进行打印。
    void DebugPrint()
    {
        cout << "------------------------------------------------------------" << endl;
        for(auto& e : req_header)
        {
            cout << e << endl << endl; 
        }
        cout << text << endl;
    }
public:
    vector<string> req_header;    //请求行
    string text;    //请求正文

};

然后我们测试一下我们写的请求类是否正确, 先不写了响应, 先Debug一下:

是正确的, 接下来, 我们就要拿到url。 所以我们要给request进一步分割:

然后就可以拿到路径了。 但是这里还有最后一个问题。就是如果我们的用户访问的是根目录/, 那么不久拿到了当前路径下的所有资源了吗? 但是实际上我们在访问网页的时候只会访问到一个网页, 比如www.baidu.com, 我们是不是就访问到了一个首页? 所以, 我们的路径还要处理一下:

然后我们的所有代码就完成了, 下面看一下运行结果:

我们可以发现, 我们访问到了!以上就是我们的所有内容啦, 下面是全部代码!

cpp 复制代码
#ifndef BE0E1813_421A_4BCD_A33B_77432A3CA8D7
#define BE0E1813_421A_4BCD_A33B_77432A3CA8D7

#include<iostream>
#include"Socket.hpp"
#include"Log.hpp"
#include<sstream>
#include<pthread.h>
#include<vector>
#include<fstream>


//创建端口号。
static const int defaultport = 8080;
const string wwwroot =  "./wwwroot"; //定义web根目录为当前目录下的wwwroot 。
const string sep = "\r\n";
const string homepage = "index.html";

class HttpServer;

struct ThreadData
{
    ThreadData(int sock) 
        : sockfd(sock)
    {}
    int sockfd;
};


//有了这个请求类之后, 我们以后所有的请求都放到这里对象里面。
class Request
{
public:
    void Deserialize(string req)
    {
        string tmp;
        int pos = 0;

        //切割字符串
        while (true)
        {
            pos = req.find(sep);
            if (pos == string::npos) break;
            string temp = req.substr(0, pos);
            if (temp.empty()) break;

            req_header.push_back(temp);
            req.erase(0, pos + sep.size());
        }

        parse();
        //剩下的都是正文
        text = req;

        DebugPrint();
    }

    //对请求进行打印。
    void DebugPrint()
    {
        cout << "------------------------------------------------------------" << endl;
        for(auto& e : req_header)
        {
            cout << e << endl << endl; 
        }

        cout << method << endl << url << endl <<  http_version << endl << path << endl; 

        cout << text << endl;
    }

    //解析第一行
    void parse()
    {
        stringstream ss(req_header[0]);  //stringstream要包含头文件sstream 
        ss >> method >> url >> http_version;

        path = wwwroot;   //    ./wwwroot
        if (url == "/" || url == "/index.html") 
        {
            path += "/";  //   ./wwwroot/
            path += homepage;     //    ./wwwroot/a/b/c
        }
        else
        {
            path += url;
        }
    }
public:
    vector<string> req_header;    //请求行
    string text;    //请求正文

    string method;
    string url;
    string http_version;

    string path;
};



class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport) 
        :port_(port)  
    {}

    static string ReadWebContent(string str)
    {
        //注意, 这里读取文件可能读到的是不完整的!所以有坑, 但是本节内容不管。
        ifstream in(str);
        if (!in.is_open()) return "404";   //文件路径没有, 直接返回404

        string content;
        string line;
        while (getline(in, line))
        {
            content += line;
        }
        in.close();
        return content;
    }

    static void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);  //从某个地方读取, 然后读取的数据放到buffer里面。 从哪里读取, 
        if(n > 0)
        {
            buffer[n] = 0;

            //处理请求
            Request req;
            req.Deserialize(buffer);
            req.DebugPrint();

 
            
            //返回响应的过程。
            //先写正文内容
            string text = ReadWebContent(req.path);
            //然后写一下响应行
            string response_line = "HTTP/1.0 200 OK\r\n"; 

            //然后再写报头, 这里我们的报头只写正文的长度, 因为我们只讲了正文长度。 剩下的属性后面再讲。
            string response_header = "Content-Length: ";
            response_header += to_string(text.size());
            response_header += "\r\n";

            //空行
            string blank_line = "\r\n";

            //然后把所有的数据加到要发送到相应里面。
            string response;
            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);
        
        HandlerHttp(td->sockfd);
      
        delete td;
        return nullptr;
    }

    void start()
    {
        listensock_.InitSocket();
        listensock_.Bind(port_);
        listensock_.Listen();
        for (;;)
        {
            string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            pthread_t tid;
            ThreadData* td = new ThreadData(sockfd);
            td->sockfd = sockfd;
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }



    ~HttpServer()
    {};
private:
    Socket listensock_;
    uint16_t port_;
};







#endif /* BE0E1813_421A_4BCD_A33B_77432A3CA8D7 */

------------------以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

相关推荐
大面积秃头7 小时前
Http基础协议和解析
网络·网络协议·http
我也要当昏君8 小时前
6.3 文件传输协议 (答案见原书 P277)
网络
Greedy Alg9 小时前
Socket编程学习记录
网络·websocket·学习
刘逸潇20059 小时前
FastAPI(二)——请求与响应
网络·python·fastapi
软件技术员9 小时前
使用ACME自动签发SSL 证书
服务器·网络协议·ssl
我也要当昏君10 小时前
6.4 电子邮件 (答案见原书 P284)
网络协议
Mongnewer10 小时前
通过虚拟串口和网络UDP进行数据收发的Delphi7, Lazarus, VB6和VisualFreeBasic实践
网络
我也要当昏君10 小时前
6.5 万维网(答案见原书P294)
网络
嶔某11 小时前
网络:传输层协议UDP和TCP
网络·tcp/ip·udp
文火冰糖的硅基工坊11 小时前
[嵌入式系统-154]:各种工业现场总线比较
网络·自动驾驶·硬件架构