博客 / 詳情

返回

他可能瘋了吧,要用 awk 語言寫網絡程序……

本文是 gawk 網絡編程的學習筆記。用 Awk 語言編寫網絡程序,這樣的想法有些癲狂,像是企圖用一柄小刀砍伐一棵巨樹,何況是對於我這樣的人,對網絡編程近乎一無所知。

對於一些在使用 Awk 語言處理文本方面頗有經驗的人,他們甚至未必認為 Awk 語言能夠實現網絡編程。的確如此,Awk 語言並不支持網絡編程,但是 gawk 改變了這個事實。gawk 對 Awk 語言進行了擴展,網絡編程便在其中,於 gawk 3.1 版本實現,詳見「Gawkinet: TCP/IP Internetworking with Gawk」。

雙向管道

學習 gawk 的網絡編程,需要先理解 gawk 對單向管道的擴展,即雙向管道。gawk 網絡編程是基於雙向管道實現的。

標準的 Awk 語言支持管道。例如

$ awk 'BEGIN { print "123456789" | "rev" }'
987654321

print 輸出的內容通過管道 | 傳送給了 Linux 命令 rev,由後者反轉文本,但是這個管道是單向的,Awk 程序將輸出內容傳給 rev 之後,而 rev 的結果無法傳回 Awk 程序。若 Awk 程序想獲得 rev 的結果,只能是基於臨時文件的方式,將 rev 的輸出保存為一份臨時文件,Awk 程序讀取該文件的內容。

gawk 對 Awk 的管道進行了擴展,使之支持雙向通信。雙向管道的符號是 |&。使用雙向管道連接 rev,從 rev 取回結果的代碼如下:

print "123456789" |& "rev" # 向 rev 發送數據
"rev" |& getline           # 從 rev 獲取數據

通過雙向管道與 Awk 程序連接的程序稱為協同進程(Coprocess),上例中的 rev 便是協同進程。不過,上述代碼實際上會導致程序死鎖。因為 rev 在等待輸入結束,但 print 語句無法給出輸入結束標識。同時,getline 在等待 rev 的輸出。在協同進程輸出全部數據後,需使用 close 函數關閉雙向管道的 to 端——向協同進程發送數據的通道,然後協同進程得到輸入結束標識,故而上述代碼需修改為

print "123456789" |& "rev"; close("rev", "to")
"rev" |& getline

雙向管道的 from 端是協同進程向 Awk 程序傳輸數據的通道。由於協同進程的輸出會帶有結束標記,故而無需顯式關閉 from 端,亦可將其顯式關閉:

print "123456789" |& "rev"; close("rev", "to")
"rev" |& getline; close("rev", "from")

特殊文件

gawk 的網絡編程基於雙向管道實現,只是與管道連接的不再是協同進程,而是一種特殊文件,其形式如下:

# /網絡類型/協議/本地端口/主機名/遠程端口
/net-type/protocol/localport/hostname/remoteport

這種特殊文件形式,即可用於表達服務器端,也可表達客户端。例如,將本機作為服務器端,使用 IP v4 網絡類型和 TCP 協議,以端口 8080 提供某種網絡服務,則特殊文件的寫法為

/inet4/tcp/8080/0/0

若某個客户端訪問該服務器端,則客户端對應的特殊文件寫法為

/inet4/tcp/0/服務器地址(IP 或域名)/8080

這種特殊文件的寫法,除了網絡類型和協議之外的其他部分,無論是服務器程序還是客户端程序,只要自身無需關心的部分,皆寫為 0。例如,作為服務器端的程序,它不必關心 hostnameremoteport,因為它不需要主動訪問其他機器上的進程,而作為客户端的程序則不必關心 localport,因為沒有主機會主動訪問它。

gawk 將網絡編程的 socket(所謂的套接字)創建過程隱含在上述特殊文件的構建過程中了,從而顯著簡化了網絡程序的編寫,但是也犧牲了實現能夠應對複雜需求的網絡程序的可能性,亦即使用 gawk 通常只能編寫簡單的網絡程序。工業級的網絡程序,例如支持網絡併發訪問和反向代理等功能的服務器程序,需使用其他編程語言在底層的 socket 層面方能實現。

服務器端程序,向與之連接的客户端程序發送信息,只需將信息發送到特殊文件。例如

