一、HTTP協(xié)議
雖然我們說(shuō), 應(yīng)用層協(xié)議是我們程序猿自己定的.
但實(shí)際上, 已經(jīng)有大佬們定義了一些現(xiàn)成的, 又非常好用的應(yīng)用層協(xié)議, 供我們直接參考使用. HTTP(超文本傳輸議)就是其中之一。
1.認(rèn)識(shí)URL
平時(shí)我們俗稱的 “網(wǎng)址” 其實(shí)就是說(shuō)的 URL
2.urlencode和urldecode
像 / ? : 等這樣的字符, 已經(jīng)被url當(dāng)做特殊意義理解了. 因此這些字符不能隨意出現(xiàn).
比如, 某個(gè)參數(shù)中需要帶有這些特殊字符, 就必須先對(duì)特殊字符進(jìn)行轉(zhuǎn)義.
轉(zhuǎn)義的規(guī)則如下:
將需要轉(zhuǎn)碼的字符轉(zhuǎn)為16進(jìn)制,然后從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編成%XY格式
“+” 被轉(zhuǎn)義成了 “%2B”
urldecode就是urlencode的逆過(guò)程;
encode:對(duì)特殊符號(hào)和漢字編碼為%XX
decode:服務(wù)器(軟件)收到url請(qǐng)求–自己對(duì)特殊%XX進(jìn)行解碼
urldecode工具
3.HTTP協(xié)議格式
HTTP請(qǐng)求
首行: [方法] + [url] + [版本]
Header: 請(qǐng)求的屬性, 冒號(hào)分割的鍵值對(duì);每組屬性之間使用\n分隔;遇到空行表示Header部分結(jié)束
Body: 空行后面的內(nèi)容都是Body. Body允許為空字符串. 如果Body存在, 則在Header中會(huì)有一個(gè)Content-Length屬性來(lái)標(biāo)識(shí)Body的長(zhǎng)度;
HTTP響應(yīng)
首行: [版本號(hào)] + [狀態(tài)碼] + [狀態(tài)碼解釋]
Header: 請(qǐng)求的屬性, 冒號(hào)分割的鍵值對(duì);每組屬性之間使用\n分隔;遇到空行表示Header部分結(jié)束
Body: 空行后面的內(nèi)容都是Body. Body允許為空字符串. 如果Body存在, 則在Header中會(huì)有一個(gè)Content-Length屬性來(lái)標(biāo)識(shí)Body的長(zhǎng)度; 如果服務(wù)器返回了一個(gè)html頁(yè)面, 那么html頁(yè)面內(nèi)容就是在body中
狀態(tài)碼有200,400,302,307,500,404等等
狀態(tài)碼描述:404 -> Not Found 200 -> OK
1.請(qǐng)求和響應(yīng)怎么保證應(yīng)用層完整讀取完畢了?
a.首先讀取完整的一行
b.while(讀取完整的一行)-所有的請(qǐng)求行+請(qǐng)求報(bào)頭全部讀完-直到空行
c.我們能保證把報(bào)頭讀完,報(bào)頭有一個(gè)屬性:Content-Length:XXX正文長(zhǎng)度
d.解析出來(lái)內(nèi)容長(zhǎng)度,再根據(jù)內(nèi)容長(zhǎng)度,讀取正文即可。
2.請(qǐng)求和響應(yīng)是怎么做到序列化和反序列化的?
http自己實(shí)現(xiàn)的,第一行+請(qǐng)求/響應(yīng)報(bào)頭,只要按照\(chéng)r\n將字符串1->n即可,正文則不需要做
響應(yīng)的正文可以是html/css/js/圖片/視頻/音頻等
4.HTTP的方法
其中最常用的就是GET方法和POST方法.
5.HTTP的狀態(tài)碼
最常見的狀態(tài)碼, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
6.HTTP常見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)要去哪里訪問;
Cookie: 用于在客戶端存儲(chǔ)少量信息. 通常用于實(shí)現(xiàn)會(huì)話(session)的功能;
7.重定向
我們使用手機(jī)或者瀏覽器的時(shí)候,屏幕什么也沒有點(diǎn),就跳轉(zhuǎn)到了其他的頁(yè)面,比如自動(dòng)跳轉(zhuǎn)到廣告商等,這是通過(guò)重定向完成的。重定向分為臨時(shí)重定向和永久重定向。
重定向過(guò)程:客戶端向服務(wù)器發(fā)送http請(qǐng)求,服務(wù)器給客戶端發(fā)送的http響應(yīng)的狀態(tài)碼為3XX,附帶一個(gè)新的url,然后客戶端重新向新的服務(wù)器發(fā)起請(qǐng)求—臨時(shí)重定向。假如一個(gè)公司的一個(gè)網(wǎng)站,因?yàn)楦鞣N原因(比如同時(shí)訪問數(shù)量受限等),所以重新寫了一個(gè)網(wǎng)站,但是客戶不知道新的網(wǎng)站鏈接,只知道老的鏈接,所以公司對(duì)老的鏈接進(jìn)行永久重定向,客戶在訪問以前的網(wǎng)站的時(shí)候自動(dòng)跳轉(zhuǎn)到新的網(wǎng)站—永久重定向
8.長(zhǎng)連接
其實(shí)我們看到的網(wǎng)頁(yè),實(shí)際上可能由多種元素構(gòu)成,即一張完整的網(wǎng)頁(yè)需要多次http請(qǐng)求
http網(wǎng)頁(yè)中可能包含多個(gè)元素,如果頻繁的發(fā)起http請(qǐng)求,http是基于TCP的,TCP是面向連接的,就會(huì)有頻繁創(chuàng)建連接的問題(客戶端和服務(wù)器都需要對(duì)連接進(jìn)行管理,先描述再組織,會(huì)對(duì)連接創(chuàng)建對(duì)應(yīng)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),對(duì)連接的管理就變成了對(duì)數(shù)據(jù)結(jié)構(gòu)的管理,就會(huì)有時(shí)間和空間的成本)
所以就提出了長(zhǎng)連接,長(zhǎng)連接需要client和server都要支持,建立好一條連接,獲取一份資源的時(shí)候,通過(guò)同一條連接來(lái)完成
Connection:Keep-alive --支持長(zhǎng)連接
Connection:close --不支持長(zhǎng)連接
9.會(huì)話保持
我們使用網(wǎng)頁(yè)的bilibili的時(shí)候,我們登錄一次之后,后面的一段時(shí)間都不需要我們重新進(jìn)行登錄了,關(guān)閉頁(yè)面,然后重新點(diǎn)進(jìn)去也不需要重新登錄,這就是http的會(huì)話保持做到的
會(huì)話保持嚴(yán)格意義不是http天然具備的,而是后面使用發(fā)現(xiàn)需要的
http協(xié)議是無(wú)狀態(tài)的,即http不關(guān)心上一次和下一次的請(qǐng)求,只負(fù)責(zé)當(dāng)前請(qǐng)求的傳輸,但是用戶需要,因?yàn)橛脩舨榭葱碌木W(wǎng)頁(yè)是常規(guī)操作,如果發(fā)送頁(yè)面跳轉(zhuǎn),那么新的頁(yè)面也就無(wú)法識(shí)別是哪一個(gè)用戶了,為了讓用戶一經(jīng)登錄,可以在整個(gè)網(wǎng)站按照自己的身份進(jìn)行隨意訪問,就需要會(huì)話保持。
我們看騰訊視頻的時(shí)候,對(duì)于需要會(huì)員的視頻,我們可以通過(guò)鏈接直接獲取而不進(jìn)行登錄嗎,答案是不行的,凡是對(duì)網(wǎng)頁(yè)訪問有權(quán)限要求的網(wǎng)頁(yè),在被獲取之前,全部都要做判斷,進(jìn)行身份認(rèn)證。
會(huì)話保持有兩種方法:
第一種方案:我們?cè)跒g覽器進(jìn)行登錄輸入信息之后,瀏覽器會(huì)把我們用戶輸入的信息:用戶名&&密碼保持起來(lái),這個(gè)被稱為cookie數(shù)據(jù)。cookie分為cookie文件級(jí)數(shù)據(jù)和cookie內(nèi)存級(jí)數(shù)據(jù)。往后只要訪問同一個(gè)網(wǎng)站,瀏覽器就會(huì)自動(dòng)推送歷史保留信息給服務(wù)器
我們登錄的時(shí)候,瀏覽器保存了cookie文件,每次請(qǐng)求賬號(hào)密碼都要進(jìn)行推送,然后服務(wù)器再返回資源,但是這里有個(gè)問題,如果黑客在我們的電腦上種植了木馬病毒,獲取了我們的cookie文件,這樣我們的賬號(hào)密碼就泄漏了,那么黑客使用他的瀏覽器登錄我們的賬號(hào),此時(shí)服務(wù)器會(huì)誤認(rèn)為這個(gè)非法用戶是你,如果是QQ,微信對(duì)我們就會(huì)造成很大的影響
第二種方案:我們進(jìn)行登錄的時(shí)候輸入用戶名和密碼,此時(shí)服務(wù)器端會(huì)根據(jù)用戶的信息形成一個(gè)session文件,它有唯一的名稱:session id來(lái)進(jìn)行唯一標(biāo)識(shí),然后將當(dāng)前用戶的session id返回給用戶,此時(shí)瀏覽器的cookie文件中保存的是session id。此后用戶發(fā)送http request 和session id給服務(wù)器,服務(wù)器根據(jù)session id進(jìn)行鑒權(quán),此時(shí)client保存了cookie,server保存了session。
如果黑客獲取了用戶的session,此時(shí)服務(wù)器還是會(huì)誤認(rèn)為這個(gè)非法用戶是你,但是此時(shí)我們已經(jīng)將矛盾轉(zhuǎn)移了,此時(shí)黑客盜取的是公司的私密數(shù)據(jù),有法律進(jìn)行維護(hù),此外還配合了其他的策略來(lái)緩解該類問題,比如我們?cè)诤芏痰臅r(shí)間內(nèi)從一個(gè)地區(qū)到另一個(gè)地區(qū),QQ就會(huì)提醒我們異地登錄,需要重新進(jìn)行登錄等等
10.基本工具
這里介紹兩個(gè)基本工具:postman 和 fiddler
postman不是抓包工具,而是一個(gè)模擬客戶端—瀏覽器的行為的工具
fiddler是一個(gè)抓包工具,http工具
postman和fiddler的原理:
瀏覽器將請(qǐng)求發(fā)送給fiddler,此時(shí)fiddler可以作為代理服務(wù)器看待,然后就fiddler將請(qǐng)求轉(zhuǎn)發(fā)給服務(wù)器,服務(wù)器響應(yīng)的信息通過(guò)fiddler轉(zhuǎn)發(fā)到用戶的手中,而postman則就是相當(dāng)于客戶端向服務(wù)器發(fā)起請(qǐng)求
我們對(duì)下文的程序進(jìn)行抓包:
我們可以看到,我們的賬號(hào)和密碼都是明文的,所以POST和GET都是不安全的,只有是否私密的區(qū)別,安全需要HTTPS來(lái)完成。
二、簡(jiǎn)單的HTTP服務(wù)器實(shí)現(xiàn)
1.err.hpp
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
2.log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"
#define NUM 1024
const char *to_levelstr(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case NORMAL:
return "NORMAL";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
char logprefix[NUM];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
char logcontent[NUM];
va_list arg;
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout << logprefix << logcontent << std::endl;
FILE *log = fopen(LOG_NORMAL, "a");
FILE *error = fopen(LOG_ERR, "a");
if (log && error)
{
FILE *cur = nullptr;
if (level == DEBUG || level == NORMAL || level == WARNING)
cur = log;
if (level == ERROR || level == FATAL)
cur = error;
if (cur)
fprintf(cur, "%s%s\n", logprefix, logcontent);
fclose(log);
fclose(error);
}
}
3.procotol.hpp
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "Util.hpp"
const std::string sep = "\r\n";
const std::string default_root = "./wwwroot";
const std::string home_page = "index.html";
const std::string html_404 = "wwwroot/404.html";
class HttpRequest
{
public:
HttpRequest() {}
~HttpRequest() {}
public:
void parse()
{
// 1. 從inbuffer中拿到第一行,分隔符\r\n
std::string line = Util::getOneLine(inbuffer, sep);
if (line.empty())
return;
// 2. 從請(qǐng)求行中提取三個(gè)字段
// std::cout << "line: " << line << std::endl;
std::stringstream ss(line);
ss >> method >> url >> httpversion;
// 3. 添加web默認(rèn)路徑
path = default_root; // ./wwwroot,
path += url; //./wwwroot/a/b/c.html, ./wwwroot/
if (path[path.size() - 1] == '/')
path += home_page;
// 4.獲取path對(duì)應(yīng)資源的后綴
// ./wwwroot.index.html
// ./wwwroot.1.jpg
auto pos = path.rfind(".");
if (pos == std::string::npos)
suffix = ".html";
else
suffix = path.substr(pos);
// 5.得到資源的大小
struct stat st;
int n = stat(path.c_str(), &st);
if (n == 0)
size = st.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;
};
class HttpResponse
{
public:
std::string outbuffer;
};
4.Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "err.hpp"
static const int backlog = 5;
class Sock
{
public:
void Socket()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof opt);
logMessage(NORMAL, "create socket success");
}
void Bind(const uint16_t &port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
int n = bind(_listensock, (struct sockaddr *)&local, sizeof local);
if (n < 0)
{
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
}
void Listen()
{
int n = listen(_listensock, backlog);
if (n < 0)
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
int Accept()
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
return sock;
}
int Fd()
{
return _listensock;
}
void Close()
{
if (_listensock > 0)
close(_listensock);
}
private:
int _listensock;
};
5.Util.hpp
#pragma once
#include <string>
#include <fstream>
class Util
{
public:
static std::string getOneLine(std::string &buffer, const std::string &sep)
{
auto pos = buffer.find(sep);
if (pos == std::string::npos)
return "";
std::string sub = buffer.substr(0, pos);
buffer.erase(0, sub.size() + sep.size());
return sub;
}
static bool readFile(const std::string resource, char *buffer, int size)
{
std::ifstream in(resource, std::ios::binary);
if (!in.is_open())
return false;
in.read(buffer, size);
in.close();
return true;
}
};
6.httpServer.hpp
#pragma once
#include <functional>
#include <sys/wait.h>
#include "Sock.hpp"
#include "protocol.hpp"
#include "log.hpp"
#include "err.hpp"
#include "Util.hpp"
namespace server
{
static const int defaultport = 8080;
using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;
class httpServer
{
public:
httpServer(const func_t func, const uint16_t &port = defaultport)
: _func(func), _port(port)
{
}
void initServer()
{
_sock.Socket();
_sock.Bind(_port);
_sock.Listen();
}
void handlerEvent(int sock)
{
// 1. 讀到完整的http請(qǐng)求
// 2. 反序列化
// 3. httprequst, httpresponse, _func(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
HttpRequest req;
HttpResponse resp;
size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
req.parse();
_func(req, resp); // req -> resp
send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
void start()
{
for (;;)
{
int sock = _sock.Accept();
if (sock < 0)
continue;
pid_t id = fork();
if (id == 0)
{
_sock.Close();
if (fork() > 0)
exit(0);
handlerEvent(sock);
close(sock);
exit(0);
}
close(sock);
waitpid(id, nullptr, 0);
}
}
~httpServer()
{
}
private:
Sock _sock;
uint16_t _port;
func_t _func;
};
}
7.httpServer.cc
#include "httpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
std::string suffixToDesc(const std::string suffix)
{
std::string ct = "Content-Type: ";
if (suffix == ".html")
ct += "text/html";
else if (suffix == ".jpg")
ct += "application/x-jpg";
ct += "\r\n";
return ct;
}
// 1. 服務(wù)器和網(wǎng)頁(yè)分離,html
// 2. url -> / : web根目錄
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// for test
cout << "----------------------http start---------------------------" << endl;
cout << req.inbuffer;
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 << "字節(jié)" << std::endl;
cout << "----------------------http end---------------------------" << endl;
// std::string respline = "HTTP/1.1 200 OK\r\n";
std::string respline = "HTTP/1.1 307 Permanent Redirect\r\n";
std::string respheader = suffixToDesc(req.suffix);
respheader += "Location: https://www.qq.com/\r\n";
std::string respblank = "\r\n";
// std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>多情只有春庭月,猶為離人照落花</p></body></html>";
std::string body;
body.resize(req.size + 1);
if (!Util::readFile(req.path, (char *)body.c_str(), req.size))
{
Util::readFile(html_404, (char *)body.c_str(), req.size);
}
respheader += "Content-Length: ";
respheader += to_string(body.size());
respheader += "\r\n";
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
cout << "----------------------http response start---------------------------" << endl;
std::cout << "resp.outbuffer" << resp.outbuffer << std::endl;
cout << "----------------------http response end---------------------------" << endl;
resp.outbuffer += body;
return true;
}
// ./httpServer port
int main(int argc, char *argv[])
{
// if(argc != 2)
// {
// Usage(argv[0]);
// exit(0);
// }
// uint16_t port = atoi(argv[1]);
// unique_ptr<httpServer> httpsvr(new httpServer(Get, port));
unique_ptr<httpServer> httpsvr(new httpServer(Get));
httpsvr->initServer();
httpsvr->start();
return 0;
}
8.總結(jié)分析
一個(gè)用戶看到的網(wǎng)頁(yè)結(jié)果,可能是多個(gè)資源結(jié)合而成的,所以要獲取一個(gè)網(wǎng)頁(yè)效果,我們的瀏覽器一定會(huì) 發(fā)起多次http請(qǐng)求。
我們進(jìn)行數(shù)據(jù)提交的時(shí)候,本質(zhì)前端要通過(guò)form表單提交的,瀏覽器會(huì)自動(dòng)將form表單中的內(nèi)容轉(zhuǎn)換成GET/POST方法請(qǐng)求
GET和POST的區(qū)別
如下是GET的方式
如下是POST的方式
二者的區(qū)別如下:
GET通過(guò)URL傳遞參數(shù),具體:http://ip:port/XXX/YYY?name=value&name1=value1
POST提交參數(shù)通過(guò)http請(qǐng)求的正文提交參數(shù)
POST方法通過(guò)正文的提交參數(shù),所以一般用戶看不到,私密性更好,私密性 != 安全性,GET方法不私密
無(wú)論是GET還是POST方法,都不安全,要談安全,必須加密 --https
通過(guò)URL傳遞參數(shù),注定了不能太大文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-807731.html
但是POST方法,通過(guò)正文,正文可以很大,甚至可以是其他的東西文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-807731.html
到了這里,關(guān)于【計(jì)算機(jī)網(wǎng)絡(luò)】HTTP協(xié)議以及簡(jiǎn)單的HTTP服務(wù)器實(shí)現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!