天天看點

《UNIX網絡程式設計 卷1:套接字聯網API(第3版)》——1.2 一個簡單的時間擷取客戶程式

本節書摘來自異步社群《unix網絡程式設計 卷1:套接字聯網api(第3版)》一書中的第1章,第1.2節,作者:【美】w. richard stevens , bill fenner , andrew m. rudoff著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

讓我們考慮一個具體的例子,引入将在本書中遇到的許多概念和說法。圖1-5所示的是tcp目前時間查詢客戶程式的一個實作。該客戶與其伺服器建立一個tcp連接配接後,伺服器以直覺可讀格式簡單地送回目前時間和日期。

《UNIX網絡程式設計 卷1:套接字聯網API(第3版)》——1.2 一個簡單的時間擷取客戶程式

這就是本書用于展示所有源代碼的格式。每個非空行都被編排行号。如稍後所示,代碼正文講解部分一開始标注該段代碼起始與結束的行号。有的段落會以一個簡短的、描述性的醒目标題起頭,對所講解代碼段進行概要說明。

每個源代碼段起始與結束處的水準線标出了該代碼段所在的源代碼檔案名,對于本例就是intro目錄下的daytimetcpcli.c檔案(intro/daytimetcpcli.c)。本書所有例子的源代碼都可免費獲得(見前言),在此标注它們的檔案名便于讀者找到其源檔案。在閱讀本書期間,編譯、運作特别是修改這些程式是學習網絡程式設計概念的好方法。

整本書中我們随時會插入縮進的小字号段落(如此處所示)來說明實作的細節和曆史上的觀點。

如果編譯該程式生成預設的a.out可執行檔案後執行它,我們會得到如下結果:

當我們展示互動的輸入和輸出時,輸入總是采用加粗的等寬字型,而計算機的輸出總是采用不加粗的等寬字型。注釋用宋體字加在右邊。作為shell提示一部分的系統名字(本例中為solaris)指明在哪個主機上執行該指令。圖1-16展示了用于運作本書中大多數例子的各個系統,它們的主機名本身通常就說明了各自的作業系統。

在這個短短27行的程式中有許多細節值得考慮。這裡我們簡短地提一下,目的是讓初次遇到網絡程式的讀者有所準備,本書後面會更詳細地說明這些内容。

包含頭檔案

1 包含我們自己編寫的名為unp.h的頭檔案,見d.1節。該頭檔案包含了大部分網絡程式都需要的許多系統頭檔案,并定義了所用到的各種常值⑤(如maxline)。

指令行參數

2~3 這是main函數的定義,其形式參數就是指令行參數。本書中的代碼假設使用ansi c編譯器(也稱為iso c編譯器)編寫。

建立tcp套接字

10~11 socket函數建立一個網際(af_inet)位元組流(sock_stream)套接字,它是tcp套接字的花哨名字。該函數傳回一個小整數描述符,以後的所有函數調用(如随後的connect和read)就用該描述符來辨別這個套接字。

if語句包含3個操作:調用socket函數,把傳回值賦給變量sockfd,再測試所賦的這個值是否小于0。雖然我們可以把該語句分割成兩條c語句:

但是把這兩行合并成一行卻是常見的c語言習慣用法。按照c語言的優先規則(小于運算符的優先級高于指派運算符),函數調用和指派語句外邊的那對括号是必需的。作為一種編碼風格,作者總是在這樣的兩個左括号間加一個空格,提示比較運算的左側同時也是一個指派運算。(這種風格借鑒自minix源代碼[tenenbaum 1987]。)該程式稍後的while語句也使用相同的樣式。

後面我們将遇到術語套接字(socket⑥)的許多不同用法。首先,我們正在使用的api稱為套接字api(sockets api)。上一段中名為socket的函數就是套接字api的一部分。上一段中我們還提到了“tcp套接字”,它是“tcp端點”(tcp endpoint)的同義詞。

如果socket函數調用失敗,我們就調用自己的err_sys函數放棄程式運作。err_sys函數輸出我們作為參數提供的出錯消息以及所發生的系統錯誤的描述(例如出自socket函數的可能錯誤之一“proto-col not supported”(協定不受支援)),然後終止程序。這個函數和以err_開頭的其他若幹個函數都是我們自行編寫的,它們的調用将貫穿全書,d.3節會描述這些函數。

指定伺服器的ip位址和端口

12~16 我們把伺服器的ip位址和端口号填入一個網際套接字位址結構(一個名為servaddr的sockaddr_in結構變量)。使用bzero把整個結構清零後,置位址族為af_inet,端口号為13(這是時間擷取伺服器的衆所周知端口,支援該服務的任何tcp/ip主機都使用這個端口号,見圖2-18),ip位址為第一個指令行參數的值(argv[1])。網際套接字位址結構中ip位址和端口号這兩個成員必須使用特定格式,為此我們調用庫函數htons(“主機到網絡短整數”)去轉換二進制端口号,又調用庫函數inet_pton(“呈現形式到數值”)去把ascii指令行參數(例如運作本例子所用的206.168.112.96)轉換為合适的格式。

