昨天,我終於找到了一個程式真正開始執行的位置。
以前我一直把 Main() 理解成「程式的起點」,但透過 ILDasm 實際觀察後,我才知道更精確的概念其實是 Entry Point,即使使用 Top-level statements,沒有親手寫下 Main(),Compiler 仍然會替程式產生對應的進入點。
所以現在我知道:
啟動程式
↓
Entry Point
↓
開始執行
但這又讓我產生了下一個問題。
假設我的程式只有:
Console.WriteLine("程式開始");
Console.WriteLine("程式結束");
它從 Entry Point 開始,兩行執行完之後,很快就結束了。
可是 Web Server 明明不是這樣。
我們開啟網站時,Server 不可能處理完一個 Request 就直接關閉。
它必須繼續存在,等待下一個 Request。
所以今天我想追的問題變成:
為什麼一般程式執行完就結束,但 Web Server 卻可以一直活著?
為什麼一般程式執行完就結束,但 Web Server 可以一直等 Request?
以前看到 Server 一直運作,我其實沒有特別思考過它為什麼不會結束。
只是很自然地認為:
Server 本來就是一直開著的。
但如果把它當成一個普通程式來看,事情就變得很奇怪。
普通程式:
開始
↓
執行程式碼
↓
執行完畢
↓
結束
Web Server:
開始
↓
等待 Request
↓
收到 Request
↓
處理 Request
↓
回傳 Response
↓
等待下一個 Request
↓
等...
兩者都是程式。
為什麼一個會走到終點,另一個卻可以一直留在那裡?
我決定先不碰真正的 Web Server,而是從最簡單的 Console 程式開始實驗。
一個普通程式為什麼會結束?
Console.WriteLine("程式開始");
Console.WriteLine("程式結束");
執行後,結果很單純:
程式開始
程式結束
然後程式就結束了。
如果把昨天學到的 Entry Point 加進來,可以把流程想成:
Entry Point
↓
「程式開始」
↓
「程式結束」
↓
沒有其他程式碼要執行
↓
結束
這讓我先得到第一個很基本、但重要的觀察:
程式不會因為「它是程式」就永遠存在。當執行流程完成,而且沒有其他需要讓程序繼續存在的工作時,程序就可以結束。

那如果我故意讓執行流程永遠走不到終點呢?
原來「程式不結束」其實很簡單?
做到這裡,我一度覺得,那 Server 不就是弄一個無限迴圈,讓程式永遠不要跑完就好了嗎?
這個實驗證明了一件事,只要執行流程一直沒有完成,程式就不會因為「後面的程式碼執行完」而自然走到終點。
但馬上又出現另一個問題我們剛才寫的:
while (true)
{
}
其實什麼有意義的事情都沒做。
它只是在不停地:
判斷 true
↓
回去
↓
判斷 true
↓
回去
↓
判斷 true
↓
等……
程式確實「活著」,但 CPU 也在不斷執行這個迴圈。
這種方式稱為 Busy Waiting(忙碌等待/空轉) 的典型形式。
所以「程式沒有結束」不代表「程式正在有效率地等待」,這兩件事情不能畫上等號。
可不可以不要一直空轉,而是真的等?
所以第三次實驗,我把程式改成:
Console.WriteLine("程式開始");
Console.WriteLine("正在等待輸入...");
Console.ReadLine();
Console.WriteLine("收到輸入,程式準備結束");


