08-常見的問題

我可以從哪邊取得那些 header 檔案呢?

如果你的系統還沒有這些檔案,你可能就不需要它們。檢查你平台的使用手冊。若你在 Windows 上開發,那麼你只需要 #include <winsock.h>。

在 bind() 回報"Address already in use"(位址已經在使用中)時,我該怎麼辦呢?

你必須使用 setsockopt() 對 listen 的 socket 設定 SO_REUSEADDR 選項。請參考 bind() 及 select() 章節的範例。

我該如何取得系統上已經開啟的 sockets 清單呢?

使用 netstat。細節請參考 man 使用手冊,不過你應該只要輸入下列的指令就能取得一些不錯的資訊:
$ netstat

我該如何檢視 routing table(路由表)呢?

執行 route 指令(多數的 Linux 系統是在 /sbin 底下),或者 netstat -r 指令。

如果我只有一台電腦,我該如何執行 client(客戶端)與 server(伺服器)程式呢?我需要網路來寫網路程式嗎?

你很幸運,全部的系統都有實作一個 loopback(繞迴)虛擬網路"裝置",這個裝置位於 kernel 中,並假裝是張網路卡[這個介面就是 routing table 中所列出的 "lo"]。

假裝你已經登入一個名為"goat"的系統,在一個視窗中執行 client,並在另一個視窗執行 server。

或者可以在背景啟動 server["server &"],並在同樣的視窗執行 client。

loopback 裝置的功能是你可以執行 client goat 或 client localhost[因為"localhost"應該已經定義在你的 /etc/hosts 檔案],而你可以讓 client 與 server 溝通而不需要網路。

簡而言之,不需要改變任何的程式碼,就可以讓程式在無網路的單機系統上執行!好耶!

我該怎麼知道對方已經關閉連線呢?

你可以辨別出來,因為 recv() 會傳回 0。

我該如何實作一個"ping"工具呢?什麼是 ICMP 呢?我可以在哪裡找到更多關於 raw socket 與 SOCK_RAW 的資料呢?

你對 raw socket 的全部疑問都可以在 W. Richard Stevens 的 UNIX Network Programming 書本上找到答案。還有,研究 Stevens 的 UNIX Network Programming 程式碼的 ping 子目錄,可以從線上下載 [37]。

我該如何改變或縮短呼叫 connect() 的逾期時間呢?

我不想跟你說一樣的答案:"W. Richard Stevens 會告訴你",我只能建議你參考 UNIX Network Programming 原始程式碼 [38] 中的 /lib/connect_nonb.c。

主要是你要用 socket() 建立一個 socket descriptor,將它設定為 non-blocking(非阻塞式),呼叫 connect(),而如果一切順利,connect() 會立刻傳回 -1,並將 errno 設定為 EINPROGRESS。接著你要呼叫 select() 並設定你想要的 timeout 時間,傳遞讀取及寫入集合(read and write sets)的 socket descriptor。如果 select() 沒有發生 timeout,這表示 connect() call 已經完成。此時,你必須使用 getsockopt() 設定 SO_ERROR 選項以取得 connect() call 的傳回值,在沒有錯誤時,這個值應該是零。

最後,在你開始透過 socket 傳輸資料以前,你可能想要再將它設定回 blocking(阻塞)。

要注意的是,這樣做的好處是讓你的程式在連線(connecting)期間也可以另外做點事情。比如:你可以將 timeout 時間設定為類似 500 毫秒,並在每次 timeout 發生時更新螢幕畫面,然後再次呼叫 select()。當你已經呼叫了 select() 時,並且 timeout 了,像這樣重複了 20 次,你就會知道應該放棄這個連線了。

如我所述的,請參考 Stevens 的既完美又優秀的範例程式碼。

我該如何寫 Windows 的網路程式呢?

首先,請刪除 Windows,並安裝 Linux 或 BSD。;-)。不是的,實際上,只要參考導讀章節中的[Windows程式設計師要注意的事情]就可以了。

我該如何在 Solaris/SunOS 上編譯程式呢?在我嘗試編譯時,一直遇到錯誤!

發生 Linker(連結器)錯誤是因為 Sun 系統在不會自動編入 socket 函式庫。請參考導讀中的[Solaris/SunOS 程式設計師要注意的事情],有如何處理這個問題的範例。

為什麼 select() 跟 signal 合不來呢?

Signal 試圖要讓 blocked system call 傳回 -1,並將 errno 設定為 EINTR。當你用 sigaction() 設定了一個 signal handler(訊號處理常式)時,你可以設定 SA_RESTART 旗標,這可以在 system call 被中斷之後重新啟用它。

自然不會每次都管用。

我最愛的解法是使用一個 goto,你明白這會讓你的教授很憤怒,所以放手去做吧!

select_restart:
if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) {
  if (errno == EINTR) {
    // 某個 signal 中斷了我們,所以重新啟動
    goto select_restart;
  }
  // 這裡處理真正的錯誤:
  perror("select");
}
當然,在這個例子裡,你不需使用 goto;你可以用其它的 structures 來控制,但是我認為用 goto 比較簡潔。