print "Hello world!" |& "/inet4/tcp/8888/0/0"

服務器端程序從與之連接的客户端程序讀取信息,只需從特殊文件讀取信息。例如

"/inet4/tcp/8888/0/0" |& getline  # 從客户端程序讀取一條記錄,存於 $0

同理,客户端程序從與之連接的服務器端程序獲取信息,只需從特殊文件讀取信息。例如

"/inet4/tcp/0/服務器地址/8888" |& getline  # 從服務器端程序讀取一條記錄,存於 $0

客户端程序向與之連接的服務器端程序發送信息,只需向特殊文件寫入數據。例如

print "hi" |& "/inet4/tcp/0/服務器地址/8888"

分別用於表示服務端和客户端的特殊文件,二者存在一個重疊,即端口。在服務端,端口是本機端口;在客户端,端口是遠程端口,二者所指對象都是服務器上的那個端口。可以將服務器理解為有許多房間的一棟樓,端口是房間的門牌號。

特殊文件的網絡協議字段是 inet4inet6,分別表示 IP v4 和 IP v6。可以簡寫為 inet,此時若系統環境使用的是 IP v4 網絡,則 inet 表示 inet4,若系統環境為 IP v6 網絡,則 inetinet6

Hello world!

下面的腳本構造了一個會説「Hello world!」的服務端程序 server.awk:

BEGIN {
    print "Hello world!" |& "/inet/tcp/8888/0/0"
}

運行 server.awk:

$ awk -f server.awk

現在 server.awk 程序會在本機的 8888 端口等待客户端的訪問。用網絡編程術語描述,這個過程是阻塞的,即上述程序通過雙向管道向特殊文件寫入數據後,會停止執行,直到有客户端程序發起連接。

客户端程序通過特殊文件訪問運行 server.awk 程序的主機的 8888 端口時,可以得到 server.awk 發送過來的數據。下面是位於運行 server.awk 的機器上的一個客户端程序 client.awk:

BEGIN {
    "/inet/tcp/0/localhost/8888" |& getline
    print $0
}

由於 client.awk 訪問的服務器就是本機,故而網絡主機地址是 localhost,亦可寫為 127.0.0.1。運行 client.awk,可在屏幕上打印來自 server.awk 的「Hello world!」,

$ awk -f client.awk
Hello world!

然後 client.awk 和 server.awk 分別自動結束運行,各自所用的特殊文件也會被自動關閉。

端口查看

上一節實現的 server.awk 和 client.awk,皆未對錶示網絡連接的特殊文件進行顯式關閉。學究一些,是需要顯式關閉的,即

BEGIN {
    service = "/inet/tcp/8888/0/0"
    print "Hello world!" |& service
    close(service)
}
BEGIN {
    server = "/inet/tcp/0/localhost/8888"
    server |& getline
    print $0
    close(server)
}

實際上在 Awk 程序退出時,會自動關閉這些特殊文件。可以使用 netstat 命令做一個試驗,驗證這一觀點。

首先,執行上一節所寫的 server.awk 程序,然後使用以下命令查看本機上哪個進程正在使用 TCP 協議並佔用端口 8888

$ sudo netstat -tlp | grep ":8888"
tcp    0    0 0.0.0.0:8888    0.0.0.0:*    LISTEN    36099/awk

結果顯示是一個 awk 程序。

現在若再一次執行 server.awk 程序,會出錯:

