Day10,我終於讓自己寫的程式真正開始監聽網路連線。
透過:
TcpListener
以及:
AcceptTcpClient()
我的程式可以等待 Client 連進來,再透過 NetworkStream 讀取對方傳來的資料。
但這時又產生了一個新的問題。
我們一直在說:
Browser 會傳送 HTTP Request 給 Server。
可是以前使用 ASP.NET Core 時,我看到的通常已經是框架整理好的 Request,例如路徑、Method、Header 等資訊。
所以我其實從來沒有真正看過,Browser 原始傳進 Server 的 HTTP Request 到底長什麼樣?
今天就直接把它印出來看看。
當我在 Browser 輸入一個網址並按下 Enter 時,它到底傳了什麼東西給 Server?
今天我沒有使用 ASP.NET Core。
而是繼續使用昨天建立的 TCP Server。
在 Client 成功連線後,我透過:
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[4096];
int length = stream.Read(
buffer,
0,
buffer.Length
);
先取得 Browser 傳來的資料。
但從網路讀進來的資料,本身其實是:
bytes
所以接著使用:
string request =
Encoding.UTF8.GetString(buffer, 0, length);
把收到的 bytes 轉換成字串。
最後:
Console.WriteLine(request);
直接把它印出來。
這次我沒有先解析它。
我就是想看看:
Browser 到底原封不動傳了什麼過來?

接著,我啟動 Server。
畫面顯示:
正在等待 Browser...
然後在 Browser 輸入:
http://127.0.0.1:5000/
Server 很快就收到了連線。
而且這次不只是:
收到連線!
而是真的印出了一大串資料。
其中最前面是:
GET / HTTP/1.1
Host: 127.0.0.1:5000
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: ...
Accept: ...
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-TW, ...
這是我第一次直接看到 Browser 傳給自己 Server 的 HTTP Request 原始內容。

其中最重要的是第一行:
GET / HTTP/1.1
它可以拆成三個部分:
GET / HTTP/1.1
│ │ │
│ │ └─ HTTP Version
│ │
│ └─ Request Target
│ (這次的 Path 是 /)
│
└─ HTTP Method
也就是說,光是第一行其實就已經告訴 Server:
Client 想用什麼方式,存取什麼資源,以及使用哪個 HTTP 版本。
第一個GET是 HTTP Method。
它表示這個 Request 想進行什麼類型的操作。
這次我們直接在 Browser 網址列輸入網址,因此 Browser 發出了:
GET
目前可以先把它理解成:
Client 想向 Server 取得某個資源。
未來還會遇到其他 Method,例如:
-GET
-POST
-PUT
-DELETE
所以 HTTP Request 不只是告訴 Server:我要去哪裡。
第二個部分:
/
就是這次 Request 要存取的位置。
我們輸入:
其中:
127.0.0.1
是主機。
5000
是 Port。
而最後的:
/
就是 Path。
所以 Browser 傳出的第一行才會是:
GET / HTTP/1.1
但我想確認一件事情,如果網址的 Path 改變,這裡真的也會跟著改嗎?
所以我又做了第二個實驗。
我把 Browser 網址改成:
再次送出 Request。
這次 Server 收到的第一行真的變成:
GET /hello HTTP/1.1
兩次放在一起就非常清楚:
http://127.0.0.1:5000/
↓
GET / HTTP/1.1
http://127.0.0.1:5000/hello
↓
GET /hello HTTP/1.1
所以我第一次親手確認:
Browser 網址中的 Path,真的會被寫進 HTTP Request 的 Request Line,這件事之後會非常重要。
因為未來我們做 Routing 時,Framework 就必須根據這些資訊判斷:
/hello
/users
/products
到底應該交給哪一段程式處理。
第一行最後還有:
HTTP/1.1
它表示這個 Request 使用的 HTTP Protocol Version。
所以:
GET /hello HTTP/1.1
可以直接讀成:
使用 HTTP/1.1,以 GET 方法要求 /hello。
到這裡,我終於不再只是把:
GET /hello HTTP/1.1
當成一串奇怪的文字,它其實有固定的結構。
今天看到的第一行有一個正式名稱:
Request Line
基本結構可以理解成:
Method + Request Target + HTTP Version
例如:
GET /hello HTTP/1.1
就是:
Method
↓
GET /hello HTTP/1.1
↑ ↑
Target Version
這也代表未來如果我要自己做 Mini Web Framework,我至少得有能力把這一行拆開。
例如:
method = "GET";
path = "/hello";
version = "HTTP/1.1";
Framework 才能知道這個 Request 到底想做什麼。
第一行下面那些東西又是什麼?
除了:
GET /hello HTTP/1.1
Browser 還傳了一大堆東西。
例如:
Host: 127.0.0.1:5000
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: ...
Accept: ...
Accept-Encoding: ...
Accept-Language: ...
這些就是:
HTTP Headers
Header 基本上是一組:
名稱: 值
例如:
Host: 127.0.0.1:5000
可以拆成:
Host
↓
Header Name
127.0.0.1:5000
↓
Header Value
不同 Header 會提供不同資訊。
例如今天實際看到的:
-Host
→ 要存取的 Host
User-Agent
→ Client/Browser 的相關資訊
-Accept
→ Client 可以接受哪些內容
-Accept-Encoding
→ Client 支援哪些內容編碼
-Accept-Language
→ Client 偏好的語言
所以 Browser 並不是只送:
GET /hello
而是同時附帶很多額外資訊給 Server。

