HTTP协议详解(2)
http方法
GET方法的特点
我们在请求的时候我们可以发现是有请求方法的!——现在我们看到的就是GET方法!
==那么我们该如何理解这些方法呢?==
当我们在进行网络访问的时候!我们其实是在进行两种行为的!
一,是获取资源!二,上传资源(一般交互网站都有这种功能,例如:图床)
当我们想要上传资源,例如:想要完成一次登录,进行一次搜索,那么我们该如何做呢?
实际上在进行一些网站交互的时候,我们是要通过表单的方式来进行提交的!
<form> 元素
HTML 表单用于收集用户输入。
<form> 元素定义 HTML 表单:
HTML 表单包含表单元素。
表单元素指的是不同类型的 input 元素、复选框、单选按钮、提交按钮等等。
==具体看上去就是==
==我们进行数据提交的时候本质就是前端要通过form表单提交的!浏览器会自动将form表单的内容转化为GET/POST方法进行请求!==
==我们现在使用的服务器方法默认都是GET==
我们可以验证一下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>我是首页!</title> </head> <body> 我是网站的首页! <a rel="nofollow" href="/test/a.html">新闻</a> <a rel="nofollow" href="/test/b.html">电商</a> <form action="/a/b/c.py" method="GET"><!--提交到服务器的那个路径下,method是用什么方法提交--> 姓名:<br> <input type="text" name="xname" value="用户姓名"> <!-- value就是预设内容即预设框的内容 --> <br><!--换行--> 密码:<br> <input type="password" name="ypwd" value="用户密码"> <br><br> <input type="submit" value="登录"> <!--value就是登录框上面的字--> </form> </body> </html>
==当GET在提交参数的是会自动的将参数拼接在url的后面!然后以?作为分隔符!分割父左边是要访问的网站资源,右侧是提交上来的参数!——因为参数有两个所以用&作为分隔符!==
==如果我们把方法修改成POST呢?==
POST方法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>我是首页!</title> </head> <body> 我是网站的首页! <a rel="nofollow" href="/test/a.html">新闻</a> <a rel="nofollow" href="/test/b.html">电商</a> <form action="/a/b/c.py" method="POST"><!--提交到服务器的那个路径下,method是用什么方法提交--><!-- 修改成POST--> 姓名:<br> <input type="text" name="xname" value="用户姓名"> <!-- value就是预设内容即预设框的内容 --> <br><!--换行--> 密码:<br> <input type="password" name="ypwd" value="用户密码"> <br><br> <input type="submit" value="登录"> <!--value就是登录框上面的字--> </form> </body> </html>
两种提参数方式的区别!
==那么我们该使用哪一种呢?——因为Post方式通过正文提交参数,所以一般用户看不到!所以私密性更好!(但是私密性不等于安全性!POST不比GET更加的安全!)==
==GET方法不私密!==
但是两种方法都是不安全的!因为都是可以在网上被别人直接抓取到的!要安全就必须加密!——我们使用http都是明文传送的都是不安全的!
==通过URL传参就注定了参数不能太大!——但是POST是通过正文传的!正文可以很大!甚至可以是其他的东西!==
例如:我们上传简历,上传照片,我们总不能将那些二进制数据显示在url里面,那样子就太丑陋了!所以我们可以使用正文传参!
==所以传输大数据,或者需要私密性的行为我们使用POST,其他的使用GET即可!==
当我们使用百度的时候
我们可以看到上面一堆的参数!——说明百度就是使用的是GET方法!
1、我们所谓的提交给指定的路径,有什么意义呢?
无论是url提参,还是正文提参,最终都是服务器来去使用我们的提交的参数去完成,例如:登录,注册,搜索等功能——但是凭什么呢?
我们服务器可以获取到这个数据!但是——==如何处理这个数据呢?以及我们如何得知我们想要怎么的去处理这个数据呢?如何知道你的请求是要登录还是注册呢?亦或者是其他事情呢?==
//Protocol.hpp #pragma once #include<iostream> #include<string> #include<vector> #include<sstream> #include"Util.hpp" #include<sys/stat.h> #include<sys/types.h> #include<unistd.h> const std::string sep = "\r\n"; const std::string default_root = "wwwroot";//web根目录! const std::string home_page = "index.html"; const std::string html_404 = "wwwroot/404.html"; class HttpRequest { public: HttpRequest(){} ~HttpRequest(){} void parse() { //1.从inbuffer中获取第一行!分隔符 \r\n std::string line = Util::getOneLine(inbuffer,sep); if(line.empty()) return; std::cout << "line:" << line << std::endl; //2.从请求行中提取三个字段! std::stringstream ss(line);//这个支持根据空格自动分割! ss >> method >> url >> httpversion; //////////////////////////////////////////////////////////////////////////////////////// //search?name=zhangsan&pwd=12345 // 2.1 根据?将左右两边分离 //如果是POST则本身就是分离的! //左边就是PATH,右边就是参数!——像是我们上面处理是没有考虑到参数问题的! /////////////////////////////////////////////////////////////////////////////////////// //3.添加web默认路径! path = default_root;//./wwwroot path += url; if(path[path.size() -1] == '/') path +=home_page; auto pos = path.rfind("."); if(pos == std::string::npos) suffix = ".html"; else suffix = path.substr(pos); //5.得到资源的大小! struct stat status; int n = stat(path.c_str(),&status); if(n == 0) size = status.st_size; else size = -1; } public: std::string inbuffer; std::string method; std::string url; std::string httpversion; std::string path; std::string suffix; int size; std::string parm; }; class HttpResponse { public: std::string outbuffer; };
//httpServer.cc void GET(const HttpRequest & req, HttpResponse &resp) { if(req.path == "/search") { //这里就执行的是我们自己写的C++search的方法 //使用parm作为参数,而不去执行下面的 } else { //.... } }
==这个url不一定是要真实存在的路径!——也可以是一个简单的字符串!——我们可以通过字符串来判断响应的服务!==
==我们可以将使用其他的语言进行服务提供==
//httpServer.cc void GET(const HttpRequest & req, HttpResponse &resp) { if(req.path == "test.py") { //建立进程间通信,pipe //fork创建子进程!然后使用execl("bin/python3",test.py);替换进程 //父进程,将req.parm通过管道写入给某些后端语言,py,java,php } else if(req.path == "/search") { //这里就执行的是我们自己写的C++search的方法 //使用parm作为参数,而不去执行下面的 } else { //.... } }
功能路由
既然可以根据不同的路径选择不同的功能!那么我们上面那么写就太麻烦了!功能的耦合度就太高了!我们可以修改一下!
//httpServer.hpp #pragma once #include<iostream> #include<functional> #include<string> #include<string.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #include<unistd.h> #include<signal.h> #include<unordered_map> #include"Protocol.hpp" namespace server { enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR, LISTEN_ERR, ACCEPT_ERR }; // req是输入型参数,resp是输出型参数! //保证解耦 const static int gbacklog = 5; const static uint16_t gport = 8080; using func_t = std::function<void(const HttpRequest &, HttpResponse &)>; class httpServer { public: httpServer(const uint16_t &port = gport) : port_(port), listensock_(-1) { } //...... void start() { for (;;) { signal(SIGCHLD, SIG_IGN); // 直接忽略子进程信号,那么操作系统就会自动回收 struct sockaddr_in peer; socklen_t len = sizeof(peer); int sock = accept(listensock_, (struct sockaddr *)&peer, &len); std::cout << sock << std::endl; if (sock == -1) { continue; } pid_t id = fork(); if (id == 0) { close(listensock_); HandlerHttp(sock); close(sock); exit(0); } close(sock); } } void RegisterCb(std::string servicename, func_t cb) { //这是我们自己提供的一个注册方法 funcs.insert({servicename,cb}); } ~httpServer() { } private: void HandlerHttp(int sock) { //1.获取完整的http请求 //2.对请求进行反序列化获得结构化数据! // HttpRequest req; //3.对请求进行处理! // HttpResponse resp; // func_(req,resp); //4.对响应进行序列化! //send发送请求! char buffer[1024]; ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0);//我们假设大概率直接就能读取到完整的http请求 HttpRequest req; HttpResponse resp; if(n>0) { buffer[n] = 0; req.inbuffer = buffer; req.parse(); funcs[req.path](req,resp);//这相当于可以根据未来的路径来进行绑定服务的!是什么路径就提供什么服务! send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0); } } private: int listensock_; // tcp服务端也是要有自己的socket的!这个套接字的作用不是用于通信的!而是用于监听连接的! uint16_t port_;//tcp服务器的端口 std::unordered_map<std::string,func_t> funcs; }; }
//httpServer.cc #include"httpServer.hpp" #include<memory> using namespace server; using namespace std; static void usage(std::string proc) { std::cout << "\nUsage:\n\t" << proc << " local_port\n\t\n"; } std::string suffixToDesc(const std::string& suffix) { std::string ct = "Content-Type: "; if(suffix == ".html") ct+= "text/html"; else if (suffix == ".png") ct+="image/png"; //为了如果想支持更多后缀可以自己继续加 ct+= "\r\n"; return ct; } void GET(const HttpRequest & req, HttpResponse &resp) { //... } void Search() { //... } void Other() { //... } int main(int argc,char* argv[]) { if(argc != 2) { usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); unique_ptr<httpServer> tsvr(new httpServer()); //这个就是我们http的功能路由!——通过unordered_map我们可以对不同的路径间设置!调用不同的方法,需要更多的功能就在这里进行插入注册! tsvr->RegisterCb("/",GET); tsvr->RegisterCb("/search",Search); tsvr->RegisterCb("/test.py",Other); tsvr->initServer(); tsvr->start(); return 0; }
其他的方法
==但是这些方法一般不怎么常用!——一般来说一个http这里只会暴露两种方法一种是GET一种是POST,其他方法是不会显示出来的!==
http状态码
1XX——说白了就是告诉用户,你的信息已经被受理了!请一下(例如:上传大文件,这种服务器无法一时间处理完成,那么就会返回这个)
3XX——常见的有301,302,307
301:Moved Permanently。永久重定向
302:Fount。临时重定向,但是会在重定向的时候改变 method: 把 POST 改成 GET,于是有了 307
307:Temporary Redirect。临时重定向,在重定向时不会改变 method
我们访问某些网站的时候会进行自动跳转
void GET(const HttpRequest & req, HttpResponse &resp) { cout << "---------------http begin-----------------------"<<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; std::cout << "suffix: " << req.suffix << std::endl; std::cout <<"size: " << req.size << std::endl; cout << "---------------http end-----------------------"<<endl; std::string respline ="HTTP/1.1 307 Temporary Redirect\r\n";//我们修改这里!让状态码变成307 std::string respheader = suffixToDesc(req.suffix); if(req.size > 0 ) { respheader += "Content_-Length: "; respheader += std::to_string(req.size); respheader +="\r\n"; } respheader += "Location: https://www.baidu.com/index.html\r\n";//这个Location就是告诉浏览器我们要重定向到哪里! std::string respblank = "\r\n"; // 空行 std::string body; body.resize(req.size); if(!Util::readfile(req.path,(char*)body.c_str(),body.size())) { Util::readfile(html_404,(char*)body.c_str(),body.size());//这个操作一定能成功! } resp.outbuffer += respline; resp.outbuffer += respheader; resp.outbuffer +=respblank; resp.outbuffer +=body; }
4XX——典型的错误是403说明被拒绝访问,404,资源不存在!
==这是属于客户端的错误!——不是服务端!==
举个例子:你回家向你爸要1亿,但是你爸 压根没有那么多钱,所以拒绝了你,你就说:爸,你怎么那么没用,一个亿都给不起我!别人听了肯定会认为是你的错误!而不是你爸的错误!
==我们请求的这个资源服务器本来就没有!你总不能在淘宝说要看动漫,淘宝给不了你这个资源,就说是淘宝的问题吧==
5XX——例如:500服务器错误,504路由器坏了
那么什么是服务器错误呢?——我们服务器里面有很多的创建进程,创建线程,字符串解析等等内容!——当服务器在创建进程,创建线程,申请空间等等行为失败了!那么就是服务错误!
==想知道更详细可以去看http状态码映射表==
但是实际上,我们看到状态码也不一定是是按照上面的来的!实际上很多的互联网公司那么服务器错误,也不会返回5XX,而是返回4XX
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
长链接
其实一张我们看到的网页,实际上可能由多种元素构成!——例如:网页上不仅会有文本,还会有图片等资源!
所以一张完整的网页是要多次的http请求的!
假如网页有100张图片!那么就要http请求100次!
==但是这会引发一个问题!因为频繁的发起http请求,http是基于tcp的!tcp是面向连接的!所以就会导致频繁创建连接的问题!==
以前使用就是都是使用这种方式的!因为很简单!而且网页资源都比较小!那时候,大部分的网站都是属于文本为主,图片也都不是高清的!
但是随着时代的发展,显示器分辨率的上升,对于网站的要求也提高了!
那么此时这种高频繁创建连接的方式就不合适了!
为了减少tcp在进行建立的时候,频繁创建连接的过程!——所以client和server都要支持一项技术——即**==长连接技术==**
==所谓的长连接的就是——建立好一条连接,获取一大份资源的时候,通过一条连接完成!==
==即创建一条连接后,网页发起请求后,回应!但是不释放这个连接!然后网页继续通过这个连接发起请求获取资源!==
在一条连接中http的每一个报头和空行以及有效载荷是可以被完整读取的!——这就注定了连接可以被重复使用
==那么我们该如何区分只请求一条连接和长链接呢?==
在报头里面其实有一个选项
Connection: keep-alive Connection: close
==Connection写的是keep-alive——就意味着双方都是支持长连接的!==
==如果是close——那么就意味着只能使用短连接!==
http周边会话保持
会话保持严格意义上来说不是http天然具备的!而是后面使用发现需要的!
那么什么是会话保持呢?
我们先看一个现象
如果我们在一个浏览器下面登录b站,在一般情况下,只要我们第一次登录了!后面我们每一次的访问b站,那么我们会发现我们的账号都已经自动的登录了!
但是如果我们换一个浏览器!我们另一个浏览器下的b站登录就失效了!还要重新登录一次!
http协议是无状态的!——这个是什么意思呢?也就是说,我们第一次,第二次,第三次请求,第二次不知道第一次请求过,第三次不知道第二次请求过,也就是说我们历史上请求的同一张图片!浏览器都要帮我们发起http请求!==无状态就是指不去记录历史上的各种状态信息,请求,也不去猜测下一次要访问什么!==http协议只会去执行自己所需要执行的功能!
但是为什么我们发现哪怕我们关闭网站http协议依旧能记住我们呢?——这和http协议有关么?只能说没有直接的关系!
==http是无状态的!但是用户需要!——比如说我们每一次打开b站!一次要加载10张图片,我们每一次打开这个网页我们浏览器都要帮我们去服务端请求资源!而每一次请求刷新,那会不会太慢呢?而且还有一个很重要的一点!当我们在b站进行网页跳转的时候!我们发现b站是永远都是认识我们登录的账号的!如果没有记录每一次跳转我们都要重新登录一次b站那么未免也太麻烦了!——所以对于变化不怎么大的资源会浏览器进行缓存!==
==这就是http是无协议的!但是用户需要,因为用户查看新的网页是常规操作,如果发生了网页跳转,那么新的页面也就无法识别是哪一个用户了!为了用户在一经登录,就可以在整个网站按照自己的身份进行随意访问!——这种现象就是会话保持!==
最后重新强调一遍——http协议只是一个简单的协议!是被使用的,就是帮助别人把资源获取下来!会话这个过程http是没有直接参与的!但是为了支撑这个,http协议间接参与了
那么这是如何做到会话保持呢?
最常使用的是cookie技术——来帮助保持会话!
cookie分为——cookie文件和cookie内存
浏览器本质也是一个进程!——我们关闭浏览器后(杀死进程),重新打开浏览器,我们打开网站依旧保持着会话状态!(仍然登录)——==则说明这个cookie是文件级别的否则,进程退出后,进程的内存就应该被释放!==
还有一种现象——就是我们登录一次后虽然可以不用登录了!但是如果我们退出浏览器(杀死进程)那么就要重新登录了!因为进程的内存都被释放了!——那么这就是内存级cookie
==但是使用cookie技术有一个问题==
其实不止浏览器使用这种方式,有些通信软件也是使用这种保存方式——例如QQ我们登录一次后选择,记住密码!那么qq也在某些文件下面里面保存我们的信息!
每一次我们打开的时候!那么qq就会自动的推送!
==如果聊天软件被盗用——那么影响就会很大了!==
==所以一共有两个问题!——1.是服务器误认的问题!2.是cookie文件里面保存了我们的账号密码!被拿到之后别人就能知道我们的账号密码,即用户信息的泄漏的问题!==
==那么为了解决这种问题!所以提出来一种新的技术==
我们账号会被别人盗走的根本原因是不是因为我们将账号密码放在文件里面了?——其实并不是!而是因为这个文件在客户端文件里面!
如果放在客户端那么不法分子就很有可能通过某些方法获取到——用户本身对自己信息的保存能力是有限的!
==那么使用这种方式的最大区别就在于——用户信息被存储在了服务端!==
我们上面说过开始的本地存储方式有两个问题!——一个是服务器误认,一个是用户信息泄露!——在这里我们可以认为已经大大改善其中一个==用户信息泄露的问题!==——即使黑客拿到了cookie里面也没有我们的用户信息了!
==如果单纯地以今天的我们讲的技术来说——这个问题是无法解决的!==
==所以要配合其他的策略来缓解问题的!==
例如:我们去旅游,一天内从新疆到了广州,进行跨地区,此时我们就会发现,会出现一个ip异常登录的情况!会直接下线要求我们重新输入账号和密码!——因为用户信息没有泄漏了!那么只要服务器一下线就会让session id失效!那这样子就很简单了!==只要服务器在其他策略的配合下发现你是非法用户,直接让其session id失效即可!只有有密码的哪一个可以重新登录!——这就是用户信息存储在服务端的优点!==
其他策略的用途就是甄别那些session id是需要重新失效的!
==client:cookie,sever:session——这就是现在我们主流的使用方案!==
现在又几个新的问题
1.服务器是如何写入cookie信息的?
2.如何验证client会携带cookie信息?
==如何网浏览器里面写入cookie呢?==
void GET(const HttpRequest & req, HttpResponse &resp) { cout << "---------------http begin-----------------------"<<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; std::cout << "suffix: " << req.suffix << std::endl; std::cout <<"size: " << req.size << std::endl; cout << "---------------http end-----------------------"<<endl; std::string respline ="HTTP/1.1 200 ok\r\n"; std::string respheader = suffixToDesc(req.suffix); if(req.size > 0 ) { respheader += "Content_-Length: "; respheader += std::to_string(req.size); respheader +="\r\n"; } std::string respblank = "\r\n"; // 空行 std::string body; body.resize(req.size); if(!Util::readfile(req.path,(char*)body.c_str(),body.size())) { Util::readfile(html_404,(char*)body.c_str(),body.size());//这 } //如何返回cookie信息? respheader += "Set-Cookie: name=12345678bcdefg\r\n";// //我们后面的串数字以后可以套上认证逻辑,形成session之后将信息保存在session文件里面!然后将session的id直接返回即可! //这就是将session信息写入到浏览器中! resp.outbuffer += respline; resp.outbuffer += respheader; resp.outbuffer +=respblank; resp.outbuffer +=body; }
==只要往报头里面加入Set-Cookie行即可!==
respheader += "Set-Cookie: aaa=xxxxxxxxxxxxxx\r\n";
xxx里面就是要写入的内容!aaa就是形成的cookie文件的名称!
cookie还能设置到期时间!
respheader += "Set-Cookie: aaa=xxxxxxxxxxxxxx\r\n; Max-Age=60\r\n";
==在这个行的后面加上Max-Age即可!——我们就给cookie设置了一个到期时间60s!==
==我们的服务端也会接收到client发送的cookie!==
==http请求里面就会有一个cookie!以空格作为分隔符!发送个服务端!==
为了就可以从请求中提取cookie重新知道它的内容!然后在后端进行认证!
==往后的每一次http请求,都会自动携带曾经设置的所有cookie!(记住是所有!)帮服务器进行鉴权行为!——进而支持http会话保持!==
可以看到客户端给我返回的cookie里面就包含了我曾经设置的两个cookie!
==我们也可以尝试扎抓取百度的网页==
telnet www.baidu.com 80
==我们可以看到当我们在访问百度的时候!百度就给我们本身设置了很多cookie!也给我们设置了cookie的失效时间!==