HTTP協議——理解相關概念、模擬實現瀏覽器訪問自定義服務器

文章目錄

  • HTTP協議
    • 理解相關概念
      • HTTP相關背景知識
        • 認識URL
        • HTTP協議在網絡通信的宏觀認識
        • urlencode & urldecode
      • HTTP請求和應答的格式
    • 模擬實現瀏覽器訪問自定義服務器
      • 關于http request
      • http request的請求行——URI
      • 使用瀏覽器完成靜態資源的訪問
      • 常用的報頭屬性
      • http response
      • 狀態碼
        • 重定向
        • 重定向使用效果
        • 簡單理解重定向的原理
      • 理解請求方法
      • HTTP的Header

HTTP協議

本篇文章,我們將來了解一下網絡中一種非常常用、也可以說是最常見的網絡協議——HTTP

對于協議,其實已經有一定的理解了!在網絡實踐的自定義協議實現網絡計算器已經大概知道了,應用層上的協議是如何進行定制并運行的:
詳情參考這篇文章:網絡實踐——自定義協議

應用層的協議是需要根據特定的需求來進行定制的。所以,這也就是為什么OSI七層模型中,上面三層是沒有辦法設置到內核中的。
這種應用協議,都是在應用層實現的。說到底,也就是程序員實現的!


這里,我們來理解一下關于HTTP協議:
在互聯網世界中,HTTP(HyperText Transfer Protocol,超文本傳輸協議),一個至關重要的協議。定義了客戶端(如瀏覽器)與服務器之間如何通信,以交換或傳輸超文本(如 HTML 文檔)。

這種協議都是由一些特別頂級的程序員進行設定編寫的!可以直接供其他人參考和使用:

HTTP 協議是客戶端與服務器之間通信的基礎。客戶端通過 HTTP 協議向服務器發送請求,服務器收到請求后處理并返回響應。HTTP 協議是一個==無連接、無狀態==的協議,即每次請求都需要建立新的連接,且服務器不會保存客戶端的狀態信息。

Tips:
這里存在著很多關于HTTP的相關概念,這里我們一下子也是解釋不清楚的!
后序我們將根據HTTP相關的知識來模擬實現HTTP協議通信的過程。邊寫代碼的同時,來理解其背后的相關原理和相關概念!!!

理解相關概念

這里我們先引入一些相關概念,就是便于后面編寫代碼的!我們不做理解。我們等到后序編寫代碼的時候可以再回頭理解這些理論知識!

HTTP相關背景知識

認識URL

我們來看這么一串網址:
在這里插入圖片描述
首先,第一個部分http://,我們把它堪稱固定的一部分,這個是指示當前協議的。
第二個部分user:pass,這個是登陸的相關信息。不過有時候可以省略不出現。
第三個部分www.example.jp是域名!即我們要找的網址是哪一個!
第四個部分就是要訪問某網址對應服務器的端口號。
第五個部分,我們發現是一個帶層次的文件路徑!我們發現,以/分割路徑!
后面?部分,都是一些相關的參數。

我們現在直接看這個URL(網址),我們肯定是很懵的,不用擔心,這些我們后面都會進行講解!我們只需要知道的是,我們可以通過這么一串URL進行訪問網絡資源即可!

HTTP協議在網絡通信的宏觀認識

對于URL來說,雖然其可以被分為很多部分,但其實,最簡單的主體應該是:
https://服務器地址:端口號,甚至有些時候,端口號都不用!


然后,我們會有一個疑問?為什么只使用類似于www.example.com,瀏覽器就能夠自動的跳轉到正確的網址呢?
在這里插入圖片描述
首先,對于這個服務器地址,其實還有另外一種我們很熟悉的叫法——域名!
比如前段時間www.ai.com,訪問該域名就訪問到了Deepseek!(現在被改了)

這里直接揭曉答案:
其實域名,就是服務器對應的IP地址!因為ip地址具有標識唯一地址性!
那么,我們是如何通過這樣一個域名來訪問對應的服務器呢?

首先,我們一般訪問服務器都是通過瀏覽器這個客戶端來進行訪問的!瀏覽器在接收到一個域名后,就會向域名解析器(這個地址瀏覽器能夠找的到,一般是被存儲在指定位置),然后通過域名解析器,將常見的域名轉化為其服務器對應的ip地址!
在這里插入圖片描述
上述的過程,被叫做DNS系統,即域名系統。
這個域名解析器,其實是屬于基礎設施的!因為它非常重要!有一些大公司,如Google,他們會有自己的域名解析器,用于將域名快速轉化為ip地址!

提出一個問題: 為什么不直接使用ip地址呢?非要進行這么一層轉化?
因為使用域名更符合我們人類的閱讀,能夠見名思義!如www.baidu.com
直接使用ip地址其實是不太知道到底要訪問哪個服務器的,而且,最大的問題是,不方便記憶和使用。直接使用域名是非常方便的!


我們理解完了域名,還需要在理解使用HTTP通信相關資源
我們知道:要訪問服務器,必須知道它的ip + port!(eg: ./tcpserver*.exe 8080)

我們上網的主要目的是什么?其實就兩大點:

1.從服務器中獲取資源(網頁、css、視頻、圖片、文本…)
2.把相關資源/數據上傳到服務器

本質上,我們上網,就是在拿著客戶端和服務器做IO操作罷了!

那么,我們訪問的資源存放在服務器哪里呢?我們要上傳的資源放在哪里呢?
在這里插入圖片描述
答案就是在這個URL中可以體現到的帶層次的文件路徑!它是以/為分隔符的!

進一步了解:這樣一個以/為分隔符,有層次的文件路徑,我們在哪里見過呢?
答案就是我們學的Linux系統!所以,我們可以大致知道,大部分的URL中,帶層次的文件路徑都是以/作為分隔符的,所以,它們都是以Linux系統來運行服務器的!
(這和我們一開始學習Linux系統就說,Linux系統常用于企業中做服務器的觀點是相吻合的!)

這里還要提出一個問題:
我們已經知道文件路徑是Linux下的文件路徑,那么第一個/代表的是服務器的根目錄嗎?

答案:其實不是!這個不可能是Linux服務器的根目錄。我們想一下都知道,根目錄底下有很多重要的東西,企業是不可能隨便讓用戶訪問的!這個其實是要訪問的web根目錄
這里不知道沒有關系,我們后面寫代碼的時候能知道!

訪問/上傳的資源都存在了對應的文件目錄下,所以,我們訪問的所有資源,都是文件!如視頻、網頁、音頻、文本… 它們都是文件,只不過是不同格式罷了!


所以,我們拿著瀏覽器訪問服務器,訪問/上傳資源,本質上就是IO操作!服務器是一個進程,我們使用的瀏覽器,作為客戶端,在我們的主機上也是一個進程!
這兩個進程是不同主機的,進行通信!這不就是網絡通信嗎?那就是使用socket進行通信!