bzero不是一個ansi c函數。它起源于早期的berkeley網絡程式設計代碼。不過我們在整本書中使用它而不用ansi c的memset函數,因為bzero(帶2個參數)比memset(帶3個參數)更好記憶。幾乎所有支援套接字api的廠商都提供bzero,如果沒有,那麼可以使用unp.h頭檔案中提供的該函數的宏定義。

事實上,在tcpv3一書首次印刷時,作者在10處出現memset函數的地方犯了錯,互換了第二和第三個參數。c編譯器發現不了這個錯誤,因為這兩個參數的類型是相同的。(其實第二個參數是int類型,第三個參數是size_t,通常定義為unsigned int類型,然而分别指定給這兩個參數的值為0和16,它們對于兩個參數的類型同樣可以接受。)對memset的這些調用仍然正常,不過沒做任何事,因為待初始化的位元組數被指定成了0。程式之是以仍然工作是因為隻有少數套接字函數要求網際套接字位址結構的最後8個位元組置0。無論如何,這确實是一個錯誤,且是一個通過使用bzero函數可以避免的錯誤,因為如果使用函數原型,c編譯器總能發現bzero的兩個參數被互換的錯誤。

此處也許是你第一次遇到inet_pton函數。它是一個支援ipv6(詳見附錄a)的新函數。以前的代碼使用inet_addr函數來把ascii點分十進制數串變換成正确的格式,不過它有不少局限,而這些局限在inet_pton中都得以糾正。如果你的系統尚未支援該函數,那你可以使用我們在3.7節中提供的它的一個實作。

建立與伺服器的連接配接

17~18 connect函數應用于一個tcp套接字時,将與由它的第二個參數指向的套接字位址結構指定的伺服器建立一個tcp連接配接。該套接字位址結構的長度也必須作為該函數的第三個參數指定,對于網際套接字位址結構,我們總是使用c語言的sizeof操作符由編譯器來計算這個長度。

在頭檔案unp.h中,我們使用#define把sa定義為struct sockaddr,也就是通用套接字位址結構。每當一個套接字函數需要一個指向某個套接字位址結構的指針時,這個指針必須強制類型轉換成一個指向通用套接字位址結構的指針。這是因為套接字函數早于ansi c标準,20世紀80年代早期開發這些函數時,ansi c的void *指針類型還不可用。問題是“struct sockaddr”長達15個字元,往往造成源代碼行超出螢幕(或者書頁,若是排印在書上)的右邊緣,是以我們把它縮減成sa。我們将在解釋圖3-3時詳細讨論通用套接字位址結構。

讀入并輸出伺服器的應答

19~25 我們使用read函數讀取伺服器的應答,并用标準的i/o函數fputs輸出結果。⑦使用tcp時必須小心,因為tcp是一個沒有記錄邊界的位元組流協定。伺服器的應答通常是如下格式的26位元組字元串:

<code>mon may 26 20:58:40 2003\r\n</code>

其中,r是ascii回車符,n是ascii換行符。使用位元組流協定的情況下,這26個位元組可以有多種傳回方式:既可以是包含所有26個位元組的單個tcp分節⑧,也可以是每個分節隻含1個位元組的26個tcp分節,還可以是總共26個位元組的任何其他組合。通常伺服器傳回包含所有26個位元組的單個分節,但是如果資料量很大,我們就不能確定一次read調用能傳回伺服器的整個應答。是以從tcp套接字讀取資料時,我們總是需要把read編寫在某個循環中,當read傳回0(表明對端關閉連接配接)或負值(表明發生錯誤)時終止循環。

本例中,伺服器關閉連接配接表征記錄的結束。http(hypertext transfer protocol,超文本傳送協定)的1.0版本也采用這種技術。還可以用其他技術标記記錄結束。例如,smtp(simple mail transfer protocol,簡單郵件傳送協定)使用由ascii回車符後跟換行符構成的2位元組序列标記記錄的結束;sun遠端過程調用(remote procedure call,rpc)以及域名系統(domain name system,dns)在使用tcp承載應用資料時,在每個要發送的記錄之前放置一個二進制的計數值,給出這個記錄的長度。這裡的重要概念是tcp本身并不提供記錄結束标志:如果應用程式需要确定記錄的邊界,它就要自己去實作,已有一些常用的方法可供選擇。

終止程式

26 exit終止程式運作。unix在一個程序終止時總是關閉該程序所有打開的描述符,我們的tcp套接字就此被關閉。

剛才已提過,本書後面會對剛才講述的所有概念深入進行探讨。

繼續閱讀