喜歡的點(diǎn)贊,收藏,關(guān)注一下把!
目前基本socket寫(xiě)完,一般服務(wù)器設(shè)計(jì)原則和方式(多進(jìn)程、多線程、線程池)+常見(jiàn)的各種場(chǎng)景,自定義協(xié)議+序列化和反序列化都已經(jīng)學(xué)過(guò)了。
那有沒(méi)有人已經(jīng)針對(duì)常見(jiàn)場(chǎng)景,早就已經(jīng)寫(xiě)好了常見(jiàn)的協(xié)議軟件,供我們使用呢?
當(dāng)然了,最典型的HTTP。未來(lái)它們做的事情和我們以前做的事情是一樣的!只不過(guò)HTTP是結(jié)合它的應(yīng)用場(chǎng)景來(lái)談的。
雖然我們現(xiàn)在關(guān)于http協(xié)議不知道它是什么。但我們知道你的http協(xié)議里面必有套接字,必有序列化和反序列的機(jī)制,也必須添加報(bào)頭和分離報(bào)頭的過(guò)程等等。
在說(shuō)這個(gè)http協(xié)議之前,我們先做一個(gè)預(yù)備工作,在網(wǎng)絡(luò)基礎(chǔ)一我們知道OSI分七層前三層分別是應(yīng)用層,表示層,會(huì)話層。在TCP/IP協(xié)議這三層合起來(lái)算一層應(yīng)用層。
在上篇文章說(shuō)過(guò)我們寫(xiě)的網(wǎng)絡(luò)版計(jì)數(shù)器軟件分成三層。第一層獲取鏈接多進(jìn)程或者多線程或線程池進(jìn)行處理,第二層handlerEntery進(jìn)行讀取完整報(bào)文、提取有效載荷、序列化反序列化等一系列工作,第三層進(jìn)行業(yè)務(wù)處理callback。其實(shí)我們自己寫(xiě)的的第一層就是會(huì)話層,第二層就是表示層,第三層callback進(jìn)行對(duì)應(yīng)的業(yè)務(wù)邏輯處理就是應(yīng)用層。
OSI定義成七層,原因就是后面寫(xiě)代碼時(shí)每一層都少不了。OSI為什么沒(méi)把這三層壓成一層呢?在于表示層有自定義的方案、Json方案、protobuf方案、xml方案等等,如果它某種方案寫(xiě)到內(nèi)核里就固定下來(lái)了,而實(shí)際上我們并沒(méi)有固定。
http作為應(yīng)用層協(xié)議它也要解決剛才說(shuō)的三個(gè)工作。
1.認(rèn)識(shí)URL
平時(shí)我們俗稱的 “網(wǎng)址” 其實(shí)就是說(shuō)的 URL
https://blog.csdn.net/fight_p/article/details/137103487
https -> 協(xié)議
blog.csdn.net -> 域名,域名等價(jià)于IP,這里會(huì)有一個(gè)域名解析的工作(把域名這個(gè)字符串結(jié)構(gòu)轉(zhuǎn)化成IP地址),IP標(biāo)識(shí)一臺(tái)網(wǎng)絡(luò)主機(jī)(Linux系統(tǒng))
/fight_p/article/details/137103487 -> 文件路徑
URL的作用就是,瀏覽器通過(guò)拿著這個(gè)URL,找到這臺(tái)Linux機(jī)器然后在這臺(tái)機(jī)器上找對(duì)應(yīng)的文件。把文件打開(kāi)返回給瀏覽器。
實(shí)際上URL有多種格式。
為什么我們剛才URL沒(méi)有端口呢?
我們對(duì)應(yīng)的協(xié)議是和端口號(hào)強(qiáng)相關(guān)的。服務(wù)器端口號(hào)是一個(gè)眾所周知的端口號(hào),一般不能隨便改變,剛才URL沒(méi)有寫(xiě)出來(lái)并不代表沒(méi)有,因?yàn)榭蛻舳嗽L問(wèn)服務(wù)器端一定要知道服務(wù)端的是IP地址和端口號(hào)。這里沒(méi)有但瀏覽器在真正請(qǐng)求時(shí)會(huì)給我們填上,瀏覽器結(jié)合我們用的協(xié)議就知道用的端口號(hào)是多少。默認(rèn)一些協(xié)議對(duì)應(yīng)的端口號(hào):
http:80
https:443
這里圈起來(lái)的是什么東西呢?
其實(shí)它并不是根目錄,而是web根目錄,一般而言,可以是Linux下的任意一個(gè)目錄。這個(gè)任意目錄必須要有對(duì)應(yīng)請(qǐng)求的資源。(后面寫(xiě)代碼的時(shí)候具體解釋)
http是文本傳輸協(xié)議。說(shuō)白了http協(xié)議就是從服務(wù)器拿下來(lái)對(duì)應(yīng)的 “資源”。
什么是資源呢?
凡是你在網(wǎng)絡(luò)中看到的都是資源!(如:刷的短視頻是視頻文件、淘寶上看到的圖片是圖片文件,網(wǎng)易云聽(tīng)的音樂(lè)是音樂(lè)文件。。。)
所有的資源都可以看做資源文件,在服務(wù)器中都是以文件形式存在磁盤中的文件系統(tǒng)中的某一個(gè)路徑下,所以需要Linux系統(tǒng)的路徑結(jié)構(gòu)。當(dāng)要的時(shí)候把磁盤中對(duì)應(yīng)的路徑所標(biāo)識(shí)的資源返回給客戶端。所以http協(xié)議本質(zhì)上是從服務(wù)器上拿文件資源。
因?yàn)槲募Y源的種類特別多,http都能搞定,所以http是 “超文本傳輸協(xié)議”。
2.urlencode和urldecode
像 / ? : 等這樣的字符, 已經(jīng)被url當(dāng)做特殊意義理解了。因此這些字符不能隨意出現(xiàn)。比如, 某個(gè)參數(shù)中需要帶有這些特殊字符,就必須先對(duì)特殊字符進(jìn)行轉(zhuǎn)義。
如果原封不動(dòng)會(huì)干擾url的正確解析,所以瀏覽器這端必須先編碼然后提給服務(wù)端。
那如何編碼&&如何解碼,需要自己做嗎?
編碼轉(zhuǎn)義的規(guī)則如下:
將需要轉(zhuǎn)碼的字符轉(zhuǎn)為16進(jìn)制。一個(gè)字符占8個(gè)比特位,然后從右到左,取4個(gè)比特位轉(zhuǎn)成16進(jìn)制數(shù)(不足4位直接處理),每2位16機(jī)制數(shù)前面加上%,編碼成%XY格式
解碼就是把收到的這些東西轉(zhuǎn)成2進(jìn)制的格式
我們寫(xiě)服務(wù)端如果從0開(kāi)始寫(xiě)這個(gè)解碼工作一定要自己做,但是我們?cè)诰W(wǎng)上搜索url decode C++源碼,當(dāng)一個(gè)CV工程師。
如何驗(yàn)證這個(gè)過(guò)程?
urlencode工具
3.HTTP協(xié)議格式
接下面我們先從宏觀方面說(shuō)說(shuō)http請(qǐng)求和響應(yīng)的格式
http請(qǐng)求是以行為單位的一種協(xié)議。
第一行
第一列:請(qǐng)求的方法
第二列:url
第三列:請(qǐng)求的http的版本
常見(jiàn)的版本:http 1.0 、1.1、 2.0版本
行以\r\n為分隔符,或者以\n
第二大塊也是以行為單位的,只不過(guò)這一大塊會(huì)存在多行,多行包含http請(qǐng)求各種屬性,屬性幾乎都以name:value的樣子
而我們又把第一行稱為請(qǐng)求行,第二大快稱為請(qǐng)求報(bào)頭
第三大塊,特別要強(qiáng)調(diào)一下是http請(qǐng)求的空行,相當(dāng)重要
最后一塊是請(qǐng)求正文,可以沒(méi)有也可以有,如果未來(lái)想把自己要登錄賬號(hào)可以把賬號(hào)和密碼放在正文,也就是說(shuō)想給服務(wù)器提上去的參數(shù)就放在正文
上面四大塊就是http request請(qǐng)求的完整報(bào)文。它會(huì)通過(guò)tcp鏈接,向服務(wù)器發(fā)送過(guò)去。
http響應(yīng)格式幾乎和請(qǐng)求格式是一樣的,也分四部分。
第一行是狀態(tài)行,也有三列構(gòu)成,中間由空格分開(kāi)。
第一列:是http版本
第二列:狀態(tài)碼,如200、400、302、307、500、404,如404我們常見(jiàn)的頁(yè)面不存在。狀態(tài)碼用來(lái)表示請(qǐng)求結(jié)果是否正確,就如網(wǎng)絡(luò)版本計(jì)數(shù)器我們寫(xiě)的exitcode。
第三列:狀態(tài)碼描述 如404 -> Not Found、200 -> OK
第二大塊也是由多行構(gòu)成的,叫做響應(yīng)報(bào)文
第三塊同樣也是空行
第四大塊,在響應(yīng)里會(huì)高頻的出現(xiàn),叫做響應(yīng)正文(有效載荷),通常帶html/css/js/圖片/視頻/音頻等
這四大部分構(gòu)成了響應(yīng)報(bào)文。在根據(jù)tcp鏈接socket,向客戶端返回響應(yīng)。未來(lái)所有http通信都采用的是這種方案進(jìn)行通信的。
現(xiàn)在我們?cè)谡勔徽勍ㄐ偶?xì)節(jié)問(wèn)題
1.請(qǐng)求和響應(yīng)怎么保證應(yīng)用層完整讀取完畢了呢?
首先我們發(fā)現(xiàn)http請(qǐng)求都是字符串按行為單位,所以
- 我可以讀取完整的一行
- while(讀取完整一行) --> 所有的請(qǐng)求行+請(qǐng)求行報(bào)文全部讀完 --> 直到空行!
- 我們沒(méi)說(shuō)正文也是按行為單位分開(kāi)的沒(méi)有辦法保證把正文讀完,但是我們能保證把報(bào)頭讀完,而報(bào)頭里有一個(gè)Content-Length:xxx(代表正文長(zhǎng)度)
- 解析出來(lái)內(nèi)容長(zhǎng)度,在根據(jù)內(nèi)容長(zhǎng)度,讀取正文即可!
2.請(qǐng)求和響應(yīng)是怎么做到序列化和反序列化的?
http是用的特殊字符自己實(shí)現(xiàn)的。http序列化什么都不做直接發(fā)就好了,反序列化 :第一行+請(qǐng)求/響應(yīng)報(bào)頭,只要按照\(chéng)r\n將字符串1->n即可!不用借助任何東西如Json
protobuf等。而正文序列化反序列也不用做直接發(fā)送就行了。如果你的正文攜帶結(jié)構(gòu)化數(shù)據(jù)就要自己處理了。
接下來(lái)我們寫(xiě)代碼的方式,驗(yàn)證上面說(shuō)的東西。
以前寫(xiě)udp和tcp我們都寫(xiě)過(guò)服務(wù)端用過(guò)套接字,這里還是直接拿過(guò)來(lái)用。
4.HTTP協(xié)議基本工作流程
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
using namespace std;
class httpRequest
{
public:
httpRequest(){};
~httpRequest(){};
public:
string inbuffer;//緩沖區(qū)
//下面我們先不管,未來(lái)都可以細(xì)分,序列化反序列也都可以寫(xiě)到類中,我們這里寫(xiě)簡(jiǎn)單一點(diǎn)主要看一下http的細(xì)節(jié)
// string reqline;//請(qǐng)求行
// vector<std::string> reqheader;//報(bào)頭
// string body;//請(qǐng)求正文
//第一行細(xì)分
//string method;
//string url;
//string httpversion;
};
class httpResponse
{
public:
string outbuffer;//緩沖區(qū)
};
httpServer.hpp
#pragma once
#include "protocol.hpp"
#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <functional>
using namespace std;
enum
{
USAGG_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
const int backlog = 5;
typedef function<void(const httpRequest&,httpResponse&)> func_t;
void handlerEntery(int sock,func_t callback)
{
// 1. 讀到完整的http請(qǐng)求
// 2. 反序列化
// 3. httprequst, httpresponse, callback(req, resp)
// 4. resp序列化
// 5. send
}
class httpServer
{
public:
httpServer(const uint16_t port) : _port(port), _listensock(-1)
{
}
void initServer()
{
// 1.創(chuàng)建socket文件套接字對(duì)象
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
exit(SOCKET_ERR);
}
// 2.bind 綁定自己的網(wǎng)絡(luò)消息 port和ip
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 任意地址bind,服務(wù)器真實(shí)寫(xiě)法
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// 3.設(shè)置socket為監(jiān)聽(tīng)狀態(tài)
if (listen(_listensock, backlog) < 0) // backlog 底層鏈接隊(duì)列的長(zhǎng)度
{
exit(LISTEN_ERR);
}
}
void start(func_t func)
{
// 子進(jìn)程退出自動(dòng)被OS回收
signal(SIGCHLD, SIG_IGN);
for (;;)
{
// 4.獲取新鏈接
struct sockaddr_in peer;
socklen_t len = (sizeof(peer));
int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一個(gè)文件描述符
if (sock < 0)
{
continue;
}
// 5.通信 這里就是一個(gè)sock,未來(lái)通信我們就用這個(gè)sock,tcp面向字節(jié)流的,后序全部都是文件操作!
// version2 多進(jìn)程信號(hào)版
int fd = fork();
if (fd == 0)
{
close(_listensock);
handlerEntery(sock,func);
close(sock);
exit(0);
}
close(sock);
}
}
~httpServer()
{
}
private:
uint16_t _port;
int _listensock;
};
httpServer.cc
#include "httpServer.hpp"
#include <memory>
void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
void Get(const httpRequest &req, httpResponse &resp)
{
}
// ./httpserver port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGG_ERR);
}
uint16_t serverport = atoi(argv[1]);
unique_ptr<httpServer> tsv(new httpServer(serverport));
tsv->initServer();
tsv->start(Get);
return 0;
}
我們發(fā)現(xiàn)udp、tcp、http所有的底層邏輯都是差不多的,而我們只要寫(xiě)上層邏輯就好了。
這里我們主要說(shuō)原理,下面1-5的工作我們都不做了,所以httpRequest,httpResponse也都給一個(gè)緩沖區(qū)就行了。
void handlerEntery(int sock,func_t callback)
{
// 1. 讀到完整的http請(qǐng)求
// 2. 反序列化
// 3. httprequst, httpresponse, callback(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
httpRequest req;
httpResponse resp;
ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);//大概率我們直接就能讀取到完整的http請(qǐng)求
if(n>0)
{
buffer[n]=0;
req.inbuffer=buffer;
callback(req,resp);
send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
}
}
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "----------------http end-----------------" << endl;
}
下面我們用瀏覽器充當(dāng)客戶端發(fā)起請(qǐng)求看一下結(jié)果
有人可能說(shuō)就發(fā)起一次請(qǐng)求那后面怎么收到這么多,瀏覽器內(nèi)部請(qǐng)求資源可能是一個(gè)多線程版的客戶端,并且它請(qǐng)求可能不僅僅請(qǐng)求這個(gè)網(wǎng)站的網(wǎng)頁(yè),還可能獲取網(wǎng)站的圖標(biāo)其他一些資源,它可能并行的向服務(wù)器發(fā)起請(qǐng)求所以看到這么多請(qǐng)求。
因?yàn)槲覀兪裁炊紱](méi)有返回,所以網(wǎng)頁(yè)什么都看不到。
下面是我們的請(qǐng)求報(bào)文,服務(wù)器在打印的時(shí)候我們多打印了一個(gè)endl,所以會(huì)看到兩個(gè)空行。實(shí)際應(yīng)該是一個(gè)空行。
第一行第一列是請(qǐng)求方法,默認(rèn)是GET方法。
第二列url,這里是/,是因?yàn)槲覀冎皇歉嬖V瀏覽器要訪問(wèn)某個(gè)ip的某個(gè)port并沒(méi)有告訴請(qǐng)求什么資源,默認(rèn)是/(web根目錄),它并不是把所有資源都給用戶。http請(qǐng)求如果沒(méi)有請(qǐng)求指定的資源,web server 會(huì)有默認(rèn)的首頁(yè)!
比如說(shuō),我們默認(rèn)請(qǐng)求百度www.baidu.com,后面什么都沒(méi)帶沒(méi)告訴要訪問(wèn)百度那個(gè)網(wǎng)頁(yè),按回車默認(rèn)就把百度首頁(yè)返回了。
第三列是http請(qǐng)求的版本,這里瀏覽器默認(rèn)用的1.1的版本,未來(lái)響應(yīng)返回的時(shí)候也要寫(xiě)版本。這里有個(gè)細(xì)節(jié),http請(qǐng)求會(huì)交換bs通信雙方的協(xié)議版本。
我們?cè)谟靡恍┸浖臅r(shí)候,有的軟件會(huì)提醒你就行升級(jí)更新,有的人會(huì)升級(jí)有的人不會(huì),一定會(huì)存在多種客戶端版本的情況,所以服務(wù)器可能面臨多種不同版本的客戶端,如果是老版本的客戶端一定用的是老版本的服務(wù),服務(wù)端給它提供老版本的服務(wù),同理是新版本客戶端服務(wù)端給它提供的是新版本的服務(wù),因此需要服務(wù)器提供對(duì)應(yīng)的版本服務(wù)。
接下面一堆東西就是請(qǐng)求報(bào)頭。
第一行Host:是這個(gè)請(qǐng)求未來(lái)是要發(fā)給那個(gè)服務(wù)端的。未來(lái)客戶端這個(gè)請(qǐng)求去到一個(gè)服務(wù)器,但這個(gè)服務(wù)器不提供服務(wù),然后該服務(wù)器拿著我的Host知道我要請(qǐng)求誰(shuí),它替我去請(qǐng)求。這個(gè)中間服務(wù)器叫做代理服務(wù)器。
第二行Connection:關(guān)于后面說(shuō)長(zhǎng)短鏈接的時(shí)候再說(shuō),目前這個(gè)是支持長(zhǎng)鏈接的。
第三行是支持協(xié)議升級(jí),http是可以被升級(jí)的,http是cs/bs模式請(qǐng)求響應(yīng)的模式,也就是說(shuō)必須是客戶端主動(dòng)發(fā)起請(qǐng)求,服務(wù)器才能給它響應(yīng)??赡茉谔厥鈭?chǎng)景下讓服務(wù)器和客戶端互發(fā)消息,客戶端沒(méi)有發(fā)任何消息,服務(wù)器主動(dòng)發(fā)信息給客戶端。
第四行User-Agent:是客戶端的信息,如操作系統(tǒng)是什么,瀏覽版本等
下面幾行Accept:是告訴服務(wù)器,客戶端能接收什么文檔格式、支持壓縮格式、支持編碼格式等等
請(qǐng)求部分我們驗(yàn)證完了,下面我們?cè)倏匆粋€(gè)響應(yīng)。
報(bào)頭我們暫時(shí)不要后面慢慢填,正文部分我們搞一個(gè)網(wǎng)頁(yè)。
網(wǎng)頁(yè)不會(huì)寫(xiě),可以搜一下w3cschool html教程
這里我們先寫(xiě)到Get函數(shù)里,后面我們?cè)诜蛛x
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "----------------http end-----------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
// string respheader;
string respblank = "\r\n";
//隨便搞個(gè)網(wǎng)頁(yè)
string body="<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>北京交通廣播《一路暢通》“交通大家談”節(jié)目,特邀北京市交通委員會(huì)地面公交運(yùn)營(yíng)管理處處長(zhǎng)趙震、北京市公安局公安交通管理局秩序處副處長(zhǎng) 林志勇、北京交通發(fā)展研究院交通規(guī)劃所所長(zhǎng) 劉雪杰為您解答公交車專用道6月1日起社會(huì)車輛進(jìn)出公交車道須注意哪些?</p></body></html>";
//序列化
resp.outbuffer += respline;
resp.outbuffer += respblank;
resp.outbuffer += body;
}
雖然我們?cè)陧憫?yīng)的時(shí)候沒(méi)有帶響應(yīng)報(bào)頭,但是我們的瀏覽器依舊是能識(shí)別的,這里想說(shuō)的是現(xiàn)在瀏覽器很智能了,可以不用告訴它正文是什么也可以根據(jù)正文內(nèi)容識(shí)別這是什么東西,但是有的瀏覽器做不到。這里我們用的是chrome瀏覽器。
但是你瀏覽器能識(shí)別是你的事情,我們還是要把自己的事做好,告訴瀏覽器我給你發(fā)回的是什么東西。
所以我們要加一個(gè)報(bào)頭里面可以帶一些屬性。如Content-Type ,告訴別人返回的是什么資源。網(wǎng)上可以搜一下Content-Type 對(duì)照表
這里我們返回的是一個(gè)網(wǎng)頁(yè),類型是text/html
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "----------------http end-----------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
//報(bào)頭
string respheader = "Content-Type: text/html\r\n";
string respblank = "\r\n";
//隨便搞個(gè)網(wǎng)頁(yè)
string body="<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>北京交通廣播《一路暢通》“交通大家談”節(jié)目,特邀北京市交通委員會(huì)地面公交運(yùn)營(yíng)管理處處長(zhǎng)趙震、北京市公安局公安交通管理局秩序處副處長(zhǎng) 林志勇、北京交通發(fā)展研究院交通規(guī)劃所所長(zhǎng) 劉雪杰為您解答公交車專用道6月1日起社會(huì)車輛進(jìn)出公交車道須注意哪些?</p></body></html>";
//序列化
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
}
可以看到這就是我們的響應(yīng)
現(xiàn)在我們大概就知道了http的請(qǐng)求和響應(yīng)它的格式了,下面我們繼續(xù)完善這個(gè)代碼,繼續(xù)挖出核心的東西。
首先解決兩個(gè)問(wèn)題:
1.服務(wù)器和網(wǎng)頁(yè)分離,然后通過(guò)服務(wù)器把網(wǎng)頁(yè)返回
2.前面我們說(shuō)過(guò),我們請(qǐng)求沒(méi)帶路徑url會(huì)給我們一個(gè)/(默認(rèn)web根目錄),這個(gè)根目錄并不是liunx服務(wù)器下的根目錄,而是web服務(wù)器自己的根目錄,怎么理解呢?
再說(shuō)前面的問(wèn)題,我們?cè)诩右粋€(gè)方法類,未來(lái)可以把收到的報(bào)文做切割。
#pragma once
#include <iostream>
#include <string>
using namespace std;
class Util
{
public:
// xxx yyy zzz\r\naaa
static string GetOneline(string &buffer, const string &sep)
{
auto pos = buffer.find(sep);
if (pos == string::npos)
return "";
string sub = buffer.substr(0, pos);
return sub;
}
};
const string sep = "\r\n";//切割符
class httpRequest
{
public:
httpRequest(){};
~httpRequest(){};
void parse()
{
// 1. 從inbuffer中拿到第一行,分隔符\r\n
string line = Util::GetOneline(inbuffer, sep);
if (line.empty())
return;
// 2. 從請(qǐng)求行中提取三個(gè)字段
istringstream iss(line);
iss >> method >> url >> httpversion;
}
public:
string inbuffer;
// string reqline;
// vector<std::string> reqheader;
// string body;
string method;
string url;
string httpversion;
};
void handlerEntery(int sock,func_t callback)
{
// 1. 讀到完整的http請(qǐng)求
// 2. 反序列化
// 3. httprequst, httpresponse, callback(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
httpRequest req;
httpResponse resp;
ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);// 大概率我們直接就能讀取到完整的http請(qǐng)求
if(n>0)
{
buffer[n]=0;
req.inbuffer=buffer;
req.parse();
callback(req,resp);
send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
}
}
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "method: " << req.method << endl;
cout << "url: " << req.url << endl;
cout << "httpversion: " << req.httpversion << endl;
cout << "----------------http end-----------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
//報(bào)頭
string respheader = "Content-Type: text/html\r\n";
string respblank = "\r\n";
//隨便搞個(gè)網(wǎng)頁(yè)
string body="<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>北京交通廣播《一路暢通》“交通大家談”節(jié)目,特邀北京市交通委員會(huì)地面公交運(yùn)營(yíng)管理處處長(zhǎng)趙震、北京市公安局公安交通管理局秩序處副處長(zhǎng) 林志勇、北京交通發(fā)展研究院交通規(guī)劃所所長(zhǎng) 劉雪杰為您解答公交車專用道6月1日起社會(huì)車輛進(jìn)出公交車道須注意哪些?</p></body></html>";
//序列化
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
}
經(jīng)過(guò)測(cè)試,一行能拿到,未來(lái)用個(gè)while循環(huán),請(qǐng)求行+請(qǐng)求報(bào)文都可以拿到了。
現(xiàn)在我們解釋一下什么是web根目錄。
實(shí)際上未來(lái)一個(gè)web服務(wù)器寫(xiě)好之后,可不僅僅有這些代碼。每一個(gè)web服務(wù)器都有web根目錄,未來(lái)所有圖片、視頻、音頻等各種web資源都在這個(gè)目錄下,按照目錄結(jié)構(gòu)組織號(hào)好,未來(lái)想請(qǐng)求資源就從url請(qǐng)求。那如何保證按照我們的需求在指定路徑下去尋找呢?
const string sep = "\r\n";
//這里我們默認(rèn)寫(xiě)一個(gè)web根目錄
const string default_root = "./wwwroot";//因?yàn)檎?qǐng)求url默認(rèn)會(huì)加上/開(kāi)始,所以./wwwroot后面不要/
const string home_page = "index.html";//默認(rèn)首頁(yè)
class httpRequest
{
public:
//。。。
void parse()
{
// 1. 從inbuffer中拿到第一行,分隔符\r\n
string line = Util::GetOneline(inbuffer, sep);
if (line.empty())
return;
// 2. 從請(qǐng)求行中提取三個(gè)字段
istringstream iss(line);
iss >> method >> url >> httpversion;
// 3. 添加web默認(rèn)路徑
path = default_root;// ./wwwroot
path += url;// ./wwwroot/a/b/c.html 請(qǐng)求具體資源
//剛才我們看到url只有/的樣子,這里也要拼 ./wwwroot/
//但是這里就遭了,還是不知道訪問(wèn)的那個(gè)具體的資源
//其實(shí)對(duì)于一個(gè)服務(wù)器來(lái)說(shuō),它有自己的主頁(yè)信息
//如果訪問(wèn)的是根目錄,就把首頁(yè)給拼上
if (path[path.size() - 1] == '/')//判斷是否是根目錄
path += home_page;
}
public:
string inbuffer;
string method;
string url;
string httpversion;
string path;
};
在url是這樣請(qǐng)求的,但是實(shí)際上web服務(wù)器它會(huì)自己拼前綴,帶著這個(gè)路徑去找對(duì)應(yīng)資源文件,如果有就返回,沒(méi)有就返回404。
接下來(lái)我們做服務(wù)器和網(wǎng)頁(yè)分離
我們讓body從文件中讀取,因此添加一個(gè)readFile接口,把文件內(nèi)容全部讀到body里。
class Util
{
public:
// xxx yyy\r\nzzz
static string GetOneline(string &buffer, const string &sep)
{
auto pos = buffer.find(sep);
if (pos == string::npos)
return "";
string sub = buffer.substr(0, pos);
return sub;
}
static bool readFile(const string &path, string &body)
{
ifstream ofs(path,ios_base::binary);//二進(jìn)制方式讀
if (!ofs.is_open())
return false;
string line;
while(getline(path, line))
{
body += line;
}
ofs.close();
return true;
}
};
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "method: " << req.method << endl;
cout << "url: " << req.url << endl;
cout << "httpversion: " << req.httpversion << endl;
cout << "----------------http end-----------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
//報(bào)頭
string respheader = "Content-Type: text/html\r\n";
string respblank = "\r\n";
string body;
if (!Util::readFile(req.path, body))
{
Util::readFile(html_404, body); // 讀取失敗返回404,一定能成功
}
//序列化
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
}
現(xiàn)在我們給網(wǎng)頁(yè)添加一下功能,比如說(shuō)網(wǎng)頁(yè)是支持點(diǎn)擊然后跳轉(zhuǎn)鏈接的
<!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>我的首頁(yè)</title>
</head>
<body>
<h1>我是網(wǎng)站的首頁(yè)</h1>
<a href="/test/a.html">新聞</a>
<a href="/test/b.html">電商</a>
</body>
</html>
點(diǎn)擊新聞鏈接,http自動(dòng)拼接然后重新請(qǐng)求
這也就是前端和后端。
這里我們?cè)谑醉?yè)在加一張圖片。
先把圖片上傳到web根目錄下
<!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>我的首頁(yè)</title>
</head>
<body>
<h1>我是網(wǎng)站的首頁(yè)</h1>
<a href="/test/a.html">新聞</a>
<a href="/test/b.html">電商</a>
<img src="/image/1.jpg" alt="向日葵">
</body>
</html>
未來(lái)我們請(qǐng)求網(wǎng)頁(yè),它要做兩件事情,第一要把這個(gè)網(wǎng)頁(yè)本身加載出來(lái),第二瀏覽器識(shí)別到網(wǎng)頁(yè)還有一個(gè)資源叫做圖片,所以它一要把網(wǎng)頁(yè)給下載下來(lái),二還要把網(wǎng)頁(yè)中要用的圖片下載下來(lái),兩個(gè)資源一合并組合才能給我們形成一個(gè)完整的網(wǎng)頁(yè)信息!
一個(gè)用戶看到的網(wǎng)頁(yè)結(jié)果,可能是多個(gè)資源組合而成的!所以要獲取一張完整的網(wǎng)頁(yè)效果,我們的瀏覽器一定會(huì)發(fā)起多次http請(qǐng)求
是網(wǎng)頁(yè)Content-Type 類型text/html,圖片呢?
所以我們需要不同的path所標(biāo)定文件的類型返回特定不同的資源。
我們要正確的給客戶端返回資源類型,我們首先要自己知道!所有的資源都有后綴??!
class httpRequest
{
public:
httpRequest(){};
~httpRequest(){};
void parse()
{
// 1. 從inbuffer中拿到第一行,分隔符\r\n
string line = Util::GetOneline(inbuffer, sep);
if (line.empty())
return;
// 2. 從請(qǐng)求行中提取三個(gè)字段
istringstream iss(line);
iss >> method >> url >> httpversion;
// 3. 添加web默認(rèn)路徑
path = default_root + url;
if (path[path.size() - 1] == '/')
path += home_page;
// 4. 獲取path對(duì)應(yīng)的資源后綴
// ./wwwroot/index.html
// ./wwwroot/test/a.html
// ./wwwroot/image/1.jpg
auto pos=path.rfind(".");
if(pos == string::npos) suffix=".html";
suffix=path.substr(pos);
}
public:
string inbuffer;
string method;
string url;
string httpversion;
string path;
string suffix;
};
string suffixtodes(const string &suff)
{
string type = "Content-Type: ";
if (suff == ".html")
type += "text/html";
else if (suff == ".jpg")
type += "application/x-jpg";
type += "\r\n";
return type;
}
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "method: " << req.method << endl;
cout << "url: " << req.url << endl;
cout << "httpversion: " << req.httpversion << endl;
cout << "suffix: " << req.suffix << endl;
cout << "----------------http end-----------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
//報(bào)頭
//string respheader = "Content-Type: text/html\r\n";
string respheader = suffixtodes(req.suffix);
string respblank = "\r\n";
string body;
if (!Util::readFile(req.path, body))
{
Util::readFile(html_404, body); // 讀取失敗返回404,一定能成功
}
//序列化
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
}
但是這個(gè)readFile還像剛才那樣使用while循環(huán)讀一行的方式讀就有問(wèn)題了,因?yàn)閳D片是二進(jìn)制文件。可能圖片太大都沒(méi)讀完整。所以上面代碼目前不完整。我把下面問(wèn)題解決了然后就可以完整把圖片顯示出來(lái)了。
這里有個(gè)問(wèn)題,你要請(qǐng)求的資源大小是多少?所以我們?cè)趫?bào)頭再加一個(gè)
Content-Length。
這里在介紹一個(gè)接口
path:你要訪問(wèn)的資源
buf:一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體包含很多屬性,其中有文件大小。
成功時(shí)返回0,失敗返回-1錯(cuò)誤碼被設(shè)置
class httpRequest
{
public:
httpRequest(){};
~httpRequest(){};
void parse()
{
// 1. 從inbuffer中拿到第一行,分隔符\r\n
string line = Util::GetOneline(inbuffer, sep);
if (line.empty())
return;
// 2. 從請(qǐng)求行中提取三個(gè)字段
istringstream iss(line);
iss >> method >> url >> httpversion;
// 3. 添加web默認(rèn)路徑
path = default_root + url;
if (path[path.size() - 1] == '/')
path += home_page;
// 4. 獲取path對(duì)應(yīng)的資源后綴
// ./wwwroot/index.html
// ./wwwroot/test/a.html
// ./wwwroot/image/1.jpg
auto pos=path.rfind(".");
if(pos == string::npos) suffix=".html";
suffix=path.substr(pos);
// 5. 得到資源的大小
struct stat sif;
if(stat(path.c_str(),&sif) == 0)
size=sif.st_size;
else
size=-1;
}
public:
string inbuffer;
string method;
string url;
string httpversion;
string path;
string suffix;
int size;
};
string suffixtodes(const string &suff)
{
string type = "Content-Type: ";
if (suff == ".html")
type += "text/html";
else if (suff == ".jpg")
type += "application/x-jpg";
type += "\r\n";
return type;
}
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "method: " << req.method << endl;
cout << "url: " << req.url << endl;
cout << "httpversion: " << req.httpversion << endl;
cout << "suffix: " << req.suffix << endl;
cout << "size: " << req.size << endl;
cout << "----------------http end-----------------" << endl;
string respline = "HTTP/1.1 200 OK\r\n";
//報(bào)頭
//string respheader = "Content-Type: text/html\r\n";
string respheader = suffixtodes(req.suffix);
if (req.size > 0)
{
respheader += "Content-Length: ";
respheader += to_string(req.size);
respheader += "\r\n";
}
string respblank = "\r\n";
string body;
body.resize(req.size + 1);
if (!Util::readFile(req.path, body))
{
// 找不到文件,文件大小是-1,要返回404.html,因此重新計(jì)算大小
//否則body大小是-1,404.html文件內(nèi)容就讀不到body里
struct stat sif;
if (stat(html_404.c_str(), &sif) == 0)
body.resize(sif.st_size + 1);
Util::readFile(html_404, body); // 一定能成功
}
//序列化
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
}
這里我們讓body開(kāi)辟好空間,然后readFile使用read讀文件。這樣就能把文件所有內(nèi)容讀完整。
class Util
{
public:
// xxx yyy\r\nzzz
static string GetOneline(string &buffer, const string &sep)
{
auto pos = buffer.find(sep);
if (pos == string::npos)
return "";
string sub = buffer.substr(0, pos);
return sub;
}
static bool readFile(const string &path, string &body)
{
ifstream ofs(path,ios_base::binary);
if (!ofs.is_open())
return false;
ofs.read((char *)body.c_str(), body.size());
ofs.close();
return true;
}
};
實(shí)際http協(xié)議基本工作流程就差不多了。
5.HTTP的方法
當(dāng)我們?cè)谶M(jìn)行網(wǎng)絡(luò)訪問(wèn)的時(shí)候,實(shí)際是有兩種行為的。
- 獲取資源
- 上傳資源
上面都是獲取資源的,可是我們想上傳資源呢?完成登錄等。。怎么做呢?
實(shí)際上我們一般在進(jìn)行網(wǎng)站交互的時(shí)候,其實(shí)是通過(guò)表單的方式進(jìn)行提交的
比如qq新聞
所以我們進(jìn)行數(shù)據(jù)提交的的時(shí)候,本質(zhì)前端要通過(guò)form表單提交的,瀏覽器會(huì)自動(dòng)將form表單中的內(nèi)容轉(zhuǎn)換成GET/POST方法請(qǐng)求
默認(rèn)是GET方法。
接下來(lái)我們?cè)谧约旱氖醉?yè)加個(gè)表單
<!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>我的首頁(yè)</title>
</head>
<body>
<h1>我是網(wǎng)站的首頁(yè)</h1>
<a href="/test/a.html">新聞</a>
<a href="/test/b.html">電商</a>
<img src="/image/1.jpg" alt="向日葵">
<form action="/a/b/c.py" method="GET">
姓名:<br>
<input type="text" name="xname">
<br>
密碼:<br>
<input type="password" name="ypwd">
<br><br>
<input type="submit" value="登陸">
</form>
</body>
</html>
form是表單的整體結(jié)構(gòu),未來(lái)我們給服務(wù)器提供參數(shù)的話,為了很好提供參數(shù)的提取所以輸入框它是KV的,未來(lái)不是在輸入框?qū)憙?nèi)容嗎,可是我怎么知道你是想那個(gè)輸入框?qū)懙膬?nèi)容呢,所以name就是這個(gè)輸入框的名字。
其實(shí)它后面還跟著value是預(yù)設(shè)內(nèi)容,就像你登錄時(shí)某一行輸入框提醒你這一行輸入什么內(nèi)容,不過(guò)暫時(shí)我們這里不要。
還有input type=“password”,會(huì)把我們輸入的內(nèi)容全部變成黑點(diǎn),text我們輸入什么就顯示什么。
action代表你想把你的表單的數(shù)據(jù)提交給后端的哪一個(gè)服務(wù)或者網(wǎng)頁(yè)或者路徑下。這里我們隨便寫(xiě)個(gè),假設(shè)這個(gè)表單未來(lái)提交給一個(gè)py的腳本,你想提交給C++程序里這都是可以的。
method當(dāng)你提交表單時(shí)你想采用什么方法提交,默認(rèn)是GET
<form action="/a/b/c.py" method="GET">
姓名:<br>
<input type="text" name="xname" value="用戶名">
<br>
密碼:<br>
<input type="password" name="ypwd" value="="密碼"">
<br><br>
<input type="submit" value="登陸">
</form>
當(dāng)我們登錄時(shí)這個(gè)/a/b/c.py是不存在的,所以返回的是404,但不重要。
我們可以看到GET方法提參的時(shí)候,它會(huì)把我們要提交的參數(shù)拼到url的后面,b把參數(shù)以u(píng)rl方式進(jìn)行提交,然后以?為分割符,左側(cè)是要訪問(wèn)網(wǎng)站的資源,右側(cè)是我們提上來(lái)的參數(shù)。
當(dāng)我們用POST方法提參時(shí),url后面只有我們要訪問(wèn)什么資源,后面沒(méi)有提交的參數(shù),我們的參數(shù)是以請(qǐng)求的正文提交參數(shù)的。
GET和POST提參區(qū)別:
GET通過(guò)url傳遞參數(shù),具體:http://ip:port/xxx/yy?name1=value1&name2=value2
POST提交參數(shù)通過(guò)http請(qǐng)求正文提交參數(shù)!
那用那個(gè)呢?
POST方法通過(guò)正文提交參數(shù),所以一般用戶看不到,私密性更好,但私密性!=安全性。
GET方法不私密。
但無(wú)論是GET和POST方法,都不安全!要談安全,必須加密 —> https。
并且通過(guò)URL傳遞參數(shù),注定不能太大,但是POST方法,通過(guò)正文,正文可以很大,甚至可以是其他的東西。
現(xiàn)在思考這樣的問(wèn)題:
1.我們提交給指定的路徑,有什么意義??
不管是url提參還是正文提參都是把參數(shù)給服務(wù)器,服務(wù)器未來(lái)拿著對(duì)應(yīng)的參數(shù)實(shí)現(xiàn)登錄注冊(cè)等,憑什么?服務(wù)器首先能拿到這個(gè)數(shù)據(jù),可是怎么讓服務(wù)器處理這個(gè)數(shù)據(jù)?其次服務(wù)器怎么知道未來(lái)想到對(duì)這個(gè)數(shù)據(jù)進(jìn)行怎樣的處理,就是說(shuō)你只是提交請(qǐng)求而服務(wù)器怎么知道登錄還是注冊(cè)呢?
這一切的玄機(jī)都在這里,不管是GET和POST都要求在表單里把數(shù)據(jù)提交給某一個(gè)資源。
下面我們搞偽代碼來(lái)說(shuō)明,以GET為例
class httpRequest
{
public:
httpRequest(){};
~httpRequest(){};
void parse()
{
// 1. 從inbuffer中拿到第一行,分隔符\r\n
string line = Util::GetOneline(inbuffer, sep);
if (line.empty())
return;
// 2. 從請(qǐng)求行中提取三個(gè)字段
istringstream iss(line);
iss >> method >> url >> httpversion;
//考慮提參的情況
// 2.1 /search?name=zhangsan&pwd=12345
// 通過(guò)?將左右進(jìn)行分離
// 如果是POST方法,本來(lái)就是分離的!
// 左邊PATH, 右邊parm
// 3. 添加web默認(rèn)路徑
path = default_root + url;
if (path[path.size() - 1] == '/')
path += home_page;
// 4. 獲取path對(duì)應(yīng)的資源后綴
// ./wwwroot/index.html
// ./wwwroot/test/a.html
// ./wwwroot/image/1.jpg
auto pos=path.rfind(".");
if(pos == string::npos) suffix=".html";
suffix=path.substr(pos);
// 5. 得到資源的大小
struct stat sif;
if(stat(path.c_str(),&sif) == 0)
size=sif.st_size;
else
size=-1;
}
public:
string inbuffer;
string method;
string url;
string httpversion;
string path;
string suffix;
int size;
string parm;
};
然后在Get方法里,進(jìn)行處理,如果path是/search就不走下面顯示網(wǎng)頁(yè)的邏輯,直接使用我們自己寫(xiě)的C++的方法,提供服務(wù)。
其次還可以把請(qǐng)求到其他語(yǔ)言寫(xiě)的腳本中。
void Get(const httpRequest &req, httpResponse &resp)
{
if(req.path == "test.py")
{
//建立進(jìn)程間通信,pipe
//fork創(chuàng)建子進(jìn)程,子進(jìn)程執(zhí)行這個(gè)腳本 execl("/bin/python", test.py)
// 父進(jìn)程,將req.parm 通過(guò)管道寫(xiě)給某些后端語(yǔ)言,py,java,php等語(yǔ)言
//這也是為什么服務(wù)器是c++寫(xiě)的,后端是其他語(yǔ)言寫(xiě)的
}
if(req.path == "/search")
{
// req.parm
// 使用我們自己寫(xiě)的C++的方法,提供服務(wù)
}
//。。。
}
這里我們還可以把服務(wù)器做更多的設(shè)計(jì)。
服務(wù)器做一個(gè)功能路由的選擇,不同路徑調(diào)用不同的服務(wù)
typedef function<void(const httpRequest&,httpResponse&)> func_t;
class httpServer
{
//。。。
void registerCb(string servicename, func_t cb)
{
funcs.insert(make_pair(servicename, cb));
}
void handlerEntery(int sock)
{
// 1. 讀到完整的http請(qǐng)求
// 2. 反序列化
// 3. httprequst, httpresponse, callback(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
httpRequest req;
httpResponse resp;
ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);// 大概率我們直接就能讀取到完整的http請(qǐng)求
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:
uint16_t _port;
int _listensock;
unordered_map<string,func_t> funcs;
};
// ./httpserver port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGG_ERR);
}
uint16_t serverport = atoi(argv[1]);
unique_ptr<httpServer> tsv(new httpServer(serverport));
// 功能路由!
tsv->registerCb("/", Get); //假設(shè)/ 默認(rèn)是網(wǎng)站
tsv->registerCb("/search", Search);//假設(shè)是/search 默認(rèn)注冊(cè)
tsv->registerCb("/test.py", Other);//假設(shè)test.py 默認(rèn)登錄
tsv->initServer();
tsv->start(Get);
return 0;
}
2.除了GET和POST,還有那些方法,重要嗎??
有,但常用的是GET和POST方法。
單純把資源從遠(yuǎn)端獲取 —> GET方法
提參GET,POST都可以,提交的參數(shù)很小沒(méi)有私密性可以用GET,參數(shù)大想有私密性用POST。
6.HTTP的狀態(tài)碼
狀態(tài)碼有五大類
最常見(jiàn)的狀態(tài)碼, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
以4開(kāi)頭是客戶端錯(cuò)誤。5開(kāi)頭是服務(wù)端錯(cuò)誤。
以3開(kāi)頭的狀態(tài)碼都是重定向
什么叫做重定向呢?
就是訪問(wèn)百度,但是它會(huì)自動(dòng)跳轉(zhuǎn)出QQ新聞首頁(yè)。
訪問(wèn)一個(gè)url,但是服務(wù)器響應(yīng)會(huì)帶以3開(kāi)頭的狀態(tài)碼還會(huì)帶一個(gè)新的url,然后瀏覽器看到這個(gè)狀態(tài)碼會(huì)自動(dòng)重新訪問(wèn)這個(gè)新的url。
重定向分為臨時(shí)重定向和永久重定向。
假如有一個(gè)www.a.com網(wǎng)站有100w用戶,但是這個(gè)網(wǎng)站現(xiàn)在進(jìn)行重新設(shè)計(jì)升級(jí),搞了一個(gè)新網(wǎng)站www.b.com,但是老網(wǎng)站100w用戶我還想要,因此在老網(wǎng)站這里用重定向,當(dāng)你還在訪問(wèn)老網(wǎng)站時(shí),老網(wǎng)站告訴你重定向,因此你的瀏覽器自動(dòng)訪問(wèn)這個(gè)新網(wǎng)站。永久重定向從技術(shù)角度是更新你本地的書(shū)簽。 臨時(shí)重定向就是每次都做臨時(shí)跳轉(zhuǎn)如打開(kāi)一個(gè)網(wǎng)站有時(shí)候給我跳轉(zhuǎn)到淘寶,有時(shí)候跳轉(zhuǎn)京東等(金錢的力量)。
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "method: " << req.method << endl;
cout << "url: " << req.url << endl;
cout << "httpversion: " << req.httpversion << endl;
cout << "path :" << req.path << endl;
cout << "suffix: " << req.suffix << endl;
cout << "size: " << req.size << endl;
cout << "----------------http end-----------------" << endl;
//臨時(shí)重定向
string respline = "HTTP/1.1 307 Temporary Redirect\r\n";
string respheader = suffixtodes(req.suffix);
if (req.size > 0)
{
respheader += "Content-Length: ";
respheader += to_string(req.size);
respheader += "\r\n";
}
//Location配套重定向,告訴瀏覽器去哪里訪問(wèn)
respheader += "Location: https://www.baidu.com/\r\n";
string respblank = "\r\n";
string body;
body.resize(req.size + 1);
if (!Util::readFile(req.path, body))
{
// 找不到文件,文件大小是-1,要返回404.html,因此重新計(jì)算大小
struct stat sif;
if (stat(html_404.c_str(), &sif) == 0)
body.resize(sif.st_size + 1);
Util::readFile(html_404, body); // 一定能成功
}
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
cout << "----------------------http response start---------------------------" << endl;
cout << resp.outbuffer << endl;
cout << "----------------------http response end---------------------------" << endl;
resp.outbuffer += body;
}
7.HTTP常見(jiàn)Header
- Content-Type: 數(shù)據(jù)類型(text/html等)
- Content-Length: Body的長(zhǎng)度
- Host: 客戶端告知服務(wù)器, 所請(qǐng)求的資源是在哪個(gè)主機(jī)的哪個(gè)端口上;
- User-Agent: 聲明用戶的操作系統(tǒng)和瀏覽器版本信息;
- referer: 當(dāng)前頁(yè)面是從哪個(gè)頁(yè)面跳轉(zhuǎn)過(guò)來(lái)的;
- location: 搭配3xx狀態(tài)碼使用, 告訴客戶端接下來(lái)要去哪里訪問(wèn);
- Cookie: 用于在客戶端存儲(chǔ)少量信息. 通常用于實(shí)現(xiàn)會(huì)話(session)的功能
8.長(zhǎng)連接
我們知道其實(shí)一張我們看到的網(wǎng)頁(yè),實(shí)際上可能由多種元素構(gòu)成,也就是說(shuō)一張完整的網(wǎng)頁(yè)需要多次http請(qǐng)求然后瀏覽器進(jìn)行組合和渲染,這是我們?cè)谇懊娑家?jiàn)過(guò)的。
這里就會(huì)有這樣一個(gè)問(wèn)題,http網(wǎng)頁(yè)中可能會(huì)包含多個(gè)元素,如果頻繁發(fā)起http請(qǐng)求,http是基于tcp的,tcp是面向鏈接的,所以會(huì)導(dǎo)致頻繁創(chuàng)建鏈接的問(wèn)題。
如果有100張圖片,http就要發(fā)起100請(qǐng)求,tcp建立100次鏈接,這成本就太大了。
所以這就需要client和server都要支持,長(zhǎng)連接,客戶端向服務(wù)器發(fā)起建立一次鏈接(這里并不是說(shuō)與服務(wù)器建立好鏈接之后請(qǐng)求所有資源就永遠(yuǎn)用的是這一個(gè)鏈接 ,而是說(shuō)我們請(qǐng)求服務(wù)器看到一張網(wǎng)頁(yè),這個(gè)網(wǎng)頁(yè)里有很多元素),與服務(wù)器這張網(wǎng)頁(yè)建立好一條鏈接,獲取一大份資源的時(shí)候,通過(guò)同一條鏈接完成。
這表示客戶端和服務(wù)器協(xié)商好支持長(zhǎng)連接。
Connection:close -----> 短鏈接(有多少請(qǐng)求,建立多少次tcp鏈接)
9.cookie&&session會(huì)話保持
嚴(yán)格來(lái)說(shuō)會(huì)話保持并不屬于http協(xié)議天然所具備的,是后面使用發(fā)現(xiàn)需要的。
http定位是一個(gè)超文本傳輸協(xié)議,它只要把資源從服務(wù)器拿到本地就可以了。
但依舊要一個(gè)會(huì)話保持。
什么是會(huì)話保持呢?
簡(jiǎn)單來(lái)說(shuō)就是你打開(kāi)bilibili網(wǎng)站,然后登錄一次賬號(hào)。以后你在網(wǎng)站里做網(wǎng)頁(yè)跳轉(zhuǎn)的時(shí)候都不需要重新登錄了,然后在重新打開(kāi)bilibili這個(gè)賬號(hào)依舊在,關(guān)閉瀏覽器然后依舊從這個(gè)瀏覽器在打開(kāi)bilibili你的賬號(hào)信息還在!如果換成其他瀏覽器進(jìn)入bilibili網(wǎng)站這次你的消息才不會(huì)存在了。這就是會(huì)話保持。
http協(xié)議是無(wú)狀態(tài)的!也就是說(shuō)第一次第二次第三次請(qǐng)求,第二次請(qǐng)求不知道第一次請(qǐng)求過(guò),第三次請(qǐng)求不知道第二次第一次請(qǐng)求過(guò)。換句話瀏覽器三次請(qǐng)求一樣的圖片,按照道理來(lái)說(shuō),瀏覽器每一次都幫我發(fā)起http請(qǐng)求。因?yàn)閔ttp協(xié)議無(wú)狀態(tài)它不會(huì)記錄歷史曾經(jīng)所涉及的狀態(tài)信息、所有曾經(jīng)的請(qǐng)求、不會(huì)猜測(cè)下一次請(qǐng)求該做什么。它只復(fù)雜自己的功能我要什么你給我拿什么就可以了。
但實(shí)際上現(xiàn)實(shí)往往和理論是不符的,雖然我們發(fā)現(xiàn)http協(xié)議無(wú)狀態(tài),但是我們發(fā)現(xiàn)我曾經(jīng)登錄過(guò),但http協(xié)議在我們通信的時(shí)候依舊能記住我。這雖然和http協(xié)議無(wú)直接關(guān)系,但間接有關(guān)。
http協(xié)議無(wú)狀態(tài)!但用戶需要,因?yàn)橛脩舨榭葱碌木W(wǎng)頁(yè)是常規(guī)操作,如果發(fā)生網(wǎng)頁(yè)跳轉(zhuǎn),那么新的頁(yè)面也就無(wú)法識(shí)別是哪一個(gè)用戶了,為了讓用戶一經(jīng)登錄,可以在整個(gè)網(wǎng)站,按照自己的身份進(jìn)行隨意訪問(wèn) -------> 會(huì)話保持(保持一個(gè)用戶始終在線的狀態(tài))
那如何實(shí)現(xiàn)呢?
老方法
首先通過(guò)http協(xié)議進(jìn)行登錄(輸入用戶名和密碼),登錄成功服務(wù)器返回首頁(yè)讓你使用。當(dāng)發(fā)生頁(yè)面跳轉(zhuǎn)時(shí),http是不記錄我的狀態(tài)的就構(gòu)建請(qǐng)求發(fā)給服務(wù)端,服務(wù)器此時(shí)說(shuō)你誰(shuí)???我不認(rèn)識(shí)你,必須是登錄用戶才能請(qǐng)求這個(gè)頁(yè)面,這時(shí)你還必須重新登錄才可以,那就扯犢子了。
所以解決方案的設(shè)計(jì)者這樣做,一旦首次登錄成功后,瀏覽器會(huì)幫我們維持一個(gè)東西。把我們用戶輸入的信息:用戶名&&密碼保存起來(lái)。然后往后只要訪問(wèn)同一個(gè)網(wǎng)站瀏覽器會(huì)自動(dòng)推送歷史保留的信息。
而服務(wù)端對(duì)凡是與網(wǎng)頁(yè)訪問(wèn)有權(quán)限要求的網(wǎng)頁(yè),在被獲取之前,全部都要做判斷??!-----> 身份認(rèn)證
這兩個(gè)搭配起來(lái)之后,只要當(dāng)首次登錄成功之后瀏覽器并將信息保存起來(lái),往后就由瀏覽器和服務(wù)器雙方配合,就不用用戶頻繁輸入用戶名和密碼了,每一次http請(qǐng)求瀏覽器都會(huì)把曾經(jīng)保存的這份信息推送給服務(wù)器。雖然服務(wù)器每一次都做身份認(rèn)證,此時(shí)在用戶看來(lái)只在一次需要登錄,往后就不需要再登錄了,因?yàn)槎加蔀g覽器和服務(wù)器在配合著進(jìn)行會(huì)話保持。
其中瀏覽器把我們賬號(hào)密碼信息保存起來(lái)的技術(shù)叫做cookie技術(shù)。
cookie技術(shù)分為:cookie文件級(jí)別,cookie內(nèi)存級(jí)別,怎么理解呢?
瀏覽器本質(zhì)也是一個(gè)進(jìn)程,如果關(guān)閉然后重新打開(kāi)瀏覽器,也就是進(jìn)程退出然后又運(yùn)行了,進(jìn)程退出進(jìn)程保存的東西都隨進(jìn)程退出而釋放掉,但當(dāng)我在訪問(wèn)b站的時(shí)候我依舊是登錄狀態(tài),這個(gè)瀏覽器采用的是cookie文件級(jí)別也就是說(shuō)瀏覽器會(huì)在本地給我維持一個(gè)cookie文件是在磁盤上真真實(shí)實(shí)存在的。甚至關(guān)機(jī),但當(dāng)我訪問(wèn)b站我還是登錄狀態(tài)。
如果隨著瀏覽器退出然后再重新打開(kāi)瀏覽器,再打開(kāi)b站的時(shí)候我的登錄信息就沒(méi)有了,此時(shí)cookie采用的是cookie內(nèi)存級(jí)別,也就是隨進(jìn)程關(guān)閉所有進(jìn)程保存的東西都沒(méi)有了。
cookie是文件的還是內(nèi)存的在瀏覽器是可以匹配的。
但是這個(gè)方法有問(wèn)題!
瀏覽器雖然會(huì)在我們第一次登錄會(huì)保存我們登錄信息,往后每次賬號(hào)密碼都要進(jìn)行推送給服務(wù)器,服務(wù)器鑒權(quán)后返回資源。然后以后都是重復(fù)這個(gè)工作。但某一天你的電腦中了木馬病毒,它并不破壞你的電腦,而只是盜取你的消息如cookie文件。然后這個(gè)盜取你消息的人拿著瀏覽器去訪問(wèn)你所訪問(wèn)的網(wǎng)站會(huì)造成什么后果?
不需要登錄瀏覽器自動(dòng)推送服務(wù)器自動(dòng)做鑒權(quán),服務(wù)器會(huì)誤認(rèn)這個(gè)非法用戶是你,這個(gè)危害對(duì)你還不是最大的,對(duì)社會(huì)影響最大。比如用你的身份進(jìn)行詐騙等。
對(duì)你來(lái)說(shuō)你的信息泄漏了。
上面的問(wèn)題并不是信息放在文件中的問(wèn)題,而是把文件放在客戶端出了問(wèn)題,用戶對(duì)自己信息保護(hù)能力有限。
接下來(lái)為了解決上面的問(wèn)題,我們有一個(gè)新的方案。這個(gè)消息不要保存在瀏覽器上,而保存在服務(wù)器上。
瀏覽器首先還是登錄輸入用戶名和密碼,服務(wù)端把用戶形成的認(rèn)證信息瀏覽痕跡在服務(wù)端形成一個(gè)session文件,服務(wù)器可能有成百上千萬(wàn)用戶,而每個(gè)用戶只要曾經(jīng)登錄過(guò)都要形成session文件,所以給每一個(gè)session文件起一個(gè)唯一名稱,通常是一個(gè)大大的字符串我們稱之為session id具有唯一性。然后服務(wù)器在登錄成功的時(shí)候把這個(gè)session id返回給瀏覽器,瀏覽器只保存session id,依舊保存在cookie文件中。以后瀏覽器在訪問(wèn)服務(wù)器的時(shí)候構(gòu)建請(qǐng)求里面必須把session id帶上,這個(gè)請(qǐng)求到了服務(wù)端結(jié)合在這個(gè)session id在session id列表中對(duì)這個(gè)session id做認(rèn)證,只要有并且內(nèi)容沒(méi)有異常服務(wù)器就直接認(rèn)為這個(gè)用戶處于登錄狀態(tài),然后把資源返回去。每次請(qǐng)求都會(huì)做這個(gè)動(dòng)作。
這個(gè)方法將用戶的私密信息通過(guò)session id保存在服務(wù)器端,所以可以直接認(rèn)為用戶信息的泄漏,已經(jīng)大大改善了!我們這些信息由那些公司進(jìn)行保護(hù)。
但是這個(gè)黑客還是把木馬安裝到你的電腦里,雖然拿不到賬號(hào)密碼的私密信息,但能,把這個(gè)session id拿到自己的瀏覽器是對(duì)服務(wù)器進(jìn)行訪問(wèn),服務(wù)器還是會(huì)認(rèn)為這個(gè)非法用戶是你,但是這些公司會(huì)配合其他策略緩解這類問(wèn)題。如即使session id一樣,但是上次你在北京登錄,過(guò)一會(huì)你就跑廣東登錄了,區(qū)域ip地址不一樣,就會(huì)讓你下線讓你重新登錄等等方法。這些策略只要識(shí)別不是你要session id失效即可。
那server如何寫(xiě)入cookie信息?如何驗(yàn)證client會(huì)攜帶cookie?
服務(wù)器可以在報(bào)頭在加一條屬性。不過(guò)這條屬性內(nèi)容是我們隨便寫(xiě)的為了驗(yàn)證,別的服務(wù)器有自己的一套算法形成唯一的session id。
// 1. 服務(wù)器和網(wǎng)頁(yè)分離,html
// 2. url -> / : web根目錄
// 3. 我們要正確的給客戶端返回資源類型,我們首先要自己知道!所有的資源都有后綴?。?/span>
void Get(const httpRequest &req, httpResponse &resp)
{
cout << "----------------http start---------------" << endl;
cout << req.inbuffer << endl;
cout << "method: " << req.method << endl;
cout << "url: " << req.url << endl;
cout << "httpversion: " << req.httpversion << endl;
cout << "path :" << req.path << endl;
cout << "suffix: " << req.suffix << endl;
cout << "size: " << req.size << endl;
cout << "----------------http end-----------------" << endl;
//string respline = "HTTP/1.1 200 OK\r\n";
string respline = "HTTP/1.1 307 Temporary Redirect\r\n";
//string respheader="Content-Type: text/html\r\n";
string respheader = suffixtodes(req.suffix);
if (req.size > 0)
{
respheader += "Content-Length: ";
respheader += to_string(req.size);
respheader += "\r\n";
}
respheader += "Location: https://www.baidu.com/\r\n";
respheader+="Set-Cookie: 123456abc\r\n";//
string respblank = "\r\n";
// string body="<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>北京交通廣播《一路暢通》“交通大家談”節(jié)目,特邀北京市交通委員會(huì)地面公交運(yùn)營(yíng)管理處處長(zhǎng)趙震、北京市公安局公安交通管理局秩序處副處長(zhǎng) 林志勇、北京交通發(fā)展研究院交通規(guī)劃所所長(zhǎng) 劉雪杰為您解答公交車專用道6月1日起社會(huì)車輛進(jìn)出公交車道須注意哪些?</p></body></html>";
string body;
body.resize(req.size + 1);
if (!Util::readFile(req.path, body))
{
// 找不到文件,文件大小是-1,要返回404.html,因此重新計(jì)算大小
struct stat sif;
if (stat(html_404.c_str(), &sif) == 0)
body.resize(sif.st_size + 1);
Util::readFile(html_404, body); // 一定能成功
}
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
cout << "----------------------http response start---------------------------" << endl;
cout << resp.outbuffer << endl;
cout << "----------------------http response end---------------------------" << endl;
resp.outbuffer += body;
}
Cookie還可以設(shè)置到期時(shí)間都可以試一下
respheader += "Set-Cookie: name=1234567abcdefg; Max-Age=120\r\n";
當(dāng)一旦設(shè)置好cookie之后,往后,每次http請(qǐng)求,都會(huì)自動(dòng)攜帶曾經(jīng)設(shè)置的所有的cookie,幫服務(wù)器進(jìn)行鑒權(quán)行為 —> http會(huì)話保持
10.基本工具(postman,fiddler)
postman,不是抓包工具,模擬客戶端 ----> 瀏覽器行為
fiddler,是一個(gè)抓包工具,專門用來(lái)抓http的,抓的是本地的,可以用來(lái)調(diào)試的
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-853450.html
fiddler原理就是,未來(lái)瀏覽器在發(fā)起請(qǐng)求時(shí)是把請(qǐng)求交給fiddler,由fiddler代替你去請(qǐng)求服務(wù)器,服務(wù)器響應(yīng)也是給fiddler,然后fiddler再把響應(yīng)轉(zhuǎn)發(fā)給瀏覽器。
而postman可以認(rèn)為就是把瀏覽器換成它發(fā)起請(qǐng)求然后服務(wù)器給響應(yīng)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-853450.html
到了這里,關(guān)于【Linux網(wǎng)絡(luò)編程】HTTP協(xié)議的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!