而服務器是需要綁定確定的端口號的!但是,很多時候,我們并沒有輸入端口號,就能訪問到正確的網頁,這是為什么?
因為,成熟的協議,都是由固定的端口號的!

http:80
https:443
ssh:22

# 可以使用下面這條指令查詢ssh服務的固定端口號22
ynp@hcss-ecs-1643:~$ sudo netstat -tulnp | grep sshd # sshd,說明是ssh的守護進程版

但是,未來我們實現的服務器(簡單實現),是必須要輸入端口號的!因為我們綁定了一個具體的端口號。我們在不升級權限到root的情況下是沒有辦法綁定0~1023的端口號的!

urlencode & urldecode

有時候我們會發現,網址后面有一長串亂七八糟的東西:
在這里插入圖片描述
我們可以發現,我們輸入的內容,會被解析成其它的內容,如+ -> %2B
這種情況都是出現在動態交互式的網站的!也就是返回一些動態處理的結果的場景!

其實是因為:
/ ? :等字符,已經被url當做特殊意義理解了。因此這些字符不能隨意出現。
比如,某個參數中需要帶有這些特殊字符,就必須先對特殊字符進行轉義。
轉義的規則如下
將需要轉碼的字符轉為16進制,然后從右到左,取4位(不足4位直接處理),每2位做一位,前面加上%,編碼成%XY格式。

比如+,ASCII:43,HEX:0x2B,從右到左取四位(不足直接操作) -> %2B

這個過程,其實就是給報文進行encode的過程。然后發送回來給我們看的,是decode后的。這點我們只需要了解一下就好了!我們不需要深入了解。

這里看這個網址能不能幫助實現url對應編碼解碼:urlencode & urldecode工具(不一定能用!)

HTTP請求和應答的格式

上面,我們是基本了解清楚了,基于HTTP的相關通信過程、本質、方式。
但是,這里學習的是HTTP協議!而且是一個應用層的協議!

我們是自行實現過這種協議的——網絡版本計算器。
我們在協議中,定義了請求、應答,通信的雙方就是基于特定的協議來進行網絡通信!
這里的HTTP也是一樣的,我們來看一下它們的請求和應答的格式:


HTTP請求:

HTTP應答:
在這里插入圖片描述

中間的報頭部分,都是一對對的key&value形式鍵值對,中間以:[空格]作為分割!


這里我們需要厘清幾個細節,方便后序理解:
首先,這個請求和應答的本質是什么?就是個結構體,協議不就是結構化的數據嗎?
但是,真正在網絡中傳輸的呢?

因為需要兼容多平臺,所以不可能直接傳結構化的數據的!所以,這就需要使用到序列化和反序列化的概念,所以,我們是否可以把上面的協議和請求看成一個大字符串?答案是可以!

這里也是提出幾個問題:
1.這個協議,報頭和有效載荷如何分離?
2.不同行之間如何分割?
3.序列化和反序列化誰來做?

回答這三個問題非常簡單:
1.在HTTP請求和應答中,我們發現都會有一行空行。所以,要進行分離有效載荷和報頭的分離是很簡單地,以空行作為分割即可。
2.不同行之間,都是通過換行符來進行分割的。提取一行是很簡單的事情。
3.序列化和反序列化,肯定是在應用層來做!這個我們是非常熟悉的了。

模擬實現瀏覽器訪問自定義服務器

這里我們直接給出整份源碼先,然后根據這一份代碼來進行相關講解:HTTP模擬

今天我們就使用HTTP協議來手搓一個簡單地服務器,旨在理解HTTP背后的相關原理。
我們前面并沒有將HTTP中其它的原理,只認識了請求和應答。但是沒有關系,我們后序都會通過寫代碼的形式來進行理解原理!

今天這里,我們不需要寫客戶端了!我們只需要寫服務器就可以了!因為我們其實已經有現成的基于HTTP/HTTPS協議通信的客戶端——瀏覽器。我們只需要拿著瀏覽器訪問即可!

關于http request

我們現在只是知道了http request的格式,但是并沒有見過真的,我們可以來看一下:
這里我們只展示此時添加新的代碼,其余的都是以前封裝的組件之類的。

Http.hpp

