目錄
一、認識HTTP協議
1.上網的本質
2.應用層的運行邏輯
3.HTTP的概念
二、url
1.認識網址
三、HTTP協議的宏觀理解
1.HTTP請求
2.HTTP響應
3.實際的HTTP請求
(1)測試代碼
(2)接收HTTP請求
(3)真實HTTP請求的結構
4.實際的HTTP響應
四、HTTP通信的簡單模擬
1.構建完整的HTTP協議通信過程
(1)增加getfirstline函數
(2)改造HttpRequest
(3)改造HttpRequest
(4)改造Delreq
(5)測試
2.網頁構建與協議通信的解耦
(1)添加html文件
(2)readfile函數
(3)改造httprequest類
(4)改造Delreq函數
(5)測試
3.在網頁增加跳轉
4.http傳遞其他類型文件
(1)修改html文件
(2)HttpRequest類
(3)修改HttpServer.cc
(4)運行
5.總代碼
五、GET與POST方法
1. 認識表單
2.觀察GET和POST方法
3.二者的區別
六、長鏈接
七、會話保持
1.認識Cookie技術
2.內存級與文件級Cookie技術
3.Session文件
八、HTTP狀態碼
1.HTTP狀態碼的分類
2.重定向狀態碼(3XX)
一、認識HTTP協議
1.上網的本質
不知道你在上網的時候是否想過,你看到的這些文字、圖片還有視頻等信息都是怎么出現在你的電腦屏幕上的?
像我們之前寫的udp和tcp協議的客戶端(client)和服務器(server)中,客戶端會把對資源的請求發送給服務器,服務器會根據請求將客戶端需要的數據發回。這種客戶端與服務端相互發送數據的機制一般稱為CS模式,我們目前市面上的各種app都使用這樣的機制運行。
2.應用層的運行邏輯
還記得我們之前實現的網絡計算器嗎?
它的執行流程如下:
我們之前說過,應用層包括應用層、表示層和會話層。
會話層負責建立連接,在我們之前的代碼中對應了socket、bind等函數建立連接和發送信息的過程。
應用層負責轉化不同形式類型的數據,在我們的代碼中對應了加去報頭和序列化反序列化過程。
會話層負責針對特定應用的協議,在我們的代碼中對應了Request和Response結構體的處理。
這三層都需要我們自己實現,雖然我們經常把這三層看為一層,但三層的功能涇渭分明,每一層都自己的工作。
3.HTTP的概念
你可能還會說,這不對呀。服務器上的數據有視頻、圖片還有其他數據。那不同的數據又都是怎么發送到我們的電腦上的呢?這就要講到HTTP了。
HTTP協議中文名為超文本傳輸協議,既是最經典的應用層協議,也是應用最廣泛的協議。它可以將服務器上的任意類型的數據拉取到本地瀏覽器,瀏覽器對其進行解釋可以得到網頁文本 、圖片 、視頻、音頻等資源。
二、url
1.認識網址
我們上網都需要網址,通過網址我們就能跳轉到某一個網頁,比如說百度搜索的地址:百度一下,你就知道
在HTTP協議中,我們常說的網址被稱為url。一個url字段的組成大致是這樣的:
-
http/https:標識當前使用的協議,原來使用較多的是http協議,近十年來大部分網站都使用了更安全的https協議。
-
//:表示 URL想訪問服務器的 什么資源。
-
www.example.jp:表示服務器的IP地址,雖然我們只能看到字符串,但它可以轉換為IP地址。
-
80:表示服務器進程的端口號,由于其特殊性質,端口號是一個不能隨便更改的值,比如說https協議常用的端口號為443,http協議常用的端口號為80。協議名稱和端口號之間是一對一,強相關的。
-
/:80后面的第一個斜杠表示web根目錄。
-
/dir/index.htm:這個字符串表示文件路徑,當請求發到服務端上時,就會在這個目錄下查找文件傳輸。
-
?和其他/:這樣的字符被url當做特殊意義理解,一般當作分隔符使用。
-
uid=1#ch1:?后面的分布可以看作一個的鍵值對,=左邊的uid可看作是key,=右邊的1可看作value。
真實的url與示例上的會不太一樣,有省略的或者新增的部分。
三、HTTP協議的宏觀理解
1.HTTP請求
HTTP的請求結構以行(hang)為單位,可分為四個部分:請求行、請求報頭、空行、有效載荷。
(1)請求行
HTTP協議請求結構的第一行被稱為請求行,以空格為分隔符包含請求方法、請求地址和協議版本。
比如:GET / HTTP/1.1,其中GET是請求方法,還有一個方法叫做POST。/表示請求地址,也就是我需要哪個目錄下的文件,這里就表示網絡的根目錄。HTTP/1.1是協議版本,HTTP常用的有三個版本http/1.0、http/1.1和http/2.0,我們以后使用的都是1.1版本。
(2)請求報頭
請求報頭是由多個Key:Value結構構成的多行結構,請求報頭中包含許多請求屬性,每一條屬性為一行,使用\r\n結尾。
(3)空行
只包含\r\n,有分隔請求報頭和有效載荷的效果。
(4)有效載荷
這里儲存的一般是用戶可能提交的參數,這部分內容不是http協議必需的。
2.HTTP響應
HTTP的相應結構也以行(hang)為單位,同樣可分為四個部分:狀態行、響應報頭、空行和有效載荷。
(1)狀態行
HTTP協議響應結構的第一行被稱為請求行,以空格為分隔符包含協議版本狀態碼和狀態碼描述。
協議版本就不說了,而對狀態碼而言,你可能不知道http協議,但你一定在上網時遇到過打不開的網站,頁面會告訴你404 not found。
這里的404就是狀態碼,而not found就是404狀態碼的描述。
(2)響應報頭
響應報頭與請求報頭基本一致,只是二者存儲的屬性會略微不同。
(3)空行
只包含\r\n,有分隔請求報頭和有效載荷的效果。
(4)有效載荷
有效載荷主要是需要傳回的資源,可能是html/css的文件資源,也可能是請求對應的圖片等等。
3.實際的HTTP請求
(1)測試代碼
為了驗證真正的HTTP請求是否和我們描述的一樣,我們使用下面TCP通信服務端改造后的代碼作為服務端,接收不同平臺瀏覽器發來的http請求。接收后將請求打印在屏幕上。
使用該代碼時,需要在云服務器官網將該機器中你需要使用的端口號設為開放,否則防火墻會拒絕所有的申請,你也不會收到http協議。
socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>using namespace std;enum
{Socket_false = 1, // 創建套接字失敗Bind_false, // 綁定失敗Listen_false, // 監聽失敗Accept_false, // 等待客戶端連接失敗Connect_false, // 連接服務器失敗
};const int backlog = 5;class Sock
{
public:// 構造函數Sock(int listensockfd = -1): _listensockfd(listensockfd){}// 創建套接字void Socket(){_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:代表IPv4,SOCK_STREAM:代表Tcp協議if (_listensockfd < 0){cout << "socket false" << endl;exit(Socket_false);}cout << "socket success" << endl;}// 綁定,傳端口號,是因為sockaddr_in需要,用來保存客戶端的IP地址和端口號void Bind(uint16_t port){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; // 代表0.0.0.0socklen_t len = sizeof(local);int n = bind(_listensockfd, (struct sockaddr *)&local, len);if (n < 0){cout << "bind false" << endl;exit(Bind_false);}cout << "bind success" << endl;}// 監聽void Listen(){int n = listen(_listensockfd, backlog);if (n < 0){cout << "listen false" << endl;exit(Listen_false);}cout << "listen success" << endl;}// 如果沒有客戶端連接服務端,則accept會阻塞等待新連接,等待客戶端的連接// 可以通過Accept函數,拿到客戶端的ip地址,端口號,用于網絡通信的描述符sockint Accept(string &clientip, uint16_t &clientport){sockaddr_in addr;socklen_t len = sizeof(addr);// 調用accpt函數,會將客戶端的數據保存在addr中(ip,port)int sock = accept(_listensockfd, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(Accept_false);}cout << "accept success" << endl;// 將客戶端的ip和port,放入clientip = inet_ntoa(addr.sin_addr); // 將in_addr_t類型的ip轉為char*類型的ipclientport = ntohs(addr.sin_port); // 將其轉為主機字節序return sock;}// 連接服務器,這個ip和port是我們在啟動客戶端的時候輸入的bool Connect(string &ip, uint16_t &port){sockaddr_in peer;memset(&peer, 0, sizeof(peer));//初始化peer.sin_family = AF_INET;peer.sin_addr.s_addr = inet_addr(ip.c_str());peer.sin_port = htons(port);socklen_t len = sizeof(peer);int n = connect(_listensockfd, (struct sockaddr *)&peer, len);if (n < 0){cout << "connect false" << endl;//exit(Connect_false);return false;}cout << "connect success" << endl;return true;}//關閉網絡文件適配符void Close(){close(_listensockfd);}//獲取網絡文件適配符int Getlistensockfd(){return _listensockfd;}// 析構函數~Sock(){}private:int _listensockfd; // 網絡文件適配符
};
httpserver.hpp
#include "socket.hpp"
#include<functional>
const int PORT = 8888;//請求
class HttpRequest
{
public:HttpRequest(){}
public:string inbuffer;
};//回應
class HttpResponse
{
public:HttpResponse(){}
public:string outbuffer;
};//http服務器
class HttpServer
{typedef function<void(HttpRequest&, HttpResponse&)> func_t;
public:HttpServer(func_t func,uint16_t port = PORT):_func(func) ,_port(port){}//初始化void Init(){_listensockfd.Socket();//創建套接字_listensockfd.Bind(_port);//綁定_listensockfd.Listen();//監聽}//運行void Start(){while (true){string clientip;uint16_t clientport;int sockfd = _listensockfd.Accept(clientip, clientport);//鏈接等待if (socket < 0){cout << "Accept false" << endl;continue;}pid_t pid = fork();//創建子進程if (pid == 0){_listensockfd.Close();//關閉監聽網絡適配符if (fork() > 0)//創建子進程,將父進程關閉,使其成為孤兒進程{exit(0);}hander_enter(sockfd);//處理客戶端的信息close(sockfd);//關閉網絡適配符exit(0);}}}//處理void hander_enter(int sockfd){HttpRequest req;//創建請求HttpResponse resp;//創建回應char buffer[4096];//存儲客戶端發來的信息ssize_t n = recv(sockfd, buffer, sizeof(buffer)-1, 0);if(n > 0){buffer[n] = 0;req.inbuffer = buffer;//將信息給請求_func(req, resp);//將請求給回應send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);//將回應寫回}}private:uint16_t _port;Sock _listensockfd;func_t _func;
};
httpservermain.cc
#include"httpserver.hpp"
#include<memory>//將請求給回應,并且將請求打印
void Delreq(HttpRequest& req, HttpResponse& resp)
{cout << "------------------http start------------------" << endl;resp.outbuffer = req.inbuffer;cout << req.inbuffer;cout << "------------------http end------------------" << endl;
}int main(int args,char* argv[])
{uint16_t port=stoi(argv[1]);unique_ptr<HttpServer> p(new HttpServer(Delreq,port));p->Init();p->Start();return 0;
}
(2)接收HTTP請求
我們首先打開電腦上的瀏覽器,輸入url:http://+云服務器公網IP+:+端口號(例如:http:12.34.56.78:8080)
我們可以觀察到進入網站后,Xshell屏幕上出現了按行打印的http請求(第一個)。
(3)真實HTTP請求的結構
第一行為請求行,下面是請求報頭,空行將有效載荷和請求報頭隔開,只是這個請求沒有有效載荷。
請求報頭中包含許多請求屬性,每個都采用name:val的鍵值對形式,并且都是一個字符串。每一條屬性占一行,使用\r\n結尾。
-
Host:43.143.106.44:8080,表示客戶端請求服務端的套接字(IP地址 + 端口號)。
-
Connection:keep-alive,表示長連接。
-
Upgrade-Insecure-Requests: 表示瀏覽器(客戶端)支持自動將HTTP請求升級到HTTPS請求,在學習https協議后再詳談。
-
User-Agent:客戶端的相關信息,內容包括客戶端的操作系統和使用的瀏覽器等信息,使用華為手機、其他安卓手機、iPhone和電腦發送http請求都可以看到不同的信息。
-
Accept: 相關信息,表示該請求要請求的資源類型。
-
Accept-Encoding:gzip, deflate,表示客戶端支持兩種encode格式。
-
服務器可以根據客戶端的支持情況采用不同的壓縮算法進行內容壓縮。常見的壓縮算法有gzip和deflate。
-
Accept-Language:zh-CN,zh;q=0.9,表示客戶端支持的語言格式。
-
請求報頭中的所有屬性都是采用name: val的鍵值對形式,并且是一個字符串。
后面就是一個空行,只有一個\r\n。
4.實際的HTTP響應
我們并不能打印瀏覽器接收到的響應,所以我們需要自己構造http請求和響應,理解HTTP的響應。
我們在handler函數中除了接收http請求,還要構建一個http的響應發回客戶端
#include"HttpServer.hpp"
#include<memory>
#include<unistd.h>
#include<fcntl.h>using namespace std;void Delreq(const HttpRequest& req, HttpResponse& resp)
{cout << "------------------http start------------------" << endl;cout << req.inbuffer;cout << "------------------http end------------------" << endl;string resp_line = "HTTP/1.1 200 OK\r\n";//構造狀態行string resp_hander = "Content-Type:text/html\r\n";//構造響應報頭string resp_black = "\r\n";//構造空行//響應的正文也不是必要的,這里就不寫了//響應序列化,即把它們按順序拼接好resp.outbuffer += resp_line;resp.outbuffer += resp_hander;resp.outbuffer += resp_black;
}int main(int argc, char* argv[])
{uint16_t port = atoi(argv[1]);unique_ptr<Httpserver> p(new Httpserver(Delreq, port));p->initserver();p->start();return 0;
}
這次我們就不需要通過瀏覽器發送請求了,可以使用telnet+IP地址127.0.0.1+端口號的方式進行本地環回發送http請求。
但是使用telnet指令需要輸入yum install telnet指令安裝telnet工具。
首先,運行上述代碼編譯的程序,我使用8081作為端口號。
然后,輸入telnet 127.0.0.1 8081向服務器發送請求。
接著,按Ctrl+ ]鍵會顯示telnet>,再按Enter鍵跳到下一行。
最后,手動輸入請求行GET / HTTP/1.1,按Enter鍵。
此時我們就向服務端發送了請求,通過處理后我們也能收到服務器的響應。
下面紅色框的部分就是我們構建的響應。可以看到狀態行(HTTP/1.1 200 OK)、響應報頭(Content-Type:text/html)和空行(\r\n),和我們學習的宏觀響應一致。
在這里我們也確實能看到,HTTP是基于請求和響應的應用層協議。使用TCP套接字,客戶端向服務端發送request請求,服務端接收到請求后經過處理返回response響應,實現了服務端和客戶端的通信。
四、HTTP通信的簡單模擬
1.構建完整的HTTP協議通信過程
前面通過代碼已經讓大家認識了HTTP的請求和響應的結構,也看到了真實的請求和響應,接下來我們使用上面的代碼構建一個基于HTTP協議的接收請求和發送響應的服務端程序。
(1)增加getfirstline函數
Util.hpp
class Util
{
public://截取該請求的第一行,并且將該請求的這一行刪除,inbuffer是請求,set是"\r\n"//將這里設置為靜態函數,是為了防止其他函數傳參給了this指針static string getfirstline(string& inbuffer){auto pos=inbuffer.find(set);//在請求中找"\r\n"if(pos==string::npos){return "";//如果找不到,則返回空}string str=inbuffer.substr(0,pos);//找到,截取第一行inbuffer.erase(0,pos);//將第一行,從請求中刪除return str;//返回第一行}
};
(2)改造HttpRequest
然后,我們在請求類中增加幾個成員變量,包括請求的訪問方法method,請求的http版本httpversion,以及請求路徑url。
通過成員函數parse對請求進行反序列化,首先使用getfirstline讀取請求中的請求行,然后將請求行中的三個字段分離出來。stringstream變量可以將字符串以空格進行分割,流提取可以將數據放入變量。
// 請求
class HttpRequest
{
public:HttpRequest(){}//將請求的第一行的方法,路徑,版本分別放入該字符串中void parse(){//得到請求的第一行string line=Util::getfirstline(inbuffer);if(line.size()==0)return;//沒有獲取到一行信息,出錯//使用這一行信息構造一個stringstream變量std::stringstream ss(line);//從該變量中以空格為分隔符分別將信息放入變量中ss >> method >> url >> httpversion;}public:string inbuffer; //完整請求string method; //請求方法string url; //請求路徑string httpversion; //請求版本
};// 回應
class HttpResponse
{
public:HttpResponse(){}public:string outbuffer;
};
(3)改造HttpRequest
服務器的處理入口HttpServer::handler_enter中使用parse對請求進行反序列化。
// 處理void hander_enter(int sockfd){HttpRequest req; // 創建請求HttpResponse resp; // 創建回應char buffer[4096]; // 存儲客戶端發來的信息ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;req.inbuffer = buffer; // 將信息給請求 // 反序列化req.parse(); //將請求反序列化,將請求中的數據存入HttpRequest類中 _func(req, resp); // 將請求給回應send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0); // 將回應寫回}}
(4)改造Delreq
Delreq函數用于處理請求并構建響應,增加將請求行中的三個字段打印出來的代碼。
這次構造響應正文的時候,我們將一段html代碼以字符串的形式拼接到了響應上。關于html的使用我們不做教學,可以去網上搜一搜html構建網頁,直接下載源碼使用。
//將請求給回應,并且將請求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{cout << "------------------http start------------------" << endl;cout << req.inbuffer;cout<<"反序列化后,成員的值"<<endl;cout<<"method: "<<req.method<<endl;cout<<"url: "<<req.url<<endl;cout<<"httpversion: "<<req.httpversion<<endl;cout << "------------------http end------------------" << endl;//客戶端自己會打印respstring resp_line = "HTTP/1.1 200 OK\r\n";//構造狀態行string resp_hander = "Content-Type:text/html\r\n";//構造響應報頭string resp_black = "\r\n";//構造空行string resp_body = "<html><head></head><body><h1>Hello HTTP</h1></body></html>";//響應正文//響應序列化,即把它們按順序拼接好resp.outbuffer += resp_line;resp.outbuffer += resp_hander;resp.outbuffer += resp_black;resp.outbuffer += resp_body;
}
(5)測試
使用telnet工具向服務端發起請求,此時就會得到服務端的響應,如上圖所示,包括響應正文(html的代碼)。
在服務端就可以看到telnet在發送請求是輸入的請求行中的三個字段。
如果用windows上的瀏覽器來訪問服務器,這段html代碼就可以得到一個如圖所示的網頁。
第二個請求是瀏覽器發來的
我們可以發現正文中的Hello HTTP字段被顯示到了網頁中。響應正文中的html代碼代表了一個網頁,這個網頁被服務端響應給客戶端。
由于Linux中使用telnet得到響應正文并沒有被解釋,html代碼并沒有被處理。而Windows的瀏覽器是我們能使用到的軟件中開發難度最大的,所以它本身就已經非常智能,瀏覽器得到響應正文會解釋它的含義,解釋后呈現給我們的結果就是一個網頁。
2.網頁構建與協議通信的解耦
(1)添加html文件
首先,我們在保存服務器代碼的目錄中創建一個wwroot目錄作為http訪問的網絡根目錄,然后在內部創建兩個html文件和一個test目錄,index.html用于構建網站的首頁,404.html用于構建非法訪問返回的404頁面,test目錄下也儲存兩個構建網站的代碼。
還是一樣的,html作為前端知識我們這里不做介紹,大家直接使用這些代碼即可。
當客戶端發起的請求中的url為\時,此時客戶端訪問的就是web根目錄,也就是./wwwroot目錄。這時我們將index.html作為響應返回。就會將該文件中的內容作為響應正文返回給客戶端。
?inde.html
<!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>我構建的網頁</title>
</head>
<body><h1>網頁首頁</h1>
</body>
</html>
當客戶端請求中url錯誤或者無效時,會將該404.html文件中的內容作為響應正文返回給客戶端,告知客戶端訪問資源不存在并顯示404錯誤碼。
404.html
<!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>資源不存在</title>
</head>
<body><h1>你所訪問的資源并不存在,404!</h1>
</body>
</html>
當客戶端發起的請求中url為/test/a.html或/test/b.html的時候,服務器就會將這兩個文件的內容作為響應正文返回給客戶端,客戶端得到a或b網頁。
a.html
<!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>我構建的網頁</title>
</head>
<body><h1>我是網頁a</h1>
</body>
</html>
b.html
<!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>我構建的網頁</title>
</head>
<body><h1>我是網頁b</h1>
</body>
</html>
(2)readfile函數
首先,既然我們要分離網頁和服務器,那我們就需要使用文件操作將儲存在文件中的html代碼讀取到服務器進程中。
在Util類中增加一個readfile函數:
//打開網頁文件,將代碼提取到緩沖區中//resourse:文件的路徑,buffer:緩沖區,size:文件的大小//將這里設置為靜態函數,是為了防止其他函數傳參給了this指針static bool readfile(string& resourse,char* buffer,int size){ifstream in(resourse,ios::binary);//打開文件if(!in.is_open()){return false;//文件打開失敗,則返回false}in.read(buffer,size);//將網頁代碼讀到buffer中in.close()//關閉文件}
(3)改造httprequest類
我們在HttpRequest類中增加兩個成員變量string path和int size,分別標識請求路徑和網絡資源的大小。
既然增加了這兩個變量,那么對反序列化成員函數prase也要增加對這兩個變量的處理。
首先將目錄設置為./wwwroot,此時再拼接傳遞過來的url,如果url為/則拼接出來的是./wwwroot/,如果是其他的內容比如a/b/c.html,最后得到的是./wwwroot/a/b/c.html。也就是說,只有對根目錄的請求是不精確到某個文件的,所以如果path的最后一個字符是/就表明它是在對根目錄做請求,所以我們只要是對根目錄做請求我們就再在index.html,也就變相滿足了對根目錄的申請。
接著我們處理size變量,對于獲取文件的大小,Linux中存在系統調用stat,可用與于獲取文件大小。
stat函數()
int stat(const char* path, struct stat* buf);
-
頭文件:sys/socket.h、sys/stat.h、unistd.h
-
功能:獲取文件的大小。
-
參數:const char* path是表示目標文件的路徑的字符串。 struct stat* buf是一個struct stat類型的結構體變量,它的成員變量off_t st_size就指示了文件的大小(以字節為單位)。
-
返回值:調用成功返回0,調用失敗返回-1。
struct stat的定義:
struct stat {mode_t st_mode; //文件對應的模式,文件,目錄等ino_t st_ino; //inode節點號dev_t st_dev; //設備號碼dev_t st_rdev; //特殊設備號碼nlink_t st_nlink; //文件的連接數uid_t st_uid; //文件所有者gid_t st_gid; //文件所有者對應的組off_t st_size; //普通文件,對應的文件字節數time_t st_atime; //文件最后被訪問的時間time_t st_mtime; //文件內容最后被修改的時間time_t st_ctime; //文件狀態改變時間blksize_t st_blksize; //文件內容對應的塊大小blkcnt_t st_blocks; //偉建內容對應的塊數量};
如果我們獲取文件的大小失敗,則意味著這個文件很可能不存在,我們將大小設置為404.html的大小就可以了,以后返回的正文也會是404.html的內容。
// 將請求的第一行的方法,路徑,版本分別放入該字符串中// 將客戶端的訪問路徑放入path,將要訪問的網絡文件的字節數放入sizevoid parse(){// 得到請求的第一行string line = Util::getfirstline(inbuffer, set);if (line.size() == 0)return; // 沒有獲取到一行信息,出錯// 使用這一行信息構造一個stringstream變量std::stringstream ss(line);// 從該變量中以空格為分隔符分別將信息放入變量中ss >> method >> url >> httpversion;// 添加路徑path += root_directory; // 先設置網絡根目錄./wwwrootpath += url; // url:表示根目錄之后的路徑,再將url追加在根目錄之后,例如:./wwwroot/text/a.html// 如果path最后1一個是'/',則表示客戶端訪問的是該網站的首頁if (path[path.size() - 1] == '/'){path += home_page; // 將首頁追加到path中: ./wwwwroot/index.html}// 網絡文件的大小struct stat st;int n = stat(path.c_str(), &st);if (n == 0) // 如果返回值為0,則資源獲取成功,將該網絡文件的字節數賦給size{size = st.st_size;}else // 如果資源獲取失敗,則設置404.html,并將其字節數賦給size{n = stat(html_404.c_str(), &st);size = st.st_size;}}
(4)改造Delreq函數
對于Delreq函數,除了需要增加兩個新變量的打印,還要增加從文件中讀取正文的代碼。
resp_body用于儲存響應正文,先給它開辟正文總字節數加一的空間,然后通過readfile讀取文件,如果讀取失敗,就讀取404文件作為正文。
//將請求給回應,并且將請求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{cout << "------------------http start------------------" << endl;cout << req.inbuffer<<endl; //完整請求cout<<"反序列化后,成員的值"<<endl;cout<<"method: "<<req.method<<endl; //請求方法cout<<"url: "<<req.url<<endl; //請求urlcout<<"httpversion: "<<req.httpversion<<endl; //請求版本cout<<"請求網絡文件路徑: "<<req.path<<endl; //請求網絡文件路徑cout<<"請求網絡文件大小: "<<req.size<<endl; //請求網絡文件大小cout << "------------------http end------------------" << endl;//客戶端自己會打印respstring resp_line = "HTTP/1.1 200 OK\r\n";//構造狀態行//設置響應的類型,將響應發給瀏覽器,并告訴瀏覽器文件類型,text/html:表示文件為html的文檔string resp_hander = "Content-Type:text/html\r\n";//構造響應報頭string resp_black = "\r\n";//構造空行string resp_body;//響應正文resp_body.resize(req.size+1);//開辟響應正文的大小,比網絡文件的大小加一,可以將網絡文件全部存儲到響應正文中//將該路徑下網絡文件的代碼,全部存儲在resp_body中,也就是放入響應正文中if(!Util::readfile(req.path,(char*)resp_body.c_str(),req.size)){//如果讀取失敗,則將html_404文件的代碼,放入響應正文中Util::readfile(html_404,(char*)resp_body.c_str(),req.size);}//響應序列化,即把它們按順序拼接好resp.outbuffer += resp_line;resp.outbuffer += resp_hander;resp.outbuffer += resp_black;resp.outbuffer += resp_body;
}
(5)測試
我們輸入url:http:公網ip:端口號,對web根目錄進行請求,得到首頁。
我們輸入url:http:公網ip:端口號/test/a.html,對a.html文件進行請求,返回網頁a。
我們輸入url:http:公網ip:端口號/test/a/b/c.html,對一個不存在的文件進行請求,返回網頁404。(其實我們應該將返回的狀態碼也改成404,但是我們只是簡單模擬,就不在意這些細節了)
3.在網頁增加跳轉
我們經常在網頁中經常使用跳轉,我們只要點擊帶有鏈接的文字就能跳轉到另一個網頁。其實很簡單,只需要增加一些html的語句就能實現這樣的操作。
比如說,我們構造了兩個語句藍字表示鏈接的html文件,黑字表示鏈接文字的內容。
<!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>我構建的網頁</title>
</head>
<body><h1>網頁首頁</h1><a href="/text/a.html">a網頁</a><a href="/text/b.html">b網頁</a>
</body>
</html>
我們啟動測試:
我們點擊a網頁,就能跳轉到a網頁。
4.http傳遞其他類型文件
http作為超文本傳輸協議,當然可以支持圖片、視頻、音頻等文件的傳輸,所以我們修改代碼使得我們的服務器也支持圖片的傳遞。
比如說,我們在wwwroot中創建一個image文件夾,文件夾中存儲一個圖片文件1.png。(這個圖片不要太大,可以將圖片拖拽進vscode保存在云服務器上)
此時我們想把這個圖片也傳遞到首頁中
(1)修改html文件
首先要在index.html增加圖片信息的代碼,代碼中的alt后面的文字表示圖片加載失敗時顯示在屏幕上的文字。
<!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>我構建的網頁</title>
</head>
<body><h1>網頁首頁</h1><a href="/text/a.html">a網頁</a><a href="/text/b.html">b網頁</a><img src="image/1.png" alt="Linux">
</body>
</html>
(2)HttpRequest類
由于發送http的request本質是在對某一個文件進行申請,而url指示了該文件的位置與名稱,所以我們在httprequest類中再增加一個成員變量suffix儲存標識文件類型的后綴。
然后,由于我們在prase函數中已經有了獲取文件路徑path的代碼,所以我們只需要從path中獲取資源后綴即可。(第四大塊,代碼后有注釋說明)
// 將請求的第一行的方法,路徑,版本分別放入該字符串中// 將客戶端的訪問路徑放入path,將要訪問的網絡文件的字節數放入sizevoid parse(){// 得到請求的第一行string line = Util::getfirstline(inbuffer, set);if (line.size() == 0)return; // 沒有獲取到一行信息,出錯// 使用這一行信息構造一個stringstream變量std::stringstream ss(line);// 從該變量中以空格為分隔符分別將信息放入變量中ss >> method >> url >> httpversion;// 添加路徑path += root_directory; // 先設置網絡根目錄./wwwrootpath += url; // url:表示根目錄之后的路徑,再將url追加在根目錄之后,例如:./wwwroot/text/a.html// 如果path最后1一個是'/',則表示客戶端訪問的是該網站的首頁if (path[path.size() - 1] == '/'){path += home_page; // 將首頁追加到path中: ./wwwwroot/index.html}//獲取path對應的資源后綴//例:./wwwroot/index.html//./wwwroot/text/a.html//./wwwroot/image/1.pngauto pos=path.find(".");//從后往前找if(pos==string::npos){suffix=".html";//找不到,默認它的后綴為html}else{suffix=path.substr(pos);//找到了,從.開始截取字符,存入suffix中}// 網絡文件的大小struct stat st;int n = stat(path.c_str(), &st);if (n == 0) // 如果返回值為0,則資源獲取成功,將該網絡文件的字節數賦給size{size = st.st_size;}else // 如果資源獲取失敗,則設置404.html,并將其字節數賦給size{n = stat(html_404.c_str(), &st);size = st.st_size;}}
(3)修改HttpServer.cc
首先,我們在Delreq中已經拿到了需求文件的后綴。
我們原先的代碼中,http響應相關屬性的Content-Type是寫死的,我們在這里要增加一個suffixToDesc函數用于拼接不同文件的Content-Type屬性字符串。
其他類型文件的Content-Type對應類型標識可以看下面的表格。
在Delreq函數中增加構造Content-Type和Content-Length兩個屬性字符串的代碼。
//根據網絡文件的后綴,來確定"Content_Type"的類型
string suffixToDesc(const string suffix)
{string ct="Content_Type: ";if(suffix==".png"){ct+="application/x-png";}else if(suffix==".html"){ct+="text/html";}ct+="\r\n";return ct;
}//將請求給回應,并且將請求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{cout << "------------------http start------------------" << endl;cout << req.inbuffer<<endl; //完整請求cout<<"反序列化后,成員的值"<<endl;cout<<"method: "<<req.method<<endl; //請求方法cout<<"url: "<<req.url<<endl; //請求urlcout<<"httpversion: "<<req.httpversion<<endl; //請求版本cout<<"請求網絡文件路徑: "<<req.path<<endl; //請求網絡文件路徑cout<<"請求網絡文件大小: "<<req.size<<endl; //請求網絡文件大小cout << "------------------http end------------------" << endl;//客戶端自己會打印respstring resp_line = "HTTP/1.1 200 OK\r\n";//構造狀態行//設置響應的類型,將響應發給瀏覽器,并告訴瀏覽器文件類型,text/html:表示文件為html的文檔string resp_hander=suffixToDesc(req.suffix);//根據文件類型構造Content-Type//構造Content-Lengthstring resp_len="Content-Length: ";resp_len+=to_string(req.size);resp_len+="\r\n";string resp_black = "\r\n";//構造空行string resp_body;//響應正文resp_body.resize(req.size+1);//開辟響應正文的大小,比網絡文件的大小加一,可以將網絡文件全部存儲到響應正文中//將該路徑下網絡文件的代碼,全部存儲在resp_body中,也就是放入響應正文中if(!Util::readfile(req.path,(char*)resp_body.c_str(),req.size)){//如果讀取失敗,則將html_404文件的代碼,放入響應正文中Util::readfile(html_404,(char*)resp_body.c_str(),req.size);}//響應序列化,即把它們按順序拼接好resp.outbuffer += resp_line;resp.outbuffer += resp_hander;resp.outbuffer += resp_len;resp.outbuffer += resp_black;resp.outbuffer += resp_body;
}
(4)運行
我們再次打開服務器訪問網頁,可以看到圖片顯示出來了。
5.總代碼
util.hpp:將網絡文件讀取到緩沖區中,和拿到請求的第一行
socket.hpp:里面包含了套接字的創建,綁定,監聽,連接等等
ios.hpp:HttpRequest類和HttpResponse類
httpserver.hpp:服務區的頭文件
httpservermain.cc:服務器的main函數
util.hpp
#include<string>
#include<fstream>using namespace std;class Util
{
public://截取該請求的第一行,并且將該請求的這一行刪除,inbuffer是請求,set是"\r\n"//將這里設置為靜態函數,是為了防止其他函數傳參給了this指針static string getfirstline(string& inbuffer,const string& set){auto pos=inbuffer.find(set);//在請求中找"\r\n"if(pos==string::npos){return "";//如果找不到,則返回空}string str=inbuffer.substr(0,pos);//找到,截取第一行inbuffer.erase(0,pos);//將第一行,從請求中刪除return str;//返回第一行}//打開網頁文件,將代碼提取到緩沖區中//resourse:文件的路徑,buffer:緩沖區,size:文件的大小//將這里設置為靜態函數,是為了防止其他函數傳參給了this指針static bool readfile(const string& resourse,char* buffer,int size){ifstream in(resourse,ios::binary);//打開文件if(!in.is_open()){return false;//文件打開失敗,則返回false}in.read(buffer,size);//將網頁代碼讀到buffer中in.close();//關閉文件return true;}};
socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>using namespace std;enum
{Socket_false = 1, // 創建套接字失敗Bind_false, // 綁定失敗Listen_false, // 監聽失敗Accept_false, // 等待客戶端連接失敗Connect_false, // 連接服務器失敗
};const int backlog = 5;class Sock
{
public:// 構造函數Sock(int listensockfd = -1): _listensockfd(listensockfd){}// 創建套接字void Socket(){_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:代表IPv4,SOCK_STREAM:代表Tcp協議if (_listensockfd < 0){cout << "socket false" << endl;exit(Socket_false);}cout << "socket success" << endl;}// 綁定,傳端口號,是因為sockaddr_in需要,用來保存客戶端的IP地址和端口號void Bind(uint16_t port){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; // 代表0.0.0.0socklen_t len = sizeof(local);int n = bind(_listensockfd, (struct sockaddr *)&local, len);if (n < 0){cout << "bind false" << endl;exit(Bind_false);}cout << "bind success" << endl;}// 監聽void Listen(){int n = listen(_listensockfd, backlog);if (n < 0){cout << "listen false" << endl;exit(Listen_false);}cout << "listen success" << endl;}// 如果沒有客戶端連接服務端,則accept會阻塞等待新連接,等待客戶端的連接// 可以通過Accept函數,拿到客戶端的ip地址,端口號,用于網絡通信的描述符sockint Accept(string &clientip, uint16_t &clientport){sockaddr_in addr;socklen_t len = sizeof(addr);// 調用accpt函數,會將客戶端的數據保存在addr中(ip,port)int sock = accept(_listensockfd, (struct sockaddr *)&addr, &len);if (sock < 0){cout << "accept false" << endl;exit(Accept_false);}cout << "accept success" << endl;// 將客戶端的ip和port,放入clientip = inet_ntoa(addr.sin_addr); // 將in_addr_t類型的ip轉為char*類型的ipclientport = ntohs(addr.sin_port); // 將其轉為主機字節序return sock;}// 連接服務器,這個ip和port是我們在啟動客戶端的時候輸入的bool Connect(string &ip, uint16_t &port){sockaddr_in peer;memset(&peer, 0, sizeof(peer));//初始化peer.sin_family = AF_INET;peer.sin_addr.s_addr = inet_addr(ip.c_str());peer.sin_port = htons(port);socklen_t len = sizeof(peer);int n = connect(_listensockfd, (struct sockaddr *)&peer, len);if (n < 0){cout << "connect false" << endl;//exit(Connect_false);return false;}cout << "connect success" << endl;return true;}//關閉網絡文件適配符void Close(){close(_listensockfd);}//獲取網絡文件適配符int Getlistensockfd(){return _listensockfd;}// 析構函數~Sock(){}private:int _listensockfd; // 網絡文件適配符
};
ios.hpp
#pragma once
#include <string>
#include "util.hpp"
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>using namespace std;const string set = "\r\n";
const string home_page = "index.html"; // 首頁
const string root_directory = "./wwwroot"; // 根目錄
const string html_404 = "./wwwroot/404.html";// 請求
class HttpRequest
{
public:HttpRequest(){}// 將請求的第一行的方法,路徑,版本分別放入該字符串中// 將客戶端的訪問路徑放入path,將要訪問的網絡文件的字節數放入sizevoid parse(){// 得到請求的第一行string line = Util::getfirstline(inbuffer, set);if (line.size() == 0)return; // 沒有獲取到一行信息,出錯// 使用這一行信息構造一個stringstream變量std::stringstream ss(line);// 從該變量中以空格為分隔符分別將信息放入變量中ss >> method >> url >> httpversion;// 添加路徑path += root_directory; // 先設置網絡根目錄./wwwrootpath += url; // url:表示根目錄之后的路徑,再將url追加在根目錄之后,例如:./wwwroot/text/a.html// 如果path最后1一個是'/',則表示客戶端訪問的是該網站的首頁if (path[path.size() - 1] == '/'){path += home_page; // 將首頁追加到path中: ./wwwwroot/index.html}//獲取path對應的資源后綴//例:./wwwroot/index.html//./wwwroot/text/a.html//./wwwroot/image/1.pngauto pos=path.find(".");//從后往前找if(pos==string::npos){suffix=".html";//找不到,默認它的后綴為html}else{suffix=path.substr(pos);//找到了,從.開始截取字符,存入suffix中}// 網絡文件的大小struct stat st;int n = stat(path.c_str(), &st);if (n == 0) // 如果返回值為0,則資源獲取成功,將該網絡文件的字節數賦給size{size = st.st_size;}else // 如果資源獲取失敗,則設置404.html,并將其字節數賦給size{n = stat(html_404.c_str(), &st);size = st.st_size;}}public:string inbuffer; // 完整請求string method; // 請求方法string url; // 請求urlstring httpversion; // 請求版本string path; // 請求路徑string suffix; //文件后綴int size; // 網絡文件的大小
};// 回應
class HttpResponse
{
public:HttpResponse(){}public:string outbuffer;
};
httpserver.hpp
#pragma once
#include "socket.hpp"
#include <functional>
#include <sstream>
#include "ios.hpp"const uint16_t PORT=8989;// http服務器
class HttpServer
{typedef function<void(const HttpRequest&, HttpResponse&)> func_t;public:HttpServer(func_t func, uint16_t port = PORT): _func(func), _port(port){}// 初始化void Init(){_listensockfd.Socket(); // 創建套接字_listensockfd.Bind(_port); // 綁定_listensockfd.Listen(); // 監聽}// 運行void Start(){while (true){string clientip;uint16_t clientport;int sockfd = _listensockfd.Accept(clientip, clientport); // 鏈接等待if (socket < 0){cout << "Accept false" << endl;continue;}pid_t pid = fork(); // 創建子進程if (pid == 0){_listensockfd.Close(); // 關閉監聽網絡適配符if (fork() > 0) // 創建子進程,將父進程關閉,使其成為孤兒進程{exit(0);}hander_enter(sockfd); // 處理客戶端的信息close(sockfd); // 關閉網絡適配符exit(0);}}}// 處理void hander_enter(int sockfd){HttpRequest req; // 創建請求HttpResponse resp; // 創建回應char buffer[4096]; // 存儲客戶端發來的信息ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;req.inbuffer = buffer; // 將信息給請求 // 反序列化req.parse(); //將請求反序列化,將請求中的數據存入HttpRequest類中 _func(req, resp); // 將請求給回應send(sockfd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0); // 將回應寫回}}private:uint16_t _port; //端口號Sock _listensockfd; //套接字func_t _func;
};
httpservermain.cc
#include"httpserver.hpp"
#include<memory>
#include"ios.hpp"//根據網絡文件的后綴,來確定"Content_Type"的類型
string suffixToDesc(const string suffix)
{string ct="Content_Type: ";if(suffix==".png"){ct+="application/x-png";}else if(suffix==".html"){ct+="text/html";}ct+="\r\n";return ct;
}//將請求給回應,并且將請求打印
void Delreq(const HttpRequest& req, HttpResponse& resp)
{cout << "------------------http start------------------" << endl;cout << req.inbuffer<<endl; //完整請求cout<<"反序列化后,成員的值"<<endl;cout<<"method: "<<req.method<<endl; //請求方法cout<<"url: "<<req.url<<endl; //請求urlcout<<"httpversion: "<<req.httpversion<<endl; //請求版本cout<<"請求網絡文件路徑: "<<req.path<<endl; //請求網絡文件路徑cout<<"請求網絡文件大小: "<<req.size<<endl; //請求網絡文件大小cout << "------------------http end------------------" << endl;//客戶端自己會打印respstring resp_line = "HTTP/1.1 200 OK\r\n";//構造狀態行//設置響應的類型,將響應發給瀏覽器,并告訴瀏覽器文件類型,text/html:表示文件為html的文檔string resp_hander=suffixToDesc(req.suffix);//根據文件類型構造Content-Type//構造Content-Lengthstring resp_len="Content-Length: ";resp_len+=to_string(req.size);resp_len+="\r\n";string resp_black = "\r\n";//構造空行string resp_body;//響應正文resp_body.resize(req.size+1);//開辟響應正文的大小,比網絡文件的大小加一,可以將網絡文件全部存儲到響應正文中//將該路徑下網絡文件的代碼,全部存儲在resp_body中,也就是放入響應正文中if(!Util::readfile(req.path,(char*)resp_body.c_str(),req.size)){//如果讀取失敗,則將html_404文件的代碼,放入響應正文中Util::readfile(html_404,(char*)resp_body.c_str(),req.size);}//響應序列化,即把它們按順序拼接好resp.outbuffer += resp_line;resp.outbuffer += resp_hander;resp.outbuffer += resp_len;resp.outbuffer += resp_black;resp.outbuffer += resp_body;
}int main(int args,char* argv[])
{uint16_t port=stoi(argv[1]);unique_ptr<HttpServer> p(new HttpServer(Delreq,port));p->Init();p->Start();return 0;
}
五、GET與POST方法
http的請求方法有很多,其中最常用的就是GET和POST這兩種方法。
1. 認識表單
不知道你們在瀏覽器中是否在網頁中是否使用過瀏覽器的開發者工具,在網頁中點擊F12即可打開。
比如說,我打開搜狗搜索并點擊F12,右側點擊元素就可以查看構造該網頁的html代碼。
我們點擊彈窗左上角的箭頭圖標(或按快捷鍵 Ctrl+Shift+C)進入選擇元素模式,從頁面中選擇需要查看的元素,可以在開發者工具元素(Elements)一欄中定位到該元素源代碼的具體位置。
我們將鼠標挪動到搜索框,它對應的html代碼對應了下圖黑色框的內容,而這塊內容就叫做表單。
其實,我們之所以能夠搜索信息,是因為搜索框本質是一個form表單。我們搜索的關鍵字會被填入這個表單中,然后瀏覽器會通過HTTP協議發送包含該表單的請求到服務端,服務端再處理發回響應,從而實現搜索。
而當我們進行數據提交的時候,推送數據的方法一般使用這兩種:GET和POST。
2.觀察GET和POST方法
我們在index.html中也增加表單代碼,其中action="/test.py"表示使用網絡根目錄下的test.py處理表單,method="GET"就表明申請的發送使用GET方法。
<!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>我構建的網頁</title>
</head>
<body><h1>網頁首頁</h1><a href="/test/a.html">a網頁</a><a href="/test/b.html">b網頁</a><img src="/image/1.jpg" alt="濱江道步行街"><form action="/test.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>
運行,姓名輸入zhangsan,密碼輸入123456
因為我們并沒有創建對應的python文件,所以給我們返回了404。
我們再將GET修改為method="POST",此時申請的發送就改為了POST方法。
重復上述動作也可以實現同樣的效果。
3.二者的區別
GET方法中,form表單中我們輸入的姓名和密碼以xname=zhangsan&ypwd=123456的形式拼接在了url中。
而使用POST方法提交form表單時,表單的內容并沒有拼接到url中。
通過上面對比我們發現,GET和POST請求方法表單信息的存儲位置是不同的:GET方法通過url提交參數,POST方法通過HTTP請求的正文提交參數。
如果客戶端使用GET方法,在提交form表單的時候,內容就會拼接到url中,在瀏覽器的網址欄中可以看到。而如果使用POST方法,在提交form表單的時候,內容是通過請求正文提交的,在瀏覽器的網址欄中看不到。
既然POST方式通過正文傳遞信息,而正文信息人們一般看不到,那么是否可以說POST方法是安全的呢?
答案是否定的,POST和GET兩種方法中信息都是暴露在外的,這些封裝后的數據還可以通過抓包的方式被他人獲取。在我們后面講到的https協議才能實現真正的安全信息傳輸。
六、長鏈接
在http請求屬性中有一行屬性表示長鏈接:Connection: keep-alive
我們構建首頁的index.html文件中有文字、網頁跳轉鏈接、圖片、表單這些代碼內容。瀏覽器申請根目錄時,收到的網頁中也出現了這么多的內容,而且需要傳輸的這些文件存儲位置不同。而我打開網頁時,只在瀏覽器上輸入了一次網址。
也就是說一個網頁中有多種類型的資源,而一次請求只能獲取一種類型的資源。
那么一個網頁的形成就大概率需要瀏覽器發起多次http請求,而服務器根據請求返回的多個響應共同組成了這個網頁。
我們觀察服務器也確實發現該網站的建立的確發送了多個http請求。
在底層HTTP協議使用的是TCP套接字,所以客戶端每發送一個請求,服務器都會經歷一次創建套接字的流程。
如果客戶端真的每發送一次數據都創建一個套接字,一方面系統調用的開銷回增加程序的運行時間,還有這樣的邏輯使得訪問一個網頁需要創建多個套接字,會導致套接字資源緊張,所以就出現了長連接,對應屬性就是上面的Connection: keep-alive。
長連接:一個客戶端對應一個套接字,客戶端的一個請求響應完后,套接字不關閉,只有客戶端退出了,或者指定關閉時,套接字才關閉。
也就是說,一個客戶端無論有多少個請求,都通過一個套接字和服務器進行網絡通信。
當然http中也存在短鏈接(Connection: close),短鏈接僅支持客戶端每發送一次信息就重新建立TCP鏈接,一般用于大量用戶使用的資源。這個屬性我們并不能直觀地看到。
七、會話保持
1.認識Cookie技術
我們在使用瀏覽器訪問CSDN等網頁時往往需要登陸賬號,比如說我在CSDN首頁登陸賬號。
那么我只需要輸用戶名和密碼登錄這一次,之后我就可以在瀏覽該網站的其他網頁時登錄也不會退出,或者關閉瀏覽器,在重新進入,都不需要輸入賬號和密碼
但是HTTP是一種無狀態協議,每次請求并不會記錄它曾經請求了什么。所以,在第一次登錄CSDN后,在站內進行網頁跳轉(從一篇文章跳轉到另一篇文章)時,你打開了一個新的網頁,理論上需要再次輸入賬號密碼登錄,瀏覽器發送表單驗證身份信息,但現實是我們不需要第二次登錄就可以瀏覽站內的各個網頁。
由于http本身不支持上述保持登錄的功能,所以Cookie技術就應運而生了。
要想實現登陸狀態的保持效果,瀏覽器就需要在我們第一次登錄CSDN時,將我們的賬號密碼等登錄信息保存下來。我們每次打開CSDN站內的網站時,瀏覽器會自動將已保存的用戶登錄信息添加到了請求報頭中,通過HTTP協議發送給服務器。服務器會根據拿到的信息進行登陸狀態的鑒別并返回對應的響應。
所以說,進入新網頁后的登錄還是需要的,只是支持Cookie技術的瀏覽器幫我們做了這件事,我們一直沒注意到而已。
如圖所示,點擊網址前面的鎖,就可以查看當前瀏覽器正在使用的Cookie。
我們將圖中和CSDN有關的Cookie數據刪除。
刷新頁面后,你就會發現你的登錄狀態失效了。
2.內存級與文件級Cookie技術
我們在登錄CSDN后,關掉瀏覽器后再次打開CSDN你還是能保持登錄狀態。
這又是怎么實現的呢?
Cookie又分為內存級和文件級
-
內存級Cookie:將登錄信息保存在瀏覽器的緩沖區中,當瀏覽器被關閉時,進程結束,保存的信息也失效了,重新打開瀏覽器后還需要重新登錄。
-
文件級Cookie:將信息保存在文件中,文件是放在磁盤上的,無論瀏覽器怎么打開關閉,文件中的信息都不會刪除,在之后發送HTTP請求時,瀏覽器從該文件中讀取信息并加到請求報頭中。
根據日常使用瀏覽器的情況,大部分網站在你登陸后,關閉瀏覽器再次打開時登陸狀態依舊保持,所以大部分情況下的Cookie都是文件級別的,而且這些文件是可以從我們的計算機中找到的。
3.Session文件
既然Cookie文件儲存了許多我們的隱私信息,那么一旦這些Cookie文件被不法份子盜取,他們就可以冒用我們的身份進行一些非法操作,并且進行一些非法操作。(比如說QQ盜號)
所以為了保證信息安全,現在的很多公司都會將用戶的賬號密碼以及瀏覽痕跡等信息保存在服務器中。每個用戶對應在服務器上創建一個Session文件儲存信息。由于服務器上的Session文件有很多,所以每個文件名都會設置為一個獨一無二的Session id。
服務器將這個Session id放入響應返回給用戶,此時用戶瀏覽器的Cookie中保存的就是這個id值而不再是儲存信息的文件。
這種服務端存儲用戶信息的技術就叫做Session技術。
為什么會話保持使用的Session技術能夠提高用戶信息的安全性呢?
這是因為,互聯網公司的服務器都是由專業的人員維護的,服務器中存在木馬病毒的可能性相比我們的計算機而言更小,所以用戶信息在服務端會更加安全。
如果客戶端儲存的Cookie中的Session id被盜用,當不法分子使用該id向服務端發起請求時,因為不法分子的IP地址與你常用IP地址大都不一樣,所以服務端就會將所有登錄該賬號的設備強制下線,此時只有手里真正有賬號密碼的人才能夠再次登錄。
當然,保證Session安全的策略非常多,有興趣的小伙伴可以自行了解。
寫入Cookie信息:
我們知道,瀏覽器的Cookie信息是服務端響應返回的,所以在我們構建響應的時候也可以構建Cookie信息讓瀏覽器去保存。
在DealReq函數中構建響應時,設置Cookie信息,內容是name=123456abc,有效時間是三分鐘,然后加到響應報頭中返回給客戶端,如上圖所示。
使用瀏覽器訪問根目錄的時候,如上圖所示,會得index.html文件表示的網頁,查看該網頁的Cookie信息,可以看到name是123456abc,有效時間是3分鐘,和我們在服務端構建響應時寫的內容一模一樣。
瀏覽器將我們在響應中設置的Cookie內容當作了Session id。
真正生成Session id是有一套復雜的算法的,它能夠保證每一個Session文件的id都是獨一無二的。
Cookie和Session兩種技術共同實現了HTTP的會話保持。
八、HTTP狀態碼
1.HTTP狀態碼的分類
在相應的報頭中,除了正常響應的200,資源不存在的404,還有很多的錯誤碼,基本上可分為五種類型,分別以1~5開頭:
我們可以這樣理解這幾類狀態碼:
-
1開頭的狀態碼稱為信息性狀態碼,表示客戶端完成了一個提交動作,但服務端處理的過程耗時較長,為了表明自己的狀態,服務器就會返回1開頭的狀態碼,告訴客戶端我已經受理了這個請求,正在處理。
-
2開頭的狀態碼稱為成功狀態碼,表示服務器已經根據你發來的請求將響應發回,客戶端可以正常處理響應。
-
3開頭的狀態碼稱為重定向狀態碼,重定向有兩種永久重定向和臨時重定向。
-
4開頭的狀態碼稱為客戶端錯誤狀態碼,比如404和403。不過,有這樣一個問題,404錯誤碼表示發生錯誤的是客戶端還是服務器呢?
-
其實404錯誤碼表示客戶端出錯了,正是因為客戶端給服務端發送了錯誤的請求,所以服務端會告知客戶端你的發送的請求有問題,我無法處理。
-
這是我們瀏覽網頁時最常見的一些狀態碼:200(OK),404(Not Found),403(Forbidden請求權限不夠),302(Redirect),504(Bad Gateway)。
2.重定向狀態碼(3XX)
相信大家都有過這樣的經歷,打開一個網址后,網站在白屏加載時突然就跳轉到了一些無關的廣告網頁,其實這就是重定向的應用。
將服務端發送的網絡請求資源轉為其他無關的網絡資源即為重定向,瀏覽器發送請求給服務端,服務端返回一個新的url,并且狀態碼是3XX,瀏覽器會自動用這個新的url向新地址的服務端發起請求。而我們看到的表現就是打開一個網頁時突然又跳轉到了另一個完全不相干的網站,此時服務器相當于提供了引路的服務。
所以說,重定向是由客戶端完成的,當客戶端瀏覽器收到的響應中狀態碼是3XX后,它就會自動從響應中尋找返回的新的url并再次發送請求。
重定向又有兩種:
- 永久重定向:狀態碼為301。
- 臨時重定向:狀態碼為302和307。
臨時重定向和永久重定向本質是影響客戶端的標簽,決定客戶端是否需要更新目標地址。
如果某個網站是永久重定向,那么第一次訪問該網站時由瀏覽器幫你進行重定向,但后續再訪問該網站時就不需要瀏覽器再進行重定向了,此時直接訪問的就是重定向后的網站。
而如果某個網站是臨時重定向,那么每次訪問該網站時都需要瀏覽器來幫我們完成重定向跳轉到目標網站。
永久重定向無法演示出來,效果和臨時重定向一樣。