目录
认识URL
urlencode和urldecode
如何编码解码和验证过程
一个基本的网络服务器的流程
代码验证请求与响应
准备工作
HTTPServer.hpp
Protocol.hpp
makefile
1请求
HTTPServer.hpp
1.0函数handlerHttp-基本流程
再次处理
HttpServer.cc(新建文件)
测试1 -- 请求测试
云服务器响应1
云服务器响应2
云服务器响应2解析
手机测试解析
游览器推送解析
爬虫原理
2响应
一个简单的网页
修改一下
测试2 -- 响应测试
telnet,一个测试工具,有兴趣可以去了解
客户端视角
服务端视角
浏览器请求视角
乱码解决方法
添加报头
测试3 -- 乱码解决
云服务器的配置较低可能申请失败
3分割字段
准备工作
新文件Util.hpp
2.0新函数getOneline分割出一行一行的字段
2.1新函数parse和认识新接口stringstream
HttpServer.cc修改
2.2打印工作放到外面来
测试结果4分割字段
注意细节
5修改默认web起始目录
补充知识
wwwroot是一个目录
http.conf是一个配置文件
5.1parse修改
6首页设置
6.1首页默认放到wwwroot下的一级目录中
6.2加一个判断就可以解决访问首页的问题
6.3再加一个path的打印
测试5路径拼接测试
1拼接路径
2拼接首页
全部源码
HttpServer.cc
HttpServer.hpp
makefile
Protocol.hpp
Util.hpp
关于浏览器引起的一些认识
认识URL
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
实例
wd:word缩写
如何编码解码和验证过程
一个基本的网络服务器的流程
代码验证请求与响应
准备工作
当前准备代码是基于上篇文章删改过来的,一个干净的多线程服务器
一个简单的协议定制_清风玉骨的博客-CSDN博客参考 :一个简单的协议定制_清风玉骨的博客-CSDN博客
HTTPServer.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>#include "Protocol.hpp"namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>; // 回调class HttpServer{public:HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象 -- 流式套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0if (_listensock < 0){exit(SOCKET_ERR);}// 2.bind绑定自己的网路信息 -- 注意包含头文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){exit(BIND_ERR);}// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑{exit(LISTEN_ERR);}}void start(){for (;;) // 一个死循环{// 4. server 获取新链接// sock 和 client 进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){continue;}/* 这里直接使用多进程版的代码进行修改version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock */pid_t id = fork();if (id == 0) // 当id为 0 的时候就代表这里是子进程{/* 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好 */close(_listensock);if (fork() > 0)exit(0); // 解决方法1: 利用孤儿进程特性// TODOclose(sock);exit(0);}/* 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了close(sock); *//* father那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了 */// 不需要等待了 version 2waitpid(id, nullptr, 0);}}~HttpServer() {}private:int _listensock;uint16_t _port;func_t _func;};} // namespace server
Protocol.hpp
#pragma once#include <iostream>
#include <string>class HttpRequest
{
public:std::string inbuffer;
};class HttpResponse
{
public:std::string outbuffer;
};
makefile
cc=g++
httpserver:HttpServer.cc$(cc) -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f httpserver
这里因为有天然的客户端(游览器),所以我们先暂时不需要写客户端
1请求
HTTPServer.hpp
1.0函数handlerHttp-基本流程
再次处理
记得把sock(套接字)传进来
HttpServer.cc(新建文件)
#include "HttpServer.hpp"
#include <memory>using namespace std;
using namespace server;void Usage(std::string proc)
{cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}bool Get(const HttpRequest &req, HttpResponse &resp)
{// for testcout << "----------------------- http start ------------------------------------" << endl;cout << req.inbuffer << endl; // 暂时不做其他处理,直接打印出来看请求内容cout << "------------------------ http end -------------------------------------" << endl;return true;
}// ./httpServer 8080 -- 这里实际是80端口号,不过这里是为了测试就不用了,并且80也绑定不了,前一千多号基本内部资源无法绑定
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = atoi(argv[1]);unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));httpsvr->initServer();httpsvr->start();return 0;
}
测试1 -- 请求测试
云服务器响应1
云服务器响应2
实际中应该如下图,这其实是一个高并发的请求,它一次性会请求多个,并不是一个
云服务器响应2解析
手机测试解析
游览器推送解析
爬虫原理
2响应
一个简单的网页
可以参考这个网页教程 HTML 简介_w3cschool
在VScode中创建一个这种后缀的文件,输入 ! 后按Tab键,得到一个网页的基本格式
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body></body>
</html>
修改一下
修剪成一行,因为是在c++中硬编码,所以使用起来比较麻烦,当遇到特殊字符的时候注意一下要斜杠转义一下,防止报错
测试2 -- 响应测试
telnet,一个测试工具,有兴趣可以去了解
客户端视角
服务端视角
浏览器请求视角
这次测试,明明我们并没有填写响应报头,并且还没有报头的长度,但是明显浏览器可以很好的解决这几点,并且暂时还没有出现很大的问题,把内容解释出来了。
现在在浏览器已经很智能了,它已经可以识别出内容是什么,是文本还是网页,甚至图片或者视频,这方面Chrome做的比较成熟,但是有些游览器并不会做这些,比如火狐它就会以文本的形式显示出来,网页并不会帮你做解释。
基于这一点,我们还是要填写好自己的报头,告诉浏览器我们发送过去的是一个网页。
乱码解决方法
其实这里大概可以了,不过为了完善,我们先填写好报头再进一步测试
添加报头
测试3 -- 乱码解决
云服务器的配置较低可能申请失败
浏览器会一次性高并发申请很多很多,不过有结果就行了,这方面不是我们要考虑的,这次测试有几次出错了,我感觉就是这个原因
3分割字段
准备工作
新文件Util.hpp
2.0新函数getOneline分割出一行一行的字段
2.1新函数parse和认识新接口stringstream
后者是一种流
HttpServer.cc修改
2.2打印工作放到外面来
测试结果4分割字段
注意细节
5修改默认web起始目录
补充知识
wwwroot是一个目录
http.conf是一个配置文件
这边我们为了方便就直接写入代码中,不进行配置了
default-root
5.1parse修改
6首页设置
这时候拼接会有一个问题出现
6.1首页默认放到wwwroot下的一级目录中
当访问根目录的情况下,会直接给path拼接首页路径
6.2加一个判断就可以解决访问首页的问题
6.3再加一个path的打印
测试5路径拼接测试
1拼接路径
2拼接首页
全部源码
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>using namespace std;
using namespace server;void Usage(std::string proc)
{cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}// 1. 服务器和网页分离,html
// 2. url -> / : web根目录
bool Get(const HttpRequest &req, HttpResponse &resp)
{// for testcout << "----------------------- http start ------------------------------------" << endl;cout << req.inbuffer << endl; // 暂时不做其他处理,直接打印出来看请求内容std::cout << "method: " << req.method << std::endl;std::cout << "url: " << req.url << std::endl;std::cout << "httpversion: " << req.httpversion << std::endl;std::cout << "path: " << req.path << std::endl;cout << "------------------------ http end -------------------------------------" << endl;std::string respline = "HTTP/1.1 200 OK\r\n";std::string respheader = "Content-Type: text/html\r\n";std::string respblank = "\r\n"; // 空行// 网页 -- 自己写一个简单的, 不要在C++中写html,这里是测试,很不方便std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>七夕节日我竟然在搞这个?</h1></head><body><p>好寂寞~</p></body></html>";// 直接拼接就可以了,本身很简单resp.outbuffer += respline;resp.outbuffer += respheader;resp.outbuffer += respblank;resp.outbuffer += body;return true;
}// ./httpServer 8080 -- 这里实际是80端口号,不过这里是为了测试就不用了,并且80也绑定不了,前一千多号基本内部资源无法绑定
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = atoi(argv[1]);unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));httpsvr->initServer();httpsvr->start();return 0;
}
HttpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>#include "Protocol.hpp"namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>; // 回调class HttpServer{public:HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象 -- 流式套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0if (_listensock < 0){exit(SOCKET_ERR);}// 2.bind绑定自己的网路信息 -- 注意包含头文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){exit(BIND_ERR);}// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑{exit(LISTEN_ERR);}}void HandlerHttp(int sock){// 1. 读到完整的http请求// 2. 反序列化// 3. 反序列化后得到httprequest, 回调填写httpresponse, 利用_func(req, resp)// 4. 序列化resp// 5. sendchar buffer[4096];HttpRequest req;HttpResponse resp;size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 大概率我们直接能读取到完整的http请求if(n > 0){buffer[n] = 0;req.inbuffer = buffer;req.parse();_func(req, resp); // 可以根据bool返回值进行判断,这里就不判断了send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);}}void start(){for (;;) // 一个死循环{// 4. server 获取新链接// sock 和 client 进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){continue;}/* 这里直接使用多进程版的代码进行修改version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock */pid_t id = fork();if (id == 0) // 当id为 0 的时候就代表这里是子进程{/* 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好 */close(_listensock);if (fork() > 0) exit(0); // 解决方法1: 利用孤儿进程特性HandlerHttp(sock);close(sock);exit(0);}/* 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了close(sock); *//* father那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了 */// 不需要等待了 version 2waitpid(id, nullptr, 0);}}~HttpServer() {}private:int _listensock;uint16_t _port;func_t _func;};} // namespace server
makefile
cc=g++
httpserver:HttpServer.cc$(cc) -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f httpserver
Protocol.hpp
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <sstream> // 可以直接以空格作为分隔符来进行分割#include "Util.hpp"const std::string sep = "\r\n"; // 分隔符
const std::string default_root = "./wwwroot"; // web起始目录,前面的 ./ 加不加都可以
const std::string home_page = "index.html"; // 默认首页,任何服务器都会有这个默认首页class HttpRequest
{
public:HttpRequest() {}~HttpRequest() {}void parse() // 解析{// 1. 从inbuffer中拿到第一行,分隔符\r\nstd::string line = Util::getOneline(inbuffer, sep);if(line.empty()) return;// 2. 从请求行中提取三个字段 -- 下面放开的三个std::cout << "line: " << line << std::endl; // 打印出来显示一下std::stringstream ss(line); // 可以直接以空格作为分隔符来进行分割ss >> method >> url >> httpversion;// 3. 添加web默认路径path = default_root; // 未来可以进行修改 变成 ./wwwrootpath += url; // 到这一步之后就会 变成 ./wwwroot/a/b/c.html// 未来访问路径都会从这个路径下开始访问// 这边会遇到一个问题,当url是一个 / 的时候就不行,拼接的时候会变成 ./wwwroot/ 没有具体目标if(path[path.size()-1] == '/') path += home_page; // 加一个判断就行了}public:std::string inbuffer;/* 我们可以细分许多字段,当需要什么就可以添加什么,这里为了简洁就不做这些工作了std::string reqline; // 请求行std::vector<std::string> reqheader; // 请求报头std::string body; // 请求正文*/std::string method; // 请求方法std::string url;std::string httpversion; // 请求版本std::string path; // web默认路径
};class HttpResponse
{
public:std::string outbuffer;
};
Util.hpp
#pragma once#include <iostream>
#include <string>class Util
{
public:// XXXX XXX XXX\r\nYYYYY -- 格式// 第二个参数是分隔符,暴露在外部,让外部传进来static std::string getOneline(std::string &buffer, const std::string &sep) // 类内静态方法可以直接使用 -- 为了方便写,就定义成静态的{auto pos = buffer.find(sep);if(pos == std::string::npos) return ""; // 没有找到分隔符std::string sub = buffer.substr(0, pos); // [ ) 左闭右开 拿到这一行字段buffer.erase(0, sub.size() + sep.size()); // 删除这一行return sub;}
};
关于浏览器引起的一些认识
浏览器是一款工业级软件
浏览器是一款工业级别的软件,它是电脑默认安装的软件当中开发工作量最大的软件!!!
它的开发难度非常大!即使是一款简单的基本浏览器都是几百万行起步(操作系统上千万行),开发成本特别高,这一点ps、vs studio、cad、12306之类的软件都是几百万行起步(工业级别),这些软件一般都是国外的(确是得承认,这一点国外做的比较好),从国内很多的游览器都是套壳Chrome(开源)中可以看出来(上图中有显示)
那么我们有能力做出来吗?为什么不做?
有!但是没必要!就算是操作系统我们国内业界也是有能力做出来的,但是没有意义!
就好比,做一款软件要三年,花上100人,每年30万,一年就算三千万,开发成本就将近一个亿就没了,并且投入市场,并不是就立马见效,在这个赛道上已经有人做了,做了几十年,比你的好并且稳定,那么结果显而易见,成绩并不会好,可能就直接噶掉了,这不就是打水漂吗?所以没必要!这一点之前华为的鸿蒙因为套壳Linux被骂的很惨,其实没必要,我们知道重新开发就意味着新的生态(参考以前的文章),没有人用就等于白搭,明白这一点我们就知道这其实是怎么一个情况了
那当别人突然不给我们用软件呢?
那就拿以前的版本做二次开发,这才是编程世界是最普遍的现象,也是最适合的方法
那我们的开发资源到哪里去了呢?
在那些没有人做的领域中去,大家都是从零开始,把较大的风险规避,把那些资源投入到可能让我们领先的地方,这才是正确的做法
身为一个有见识的程序员,我们得知道这一点,千万不能道听途说,随意相信那些自媒体(为黑而黑),我们得有自己的观点,知行合一