#pragma once
#include <memory>
#include <unordered_map>
#include <sstream>
#include <fstream>
#include <functional>
#include "Common.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include "Log.hpp"const std::string space = " ";
const std::string line_break = "\r\n";
const std::string headers_sep = ": ";using namespace myLog;const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";
const std::string page_404 = "404.html";//這里要說明的是:Http協議,是不依賴于第三方庫進行序列化和反序列化的!//Http請求格式
class HttpRequest{
public:HttpRequest():_blank_line(line_break),_is_interact(false),_args(""){}~HttpRequest(){}//其實,今天來說,客戶端是瀏覽器!我們可以直接拿瀏覽器來訪問我們寫的服務器。所以,請求的序列化寫不寫都可以!
//但是就不寫了,就留一個方法
//因為http協議已經有固定的序列化方式和反序列化格式了!std::string Serialize(){return "";}void ParseRequestLine(std::string& request_line){std::stringstream ss(request_line);//以空格作為分隔符,將字符串分割后依次插入對應的字段ss >> _req_method >> _uri >> _http_version;}bool GetKV_AndSet(std::string headline){//key: valuesize_t pos = headline.find(headers_sep);if(pos == std::string::npos) return false;std::string key = headline.substr(0, pos);std::string value = headline.substr(pos + headers_sep.size());_headers[key] = value;return true;}//反序列化還是要寫的bool DeSerialize(std::string& req_str){//1.提取請求行std::string request_line;Util::ReadOneLine(req_str, &request_line, line_break);LOG(LogLevel::DEBUG) << "request_line: " << request_line;//2.把請求行放入到對應的字段 -> 使用stringstreamParseRequestLine(request_line);//這里要注意,uri對應的就是要訪問的服務器上對應的資源if(_uri == "/") _uri = webroot + _uri + homepage;else _uri = webroot + _uri; LOG(LogLevel::DEBUG) << "req_method: " << _req_method;LOG(LogLevel::DEBUG) << "uri: " << _uri;LOG(LogLevel::DEBUG) << "http_version: " << _http_version;if(_req_method == "POST" || _req_method == "post") _is_interact = true;//3.把報頭提取出進行分析std::string header_line;Util::ReadOneLine(req_str, &header_line, line_break);//到這里都正常//只要不是讀出來 "",就表明還是報頭  while(header_line != ""){//此時讀到了一行 -> key: value(正常來說,如果是空行讀出來就是 "")GetKV_AndSet(header_line);header_line.clear();Util::ReadOneLine(req_str, &header_line, line_break);}//此時讀到了空行就退出循環了,并且ReadOneLine中已經把讀到的給刪除了DebugHeaders();//如果使用POST來進行傳參,那么參數在正文,這里就不管了//req_str剩下的就是正文了!_text = req_str;LOG(LogLevel::DEBUG) << _text;//如果使用GET方法傳內容給服務器,那么參數在uri上,所以得對uri作進一步解析std::string tmp = "?";auto pos = _uri.find(tmp);if(pos != std::string::npos) {// /login?username=adasdad&password=adaadsasd_args = _uri.substr(pos + tmp.size());//截取參數_uri = _uri.substr(0, pos);//獲取真正的服務!_is_interact = true;}LOG(LogLevel::DEBUG) << "req_method: " << _req_method;LOG(LogLevel::DEBUG) << "uri: " << _uri;LOG(LogLevel::DEBUG) << "http_version: " << _http_version;if(_args != "") LOG(LogLevel::DEBUG) << "uri args: " << _args;LOG(LogLevel::DEBUG) << "text: " << _text;return true;}void DebugHeaders(){for(auto& head : _headers){LOG(LogLevel::DEBUG) << head.first << headers_sep << head.second;}}std::string GetUri(){return _uri;}std::string GetText(){return _text;}bool Is_Interact(){return _is_interact;}std::string GetArgs(){return _args;}
private:std::string _req_method;std::string _uri;std::string _http_version;std::unordered_map<std::string, std::string> _headers;std::string _blank_line;std::string _text;bool _is_interact;//判斷是否有交互 -> 后序來實現交互功能(如登錄請求...)   /login   /registerstd::string _args;//參數(如果使用GET方法,參數會被設置到_uri上)
};//Http應答格式
class HttpResponse{
public:HttpResponse():_blank_line(line_break),_http_version("/HTTP/1.0"){}~HttpResponse(){}std::string Serialize(){std::string status_line = _http_version + space + std::to_string(_status_code) + space + _code_description + line_break;std::string head_line;for(auto& head : _headers){std::string oneline = head.first + headers_sep + head.second + line_break;head_line += oneline;}return status_line + head_line + _blank_line + _text;}//今天來講,反序列化Response是客戶端做 -> 瀏覽器做,我們不需要寫bool DeSerialize(){return true;}void SetTargetFile(const std::string target_file){_target_file = target_file;}void SetCodeAndDesc(int code){_status_code = code;switch(code){case 404:_code_description = "Not Found";break;case 200:_code_description = "OK";break;case 301:_code_description = "Moved Permanently";break;case 302:_code_description = "See Other";break;default:break;}}std::string UriToSuffix(const std::string uri){auto pos = uri.rfind(".");if(pos == std::string::npos) return "text/html";std::string suffix = uri.substr(pos);MimeTypes mime;return mime.getMimeType(suffix);}void SetHeaders(const std::string& key, const std::string& value){if(_headers.find(key) != _headers.end()) return;_headers.emplace(key, value);}bool MakeResponse(){bool res = Util::ReadFileContent(_target_file, &_text);int text_size = 0;//正文長度,后序設置長度Content-Lengthif(!res){#ifdef  TWO//這里可以嘗試試用一下重定向的方式,設置狀態碼301 / 302//SetCodeAndDesc(301);  //永久重定向SetCodeAndDesc(302);    //短暫重定向SetHeaders("Location", "/404.html");
#endif#define ONE
#ifdef ONELOG(LogLevel::DEBUG) << "client want get" << _target_file << "but not found";_text = "";SetCodeAndDesc(404);_target_file = webroot + "/" + page_404;text_size = Util::GetFileSize(_target_file);Util::ReadFileContent(_target_file, &_text);//此時讀到了內容(404page)就放到正文內了!//然后需要設置一些字段進入到報頭中(這里就先只設置兩個)std::string content_type = UriToSuffix(_target_file);SetHeaders("Content-Type", content_type);SetHeaders("Content-Length", std::to_string(text_size));    
#endif        }else{LOG(LogLevel::DEBUG) << "client read form: " << _target_file;SetCodeAndDesc(200);text_size = Util::GetFileSize(_target_file);std::string content_type = UriToSuffix(_target_file);SetHeaders("Content-Type", content_type);SetHeaders("Content-Length", std::to_string(text_size));}return true;}void SetText(std::string& text){_text = text;}//為了方便服務端使用,這里的應答相關字段就用public修飾了。要不然進行修改的時候很麻煩!
public:std::string _http_version;int _status_code;std::string _code_description;std::unordered_map<std::string, std::string> _headers;std::string _blank_line;std::string _text;std::string _target_file;//要訪問的資源 -> 以便后序方便輸入正文!
};using http_route_t = std::function<void(HttpRequest&, HttpResponse&)>;class Http{
public:Http(uint16_t port):_server(std::make_unique<TcpServer>(port)){}~Http(){}bool RegisterRoute(std::string func_name, http_route_t func){//如(/login, Login)func_name = webroot + func_name; //./wwwroot/loginif(_route.find(func_name) == _route.end()){//該任務不存在于表中 -> 可以插入 //這里就規定,插入的名字就是傳入func的對應的小寫auto it = _route.emplace(func_name, func);return it.second;}return false;}//這里就默認都是找的到的!bool AnalyseRequestLine(std::string& reqline, const std::string& key, std::string* value){//從請求行報文中,根據key讀取對應的valuesize_t key_pos = reqline.find(key);//key_pos為key字符子串的起始位置if(key_pos == std::string::npos) return false;size_t value_pos = key_pos + key.size() + headers_sep.size();//value_pos為value字符子串的起始位置size_t value_end = reqline.find(line_break, value_pos);//從value_pos開始找"\r\n"if(value_end == std::string::npos) return false;*value = reqline.substr(value_pos, value_end - value_pos);return true;}bool ReadAllRequestHeader(std::string& inbuffer, std::string* text){//我們已經有了從一個大字符串中,切割字符串的能力了//這里的大報文都是以"\r\n"作為分割的,我們讀取的時分隔符前面的//所以,我們可以一直讀取,直到讀到空行了,能得到完整的報頭!//header內有一個屬性:Content-length,其存儲的時正文的長度!std::string oneline;//報文可能有若干情況://四分之一條,半條、一整條,多條...//但是,今天是一個客戶端對應一個進程 -> 發送來多條那就可能是客戶端多次請求//這里一次只弄一條出來!while(Util::ReadOneLine(inbuffer, &oneline, line_break)){//只要為真,就說明還有"\r\n"可以讀到,就還有機會出現空格*text += oneline + line_break;if(oneline == "") return true;oneline.clear();           }return false;}//這里不寫死循環了,只做短服務void HandleHttpRequest(std::shared_ptr<Socket> socket, const InetAddr& client){//大概率是能讀到至少一個完整報文的!std::string readbuffer;int n = socket->Recv(&readbuffer);//但是,這里怎么能夠保證報文的完整性呢? -> 在Netcal那里實現過,這里模擬一下//這里只需要實現Decode即可,因為前面已經有一堆的字段了(已經具備Encode了)!(有效載荷在blank_line后面)if(n > 0){LOG(LogLevel::DEBUG) << client.GetFormatStr() << ": 發送來一個請求報文";//首先,得保證讀到完整的請求->如果這一次沒能成功讀到完整請求,就不進行處理了!std::string all_reqline;if(ReadAllRequestHeader(readbuffer, &all_reqline) == false) return;//讀到完整的請求報頭 -> all_reqline//all_reqline里面有一個字段是指向正文長度的(前提是,Http請求中,正文部分長度 > 0,要不然其實是看不到的!)//如果發送來的正文長度 == 0,看不到這個字段!std::string text_len;if(AnalyseRequestLine(all_reqline, "Content-Length", &text_len) == false) text_len = "0";//成功讀取長度到text_len -> 需要轉成整數int len = std::stoi(text_len);//讀取正文(從readbuffer中, 長度為len)if(readbuffer.size() < len) return; //正文長度不對!std::string text = readbuffer.substr(0, len);std::string req_str = all_reqline + text;//反序列化HttpRequest hreq;hreq.DeSerialize(req_str);//應答對應的協議結構HttpResponse hresp;//今天這里加多一步,反序列化后,就需要知道當前是否需要進行交互了if(hreq.Is_Interact()){//需要進行交互std::string service_name = hreq.GetUri();//但是,這個服務可能不存在于_route表中if(_route.find(service_name) == _route.end()){//重定向到對應的404網頁hresp.SetCodeAndDesc(301);hresp.SetHeaders("Location", "/404.html");socket->Send(hresp.Serialize());                    }else{_route[service_name](hreq, hresp);std::string res_str = hresp.Serialize();socket->Send(res_str);}return;}//如果不需要進行交互訪問,只訪問靜態資源,就走原來的邏輯!//分析請求 + 制作應答//反序列化的時候,已經把要訪問的web根目錄底下的文件進行處理了!hresp.SetTargetFile(hreq.GetUri());hresp.MakeResponse();//應答進行序列化std::string resp_str = hresp.Serialize();//發送應答socket->Send(resp_str);}//用來測試是否能讀到報文并反序列化/* HttpRequest req;req.DeSerialize(readbuffer); */}void HttpServerInit(){_server->Init();}void HttpServerStart(){_server->Run([this](std::shared_ptr<Socket> socket, const InetAddr& client){this->HandleHttpRequest(socket, client);});}private:std::unique_ptr<TcpServer> _server;std::unordered_map<std::string, http_route_t> _route;
};

Util.hpp

#pragma once
#include <fstream>
#include "Common.hpp"
#include "Log.hpp"using namespace myLog;//用來實現一些常用的方法 -> 全是靜態成員函數
class Util{
public:
//從字符串str中,讀取出sep前面的串(不包含sep!!!!!),帶出去給outbufferstatic bool ReadOneLine(std::string& str, std::string* outbuffer, const std::string sep){size_t pos = str.find(sep);if(pos == std::string::npos) return false;else{*outbuffer = str.substr(0, pos);str.erase(0, pos + sep.size());return true;}}//獲取某個文件的大小static int GetFileSize(const std::string& file_path){std::ifstream in(file_path, std::ios::binary);if(!in.is_open()) return -1;in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);in.close();return filesize;}//以二進制方式,把文件內的內容以字節流方式讀取出來!!!static bool ReadFileContent(const std::string& file_path, std::string* out){//這里不能單純的使用文本讀取!應當使用二進制讀取 -> 因為可能有圖片,有視頻...這些都是二進制的std::ifstream in(file_path);if(!in.is_open()) return false;//這個時候就知道,大致有多少二進制數據要被讀取出來了 -> 可以獲取文件的字節數int readsize = GetFileSize(file_path);if(readsize <= 0) return false; out->resize(readsize);//使用二進制讀取in.read((char*)out->c_str(), readsize);in.close();return true;}private:
};//這里搞多一個類,以便于Content-Type字段使用
class MimeTypes {
private:std::unordered_map<std::string, std::string> mimeTypes;public:MimeTypes() {// 初始化常用 MIME 類型// 文本類型mimeTypes[".txt"] = "text/plain";mimeTypes[".html"] = "text/html";mimeTypes[".htm"] = "text/html";mimeTypes[".css"] = "text/css";mimeTypes[".js"] = "text/javascript";mimeTypes[".csv"] = "text/csv";mimeTypes[".xml"] = "text/xml";// 圖像類型mimeTypes[".jpg"] = "image/jpeg";mimeTypes[".jpeg"] = "image/jpeg";mimeTypes[".png"] = "image/png";mimeTypes[".gif"] = "image/gif";mimeTypes[".svg"] = "image/svg+xml";mimeTypes[".webp"] = "image/webp";mimeTypes[".ico"] = "image/x-icon";// 應用程序類型mimeTypes[".json"] = "application/json";mimeTypes[".pdf"] = "application/pdf";mimeTypes[".zip"] = "application/zip";mimeTypes[".doc"] = "application/msword";mimeTypes[".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";// 多媒體類型mimeTypes[".mp3"] = "audio/mpeg";mimeTypes[".ogg"] = "audio/ogg";mimeTypes[".mp4"] = "video/mp4";mimeTypes[".mov"] = "video/quicktime";mimeTypes[".avi"] = "video/x-msvideo";// 字體類型mimeTypes[".woff"] = "font/woff";mimeTypes[".woff2"] = "font/woff2";mimeTypes[".ttf"] = "font/ttf";mimeTypes[".otf"] = "font/otf";}// 根據文件擴展名獲取 MIME 類型std::string getMimeType(const std::string& fileExtension) const {auto it = mimeTypes.find(fileExtension);if (it != mimeTypes.end()) {return it->second;}// 默認返回二進制流類型return "application/octet-stream";}// 添加自定義 MIME 類型void addMimeType(const std::string& fileExtension, const std::string& mimeType) {mimeTypes[fileExtension] = mimeType;}
};

今天的調用邏輯是:
Http類底層包含著一個TCP服務器,今天我們就寫一個短連接服務!
每次接收到一個請求,我們就讓TCP服務器回調Http類中的HandleHttpRequest
方法,這樣子,就完成了服務器端和應用層的解耦!

HandleHttpRequest接收請求,然后對其反序列化、分析、制作應答、序列化后在發送給客戶端瀏覽器進行解析展示即可!

到這里,我們就想要做一件事情:看一下真正的http request到底是長什么樣子:

啟動服務器,然后再瀏覽器的URL搜索框中輸入:服務器主機ip:綁定端口號
(其實就是直接把服務器從套接字收到的字節流提取出來然后打印!)這個我沒有在展示的代碼中體現,自己加上去看一下就可以了!
在這里插入圖片描述

我們可以發現,確實是有這么樣的一個請求協議:

GET / HTTP/1.1
Host: 113.45.70.51:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

注意,這里操作的方式并沒有通過客戶端向服務器傳輸資源,所以正文部分是空的,所以,我們可以看到打印出來是有兩行的空行的!

第一行是請求行,中間以空格作為分隔符。
中間就是以key&value形式的鍵值對,就是相關的報頭屬性!
空行作為完整請求報頭和有效載荷的分割符號。
這里正文是空的,所以看起來也是空行!

至此,我們就切實感受到了協議是長什么的!后序,我們將根據我們對協議的認識,以及HTTP協議的原理,來進行相關代碼編寫和結論的認證!

http request的請求行——URI

在這里插入圖片描述
這里我們先不講請求方法,也先不講HTTP版本,這些都是客戶端發來的。這些我們后序一開始寫代碼的時候不用太過關注。

但是,請求行的第二個位置,URI,這是一個非常重要的部分!
URI就是我們前面所說的:一個帶有層次的文件路徑。

這里就是單純的一個/,前面說了,這個訪問的是web根目錄,是什么意思呢?


我這里直接展示我的代碼結構了,一看就明白:
在這里插入圖片描述
所以,這里的URI就是我們當前服務器中,存儲資源的目錄!
后序,我們將讓AI形成若干網頁,我們就可以實現通過瀏覽器來訪問我們自己的服務器了。

使用瀏覽器完成靜態資源的訪問

我們需要先來了解一下,什么是靜態資源?
比如我們今天存放在服務器指定目錄下的文本、音頻、網頁,這些都是靜態資源!這些是不涉及客戶端和服務端的動態交互的。就是申請對應的資源并返回。

我們先把靜態資源訪問的邏輯寫完,到時候再來補充動態交互的內容(如登錄、注冊)!

我們今天就實現簡單地短服務!即底層的TCP服務器,accept到一個客戶端后,回調處理對應的請求,然后就結束,關閉套接字!下次客戶端要訪問就得重新connect!


代碼主邏輯:

void HandleHttpRequest(std::shared_ptr<Socket> socket, const InetAddr& client){//大概率是能讀到至少一個完整報文的!std::string readbuffer;int n = socket->Recv(&readbuffer);//但是,這里怎么能夠保證報文的完整性呢? -> 在Netcal那里實現過,這里模擬一下//這里只需要實現Decode即可,因為前面已經有一堆的字段了(已經具備Encode了)!(有效載荷在blank_line后面)if(n > 0){LOG(LogLevel::DEBUG) << client.GetFormatStr() << ": 發送來一個請求報文";//首先,得保證讀到完整的請求->如果這一次沒能成功讀到完整請求,就不進行處理了!std::string all_reqline;if(ReadAllRequestHeader(readbuffer, &all_reqline) == false) return;//讀到完整的請求報頭 -> all_reqline//all_reqline里面有一個字段是指向正文長度的(前提是,Http請求中,正文部分長度 > 0,要不然其實是看不到的!)//如果發送來的正文長度 == 0,看不到這個字段!std::string text_len;if(AnalyseRequestLine(all_reqline, "Content-Length", &text_len) == false) text_len = "0";//成功讀取長度到text_len -> 需要轉成整數int len = std::stoi(text_len);//讀取正文(從readbuffer中, 長度為len)if(readbuffer.size() < len) return; //正文長度不對!std::string text = readbuffer.substr(0, len);std::string req_str = all_reqline + text;//反序列化HttpRequest hreq;hreq.DeSerialize(req_str);//應答對應的協議結構HttpResponse hresp;//分析請求 + 制作應答//反序列化的時候,已經把要訪問的web根目錄底下的文件進行處理了!hresp.SetTargetFile(hreq.GetUri());hresp.MakeResponse();//應答進行序列化std::string resp_str = hresp.Serialize();//發送應答socket->Send(resp_str);}

上述就是主邏輯。很多接口就是完成具體任務的。這里不講解實現,只講功能!


1.讀取到客戶端的報文后,服務器不能立馬反序列化!因為沒有辦法保證當前讀到的是一個完整的http request請求報文!所以,我們需要做一步工作,確保讀到的報文正確!

如何保證讀到的報文完整呢?
1.我們可以先把完整的請求報頭讀完【空行之前】
2.對讀到的報頭進行解析!如果說正文長度>0,在中間報頭部分會存在:Content-Length: xxx
這個是用來指示正文長度的。如果長度為0可能會不顯示!
3.根據Content-Length讀取正文長度

這些工作,都放在了接口Http.ReadAllRequestHeader來做!
今天這里規定,一旦發現保溫不完整、正文長度不匹配時,我們都不予受理!

2.講請求行進行分析,讀取到正確的正文長度,并提取正文:
在這里插入圖片描述
這里為了代碼更好的復用性,寫了一個接口AnalyseRequestLine,即分析請求行。就是把完整的請求行傳入,傳入要找的key,最后把value帶出給text_len!

經過上面的步驟,我們就可以提取正文了,然后就是拼接處完整的http request報文!
在這里插入圖片描述

3.對http request進行反序列化
在這里插入圖片描述

這里需要注意有一個_is_interact,這個是表示當前是否交互用的!這里我們不需要管!這個我們放在后面講解如何進行交互的時候來說。

注意,這里如果URI是單純的一個/,我們不可能把整個web根目錄下的內容返回給客戶端,一般來說,這個是在請求首頁,就類似于www.baidu.com的搜索框那樣!

4.分析對應的請求,制作應答序列化后返回
在這里插入圖片描述

那么,要訪問的資源,或者上傳的資源,都是通過MakeResponse這個接口來進行處理!制作好對應的應答。這里是訪問靜態資源!!!
(制作應答的邏輯就不展示了,總之就是設置好對應的應答協議字段,把資源設置到正文部分(字節流)即可!)

所以,我們讓ai生成一點網頁,在我們的資源站上放入一些圖片:
在這里插入圖片描述
我們來運行看一下效果:
在這里插入圖片描述
注意,當前路徑下有一個favicon.ico文件,這個其實就是圖片文件。就像我們打開一個網頁,瀏覽器選項卡左上角的那個小圖標:
因為輸入對應的ip:端口號訪問,其實瀏覽器不止是申請首頁資源(index.html,如果不帶任何的路徑默認就是訪問/,即首頁)。同時,瀏覽器還會申請/favicon.ico資源,就是訪問這個小圖標。直接網上找一個放在web根目錄下即可!

同時,我在首頁位置插入了兩張圖片:
在這里插入圖片描述
這些是前端的內容,這里就不說了。我們可以到大模型詢問使用,或者相關網站:
https://cn.w3schools.com/html/html5_video.asp

常用的報頭屬性

上述我只是簡單地了介紹了,訪問服務器靜態資源的主要邏輯。并沒有說到其中的一些問題:
就是我們今天設置應答的時候,我們是需要返回個別中間報頭的!也是常用的。

第一個報頭就是Content-Length,這個是指示正文長度的。
也就是今天我們制作應答的時候,返回資源都是放在應答的正文部分,讓瀏覽器去解釋。
其實這個報頭設置不設置都可以,現在的瀏覽器很強大,是可以解析出來的。但是可以設置一下,就設置到底層的哈希表中即可。

第二個報頭是非常非常重要的!如果不進行設置,會導致一個很嚴重的問題:
當我們發圖片的時候,如果沒有設置對應的報頭,就會導致瀏覽器無法顯示該圖片!
因為有一個報頭Content-Type,指示這次發送的資源是什么類型的,這是有常見的轉化表的。為了方便使用,定義了一個MimeTypes類進行轉化使用:
在這里插入圖片描述
所以,我們就需要主動的設置報頭屬性,要不然瀏覽器解釋不了!!!!


還有就是,今天的服務器,只能支持短連接,也就是完成一次請求,服務器處理后就會直接關閉連接。如果說,我們直接把圖片,視頻等其他資源放到該網頁上,服務器是要進行多次請求的。這里就不演示了,知道即可。

但是,我們今天這里全是使用HTML的a標簽,這就是一個跳轉鏈接,只有點擊它,瀏覽器會再次進行請求對應的資源。

http response

在這里插入圖片描述
我們知道,上面是http response的格式。
中間的報頭我們已經說了兩個了:Content-LengthContent-Type
還有其他的報頭,但是我們會放在后面去說。

正文部分就是我們要交給用戶端的資源。就是把資源文件以二進制方式讀取出來放到正文。

我們最需要了解的就是,第一行狀態行!


第一個是服務器版本,我們這里直接默認是HTTP/1.0即可。因為HTTP/1.0使用短連接多,1.1版本才是長連接。這里我們寫的是短連接。
這個版本是為了對照服務器和客戶端兩端版本的。因為有時候一些服務器是沒有辦法支持過高的標準的,或者服務器不支持一些老標準!所以需要兩端版本對照!

第二三個可以一起說,狀態碼和描述,這是什么意思?
其實就是當前服務器處理請求的時候,可能會出現一些問題,又或是一些特殊的處理方式。這個時候,就有可能需要向客戶端返回當前處理的狀態!
最常見的就是:404 Not Found

狀態碼

在這里插入圖片描述
1開頭的就是可能當前服務器還在處理客戶端發來的請求。
2開頭的一般都是處理請求成功
3重定向,這個我們需要好好了解一下。比如我們有時候可以發現,我們訪問一個網站,突然換網址了,需要我們跳轉到另外一個網站,這就是簡單地重定向!
又比如我們在某些app里面的搖一搖跳轉,這也是屬于重定向!

當然,第三點也不止局限于這里。也有可能是某些服務器不是真正提供服務的,可能是代理服務器。就是客戶端向該代理服務器發送請求,服務器會返回重定向的網址和信息給客戶端,此時瀏覽器一旦接收到重定向的信息,就會立馬再次向新的服務器申請服務!

4開頭表示的是客戶端錯誤!比如最常見的404,找不到資源。
5.這個表示的是服務器的錯誤,比如服務器過載、進程創建失敗等。

但是,這里要說的是,其實很多公司內部是對于返回狀態碼是寫的比較隨意的。特別像是5開頭的,如果返回這個錯誤碼,就等同于告訴外界公司服務器的軟肋了!所以,有可能有時候服務器錯誤返回200都是有可能的事情。

重定向

我們需要來重點了解一下重定向:

狀態碼狀態短語說明
300Multiple Choices請求的資源有多個選擇,用戶或瀏覽器需要選擇其中一個進行訪問。
301Moved Permanently請求的資源已永久移動到新位置,未來所有請求應使用新URI。
302Found請求的資源臨時從不同URI響應請求,客戶端應繼續使用原始URI。
303See Other對當前請求的響應可以在另一個URI找到,且必須使用GET方法獲取。
304Not Modified資源未修改(用于緩存重定向),客戶端可以繼續使用緩存的版本。
307Temporary Redirect臨時重定向,與302類似,但明確要求方法和主體不能更改。
308Permanent Redirect永久重定向,與301類似,但明確要求方法和主體不能更改。

我們就舉兩個例子來說:
301 Moved Permanently 請求的資源已永久移動到新位置,未來所有請求應使用新URI。
302 Found/See Other 求的資源臨時從不同URI響應請求,客戶端應繼續使用原始URI。

這兩個其實真正使用起來,區別是不大的。但是我們還是要講解一下他們的區別:

狀態碼類型搜索引擎行為瀏覽器行為形象比喻
301永久重定向更新權重到新URL緩存跳轉,后續直接訪問新地址“永久搬家”
302臨時重定向保留原URL權重每次訪問都重新跳轉“臨時借住”

關鍵區別

  • 永久性:301是永久變更,302是臨時變更
  • SEO影響:301轉移權重,302保留原權重
  • 緩存行為:301會被瀏覽器緩存,302每次重新跳轉
類型工作流程緩存機制
3011. 客戶端首次請求 → 服務器返回301和新地址
2. 瀏覽器自動緩存該重定向
3. 后續所有請求直接跳轉新地址(不再詢問服務器)
永久緩存(直到清除瀏覽器緩存)
3021. 每次請求 → 服務器都返回302和新地址
2. 瀏覽器不會緩存重定向
3. 每次都要完整走請求流程
不緩存
重定向使用效果

其實重定向的方式有很多種,但是真正使用起來是肉眼看不出區別的:

在這里插入圖片描述
在HttpResponse的MakeResponse接口中,我們對于找不到的內容是做了處理的。即讓正文部分返回一個404 Not Found的網頁。現在,我們其實也可以直接重定向過去該網頁。
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

其實經過實驗,使用301進行重定向或者302都是差不多的!看不出區別。

簡單理解重定向的原理

我們需要了解的是,重定向的過程是什么?為什么瀏覽器能夠找到對應的資源進行重定向呢?
其實是因為,在Http Response的Header內,有一個字段叫做:Location: xxx

這個是要配合3xx的狀態碼來進行使用的!
在這里插入圖片描述

具體的過程如上所示!

理解請求方法

現在,我們需要來理解一下http request中的請求方法。
在這里插入圖片描述


我們先來看最重要的兩個:GET / POST
先直接說區別:
它們二者最大的區別就是,在與服務器進行動態交互的時候,客戶端需要提交一些參數。GET方法提交的參數會放在URL,POST方法是放在正文中!

接下來我們來做個實驗看,首先我們要知道,如何通過瀏覽器,從前端傳遞參數到后端:
在這里插入圖片描述
在這里插入圖片描述

我們看這里ai生成的登錄注冊網頁,使用到了一個叫form表單的東西:
其實,我們在瀏覽器中的一些登錄界面也是使用這個表單的,我們可以看看:
在這里插入圖片描述
然后,通過這個表單,就可以把輸入的參數(如我們這里的賬戶密碼)傳送給后端進行驗證了!


接下來,我們就以登陸注冊模塊來看看這兩個方法的不同!!!
login和regsiter是類似的這里,我們就拿登錄網頁來試一下就好了。

(這里需要注意,這種服務一般是需要進行交互的。我們之前說的都是靜態資源的獲取。所以,對于HTTP來說,需要多一個交互的服務。所以我在HTTP類中加入了一個_route哈希,用來進行檢索交互任務的。邏輯其實很簡單,看代碼是一定能看得懂的。這里就不說代碼了,直接上演示結果。)

method = GET
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

method = POST
我們只需要修改網頁即可,服務器是不用重新啟動的!因為網頁和服務器是互相獨立的。
在這里插入圖片描述
在這里插入圖片描述

在這里插入圖片描述

最后我們發現,事實確實如此。二者的區別就是在于傳參的不同。
只不過說,一般來說更習慣用GET來進行資源獲取,POST來進行參數提交。相對于GET來說,POST把參數(隱私信息)放在正文內,私密性可能更好一些些吧。

但其實,二者這樣提交都是不安全的!因為可以被別人抓包,這里推薦使用一款工具叫做fiddler classic,它可以進行抓包!這里就不演示了。


其他方法(選幾個進行介紹):

PUT方法:
用途:用于傳輸文件,將請求報文主體中的文件保存到請求 URL 指定的位置。
示例:PUT /example.html HTTP/1.1
特性:不太常用,但在某些情況下,如 RESTful API 中,用于更新資源。
(常用的是POST或者GET)

DELETE方法:
用途:用于刪除文件,是 PUT 的相反方法。
示例:DELETE /example.html HTTP/1.1
特性:按請求 URL 刪除指定的資源。
但是,一般來說,DELETE是不被允許的!因為一些公司不希望用戶能夠隨意刪除服務器內的一些資源,所以可能會拒絕該請求!

HEAD方法
用途:與 GET 方法類似,但不返回報文主體部分,僅返回響應頭。
示例:HEAD /index.html HTTP/1.1
特性:用于確認 URL 的有效性及資源更新的日期時間等。
但是這個方法,也大部分情況是被禁止的!而且也很少用。

OPTION方法
用途:用于查詢針對請求 URL 指定的資源支持的方法。
示例:OPTIONS * HTTP/1.1
特性:返回允許的方法,如 GET、POST 等。

HTTP的Header

Content-Type: 數據類型(text/html 等) ? Content-Length: Body 的長度
Host: 客戶端告知服務器, 所請求的資源是在哪個主機的哪個端口上;
User-Agent: 聲明用戶的操作系統和瀏覽器版本信息;
referer: 當前頁面是從哪個頁面跳轉過來的;
Location: 搭配 3xx 狀態碼使用, 告訴客戶端接下來要去哪里訪問;
Cookie: 用于在客戶端存儲少量信息. 通常用于實現會話(session)的功能;

Cookie我們放在后序的cookie和session話題來講解。


這里需要講一下connection報頭相關的內容。

我們今天寫的HTTP協議通信,是短連接的。也就是,客戶端發送來一個請求,服務端處理完該請求后,然后立馬斷開連接。如果服務端還有內容要申請,那就需要繼續請求!
在這里插入圖片描述

這種是短服務!也就是服務器一次只處理一個請求。這種在早期的HTTP/1.0協議中用的比較多。因為早期的HTTP協議中,一個網頁內是沒有像現在這樣如此多的資源的。

但是,如果像是今天的瀏覽器中的一張網頁,有大量的連接、視頻、圖片…如果還是采用短連接的方式,那么光是申請一張網頁,服務器就要不斷地進行:連接、創建子進程/線程,處理、關閉連接。更何況,建立連接也不是沒有成本的!

所以這就導致,如果對于一些資源比較多的網頁來講,使用短連接的服務肯定是不行的!所以,在HTTP/1.1協議后,就引入了這么一個報頭:connection


HTTP 中的 Connection 字段是 HTTP 報文頭的一部分,它主要用于控制和管理客戶端與服務器之間的連接狀態。可以做到管理持久連接:Connection 字段還用于管理持久連接(也稱為長連接)。持久連接允許客戶端和服務器在請求/響應完成后不立即關閉 TCP 連接,以便在同一個連接上發送多個請求和接收多個響應。

語法格式

  • Connection: keep-alive:表示希望保持連接以復用 TCP 連接。
  • Connection: close:表示請求/響應完成后,應該關閉 TCP 連接。

其實我們自主實現的網絡版本計算器,就是長連接形式的!因為服務器是一直在進行服務,不會主動斷開與客戶端的連接!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/95578.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/95578.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/95578.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【服務器】英偉達M40顯卡風冷方案心得

在之前的博文中&#xff0c;博主說到最近準備自己組裝一臺服務器&#xff0c;主要用于有限元仿真&#xff0c;其次兼顧一部分AI機器學習的工作&#xff0c;于是博主就入手了一張英偉達Tesla M40的12G顯卡GPU。本來博主也糾結過是買M40還是M60&#xff0c;后來在網上看到說M60看…

Java中的鎖升級機制

目錄 核心思想 Java對象頭&#xff08;Object Header&#xff09;與Mark Word 鎖升級的詳細步驟 1. 無鎖&#xff08;No Lock&#xff09; 2. 偏向鎖&#xff08;Biased Locking&#xff09; 3. 輕量級鎖&#xff08;Lightweight Lock&#xff09; 4. 重量級鎖&#xff…

Scikit-learn Python機器學習 - 特征預處理 - 標準化 (Standardization):StandardScaler

鋒哥原創的Scikit-learn Python機器學習視頻教程&#xff1a; 2026版 Scikit-learn Python機器學習 視頻教程(無廢話版) 玩命更新中~_嗶哩嗶哩_bilibili 課程介紹 本課程主要講解基于Scikit-learn的Python機器學習知識&#xff0c;包括機器學習概述&#xff0c;特征工程(數據…

windows下wsl2 ubuntu開發配置

配置環境變量# 設置方式 命令/文件 生效范圍 適用場景 # 臨時 export FORCE_UNSAFE_CONFIGURE1 當前終端 臨時編譯軟件 # 用戶級永久 ~/.bashrc或~/.profile 當前用戶 長期使用&#xff08;單用戶&#xff09; # 系統級永久 /etc/environment或/…

網絡編程 05:UDP 連接,UDP 與 TCP 的區別,實現 UDP 消息發送和接收,通過 URL 下載資源

一、概述 記錄時間 [2025-09-02] 前置文章&#xff1a; 網絡編程 01&#xff1a;計算機網絡概述&#xff0c;網絡的作用&#xff0c;網絡通信的要素&#xff0c;以及網絡通信協議與分層模型 網絡編程 02&#xff1a;IP 地址&#xff0c;IP 地址的作用、分類&#xff0c;通過 …

告別線纜束縛!AirDroid Cast 多端投屏,讓分享更自由

AirDroid Cast 是一款功能強大的跨平臺投屏應用&#xff0c;能夠輕松實現手機、電腦之間以及手機之間的屏幕共享與控制。無論是工作演示、在線教學還是游戲直播&#xff0c;AirDroid Cast 都能提供流暢穩定的投屏體驗。 1. 下載與安裝 您可以通過以下鏈接下載 AirDroid Cast&…

從零開始學大模型之大模型訓練流程實踐

大模型訓練流程實踐 本文較長&#xff0c;建議點贊收藏&#xff0c;以免遺失。更多AI大模型開發 學習視頻/籽料/面試題 都在這>>Github<< >>Gitee<< 6.1 模型預訓練 在上一章&#xff0c;我們逐步拆解了 LLM 的模型結構及訓練過程&#xff0c;從零手…

一文從零部署vLLM+qwen0.5b(mac本地版,不可以實操GPU單元)

第一步&#xff1a;下載anaconda for mac https://zhuanlan.zhihu.com/p/350828057 知乎保姆級教程 https://www.anaconda.com/docs/getting-started/anaconda/install#macos-linux-installation 下載地址 第二步&#xff1a;部署vllm的虛擬環境 https://www.53ai.com/news/Op…

Go語言Range用法全解析

引言Go 語言中的 range 關鍵字是集合遍歷的核心語法結構&#xff0c;它提供了一種高效且類型安全的方式來迭代各種數據結構。range 的設計完美體現了 Go 語言的工程哲學 - 通過最小化的語法提供最大化的功能。標準庫中的許多關鍵組件&#xff08;如 sync.Map、bufio.Scanner 等…

mysql進階語法(視圖)

1、視圖概念 是從一個或多個表中導出來的表&#xff0c;它是一種虛擬存在的表&#xff0c;表的結構和數據都依賴于基本表 應用場景&#xff1a; 多個地方用到同樣的查詢結果該查詢結果用到復雜的select語句 視圖優點&#xff1a; 簡化查詢語句&#xff1a;簡化用戶的查詢操作&a…

編程范式:提升抽象能力的思維工具

這是一個編程中的核心概念&#xff0c;它代表了編寫程序的一套基本風格、方法論和哲學。學習不同的編程范式&#xff0c;就像學習用不同的工具和思維方式來解決問題&#xff0c;能極大地提升你作為程序員的抽象能力和解決問題的能力。一、什么是編程范式&#xff1f;編程范式 是…

阿里云-基于通義靈碼實現高效 AI 編碼 | 1 | 在 Visual Studio Code 中安裝和使用靈碼

文章目錄一、在 Visual Studio Code 中安裝和使用靈碼1.1 準備工作1.2 在 Visual Studio Code 安裝通義靈碼1.3 登錄阿里云賬號免費個人運維知識庫&#xff0c;歡迎您的訂閱&#xff1a;literator_ray.flowus.cn 一、在 Visual Studio Code 中安裝和使用靈碼 本安裝步驟適用于…

WordPress搭建個人網站(Linux版)

WordPress搭建個人網站&#xff0c;使用Linux系統。我需要詳細說明整個過程&#xff0c;包括環境準備、安裝步驟、配置優化等。首先&#xff0c;用戶可能對Linux不太熟悉&#xff0c;所以需要從基礎開始&#xff0c;比如選擇合適的Linux發行版&#xff0c;如Ubuntu或CentOS。然…

ES模塊(ESM)、CommonJS(CJS)和UMD三種格式

vite的build.lib配置生成了三種格式&#xff1a;ES模塊&#xff08;ESM&#xff09;、CommonJS&#xff08;CJS&#xff09;和UMD。它們的主要區別和適用場景如下&#xff1a; ES模塊&#xff08;.mjs&#xff09;&#xff1a; 使用現代JavaScript的模塊語法&#xff08;import…

2026屆IC秋招聯蕓科技IC面經(完整面試題)

聯蕓科技2026屆數字IC后端面經數字后端培訓實戰項目六大典型后端實現案例 首先是自我介紹。這個每家公司必備環節。這部分內容需要自己提前準備&#xff0c;避免太過緊張影響發揮。 數字IC后端經典筆試題IC秋招筆試題之時序報告解析 1&#xff09;拿到netlist和sdc后你會如何…

一維水動力模型有限體積法(四):高級實現——平衡源項、邊界條件與算法總成

引言 成功實現一個穩定且精確的水動力學模型&#xff0c;關鍵在于妥善處理源項和邊界條件。這兩個環節是數值格式產生非物理振蕩和誤差的主要來源。本章將詳細介紹“守恒-平衡”&#xff08;well-balanced&#xff09;格式的核心技術&#xff0c;以及通過“虛擬單元”實現各類物…

VAE(變分自動編碼器)技術解析

VAE&#xff08;Variational Auto-Encoder, 變分自動編碼器&#xff09;1、VAE的結構為什么使用重參數化&#xff1f;2、VAE的代碼實現1.重構損失&#xff08;Reconstruction Loss&#xff09;2.KL散度&#xff08;Kullback-Leibler Divergence Loss&#xff09;1&#xff09;E…

嵌入式單片機---串口通信及相關通信技術

一、通信方式分類&#xff08;一&#xff09;按數據傳輸線路數量劃分&#xff1a;串行通信與并行通信類別定義特點并行通信多個比特同時通過并行線進行傳輸優點&#xff1a;傳輸速率較高&#xff1b;缺點&#xff1a;占用大量芯片資源串行通信將數據拆分成一個個比特&#xff0…

Elasticsearch面試精講 Day 8:聚合分析與統計查詢

【Elasticsearch面試精講 Day 8】聚合分析與統計查詢 文章標簽&#xff1a;Elasticsearch, 聚合查詢, 統計分析, Aggregations, 面試, 大數據, 搜索引擎, 后端開發, 數據分析 文章簡述&#xff1a; 本文是“Elasticsearch面試精講”系列的第8天&#xff0c;聚焦聚合分析與統計…

HTML HTML基礎(2)

1.開發者文檔W3C官網&#xff1a; www.w3c.org W3School&#xff1a; www.w3school.com.cn MDN&#xff1a; developer.mozilla.org —— 推薦。2.排版標簽標簽名標簽含義單 / 雙 標簽h1 ~ h6標題雙p段落雙div沒有任何含義&#xff0c;用于整體布局雙(1). h1 最好寫一個&#x…