awk: server.awk:2: fatal: cannot open two way pipe 
  `/inet4/tcp/8888/0/0' for input/output: Address already in use

現在執行上一節的 client.awk 程序,訪問 server.awk,然後二者自動退出。

再一次執行上述的 netstat 命令,則沒有任何信息輸出了,這意味着 /inet4/tcp/8888/0/0 已被關閉。

上述 netstat 命令所使用的各選項的含義如下:

  • -t:查看 TCP 連接。若查看 UDP 連接,使用 -u
  • -l:顯示所有處於監聽(LISTEN)狀態的端口(常用於檢查服務是否啓動)。
  • -p:顯示佔用端口的進程名和 PID(需超級用户權限)。

簡單的 HTTP 服務器

有的時候,必須關閉連接,例如實現一個可以持續運行的服務器。前兩節所實現的 server.awk,在客户端訪問一次後便終止退出了,它無法持續運行。要讓一個服務端程序持續運行,只需要在一個無限循環中不斷開啓和關閉即可,即

BEGIN {
    while (1) {
        service = "/inet/tcp/8888/0/0"
        print "Hello world!" |& service
        close(service)
    }
}

對上述代碼略加修改,便可構造一個簡單的可持續運行的 HTTP 服務器:

BEGIN {
    RS = ORS = "\r\n"
    http_service = "/inet/tcp/8888/0/0"
    hello = "<html><head>" \
            "<meta charset=\"utf-8\" />" \
            "<title>一個著名的問候</title></head>" \
            "<body><h1>你好,世界!</h1></body></html>"
    n = length(hello) + length(ORS)
    while (1) {
        print "HTTP/1.0 200 OK"        |& http_service
        print "Content-Length: " n ORS |& http_service
        print hello                    |& http_service
        while ((http_service |& getline) > 0) {
            continue
        }
        close(http_service)
    }
}

假設將上述代碼保存為 http-server.awk,執行該程序:

$ awk -f http-server.awk

在運行該 HTTP 服務器程序的機器上,打開網絡瀏覽器,在地址欄輸入 localhost:8888,便可訪問該服務器。

若要理解上述 http-server.awk 的代碼,需要懂得 HTTP 報文的基本知識,不懂也沒關係,知道 hello 的值是 HTTP 報文並且知道 HTTP 報文是 HTTP 協議的一部分即可。

上述代碼中第一層 while 循環可以保證服務器持續運行,而在該循環內部,除了將 HTTP 報文發送給 http_service 連接的代碼外,最為關鍵的是下面這段代碼:

while ((http_service |& getline) > 0) {
    continue
}
close(http_service)

上述這段代碼,用於讀取客户端(網絡瀏覽器)向連接傳送的信息,但是這些信息皆被忽略了,當客户端的信息傳送完畢後,while 中的雙向管道的結果不再是正數,故而 while 循環終止,繼而連接被關閉。若對來自客户端的信息感興趣,可在 while 循環中將信息打印出來:

while ((http_service |& getline) > 0) {
    print $0
    continue
}
close(http_service)

至於 http-server.awk 向連接發送的報文是如何被網絡瀏覽器端獲得並呈現,那是網絡瀏覽器的任務,在本質上它與前文我們所寫的 client.awk 並無不同,當然在實現上會複雜好多個數量級。

可交互的 HTTP 服務器

若懂得 CGI 協議,可以將 http-server.awk 修改為一個可以支持在網頁上動態交互的 HTTP 服務器。對此,我現在沒興趣,暫且略過。「Gawkinet: TCP/IP Internetworking with Gawk」的 2.9 節實現了一個可交互的 HTTP 服務器,但它也是假設讀者對 CGI 協議有所瞭解,而且它的示例代碼並不穩健——服務器的連接可能會因超時而意外斷開。

gawk 網絡編程的侷限性

gawk 基於雙向管道實現的網絡連接和數據傳輸,服務端無法支持併發訪問。例如上一節實現的 http-server.awk,運行該服務器程序後,可以使用 telnet 訪問它:

$ telnet localhost 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
HTTP/1.0 200 OK
Content-Length: 102

<html><head><meta charset="utf-8" /><title>一個著名的問候</title></head><body><h1>你好,世界!</h1></body></html>

此時保持上述 telnet 的連接未斷,再次向服務端發起連接,會被拒絕:

$ telnet localhost 8888
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused

這意味着 http-server.awk 所實現的服務器無法支持兩個併發連接。原因是什麼呢?相當於電話佔線。gawk 將複雜的網絡連接過程封裝為可與雙向管道配合使用的特殊文件形式,便意味着無法讓網絡連接支持更為複雜的需求了。

總結

用 Awk 語言編寫的網絡程序雖無大用,但是對於熟悉網絡編程並建立一些工程直覺有所裨益。對於 Awk 編程本身而言,由於網絡的透明性,Awk 程序可以將一些複雜的計算任務交於其他進程,這個進程可以是運行於本機的,也可以是運行於同一網絡上的其他機器上的,且其對應的程序也可以是由其他語言編寫,從而可以彌補 Awk 語言的不足,從這一點而言,gawk 為 Awk 語言所作的網絡編程擴展,其意義深遠。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.