要怎麼樣我才能實作呼叫 recv() 的 timeout 呢?

使用 select()!它可以讓你對正在讀取的 socket descriptors 指定 timeout 的參數。或者你可以將整個功能包在一個獨立的函式中,類似這樣:
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>

int recvtimeout(int s, char *buf, int len, int timeout)
{
  fd_set fds;
  int n;
  struct timeval tv;

  // 設定 file descriptor set
  FD_ZERO(&fds);
  FD_SET(s, &fds);

  // 設定 timeout 的資料結構 struct timeval
  tv.tv_sec = timeout;
  tv.tv_usec = 0;

  // 一直等到 timeout 或收到資料
  n = select(s+1, &fds, NULL, NULL, &tv);
  if (n == 0) return -2; // timeout!
  if (n == -1) return -1; // error

  // 資料一定有在這裡,所以執行一般的 recv()
  return recv(s, buf, len, 0);
}
.
.
.
// 呼叫 recvtimeout() 的範例:
n = recvtimeout(s, buf, sizeof buf, 10); // 10 second timeout

if (n == -1) {
  // 發生錯誤
  perror("recvtimeout");
}
else if (n == -2) {
  // 發生 timeout
} else {
  // 從 buf 收到一些資料
}
.
.
.
請注意到,recvtimeout() 在 timeout 的例子中會傳回 -2,那為什麼不是傳回 0 呢?好的,如果你還記得,在呼叫 recv() 傳回 0 值時所代表的意思是對方已經關閉了連線。所以該傳回值已經用過了,而 -1 表示"錯誤",所以我選擇 -2 做為我的 timeout 表示。

我該如何在將資料送給 socket 以前將資料加密或壓縮呢?

一個簡單的加密方法是使用 SSL(secure sockets layer),只是這超過本文件的範疇了[細節請參考 OpenSSL 專案 [39]]。

不過假設你想要安插或實作你自己的壓縮器(compressor)或加密系統(encryption system),這只不過是將你的資料想成在兩端點間執行連續的步驟,每個步驟以同樣的方式改變資料:

1. server 從檔案讀取資料[或是什麼地方]

2. server 加密/壓縮資料[你新增這個部分]

3. server 用 send() 送出加密資料

而另一邊則是:

1. client 用 recv() 接收加密資料

2. client 解密/解壓縮資料[你新增這個部分]

3. client 寫資料到檔案[或是什麼地方]

如果你正要壓縮與加密,只要記得先壓縮。:-)

只要 client 適當地還原 server 所做的事情,資料在另一端就會完好如初,不論你在中間增加了多少步驟。

所以你用我的程式碼所需要做的只有:找出讀資料與透過網路傳送[使用 send()]這中間的段落,並在那裡加上編碼的程式碼。

我一直看到的 "PF_INET"是什麼呢?他跟 AF_INET 有關係嗎?

是的,有關係,細節請參考 socket() 章節。

我該怎麼寫一個 server,可以接受來自 client 的 shell 指令並執行指令呢?

為了簡化,我們說 client 的連線用 connect()、send() 以及 close()[即為,沒有後續的 system calls,client 沒有再次連線。]

client 的處理過程是:

1. 用 connect() 連線到 server

2. send("/sbin/ls > /tmp/client.out")

3. 用 close() 關閉連線

此時,server 正在處理資料並執行指令:

1. accept() client 的連線

2. 使用 recv(str) 接收命令字串

3. 用 close() 關閉連線

4. 用 system(str) 執行指令

注意!server 會執行全部 client 所送的指令,就像是提供了遠端的 shell 存取權限,人們可以連線到你的 server 並用你的帳號做點事情。例如:若 client 送出 "rm -rf ~"會怎麼樣呢?這會刪掉你帳號裡的全部資料,就是這樣!

所以你學聰明了,你會避免 client 使用任何危險的工具,比如 foobar 工具:
if (!strncmp(str, "foobar", 6)) {
  sprintf(sysstr, "%s > /tmp/server.out", str);
  system(sysstr);
}
可是這樣還是不安全,沒錯:如果 client 輸入 "foobar; rm -rf ~" 呢?