做到這裡,我發現 HTTP Request 其實不是隨便的一串文字。
至少今天看到的 GET Request,可以先理解成:
┌──────────────────────────────┐
│ Request Line │
│ GET /hello HTTP/1.1 │
├──────────────────────────────┤
│ Headers │
│ Host: 127.0.0.1:5000 │
│ User-Agent: ... │
│ Accept: ... │
│ Accept-Language: ... │
├──────────────────────────────┤
│ 空白行 │
└──────────────────────────────┘
之後某些 Request 還可能帶有:
Body
所以更完整的概念會是:
HTTP Request
│
├── Request Line
│
├── Headers
│
├── 空白行
│
└── Body(不一定有)
這跟以前只知道「Browser 送 Request」相比,已經具體非常多。
以前的我:
Browser
↓
HTTP Request
↓
ASP.NET Core
↓
Controller
現在的我開始看到中間真正發生的事情:
Browser
↓
建立 TCP Connection
↓
Server Accept
↓
NetworkStream
↓
讀取 bytes
↓
把 bytes 解碼
↓
GET /hello HTTP/1.1
Host: ...
...
↓
解析 HTTP Request
↓
之後才有可能做 Routing
也就是說,ASP.NET Core 平常交給我的 Request,其實已經是底層做完很多工作的結果。
今天最大的發現是:
URL 裡面的 /hello 並不會神奇地直接變成 ASP.NET Core 裡的 Route。
Browser 其實只是先送出:
GET /hello HTTP/1.1
然後 Server/Framework 必須:
收到 bytes
↓
理解 HTTP 格式
↓
解析 Request Line
↓
取得 /hello
↓
才有可能拿 /hello 去做 Routing
也就是說,Routing 並不是網路本身提供的能力,它是 Framework 在收到並理解 HTTP Request 之後,
才建立出來的更高層功能。
這對我們接下來自己做 Mini Web Framework 非常重要。
昨天,我們的 Mini Web Framework 已經可以:
監聽 Port
↓
接受 TCP Client
↓
讀取資料
今天又往上走了一層:
監聽 Port
↓
接受 TCP Client
↓
讀取 bytes
↓
轉成文字
↓
看到 HTTP Request
但目前我們仍然只是:
Console.WriteLine(request);
也就是:
看得到 Request,但還不懂 Request。
真正的 Framework 不可能每次都把:
GET /hello HTTP/1.1
整串文字丟給開發者自己處理。
它應該幫我們整理成類似:
Method → GET
Path → /hello
Version → HTTP/1.1
Headers → ...
所以 Mini Web Framework 下一個需要的能力已經很明顯了,解析 HTTP Request。
現在 Server 已經收到:
GET /hello HTTP/1.1
但對我們的程式來說,它目前仍然只是一段 string。
那 Framework 到底要怎麼從這串文字裡知道:
GET
是 Method, /hello 是 Path,
而HTTP/1.1又是 Version?
換句話說:
Framework 到底怎麼把一串 HTTP 文字,變成可以使用的 Request 物件?
這就是下一步我們要開始自己實作的東西。