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

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

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

目录

 准备文件

 makefile

HttpServer.hpp

类内成员

封装sockfd

start

 ThreadRun

 全部代码

运行结果

响应书写

Web路径


 准备文件

        首先准备文件:

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

日志程序:

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



Socket套接字:

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

 makefile

        先将mkefile准备出来:

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

HttpServer.hpp

类内成员

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进行一下封装:

struct ThreadData { int sockfd; }; 

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

start

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

 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中读取数据到缓冲区当中。 

 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; }

 全部代码
 

#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。 这个就是请求到服务器的机器的信息。 就是我们利用一台机器访问一个网站或者网页,笼统的说叫做资源。 然后浏览器就把我们的机器的信息传给了服务端。服务端接收的时候就能接收到了。

响应书写

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

 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就是处理请求的函数。 这个函数里面是先接收请求。 然后就进行响应。

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

 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语句, 我们把正文部分改成下面的语句:

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

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

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文件:

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

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

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

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

        封装代码:

 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根目录, 就根据我们的想法, 想在哪就在哪!不需要改变程序, 只需要改配置文件!!!!

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

 //有了这个请求类之后, 我们以后所有的请求都放到这里对象里面。 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, 我们是不是就访问到了一个首页? 所以, 我们的路径还要处理一下:

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

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

#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 */ 

 

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

Read more

Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析

Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析

◆ 博主名称: 晓此方-ZEEKLOG博客大家好,欢迎来到晓此方的博客。⭐️C++系列个人专栏: 主题曲:C++程序设计⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰 0.1概要&序論 这里是「此方」,好久不见。 今天我们要学习的是二叉搜索树。它是在普通二叉树的基础上加入特定约束,从而具备了高效的搜索能力。虽然这种结构能够支持高效的插入、删除与查找操作,但其性能背后也隐藏着潜在的 效率风险 。同时,在 key 与 key-value 两种不同的应用场景 下,二叉搜索树的设计与实现方式也会产生不同的变化。这里是「此方」。让我们现在开始吧! 前情提要,没有系统学习过一般二叉树的小伙伴直接看这篇文章可能会有些吃力,此方在这里留一个传送门:Re:从零开始的链式二叉树:建树、遍历、计数、查找、判全、销毁全链路实现与底层剖析 一,二叉搜索树的概念

By Ne0inhk
第十六届蓝桥杯省赛(软件类真题)C/C++ 大学A组

第十六届蓝桥杯省赛(软件类真题)C/C++ 大学A组

大纲: A.寻找质数 B:黑白棋 题目&解析&代码 A题 题目解析 本题的目标是枚举质数并计数,直到数到第2025个。由于2025不算太大,第2025个质数大约在17000~18000之间,完全可以在合理时间内通过简单枚举得到。 解题步骤: 从2开始遍历每个整数,判断它是否是质数。 质数判断采用试除法:对于一个数n,只需检查从2到√n的所有整数是否能整除n。若存在能整除的数,则n不是质数;否则是质数。 每找到一个质数,计数器加1。 当计数器达到2025时,输出当前的质数并结束。 优化点: 除了2以外,偶数不可能是质数,因此可以跳过偶数判断(直接步进2)。 在isPrime函数中,可以先处理特殊情况(n<2返回false),然后单独判断偶数,再对奇数进行试除,步进也可以设为2。 C++ 参考代码 以下代码实现了上述算法,并输出第2025个质数。 cpp

By Ne0inhk
C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板 💡 学习目标:掌握模板进阶技术的核心用法,理解模板特化的深层应用、类型萃取的实现原理,以及可变参数模板的灵活使用,提升泛型编程的实战能力。 💡 学习重点:模板特化的进阶场景、类型萃取工具的设计与应用、可变参数模板的展开技巧、折叠表达式的使用方法。 一、模板特化进阶:处理复杂类型场景 💡 模板特化不只是针对单一类型的定制,还能处理指针、引用、数组等复杂类型,实现更精细的类型适配逻辑。 1.1 指针类型的模板特化 通用模板默认处理普通类型,我们可以为指针类型单独编写特化版本,实现指针专属的逻辑。 #include<iostream>#include<string>usingnamespace std;// 通用模板:处理普通类型template<typenameT>classTypeProcessor{public:staticvoidprocess(T data){ cout

By Ne0inhk

JSP 文件上传详解

JSP 文件上传详解 引言 在Web开发中,文件上传是一个常见的功能,它允许用户将文件从客户端发送到服务器。Java Server Pages(JSP)作为一种强大的服务器端技术,也支持文件上传功能。本文将详细讲解JSP文件上传的实现过程,包括技术原理、实现步骤和注意事项。 技术原理 JSP文件上传主要依赖于HTTP协议的multipart/form-data编码类型。这种编码类型允许表单中包含文件类型的输入字段。当用户提交表单时,浏览器会将表单数据以文件的形式发送到服务器。 服务器端使用Java的javax.servlet包中的HttpServletRequest和HttpServletResponse对象来接收这些文件。同时,javax.servlet包中的javax.servlet.http模块提供了Part接口,用于访问上传的文件内容。 实现步骤 以下是使用JSP实现文件上传的基本步骤: 1. 创建HTML表单 首先,我们需要创建一个HTML表单,其中包含一个文件类型的输入字段。以下是一个简单的示例: <form action="upload.jsp"

By Ne0inhk