最安全的方式是寫一個小機制,將命令參數中的非字母數字字元前面放個['\']字元[如果適合的話,要包括空白]。

如你所見,當 server 開始執行 client 送來的東西時,安全性(security)是個問題。

我正在傳送大量資料,可是當我 recv() 時,它一次只收到 536 bytes 或 1460 bytes。可是如果我在我本機上執行,它就會一次就收到全部的資料,這是怎麼回事呢?

你碰到的是 MTU,即實體媒介(physical medium)能處理的最大尺寸。在本機上,你用的是 loopback 裝置,它可以處理 8K 或更多資料也沒有問題。但是在 Ethernet(乙太網路),它只能處理 1500 bytes[有 header],你碰到這個限制。透過 modem 的話,MTU 是 576 bytes[一樣,有 header],你遇到比較低的限制。

你必須確認有送出全部的資料。[細節請參考 sendall() 函式的實作]。一旦你有確認,那麼你就需要在迴圈中呼叫 recv(),直到收到全部的資料。

對於使用多重呼叫 recv() 來接收完整資料封包的細節,請參考資料封裝(Son of Data Encapsulation)一節。

我用的是 Windows 系統,而且我沒有 fork() system call 或任何的 struct sigaction 可以用,該怎麼辦呢?

如果你問的是它們在哪裡,它們會在 POSIX 函式庫裡,這個會包裝在你的編譯器中。因為我沒有 Windows 系統,所以我真的無法回答你,不過我似乎記得 Microsoft 有一個 POSIX 相容層,那裏會有 fork()。[而且甚至會有 sigaction。]

在 VC++ 的使用手冊搜尋 "fork" 或 "POSIX",看它是否能給你什麼線索。

如果這樣一點都沒有用,拿掉 fork()/sigaction 這些東西,用 Win32 中等價的函式來取代:CreateProcess()。我不知道怎麼用CreateProcess(),它有多的數不清的參數,不過在 VC++ 的文件中應該可以找到怎麼使用它。

我在防火牆(firewall)後面,我該如何讓防火牆外面的人知道我的 IP 位址,讓他們可以連線到我的電腦呢?

毫無疑問地,防火牆的目的就是要防止防火牆外面的人連到防火牆裡面的電腦,所以你讓他們進來基本上會被認為是安全上的漏洞。

但也不是說完全不行,有一個方法,你仍然可以透過防火牆頻繁的進行 connect(),如果防火牆是使用某種偽裝(masquerading)或 NAT 或類似的方式。你只要讓程式一直在做初始化連線,那麼你有機會成功的。

如果這樣還不是很滿意,你可以要求系統管理員在防火牆開一個小洞(hole),讓人們可以連進你的電腦。防火牆可以透過 NAT 軟體或 proxy(代理)或類似的方法將封包轉送給你。

要留意,不要對防火牆中的一個小洞掉以輕心。你必須確保你不會放壞人進來存取內部網路;如果你是新手,做軟體安全是遠遠難於你的想像。

不要讓你的系統管理員對我發脾氣 ;-)

我該怎麼寫 packet sniffer 呢?我要怎麼將我的 Ethernet interface(網路卡)設定為 promiscuous mode(混雜模式)呢?

這些事情是在底層運作的,當網路卡設定為 "promiscuous mode" 時,它會轉送全部的封包給作業系統,而不只是位址屬於這台電腦的封包而已。[我們這裡談的是 Ethernet 層的位址,而不是 IP 位址,可是因為 ethernet 是在 IP 底層,所以全部的 IP 位址實際上都會轉送。細節請參考"底層漫談與網路理論"一節]。

這是 packet sniffer 如何運作的基礎,它將網路介面卡設定為 promiscuous mode,接著 OS 會收到經過網路線的每個封包,你會有一個可以用來讀取資料的某種型別 socket。

毫無疑問地,這個問題的答案依平台而異,不過如果你用 Google 搜尋,例如:"windows promiscuous ioctl",你或許會在某個地方找到,看起來跟 Linux Journal [40] 中寫的一樣好的。

我該如何為 TCP 或 UDP socket 設定一個自訂的 timeout 值呢?

這個按照你的系統而定,你可以在網路搜尋 SO_RCVTIMEO 與 SO_SNDTIMEO[用在 setsockopt()],看看是否你的系統有支援這樣的功能。

Linux man 使用手冊建議使用 alarm() 或 setitimer() 作為替代品。

我要如何辨別哪些 ports 可以使用呢?有沒有"官方"的 port numbers 呢?

通常這不會有問題,如果你正在寫像 web server 這樣的程式,那麼在你的程式使用 port 80 是個好主意。如果你只是想要寫自己的 server,那麼隨機選擇一個 port[不過要大於 1023],然後試試看。

如果 port 已經在使用中,你將會在嘗試 bind() 時遇到 "Address already in use" 錯誤。選擇另一個 port。[利用 config 組態檔或命令列參數設定,讓你的軟體使用者能指定 port 也是個不錯的想法]。

有一個官方的 port nubmer [41] 清單,由 Internet Assigned Numbers Authority(IANA)所維護的。在清單中的 port[超過 1023]並不代表你就不能使用,比如,Id 軟體的 DOOM 跟 "mdqs" 用一樣的 port,不管那是什麼,最重要的是在同一台機器上沒有人用掉你要用的 port。

[37] http://www.unpbook.com/src.html
[38] http://www.unpbook.com/src.html
[39] http://www.openssl.org/
[40] http://interactive.linuxjournal.com/article/4659
[41] http://www.iana.org/assignments/port-numbers
Comments