執行後,我先看到:
程式開始:正在等待輸入...
然後程式停在那裡,這次跟剛才一樣程式沒有結束。
但兩者的狀況卻完全不同。
while (true) 是:
沒有事情
↓
一直跑
↓
一直檢查
↓
一直跑
而 Console.ReadLine() 則是在等待輸入,當沒有輸入時,這個呼叫不會立刻完成,因此後面的程式碼暫時不會繼續執行。
直到我在 Console 輸入文字並按下 Enter:
輸入資料
↓
Console.ReadLine() 完成
↓
程式繼續往下
↓
「收到輸入,程式準備結束」
↓
結束
這是今天三個實驗裡,我覺得最重要的一次。
一開始,我看到兩種程式都沒有結束:
while (true)
{
}
以及:
Console.ReadLine();
表面上好像沒有差別。
Console 視窗都還在。
程式都沒有走到最後。
但實際上它們代表的是兩種不同的狀態。
這時候我也第一次碰到一個很重要的概念:
Blocking(阻塞)
以今天的:
Console.ReadLine();
來說,程式執行到這裡時,需要等待輸入完成。
在這個呼叫完成以前,目前這條同步執行流程不能直接越過它執行下一行:
Console.WriteLine("收到輸入,程式準備結束");
可以先用一個簡化的方式理解:
執行到 ReadLine()
↓
等待輸入
│
│ 尚未輸入
│
└────── 等待
│ 收到輸入
▼
ReadLine() 完成
↓
執行下一行
所以 Blocking 並不是「程式壞掉了。」
而是目前的執行流程正在等待某個操作完成,因此暫時不能繼續往下。
這個概念之後碰到檔案、資料庫、網路 I/O 時,都還會再次出現。
現在終於可以把今天的實驗拉回 Mini Web Framework。
真正的 Web Server 當然不是寫:
Console.ReadLine();
來等待 Request。
今天的 Console.ReadLine() 只是為了讓我用最簡單的方法親手觀察:
程式可以等待外部事件發生,而不是執行完幾行程式碼就立刻結束。
Console 程式今天等待的是:
鍵盤輸入
而 Web Server 關心的則是:
網路上的連線與資料
可以先用非常簡化的方式比較:
Console 程式
等待鍵盤輸入
↓
使用者輸入
↓
繼續處理
Web Server 則像:
等待網路事件
↓
Client 傳來資料
↓
Server 處理
↓
回應
↓
繼續等待
這裡的關鍵不是 Console.ReadLine()。
做到這裡,我還不能直接說:
「Web Server 就是用 Blocking 等 Request。」
因為真正的 Server 還會遇到更多問題。
例如:
假設第一個 Client 的 Request 還沒處理完:
Client A
↓
Server 正在處理
這時 Client B 又來了:
Client B
↓
???
難道 B 必須一直等 A 完全處理完嗎?
如果同時有:
100 個 Request
甚至:
10,000 個 Request
又該怎麼辦?
這就會慢慢帶出之後非常重要的:
Thread
I/O
Blocking
Non-blocking
Asynchronous
async / await
但這些都不是今天要一次解決的問題。
今天只需要先建立最重要的第一層:
Server 能長時間存在,不是因為它單純「永遠執行同一段空迴圈」,而是它的生命週期中包含等待、接收與處理外部事件的過程。
到目前為止,我的 Mini Web Framework 還只是一個 Console 專案。
但如果最後真的要讓它成為 Web Framework,就不能只是:
Console.WriteLine("Hello");
然後:
執行
↓
結束
它未來必須能做到:
MiniWebFramework 啟動
↓
準備接收連線
↓
等待
↓
收到 Client 的資料
↓
處理
↓
產生 Response
↓
回傳
↓
繼續等待
今天雖然還沒有真正接收到 HTTP Request,但我至少已經理解:
為什麼 Server 類型的程式不能像普通 Console 範例一樣,執行完幾行就直接離開。
而下一步,就是搞懂:
它到底在等什麼?
今天我最大的發現其實不是:
while (true) 可以讓程式不要結束。
而是:
「程式活著」和「CPU 一直執行我的程式碼」並不是同一件事情。
以前看到 Server 一直開著,我很容易想像成:
它是不是一直在跑?
但今天透過兩個非常簡單的實驗,我第一次看到:
一直空轉
和:
等待事件
是完全不同的事情。
這個差異也讓我開始理解,為什麼 Web Server 的核心不只是:
「想辦法不要讓程式關掉。」
真正的問題其實是:
如何讓程式持續存在,同時有效率地等待、接收並處理外部事件?
這才是我們接下來真正要解決的問題。
現在,我已經知道 Web Server 必須「等待」。
但這句話馬上又產生了一個更具體的問題:
Console 程式可以用:
Console.ReadLine();
等待鍵盤輸入,那 Web Server 呢?
它總不可能等待鍵盤,Browser 傳來的 Request 究竟是透過什麼東西進入我們的程式?
換句話說:
如果 Web Server 要等待網路上的資料,它到底是在等什麼?
這就會把我們帶到一個真正屬於網路世界的概念:
Socket。
而當我們開始碰到 Socket,我們的 Mini Web Framework 也會第一次真正從普通 Console 程式開始往,
可以接收網路連線的程式前進。