HTTP協定
HTTP協定是一種應用層的協定,全稱為超文本傳輸協定。
URL
URL值統一資源定位标志,也就是俗稱的網址。
協定方案名
http://表示的就是協定方案名,常用的協定有HTTP協定、HTTPS協定、FTP協定等。HTTPS協定是以HTTP協定為基礎,通過傳輸加密和身份認證保證了傳輸過程的安全性。
登入資訊
user:pass表示登入認證資訊。絕大多數情況下,該字段是被省略。一般通過登入視窗的方式讓使用者輸入。比如gitee的登入視窗:
伺服器位址
伺服器位址也叫做域名。在進行網絡通路時,網絡位址通過DNS域名解析轉換為辨別唯一主機的IP位址。比如使用ping指令通路百度和京東的官網,最後會被轉換為ip位址:
實際上,可以認為域名和IP位址是等價的;在計算機世界中使用的時候既可以使用域名,也可以使用IP位址。但URL呈現出來是可以讓使用者看到的,是以URL當中是以域名的形式表示伺服器位址的。
伺服器端口
一般0-1023号端口已經被一些特定的服務占有。比如HTTP協定預設的端口是80,HTTPS預設的端口是443。
帶層次的檔案路徑
/dir/index.htm
表示的是要通路的資源所在的路徑。通路伺服器的目的是擷取伺服器上的某種資源,通過前面的域名和端口已經能夠找到對應的伺服器程序了,此時要做的就是指明該資源所在的路徑。
這裡的’
/
'并不是指根目錄,而是指web更目錄。具體的資訊将在後面解釋。
查詢字元串
uid=1
表示的是請求時提供的額外的參數,這些參數是以鍵值對的形式,通過
&
符号分隔開的。
比如我們查詢曉歌的燈如晝新皮膚的資訊:
在上面的URL中,存在wd這個字段。這個字段也就是我們想要查詢的關鍵字。
片段辨別符
片段辨別符是對資源的補充
urlencode和urldecode
像 / ? : 等這樣的字元, 已經被url當做特殊意義了解了. 是以這些字元不能随意出現。
比如, 某個參數中需要帶有這些特殊字元, 就必須先對特殊字元進行轉義 。
比如我們搜尋C++關鍵字:
轉義的規則如下:
将需要轉碼的字元轉為16進制,然後從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY格式。
線上編碼工具
市面上存在很多免費的線上編碼工具:https://tool.chinaz.com/Tools/urlencode.aspx
比如輸入C++并進行編碼,就可以得到C++對應的編碼為C%2B%2B
比如輸入C%2B%2B進行解密,就可以得到對應的解碼為C++
HTTP協定的格式
HTTP協定能夠幹什麼?
HTTP協定是向特定的伺服器申請特定的資源,并擷取到本地的協定。
通過wget指令申請百度首頁的資源。并得到一個html檔案到本地。我們将html中的内容在浏覽器中打開:
是以成功擷取百度首頁的靜态資源。
HTTP協定的請求格式
HTTP請求格式可以分為四個部分,格式如下:
請求格式包含以下四個部分:
- 請求行:請求方法+url(檔案路徑格式)+http版本
- 請求報頭:請求的屬性,這些屬性都是以
的形式按行陳列的。key: value
- 空行:作為請求報頭和請求正文的的分割線
- 請求正文:請求正文允許為空字元串,如果請求正文存在,則在請求報頭中會有一個Content-Length屬性來辨別請求正文的長度。
HTTP如何保證自己的報頭和有效載荷全部被讀取?
- 讀取完整的報頭:逐行讀取,直到讀取到空行。
- 讀取完整的正文:在報頭中一定存在一個關于key:value儲存正文長度的屬性。
擷取HTTP請求
HTTP協定的底層通常使用的傳輸層協定是TCP協定,是以可以通過一個TCP伺服器擷取HTTP請求。
int main()
{
//建立套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
cerr << "socket error!" << endl;
return 1;
}
//綁定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8888);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
return 2;
}
//監聽
if (listen(listen_sock, 5) < 0){
cerr << "listen error!" << endl;
return 3;
}
//啟動伺服器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;){
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0){ //爸爸程序
close(listen_sock);
if (fork() > 0){ //爸爸程序
exit(0);
}
//孫子程序
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); //讀取HTTP請求
cout << "--------------------------request begin--------------------------" << endl;
cout << buffer << endl;
cout << "---------------------------request end---------------------------" << endl;
close(sock);
exit(0);
}
//爺爺程序
close(sock);
waitpid(-1, nullptr, 0); //等待爸爸程序
}
return 0;
}
上面的伺服器通過監聽8888端口擷取HTTP請求資訊。
說明:
- 浏覽器向伺服器發起HTTP請求後,由于伺服器沒有對其進行響應,此時浏覽器就會認為伺服器沒有收到請求,然後再不斷發起新的HTTP請求。是以雖然我們隻用浏覽器通路了一次,但會受到多次HTTP請求。
- 由于浏覽器發起請求時預設用的就是HTTP協定,是以我們在浏覽器的url框當中輸入網址時可以不用指明HTTP協定。
- 這裡URL中的
并不是指雲伺服器的根目錄,而是web根目錄。web根目錄可以由自己指定。/
下面通路其他路徑下的資源:
當通路的資源發生變化時,請求頭中的URL也跟着改變。
HTTP的響應格式
HTTP響應格式可以分為四個部分,格式如下:
響應格式包含以下四個部分:
- 狀态行:HTTP版本+狀态碼+狀态碼描述符
- 響應報頭:響應的屬性,以
鍵值對的形式按行陳列的。key: value
- 空行:響應報頭和響應正文的分割線
- 響應正文:響應正文允許為空字元串,如果響應正文存在,則響應報頭中會有一個Content-Length屬性來辨別響應正文的長度。
比如我們通路百度搜尋曉歌的資訊:
模拟HTTP的響應
下面我們在伺服器中建構HTTP響應:當浏覽器發送請求時,在網頁上顯示
accept your request
。
int main()
{
//建立套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
cerr << "socket error!" << endl;
return 1;
}
//綁定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8888);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
return 2;
}
//監聽
if (listen(listen_sock, 5) < 0){
cerr << "listen error!" << endl;
return 3;
}
//啟動伺服器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;){
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0){ //爸爸程序
close(listen_sock);
if (fork() > 0){ //爸爸程序
exit(0);
}
//孫子程序
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); //讀取HTTP請求
cout << "--------------------------request begin--------------------------" << endl;
cout << buffer << endl;
cout << "---------------------------request end---------------------------" << endl;
//建構HTTP響應
string response="http/1.1 200 ok\r\n";
string world="accept your request";
response+=("Content-Length: "+ to_string(world.size()) + "\r\n");
response+="\r\n";
response+=world;
send(sock,response.c_str(),response.size(),0);
close(sock);
exit(0);
}
//爺爺程序
close(sock);
waitpid(-1, nullptr, 0); //等待爸爸程序
}
return 0;
}
在實際的使用中,難道每一個請求都需要程式員去構造響應正文?實際上,HTTP請求的請求行中存在URL,URL就是請求需要通路資源的存放位址。
響應正文存放在哪?
在URL中,存在一個帶層次的檔案路徑:(比如) /dir/index.html。是以可以從請求行的第二個字段中擷取資源路徑。
下面的函數實作了從請求行提取讀取URL路徑:
#define CRLF "\r\n"
#define SPACE " "
#define SPACELEN strlen(SPACE)
#define ROOT_PATH "wwwpath"
#define HOME_PAGE "index.html"
string readURL(string buffer){
//讀取第一行
size_t pos=buffer.find(CRLF);
if(pos==std::string::npos) return "";
string firstline=buffer.substr(0,pos);
size_t first=firstline.find(SPACE);
if(first==std::string::npos) return "";
size_t second=firstline.rfind(SPACE);
if(second==std::string::npos) return "";
string URL=buffer.substr(first+SPACELEN,second-SPACELEN-first);
if(URL.size()==1&&URL[0]=='/')
{
URL+=HOME_PAGE;
}
return URL;
}
注意:
對于通路web根目錄
/
,需要進行特殊處理。一般預設為web根目錄下的index.html檔案。
實驗
當我們通路對應URL路徑在資源時,便打開對應檔案夾,并添加到HTTP響應正文并傳回。
string readFile(const string& filepath)
{
std::ifstream in(filepath,std::ifstream::binary);
if(!in.is_open()) return "404";
std::string content;
std::string line;
while (getline(in,line))
{
content+=line;
}
cout<<content<<endl;
in.close();
return content;
}
假設我們将目前目錄設定為根目錄,并建立一個index.html檔案。
<html>
<head></head>
<body>
<h1>Hello webroot</h1>
</body>
</html>
并建立檔案夾a/b/c,在該檔案夾下建立一個d.html檔案
<html>
<head></head>
<body>
<h1>Hello d.html</h1>
</body>
</html>
通路web根目錄結果:
通路/a/b/c/d.html資源結果:
完整的主程式
int main()
{
//建立套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
cerr << "socket error!" << endl;
return 1;
}
//綁定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
return 2;
}
//監聽
if (listen(listen_sock, 5) < 0){
cerr << "listen error!" << endl;
return 3;
}
//啟動伺服器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;){
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0){ //爸爸程序
close(listen_sock);
if (fork() > 0){ //爸爸程序
exit(0);
}
//孫子程序
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); //讀取HTTP請求
//讀取URL
string URL=readURL(buffer);
cout<<"URL:"<<URL<<endl;
//拼接路徑
string filepath=ROOT_PATH+URL;
cout<<filepath<<endl;
//建構HTTP響應
string response="http/1.1 200 ok\r\n";
string world=readFile(filepath);
response+=("Content-Length: "+ to_string(world.size()) + "\r\n");
response+="\r\n";
response+=world;
send(sock,response.c_str(),response.size(),0);
close(sock);
exit(0);
close(sock);
exit(0);
}
//爺爺程序
close(sock);
waitpid(-1, nullptr, 0); //等待爸爸程序
}
return 0;
}
POST和GET方法
GET方法
網絡行文無非有兩種:
- 把遠端的資源拿到本地:GET
- 将自己的屬性送出到遠端:POST或者GET方法。
我們在web根目錄下的index.html檔案中添加一個表單用于比較兩者的差別:
<html>
<head></head>
<body>
<h1>Hello webroot</h1>
<form action="/a/b/c/d.html" method="get">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
得到結果為:
觀察URL的變化。送出的user和passwd以明文的方式出現在URL中。
這也是GET方法的特點:把參數以明文的方式按照Key:value格式拼接到URL後面。
POST方法
将表單的方法修改為POST。
<html>
<head></head>
<body>
<h1>Hello webroot</h1>
<form action="/a/b/c/d.html" method="post">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
再次通路并送出使用者密碼:
這次并沒有将參數添加到URL中,觀察HTTP請求,可以看到user和password參數都出現在HTTP請求正文中。
POST和GET方法的差別
- GET方法以URL傳參
- POST通過HTTP請求正文傳參
-
GET傳參的方式不私密。
注意:一定不是不安全,因為GET方法和POST方法通過代理伺服器或者抓包等方法都可以擷取對應的參數,要想實作資料安全,就需要對傳輸的資料進行加密。比如HTTPS協定
- GET通過URL傳參,URL有長度的限制,是以資料量較大的參數都會通過POST方法傳遞。
- URL是文本類,沒有嚴格意義上的資料類型。而請求正文有。
HTTP其他的請求方法
HTTP狀态碼
最常見的狀态碼, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
常見的狀态碼
重定向就是通過各種方法将各種網絡請求重新定個方向轉到其它位置,此時這個伺服器相當于提供了一個引路的服務。
重定向又可分為臨時重定向和永久重定向,其中狀态碼301表示的就是永久重定向,而狀态碼302和307表示的是臨時重定向。
臨時重定向實驗
下面我們實作:通路我們的服務時,會跳轉到B站的首頁。
在實作的過程中,将HTTP的響應碼設定為302,還需要在HTTP響應報頭當中添加Location字段,這個Location後面跟的就是你需要重定向到的網頁,比如我們這裡将其設定為CSDN的首頁。
int main()
{
//建立套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
cerr << "socket error!" << endl;
return 1;
}
//綁定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
return 2;
}
//監聽
if (listen(listen_sock, 5) < 0){
cerr << "listen error!" << endl;
return 3;
}
//啟動伺服器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;){
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0){ //爸爸程序
close(listen_sock);
if (fork() > 0){ //爸爸程序
exit(0);
}
//孫子程序
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); //讀取HTTP請求
cout<<"------------------------request begin----------------------"<<endl;
cout<<buffer<<endl;
cout<<"-------------------------request end-----------------------"<<endl;
string response="http/1.1 302 Temporary Redirect/\r\n";
response+="Location: https://www.bilibili.com/\r\n";
response+="\r\n";
send(sock,response.c_str(),response.size(),0);
close(sock);
exit(0);
close(sock);
exit(0);
}
//爺爺程序
close(sock);
waitpid(-1, nullptr, 0); //等待爸爸程序
}
return 0;
}
使用telnet指令隻是接收到了伺服器發送的HTTP響應,并沒有實作重定向功能。實際上的重定向功能是由浏覽器實作完成。
下面我們用浏覽器通路該網址:
回車發送請求:重定向到B站官網
臨時重定向和永久重定向的差別
- 兩者的差別主要展現在使用者體驗方面
- 臨時重定向主要用于網站維護、網站服務更新等。服務更新結束和維護成功後,原網址依然可以被使用。臨時重定向不需要客戶記住新的網址。
- 永久重定向,比如用于使用新的網址,原網址被廢棄的情況。永久重定向需要使用者記住新的網址。
HTTP常見的Header
- Content-Type: 資料類型(text/html等)
- Content-Length: Body的長度
- Host: 用戶端告知伺服器, 所請求的資源是在哪個主機的哪個端口上;
- User-Agent: 聲明使用者的作業系統和浏覽器版本資訊;
- referer: 目前頁面是從哪個頁面跳轉過來的;
- location: 搭配3xx狀态碼使用, 告訴用戶端接下來要去哪裡通路;
- Cookie: 用于在用戶端存儲少量資訊. 通常用于實作會話(session)的功能;
Cookie
HTTP協定的特點之一:無狀态。對于使用者的狀态,HTTP協定不會記錄使用者的資訊和行為。
而Cookie是實作HTTP狀态化的一種手段。比如實作網址的無登陸通路,VIP通路VIP資源。
(回憶下面的場景:我們在使用B站時,第一次需要我們輸入賬号密碼登入。後續再使用時,我們可以直接進入B站而不需要登入。這就是HTTP狀态化的一種手段;
比如你是某個視訊網站的VIP,這個網站裡面的VIP視訊有成百上千個,你每次點選一個視訊都要重新進行VIP身份認證。而HTTP不支援記錄使用者狀态,那麼我們就需要有一種獨立技術來幫我們支援,這種技術目前現在已經内置到HTTP協定當中了,叫做cookie。)
Cookie實驗一
此時已經登入了賬号。點選網址前面的小鎖,可以看到網頁的Cookie。
下面我們将關于B站的Cookie全部删除,再通路B站。
回車通路B站,可以看到需要我們重新登入。
Cookie的原理
在我們第一次登入輸入賬号和密碼進行身份認證時,如果認證成功,服務端就會向用戶端發送對應的響應,其中就包含Set-Cookie字段
(Set-Cookie也是HTTP報頭當中的一個字段)
。該字段通知用戶端設定Cookie。輸入的賬号和密碼就儲存在本地浏覽器Cookie檔案當中。
後續再次通路相同的網址時,浏覽器發送的HTTP請求當中就會包含一個Cookie資訊。服務端在需要認證時會提取Cookie當中賬号和密碼。
Cookie檔案:記憶體級和磁盤級别
cookie就是在浏覽器當中的一個小檔案,檔案裡記錄的就是使用者的私有資訊。cookie檔案可以分為兩種:一種是記憶體級别的cookie檔案,另一種是磁盤檔案級别的cookie檔案。
- 浏覽器關閉後再打開,通路之前的網站,需要輸入賬号和密碼。此時浏覽器儲存的是記憶體級别的Cookie檔案
- 浏覽器關閉再打開,甚至重新開機電腦。通路之前的網站,不需要輸入賬号和密碼。浏覽器儲存的是磁盤檔案級别的Cookie檔案。
設定Cookie
設定Cookie可以在HTTP響應中添加一個Set-Cookie字段。
(Set-Cookie:Cookie資訊)
設定Cookie實驗
int main()
{
//建立套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
cerr << "socket error!" << endl;
return 1;
}
//綁定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
return 2;
}
//監聽
if (listen(listen_sock, 5) < 0){
cerr << "listen error!" << endl;
return 3;
}
//啟動伺服器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;){
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0){ //爸爸程序
close(listen_sock);
if (fork() > 0){ //爸爸程序
exit(0);
}
//孫子程序
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); //讀取HTTP請求
cout<<"------------------------request begin----------------------"<<endl;
cout<<buffer<<endl;
cout<<"-------------------------request end-----------------------"<<endl;
//讀取URL
string URL=readURL(buffer);
//拼接路徑
string filepath=ROOT_PATH+URL;
//建構HTTP響應
string response="http/1.1 200 ok/\r\n";
response+="Set-Cookie:this is my Cookie content\r\n";
string world=readFile(filepath);
response+=("Content-Length: "+ to_string(world.size()) + "\r\n");
response+="\r\n";
response+=world;
send(sock,response.c_str(),response.size(),0);
close(sock);
exit(0);
close(sock);
exit(0);
}
//爺爺程序
close(sock);
waitpid(-1, nullptr, 0); //等待爸爸程序
}
return 0;
}
Session
單純的使用Cookie是不安全的。cookie檔案當中就儲存的是你的私密資訊,一旦cookie檔案洩漏你的隐私資訊也就洩漏。
為了保證Cookie的安全性。後來又引入了Session的概念。當我們第一次登入某個網站輸入賬号和密碼後,伺服器認證成功後還會服務端生成一個對應的SessionID,這個SessionID與使用者資訊是不相關的。系統會将所有登入使用者的SessionID值統一維護起來。
此時當認證通過後服務端在對浏覽器進行HTTP響應時,就會将這個生成的SessionID值響應給浏覽器。浏覽器收到響應後會自動提取出SessionID的值,将其儲存在浏覽器的cookie檔案當中。後續通路該伺服器時,對應的HTTP請求當中就會自動攜帶上這個SessionID。
服務端會根據session_id找到對應的Session檔案。再提取session檔案中的私密資訊。
Cookie+Session的安全保證:着手于Session_id的加密方式。比如使用者的IP位址等。
Connection字段
HTTP有兩種連接配接方式:長連接配接和短連結。
- Connection:keep_alive
- Connection:closed
HTTP的長連接配接和短連結本質上是TCP的長連接配接和短連結。
HTTP短連接配接
在HTTP/1.0中,預設使⽤的是短連接配接。也就是說,浏覽器和伺服器每進⾏⼀次HTTP操作,就建⽴⼀次連接配接,但任務結束就中斷連接配接。
随着網頁資訊不斷的增大,需要傳輸的資料量也不斷增加。如果用戶端通路的某個HTML或其他類型的web頁中包含有其他的web資源,如JavaScript⽂件、圖像⽂件、CSS⽂件等,當浏覽器每遇到這樣⼀個web資源,就會建⽴⼀個HTTP會話。如果建立多個HTTP短連接配接傳輸資料,**傳輸層會不斷的進行三次握手和四次揮手,**過于浪費網絡資源。
是以HTTP長連接配接誕生。
HTTP長連接配接
但從HTTP/1.1起,預設使⽤長連接配接,⽤以保持連接配接特性。使⽤長連接配接的HTTP協定,會在請求頭和響應頭加⼊這⾏代碼。Connection: keep-alive。
在使⽤長連接配接的情況下,當⼀個⽹頁打開完成後,用戶端和伺服器之間⽤于傳輸HTTP資料的TCP連結不會關閉,如果用戶端再次通路這個伺服器上的⽹頁,會繼續使⽤這⼀條已經建⽴的連接配接(HTTP長連接配接利⽤同⼀個TCP連接配接處理多個HTTP請求和響應)。
Keep-Alive不會永久保持連接配接,它有⼀個保持時間,可以在不同的伺服器軟體(如Apache)中設定這個時間。實作長連接配接要用戶端和服務端都⽀持長連接配接。長連接配接中關閉連接配接通過Connection:closed頭部字段。如果請求或響應中的Connection被指定為closed,表⽰在目前請求或相應完成後将關閉TCP連接配接。
TCP的keep-alive是檢查目前TCP連接配接是否活着;HTTP的Keep-Alive是要讓⼀個TCP連接配接活久點。
長連接配接如何保證讀取完整的資訊?
HTTP報頭中存在Conten-Length字段。通過控制讀取的長度判斷是否讀取完整的資訊。
如何保證長連接配接的響應順序?
pipeline技術。
HTTP VS HTTPS
HTTPS協定也是⼀個應⽤層協定。是在HTTP協定的基礎上引⼊了⼀個加密層。這場加密層也輸入應用層,他會對使用者傳輸的資訊進行加密。
而在使用者和服務端使用時,對應拿到的都是明文資料。
為什麼要加密?
QQ浏覽器的營運商劫持例子。
下載下傳天天動聽,在未被QQ浏覽器劫持的情況:
已被劫持的效果,點選下載下傳按鈕,就會彈出QQ浏覽器的下載下傳連結:
通過劫持天天動聽的下載下傳連結進而達到推廣産品的目的。
http的内容是明⽂傳輸的,明⽂資料會經過路由器、wifi熱點、通信服務營運商、代理服務
器等多個實體節點,如果資訊在傳輸過程中被劫持,傳輸的内容就完全暴露了。劫持者還可以篡改傳輸的資訊且不被雙⽅察覺,這就是中間⼈攻擊 ,是以我們才需要對資訊進⾏加密。
對稱加密和非對稱加密
對稱加密
- 采⽤單鑰密碼系統的加密⽅法,同⼀個密鑰可以同時⽤作資訊的加密和解密,這種加密⽅法稱為對稱加密,也稱為單密鑰加密,特征:加密和解密所⽤的密鑰是相同的
- 常⻅對稱加密算法:DES、3DES、AES、TDEA、Blowfish、RC2等
- 特點:算法公開、計算量⼩、加密速度快、加密效率⾼
最簡單的對稱加密算法:異或加密
假設明⽂a=1234,密鑰key=8888。則加密akey得到的密⽂b為9834。然後針對密⽂9834再次進⾏運算bkey,得到的就是原來的明⽂1234。
非對稱加密
- 需要兩個密鑰來進⾏加密和解密,這兩個密鑰是公開密鑰(publickey,簡稱公鑰)和私有密鑰(private key,簡稱私鑰)。
- 常見非對稱加密算法:RSA,DSA,ECDSA
- 特點:算法強度複雜、安全性依賴于算法與密鑰但是由于其算法複雜,⽽使得加密解密速度沒有對稱加密解密的速度快
非對稱加密要⽤到兩個密鑰,⼀個叫做"公鑰",⼀個叫做"私鑰"。公鑰和私鑰是配對的。非對稱加密最⼤的缺點就是運算速度⾮常慢,比對稱加密要慢很多。
- 通過公鑰對明⽂加密,變成密⽂
- 通過私鑰對密⽂解密,變成明⽂
也可以反着⽤
• 通過私鑰對明⽂加密,變成密⽂
• 通過公鑰對密⽂解密,變成明⽂
對稱加密和非對稱加密聯合使用
對稱加密的效率更高,是以雙方在進行正常通信時使用的是對稱加密。
對稱加密的過程
- 通信雙方建立連接配接的時候,雙方就可以把支援的加密算法作協商,協商之後在伺服器端形成非對稱加密時使用的公鑰和私鑰,在用戶端形成對稱加密時使用的密鑰。
- 然後伺服器将公鑰交給用戶端(這個公鑰全世界都可以看到),然後用戶端用這個公鑰對用戶端形成的密鑰進行加密,将加密後的密鑰發送給伺服器,伺服器拿到後再用它的私鑰進行解密,最終伺服器就拿到了用戶端的密鑰。
- 這時用戶端和伺服器都有了這個密鑰,并且其他人是不知道的,此時用戶端和伺服器就可以進行對稱加密通信了。
資料摘要和資料指紋
- 數字指紋(資料摘要),其**基本原理是利⽤單向散列函數(Hash函數)**對資訊進⾏運算,⽣成⼀串固定⻓度的數字摘要。數字指紋并不是⼀種加密機制,但可以⽤來判斷資料有沒有被竄改。
- 摘要常⻅算法:有MD5、SHA1、SHA256、SHA512等。
- 摘要特征:和加密算法的差別是,摘要嚴格意義不是加密,因為沒有解密,隻不過從摘要很難反推原資訊,通常⽤來進⾏資料對比(哈希函數是不可逆的,是以無法從資料摘要反推資料全文,是以資料摘要常用來資料的對比)。
雲盤的秒傳功能
一些雲盤實作了一秒上傳檔案的功能。其原理就使用了資料摘要。
比如使用者要上傳一部電影,雲盤接收到請求後,先對電影資料進行資料摘要得到摘要資訊。此時服務端會對該資料摘要和其他使用者已經上傳的檔案的資料摘要進行對比。如果存在使用者想要上傳的檔案,就建立軟連接配接,指向原來使用者上傳好的相同檔案。
HTTPS的工作過程加密
既然要保證資料安全,就需要進行加密。
方案一:隻使用對稱加密
伺服器同⼀時刻其實是給很多用戶端提供服務的。每個客⼾端,每個⼈⽤的秘鑰都必須是不同的(如果是相同那密鑰,⿊客就也能拿到了)。是以伺服器就需要維護每個用戶端和每個密鑰之間的關聯關系 。
而同時維護多個密鑰,服務端的壓力就比較大。是以理想的做法是所有的用戶端使用同一個密鑰。
如果直接把密鑰明⽂傳輸,那麼⿊客也就能獲得密鑰了。是以就需要對密鑰進行加密。但是要想對密鑰進⾏對稱加密,就仍然需要先協商确定⼀個"密鑰的密鑰"。這就成了"先有雞還是先有蛋"的問題。
方案二:隻使用非對稱加密
由于服務端在傳輸公鑰時沒有進行加密,是以所有人都可以得到公鑰。是以服務端發送消息可能被中間人劫持。服務端到用戶端的資訊傳輸是不安全的。
方案三:雙方都使用非對稱加密
- 1.服務端擁有公鑰S與對應的私鑰S’,用戶端擁有公鑰C與對應的私鑰C’
- 2.客戶和服務端交換公鑰
- 3.客⼾端給服務端發資訊:先⽤S對資料加密,再發送,隻能由伺服器解密,因為隻有伺服器有私鑰S’
- 4.服務端給用戶端端發資訊:先⽤C對資料加密,在發送,隻能由用戶端解密,因為隻有用戶端有私鑰C’
使用2對非對稱密鑰可以實作用戶端和服務端的資訊安全。缺點:效率太低并且依然有安全問題(存在中間人攻擊)
方案四:使用對稱加密+非對稱加密
- 服務端具有⾮對稱公鑰S和私鑰S’
- 客⼾端發起https請求,擷取服務端公鑰S
- 客⼾端在本地⽣成對稱密鑰C,通過公鑰S加密,發送給伺服器。
- 這樣用戶端和服務端都擁有密鑰C,可以進行通信。
該方案相對于方案三解決了效率低的問題。但是依然存在安全問題:中間人攻擊
中間人攻擊
在⽅案2/3/4中,客⼾端擷取到公鑰S之後,對客⼾端形成的對稱秘鑰X⽤服務端給客⼾端的公鑰
S進⾏加密,中間⼈即使竊取到了資料,此時中間⼈确實⽆法解出客⼾端形成的密鑰X,因為隻有伺服器有私鑰S’。但是中間⼈的攻擊,如果在最開始握⼿協商的時候就進⾏了,就會發生安全問題。
以方案四為例
假設黑客一開始就成為了中間人。
- 伺服器具有⾮對稱加密算法的公鑰S,私鑰S’
- 中間⼈具有⾮對稱加密算法的公鑰M,私鑰M’
- 客⼾端向伺服器發起請求,伺服器明⽂傳送公鑰S給客⼾端
- 中間⼈劫持資料報⽂,提取公鑰S并儲存好,然後将被劫持報⽂中的公鑰S替換成為⾃⼰的公鑰M,并将僞造報⽂發給客⼾端
- 客⼾端收到報⽂,提取公鑰M(⾃⼰當然不知道公鑰被更換過了),⾃⼰形成對稱秘鑰X,⽤公鑰M加密X,形成報⽂發送給伺服器
- 中間⼈劫持後,直接⽤⾃⼰的私鑰M’進⾏解密,得到通信秘鑰X,再⽤曾經儲存的服務端公鑰S加密後,将報⽂推送給伺服器
- 伺服器拿到報⽂,⽤⾃⼰的私鑰S’解密,得到通信秘鑰X
- 用戶端和服務端雙方開始采⽤X進⾏對稱加密,進⾏通信。但⼀切都在中間⼈的掌握中,劫持資料,進⾏竊聽甚⾄修改,都是可以的
雙方通信的密鑰X被中間人掌握。
問題的本質是:客⼾端⽆法确定收到的含有公鑰的資料報⽂,就是⽬标伺服器發送過來的
證書
為了應對中間人攻擊,網絡通信又引入了證書。
CA憑證
服務端在使⽤HTTPS前,需要向CA機構申領⼀份數字證書,數字證書⾥含有證書申請者資訊、公鑰資訊等。伺服器把證書傳輸給浏覽器,浏覽器從證書⾥擷取公鑰就可。
證書包含以下的資訊(證書明文和資料簽名):
- 證書頒發的機構
- 證書有效期
- 公鑰B
- 證書持有者
- 資料簽名
在申請證書時,需要在特定的權威平台生成,并且會産生一對公鑰B和私鑰B’。公鑰會附加在證書上,私鑰被CA機構和服務端儲存。這對密鑰在網絡通信中進行明文加密和數字簽名
資料簽名
簽名的形成是基于⾮對稱加密算法的。資料簽名是由資料摘要通過私鑰加密得到。
**證書包括證書明文和資料簽名兩個部分。**資料簽名部分是由證書明文通過摘要算法形成資料摘要,資料摘要被再CA公司的私鑰A加密得到。
- CA機構擁有⾮對稱加密的私鑰A和公鑰A’
- CA機構對服務端申請的證書明⽂資料進⾏hash,形成資料摘要
- 然後對資料摘要⽤CA私鑰A’加密,得到數字簽名S。
- 資料簽名附加在證書明文上形成證書。
服務端申請的證書明⽂和數字簽名S共同組成了數字證書,這樣⼀份數字證書就可以頒發給服務端了。
注意:上述過程中一共出現了兩對密鑰。
一對是服務端申請的密鑰,也就是CA憑證上公鑰B對應的密鑰。這對密鑰的作用是:在網絡通信中對資料資訊進行加密。且被服務端和CA公司都持有。
一對是CA公司持有的密鑰,也就是對證書明文的資料摘要進行加密的私鑰A’對應的密鑰。該對密鑰的作用是:形成資料簽名并被用于檢查證書的合法性。隻被CA公司持有私鑰,公鑰被嵌入到作業系統中。
方案五:非對稱加密+對稱加密+證書
首先需要明确:CA公司的公鑰被嵌入到了作業系統中。
在使用方案五時。證書上已經包含了服務端進行通信的公鑰B。服務端與用戶端要進行通信時,服務端先将證書發送給用戶端,用戶端檢查證書的合法性(檢查是否被篡改),并提取公鑰B。
随後用戶端使用B加密用戶端形成的對稱密鑰X,發送給用戶端。用戶端使用私鑰B’解密得到對稱密鑰X。
如何檢查證書的合法性?
證書包含證書明文和資料簽名兩部分。資料簽名被CA機構的密鑰A’加密無法被更改。
如果有第三方修改了證書明文,比如證書上用于通信的公鑰。用戶端在拿到服務端發送的證書後,會使用被嵌入到作業系統的CA機構公鑰A進行解密得到資料摘要。再将證書明文通過摘要算法形成資料摘要。
如果兩個摘要相同,證書就合法。否則,證書被修改,不合法。
以證書明文資料為hello為例:
假設我們的證書隻是⼀個簡單的字元串hello,對這個字元串計算hash值(⽐如md5),結果為
BC4B2A76B9719D91
如果相同表示證書是合法的。
如果⿊客把hello篡改為了hella。
用戶端通過比較兩個資料摘要發現不同,說明證書内容被篡改,證書不合法。用戶端停止對服務端發送消息。
檢視浏覽器的受信任證書釋出機構
打開浏覽器,點選右上⻆的設定
找到”隐私設定和安全性“裡面的安全屬性:
往下翻可以看到相關的字段:
總結
下面是HTTPS通信的完整流程:
HTTPS工作流程一共包含三組密鑰:
第一組(非對稱加密):用于形成資料簽名和校驗證書是否被篡改。CA機構和服務端持有私鑰,公鑰被嵌入到作業系統中(作業系統包含了可信任的CA認證機構有哪些,同時持有對應的公鑰) 。伺服器在客⼾端請求時,傳回攜帶簽名的證書。客⼾端通過這個公鑰進⾏證書驗證,保證證書的合法性,進⼀步保證證書中攜帶的服務端公鑰權威性。
第二組(非對稱加密):用于用戶端和服務端協商對稱加密的密鑰。服務端生成公鑰和私鑰。公鑰被填寫到CA憑證上。客⼾端⽤收到的CA憑證中的公鑰(是可被信任的)給随機⽣成的對稱加密的密鑰加密,傳輸給伺服器,伺服器通過私鑰解密擷取到對稱加密密鑰。
第三組(對稱加密):用戶端生成。用于服務端和用戶端進行加密通信。