UNIX网络编程卷一 学习笔记 第一章 简介
編寫通過計算機網絡通信的程序時,首先要發明一種協議,即這些程序怎樣進行通信。在深入設計一個協議的細節前,要在更高層次決定通信由哪個程序發起以及響應在何時產生,舉例來說,一般認為web服務器是一個長時間運行的程序(即所謂守護程序),它只在響應來自網絡的請求時才發送網絡消息。協議另一端是web客戶程序,如某種瀏覽器,與服務器進程的通信總是由客戶進程發起。
大多網絡應用都是由客戶進程發起通信請求,確定這一點有助于簡化協議和程序。一些較為復雜的網絡還需異步回調通信,即由服務器向客戶發起請求信息。然而堅持采納下圖所示的客戶/服務器模型的網絡應用要更普遍:
通常客戶每次只與一個服務器通信,不過以web瀏覽器為例,我們也許10分鐘就與許多不同web服務器通信。一個服務器可同時處理多個客戶請求。
可認為客戶與服務器間是通過某個網絡協議通信的,但實際上,這樣的通信涉及多個網絡協議層,本書的焦點是TCP/IP協議族,也稱為網際協議族。舉例來說,web客戶與服務器之間使用TCP通信,TCP又轉而使用IP通信,IP再通過某種形式的數據鏈路層通信,如果客戶與服務器處于同一個以太網,會有以下通信層次:
客戶與服務器之間的信息流在其中一端是向下通過協議棧的,跨越網絡后,在另一端是向上通過協議棧的。客戶和服務器通常是用戶進程,而TCP和IP協議通常是內核中協議棧的一部分。
上圖中術語IP自20世紀80年代以來一直在使用,其正式名稱是IPv4(IP version 4),IPv4的新版本IPv6是在20世紀90年代中期開發出來的,將來會取代IPv4。
客戶和服務器無需如上圖一樣都處于一個局域網,可通過路由器將兩個局域網連接到廣域網:
路由器是廣域網的架構設備。當今最大的廣域網是因特網,許多公司也構建自己的廣域網,這些私用的廣域網既可以連接到因特網,也可以不連接。
以下是一個簡單卻完整的TCP客戶程序,它只能在IPv4上運行,如果想讓它在IPv6上運行,需要進行修改,但更好的方法是編寫獨立于協議的客戶和服務器程序。向服務器查詢時間的客戶程序(本書中所有代碼假設使用ANSI C,也稱為ISO C編譯器編寫):
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h>#define MAXLINE 512int main(int argc, char **argv) {int sockfd, n;char recvline[MAXLINE + 1];struct sockaddr_in servaddr; // 此結構位于頭文件netinet/in.hif (argc != 2) {printf("usage: a.out <IPaddress>\n");exit(1);}// 作為一種編碼風格,作者在兩個左括號間加了一個空格,提示比較運算的左側同時也是一個賦值運算// 這種風格借鑒自Minix源代碼,下面的while語句也用了相同的格式if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { // 創建一個網際(AF_INET)字節流(SOCK_STREAM)套接字,即TCP套接字,它返回一個小整數描述符,以后的函數調用(如connect和read函數)就用該描述符來標識這個套接字printf("socket error\n");exit(1);}bzero(&servaddr, sizeof(servaddr)); // bzero函數位于頭文件string.h,把指定字節大小的地址區域都置為0字節servaddr.sin_family = AF_INET; // 置地址族為AF_INETservaddr.sin_port = htons(13); // daytime server的端口號為13,htons函數將短整型變量從主機字節順序轉變成網絡字節順序(高位字節存在低地址處)if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { // inet_pton函數將點分十進制IP地址轉換為二進制// inet_pton函數是支持IPv6的新函數,以前的代碼使用inet_addr函數將ASCII點分十進制串變成正確形式,但inet_addr函數有很多局限,這些局限都在inet_pton函數中被糾正printf("inet_pton error for %s\n", argv[1]);exit(1);}// connect函數與它的第二個參數指向的套接字地址結構所指定的服務器建立TCP連接// 第三個參數是這個套接字地址結構的長度,對于網際套接字地址結構,我們總是使用C語言的sizeof操作符由編譯器來計算這個長度// 第二個參數我們用sockaddr類型指針指向了sockaddr_in類型,因為sockaddr類型是通用套接字地址結構// 每當一個套接字函數需要一個指向某個套接字地址結構的指針時,這個指針必須強制類型轉換成一個指向通用套接字地址結構的指針// 這是因為套接字函數早于ANSI C標準,20世紀80年代開發這些函數時,ANSI C的void *指針類型還不可用// 但轉換時,struct sockaddr長達15個字符,往往造成源代碼超出屏幕右邊緣,因此我們可以使用#define將其簡化為SAif (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { // sockaddr結構位于頭文件sys/socket.hprintf("connect error\n");perror("connect"); // perror函數將其參數和errno所對應的錯誤一起輸出到標準錯誤exit(1);}// 此處需要while循環,因為服務器可能會將TCP分節,我們一次只能讀取單個分節,需要一直讀取,直到read函數返回0(對端關閉連接)或負數(發生錯誤),此程序中,服務器關閉連接表示記錄接收結束while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {recvline[n] = 0; // null terminateif (fputs(recvline, stdout) == EOF) {printf("fputs error\n");exit(1);}}if (n < 0) {printf("fputs error\n");}exit(0); // 結束程序,內核會關閉所有打開的文件描述符,套接字就此被關閉 }編譯以上程序以生成默認的a.out可執行文件,執行它:
以上程序中使用的網絡API稱為套接字API(socket API),如函數socket。TCP套接字是TCP端點的同義詞。
以上程序中的bzero函數不是一個ANSI C函數,它起源于早期的Berkeley網絡編程代碼,本書使用它而不使用ANSI C的memset函數,因為bzero函數帶兩個參數,比帶三個參數的memset函數更好記憶,且幾乎所有支持套接字API的廠商都提供bzero函數。
如果把以上程序中socket函數的第一個參數改為9999,運行它:
關于這個錯誤的詳細信息,我們可以先在sys/errno.h頭文件中查找錯誤字符串:
EPROTONOSUPPORT是由socket函數返回的errno值。我們可以通過man手冊man socket查找這個錯誤的額外信息。
事實上,在TCPv3一書首次印刷時,作者在10處出現memset函數的地方犯了錯,互換了第二個第三個參數,C編譯器發現不了這個錯誤,因為這兩個參數的類型一個是int,一個是size_t,后者通常定義為unsigned int類型,當分別指定這兩個參數的值為0和16時,這兩個參數類型都可以接收這兩個值。memset函數對這樣的調用仍然正常,不過沒做任何事,因為待初始化的字節數被指定為了0,程序仍可以正常工作是因為只有少數套接字函數要求網際套接字地址結構的最后8個字節置0。這個錯誤可通過bzero函數避免,因為C編譯器總能發現bzero函數的兩個參數被互換的情況。
TCP是一個沒有記錄邊界的字節流協議,而daytime服務器的應答通常是如下26字節字符串:
其中\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遠程過程調用和域名系統在每個要發送的記錄前放置一個二進制計數值,給出這個記錄的長度。這里的重要概念是TCP本身不提供記錄結束標志,應用需要自己實現記錄邊界的確定。
以上程序是與IPv4協議相關的:我們分配并初始化了一個sockaddr_in類型的結構,把該結構的協議族成員設為AF_INET,并指定socket函數的第一個參數為AF_INET。為了讓以上程序在IPv6上運行,修改以上程序,把被替換的源代碼注釋掉:
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h>#define MAXLINE 512int main(int argc, char **argv) {int sockfd, n;char recvline[MAXLINE + 1]; // struct sockaddr_in servaddr; struct sockaddr_in6 servaddr;if (argc != 2) {printf("usage: a.out <IPaddress>\n");exit(1);}// if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {if ( (sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) { printf("socket error\n");exit(1);}bzero(&servaddr, sizeof(servaddr)); // servaddr.sin_family = AF_INET;servaddr.sin6_family = AF_INET6; // servaddr.sin_port = htons(13);servaddr.sin6_port = htons(13); // if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { if (inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0) { printf("inet_pton error for %s\n", argv[1]);exit(1);}if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { printf("connect error\n");perror("connect"); exit(1);}while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {recvline[n] = 0; if (fputs(recvline, stdout) == EOF) {printf("fputs error\n");exit(1);}}if (n < 0) {printf("fputs error\n");}exit(0);我們修改了程序的5行代碼,得到了另一個與協議相關的程序,這次是與IPv6協議相關的。更好的做法是編寫協議無關的程序。
以上程序的另一個不足之處是,用戶必須以點分十進制數格式給出服務器的IP地址,人們更習慣于用域名來代替數字。
計算機網絡各層對等實體間交換的單位信息稱為協議數據單元(Protocol Data Unit,PDU),以上說的分節(segment)就是對應于TCP傳輸層的PDU。除了最底層(物理層)外,每層的PDU通過由緊鄰下層提供給本層的服務接口,作為下層的服務數據單元(Service Data Unit,SDU)傳遞給下層,并由下層間接完成本層的PDU交換。如果本層的PDU大小超過緊鄰下層的最大SDU限制,那么本層還要事先把PDU劃分成若干個合適的片段讓下層分開載送,再在相反方向把這些片段重組成PDU。同一層內SDU作為PDU的凈荷(payload)字段出現,因此可以說上層PDU由本層PDU(通過其SDU字段)承載。每層的PDU除用于承載緊鄰上層的PDU外,也用于承載本層協議內部通信所需的控制信息。
應用層實體(如客戶和服務器進程)間交換的PDU稱為應用數據,其中在TCP應用進程之間交換的是沒有長度限制的單個雙向字節流;在UDP應用進程間交換的是其長度不超過UDP發送緩沖區大小的單個記錄;在SCTP應用進程之間交換的是沒有總長度限制的單個或多個雙向記錄流。
傳輸層實體間(如對應某個端口的傳輸層協議代碼的一次運行)交換的PDU稱為消息(message),其中TCP的PDU特稱為分節(segment)。消息或分節的長度是有限的,在TCP傳輸層中,發送端TCP把來自應用進程的字節流數據(即由應用進程通過一次次輸出操作寫出到發送端TCP套接字中的數據)按順序經分割后封裝在各個分節中傳送給接收端TCP,其中每個分節鎖封裝的數據可能是既可能是發送端應用進程單次輸出操作的結果,也可能是連續數次輸出操作的結果,而且每個分節所封裝的單次輸出操作的結果或多個輸出操作的首尾兩次輸出操作的結果既可能是完整的,也可能是不完整的,具體取決于可在連接建立階段由對端通告的最大分節大小(Maximum Segment Size,MSS)以及外出接口的最大傳輸單元(Maximum Transmission Unit,MTU)或外出路徑的路徑MTU(如果網絡層有路徑MTU發現功能,如IPv6)。分節除了用于承載應用數據外,也用于建立連接(SYN分節)、終止連接(FIN分節)、中止連接(RST分節)、確認數據接收(ACK分節)、刷送待發數據(PSH分節)和攜帶緊急數據指針(URG分節),這些功能(包括承載數據)可靈活組合。
UDP傳輸層很簡單,發送端UDP把來自應用進程的單個記錄整個封裝在UDP消息中傳送給接收端UDP。
SCTP引入了稱為塊(chunk)的數據單元,SCTP消息由一個公共首部加上一個或多個塊構成。公共首部類似UDP消息的首部,僅給出源目的端口號和整個SCTP消息的校驗和。塊既可以承載數據(稱為DATA塊),也可以承載控制信息(如SACK塊、INIT塊、INIT ACK塊、COOKIE ECHO塊、COOKIE ACK塊、SHUTDOWN塊、SHUTDOWN ACK塊、SHUTDOWN COMPLETE塊、ABORT塊、ERROR塊、HEARTBEAT塊、HEARTBEAT ACK塊,總稱為控制塊)。發送端SCTP把來自應用進程的一個或多個記錄流數據按照流內順序和記錄邊界封裝在各個DATA塊中,并在DATA塊首部記上各自的流ID。一個記錄通常對應一個DATA塊,對于過長的記錄,發送端SCTP既可以像UDP那樣拒絕發送,也可以把它們拆分到多個DATA塊中以便發送,接收端SCTP收取后把它們組合成單個記錄上傳。作為傳輸層PDU的SCTP消息既可以只包含單個控制塊或DATA塊,也可以在接口MTU或路徑MTU的限制下包含多個塊(稱為塊的捆綁,控制塊在前,DATA塊在后),但INIT塊、INIT ACK塊、SHUTDOWN COMPLETE塊不能跟其他塊捆綁。SCTP收發兩端均獨立處理捆綁在同一個消息中的各個塊,因此我們可以直接把塊作為傳輸層PDU看待。
網絡層實體間交換的PDU稱為IP數據報,其長度有限,IPv4數據報最大65535字節,IPv6數據報最大65575字節。發送端IP把來自傳輸層的消息(或TCP分節)整個封裝在IP數據報中傳送。鏈路層實體間交換的PDU稱為幀,其長度取決于具體的接口。IP數據報由IP首部和所承載的傳輸層數據(即網絡層的SDU)構成。過長的IP數據報無法封裝在單個幀中,需要先對其SDU進行分片,再把分成的各個片段冠以新的IP首部封裝到多個幀中。在一個IP數據報從源端到目的端的傳送過程中,分片操作既可能發生在源端,也可能發生在途中,而其逆操作重組一般只發生在目的端。SCTP為了傳送過長的記錄采取了類似的分片和重組措施。TCP/IP協議族為提高效率會盡可能避免IP的分片/重組操作:TCP根據MSS和MTU限定每個分節的大小;SCTP根據MTU分片/重組過長記錄(SCTP的塊捆綁則是為了在避免IP分片/重組操作的前提下提高塊傳輸效率)。IPv6禁止在途中的分片操作(基于其路徑MTU發現功能),IPv4也盡量避免這種操作。不論是否分片,都由IP作為鏈路層SDU傳入鏈路層,由鏈路層封裝在幀中的數據稱為分組(packet,俗稱包),可見分組既可能是一個完整的IP數據報,也可能是某個IP數據報的SDU的一個片段被冠以新的IP首部的結果。
MSS是應用層(TCP)與傳輸層之間的接口屬性,MTU是網絡層和鏈路層之間的接口屬性。
以上程序中,當函數調用發生錯誤時,我們輸出一個出錯消息并終止程序運行,這是大多情況下的做法,個別情況下,我們要做的事并非簡單地終止程序運行,如需要檢查系統調用是否被中斷了。既然大多情況下發生錯誤時需要終止程序,我們可以定義包裹函數來縮短程序,每個包裹函數完成實際的函數調用,檢查返回值,并在發生錯誤時終止進程。包裹函數名一般是實際函數名的首字母大寫形式,這是約定,如:
sockfd = Socket(AF_INET, SOCK_STREAM, 0);其中Socket函數是socket函數的包裹函數:
int Socket(int family, int type, int protocol) {int n;if ( (n = socket(family, type, protocol)) < 0) {err_sys("socket error\n");exit(1);}return n; }線程函數在遇到錯誤是并不設置標準Unix errno變量,而是把errno的值作為函數返回值返回調用者,我們每次調用以pthread_開頭的函數時,必須分配一個變量來存放函數返回值,以便在輸出錯誤消息前把errno變量設置為該值,如:
int n;if ( (n = pthread_mutex_lock(&ndone_mutex)) != 0) {errno = n, err_sys("pthread_mutex_lock error\n"); }我們也可以定義一個新的錯誤處理函數,它接收一個errno參數,但通過將以上代碼設為包裹函數Pthread_mutex_lock可以讓其更易讀:
void Pthread_mutex_lock(pthread_mutex_t *mptr) {int n;if ( (n = pthread_mutex_lock(mptr)) == 0) {return;}errno = n;err_sys("pthread_mutex_lock error"); }如果仔細推敲C代碼編寫,我們可以用宏來代替包裹函數,從而稍微提高運行時效率,但包裹函數很少是性能的瓶頸。
還有一些其他的包裹函數命名規則,如給函數名前加一個e,或加一個_e后綴,但大寫首字母看來是最少分散注意力的。
包裹函數還有助于檢查那些錯誤返回值通常被忽略的函數是否出錯,如close和listen函數。
只要一個Unix函數中有錯誤發生,全局變量errno就被置為一個指明該錯誤類型的正值,函數本身則通常返回-1。自定義函數err_sys查看errno變量的值并輸出相應的出錯消息,如errno值等于ETIMEDOUT時,輸出"Connection timed out"。
errno的值只在函數發生錯誤時設置,如果函數沒有出錯,errno的值就沒有定義。errno的所有正數錯誤值都是常值,并有以E開頭的全大寫字母名,并通常在<sys/errno.h>頭文件中定義。errno值0不表示任何錯誤。
以下是一個簡單的TCP時間獲取服務器程序,與上面的時間獲取客戶程序一道工作:
#include "unp.h" #include <time.h>int main(int argc, char **argv) {int listenfd, connfd;struct sockaddr_in servaddr;char buff[MAXLINE];time_t ticks;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(13); /* daytime server */Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));// 調用listen函數將該套接字轉換成一個監聽套接字,這樣來自客戶的外來連接就可以在該套接字上由內核接收// LISTENQ在頭文件unp.h中定義,它指定系統內核允許在這個監聽描述符上排隊的最大客戶連接數Listen(listenfd, LISTENQ);// socket、bind、listen這3個調用步驟是任何TCP服務器準備監聽描述符的正常步驟for (; ; ) {// 通常,服務器進程在accept調用中被投入睡眠,等待某個客戶連接的到達并被內核接受// TCP使用三路握手來建立連接,握手完畢時accept函數返回,其返回值是一個稱為已連接描述符的新描述符,該描述符用于同新近連接的那個客戶通信// accept函數為每個連接到本服務器的客戶返回一個新描述符connfd = Accept(listenfd, (SA *)NULL, NULL);// time函數返回自Unix紀元(即19700101000000)以來的秒數ticks = time(NULL);// 相比于sprintf函數,snprintf函數要求其第二個參數指定目的緩沖區大小,因此可確保該緩沖區不溢出// snprintf函數在ISO C99版本中才加入到ANSI C標準中,但幾乎所有廠商都把它作為標準C函數庫的一部分提供,出于可靠性考慮,可將其改為sprintf函數// 但許多網絡入侵是由黑客通過發送數據,導致服務器對sprintf調用使其緩沖區溢出而發生的// 需要小心的函數還有gets、strcat、strcpy,通常應分別改為fgets、strncat、strncpy函數,更好的替代函數是strlcat和strlcpy,它們確保結果是正確終止的字符串snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); // %.24s表示最多打印24個字符,ctime函數返回一個25個字節的串,如"Wed Jun 30 21:49:08 1993\n"// %24s表示最少打印24個字符Write(connfd, buff, strlen(buff));// 通過close調用關閉與客戶的連接,該調用引發正常的TCP連接終止序列:每個方向上發送一個FIN,每個FIN又由各自的對端確認Close(connfd);} }以上服務器程序也是IPv4協議相關的。
以上服務器一次只能處理一個客戶,如果多個客戶連接差不多同時到達,系統內核會在某個最大數目限制下把它們排入隊列,然后每次返回一個給accept函數。本服務器很快,如果服務器需用較多時間(如幾秒或一分鐘)服務每個客戶,那么我們需要以某種方式重疊對客戶的服務。以上服務器稱為迭代服務器,因為它對每個客戶迭代執行一次;同時能處理多個客戶服務器稱為并發服務器,它有多種編寫技術,最簡單的是調用fork,為每個客戶創建一個子進程,其他技術包括使用線程代替fork函數,或在服務器啟動時預先fork一定數量子進程。
因為服務器往往在系統工作期間一直運行,這要求我們添加一些代碼,以便它能夠作為Unix守護進程運行,即在后臺運行且不與任何終端關聯。
國際標準化組織(International Organization for Standardization,ISO)的開放系統互連(Open Systems Interconnection,OSI)模型,這是一個七層模型(圖中還給出了它與網際協議族的近似映射):
我們認為OSI模型的底下兩層是隨系統提供的設備驅動程序和網絡硬件,通常,除需知道數據鏈路層的某些特性外(如1500字節的以太網MTU大小),我們不關心這兩層情況。
上圖網際協議族中,傳輸層的TCP和UDP中間留有空隙,表明網絡應用可能繞過傳輸層直接使用IPv4或IPv6,這是所謂的原始套接字。我們甚至可以繞過IP層直接讀寫數據鏈路層的幀。
OSI模型的頂上三層在網際協議中被合并為一層,稱為應用層,這是Web客戶(瀏覽器)、Telnet客戶、Web服務器、FTP服務器和其他我們使用的網絡應用所在的層。對于網際協議,OSI模型的頂上三層協議幾乎沒有區別。
本書講述的套接字編程接口是從OSI的頂上三層(即網際協議的應用層)進入傳輸層的接口。為什么套接字提供的是從OSI模型的頂上三層進入傳輸層的接口?這樣設計有兩個理由:一是頂上三層處理具體網絡應用(如FTP、Telnet、HTTP)的所有細節,卻對通信細節了解很少;底下四層對具體網絡應用了解不多,卻處理所有通信細節(發送數據、等待確認、給無序到達的數據排序、計算并驗證校驗和等)。二是頂上三層通常構成所謂用戶進程,底下四層通常作為操作系統內核的一部分提供,Unix與其他現代操作系統都提供分隔用戶進程與內核的機制。由以上可見,OSI模型的第4層和第5層之間的接口是構建API的自然位置。
套接字API起源于1983年發行的4.2 BSD操作系統,以下是各種BSD發行版本的發展史,1990年面世的4.3 BSD Reno發行版本中socket API有一些改動,此時OSI模型協議加入到了BSD內核:
上圖中從4.2 BSD往下到4.4 BSD的通路展示了源自Berkeley計算機系統研究組(Computer Systems Reserch Group, CSRG)的各個版本,這些版本要求獲取者擁有Unix的源代碼許可權。然而這些系統中的所有網絡支持代碼,無論是內核支持(如TCP/IP協議棧、Unix域協議棧、套接字API),還是應用程序(如Telnet和FTP客戶和服務器程序),都是從起源于AT&T的代碼單獨地開發的,因此從1989年起,Berkeley開始提供第一個BSD網絡支持release,它包含所有的網絡支持代碼以及不受Unix源代碼許可權約束的其他BSD系統軟件,這些包含網絡支持代碼的release任何人都可通過匿名FTP獲取。
Berkeley的最終版本是1994年的4.4 BSD-Lite和1995年的4.4 BSD-Lite2,這兩個版本是其他多個系統(包括BSD/OS、FreeBSD、NetBSD、OpenBSD)的基礎。
許多Unix系統一開始就包含某個版本的BSD網絡代碼(包括套接字API),我們稱這些實現為源自Berkeley的實現。許多商業版本的Unix是基于System V版本4(System V Release 4,SVR4)的,其中有一些基于SVR 4的系統使用源自Berkeley的網絡支持代碼(如Unix Ware 2.x),其他基于SVR 4的系統的網絡支持代碼卻是獨立起源的(如Solaris 2.x)。Linux這種免費可獲得的Unix實現的網絡支持代碼和套接字API都是從頭開始開發的。
以下是本書所用的各個網絡和主機,其中標明了主機的操作系統和硬件類型(因為有些操作系統可運行在不止一種硬件上),方框中是主機名:
上圖中的機器大范圍地散步在因特網上,物理拓撲實際不太重要。事實上虛擬專用網絡(Virtual Private Network,VPN)或安全shell(secure shell,SSH)連接提供這些機之間的連通性,無須顧及這些主機的物理位置。
Sun操作系統的真實名字是SunOS 5.x,而不是Solaris 2.x,大家習慣稱它為Solaris,Solaris是操作系統和與之捆綁的其他軟件的合稱。
大多數UNIX都提供了用于發現某些網絡細節的兩個命令:netstat和ifconfig。有些廠商把這兩個命令放在/sbin或/usr/sbin目錄中(這些目錄通常不在shell的搜索路徑中,而shell的搜索路徑由PATH環境變量指定),而非通常的/usr/bin目錄。
netstat的-i選項可提供網絡接口信息,下例給出了接口名和統計信息:
上圖中的lo接口為環回接口(loopback),eth0是以太網接口。下圖是在一個支持IPv6的主機上運行相同命令:
netstat的-r選項可展示路由表:
ifconfig命令可獲取每個接口的詳細信息:
ifconfig給出了接口的IP地址、子網掩碼、廣播地址。其中的MULTICAST標志通常指明改接口所在主機支持多播。有些ifconfig的實現提供-a標志,用于輸出所有已配置接口的信息。
針對上圖中找到的本地接口的廣播地址執行ping命令(-b選項可廣播ping),可找出本地網絡中其他主機的IP地址:
本書編寫時最引人注目的Unix標準化活動是由Austin公共標準修訂組(CSRG,The Austin Common Standards Revision Group)主持的,他們努力的結果是涵蓋1700多個編程接口的約4000頁內容的規范,這些規范同時帶有IEEE POSIX的設計和開放團體技術標準(The Open Group’s Technical Standard)的設計。最終結果就是一個Unix標準有多個名字:ISO/IEC 9945:2002、IEEE Std 1003.1-2001、SUSv3(Single Unix Specification Version 3)都指同一個標準(我們簡單地稱該標準為POSIX規范)。
POSIX(Portable Operating System Interface,可移植操作系統接口)不是單個標準,而是由電氣與電子工程師學會(IEEE,the Institute for Electrical and Electronics Engineers)開發的一系列標準。POSIX標準已被國際標準化組織ISO和國際電工委員會(IEC,the International Electrotechnical Commission)采納為國際標準,這兩個組織合稱為ISO/IEC。
POSIX標準發展簡史:
1.第一個POSIX標準是IEEE Std 1003.1-1988,它詳述了進入類UNIX內核的C語言接口,包含以下領域:進程原語(fork、exec、信號和定時器)、進程環境(用戶ID、進程組)、文件與目錄(所有I/O函數)、終端I/O、系統數據庫(口令文件和用戶組文件)、tar和cpio歸檔格式。第一個POSIX標準在1986年稱為IEEE-IX的試用版。
2.第二個POSIX標準是IEEE Std 1003.1-1990,也稱為ISO/IEC 9945-1:1990,與第一個標準相比只做了少量修改。
3.下一個標準是IEEE Std 1003.2-1992,它的一部分定義了shell(基于System V的Bourne Shell)和大約100個實用程序(從shell啟動執行的程序,如awk、basename、vi、yacc等),本書將該部分稱為POSIX.2。
4.再下一個標準是IEEE Std 1003.1b-1993,以前稱其為IEEE P1003.4,它是對1003.1-1990的更新,添加了由P1003.4工作組開發的實時擴展,它相比1990年版標準新增了文件同步、異步I/O、信號量、存儲管理(mmap、共享內存)、執行調度、時鐘與定時器、消息隊列。
5.更下一個標準為IEEE Std 1003.1 1996年版,也稱為ISO/IEC 9945-1:1996,它包括1003.1-1990(基本API)、1003.1b-1993(實時擴展)、1003.1c-1995(pthreads)、1003.li-1995(對1003.1b的技術性修訂)。該標準新增了3章關于線程的內容和有關線程同步(互斥鎖、條件變量)、線程調度、同步調度的各節。本書稱該標準為POSIX.1。該標準有一個前言,其中聲明ISO/IEC 9945由以下3部分構成:
(1)Part 1: System API (C language)
(2)Part 2: Shell and utilities
(3)Part 3: System administration(正在開發中)
其中第一部分和第二部分就是我們所說的POSIX.1和POSIX.2。
6.最后一個標準是2000年被認可(被認可的標準,approved standard,是成為正式標準前的一個階段)的IEEE Std 1003.1g: Protocol-independent interfaces(PII)。它是聯網API標準,它定義了兩個API集,并稱它們為詳盡網絡接口(Detailed Network Interface,DNI),分別為:
(1)DNI/Socket,基于4.4 BSD的套接字API。
(2)DNI/XTI,基于X/Open的XPG4規范。
該標準的工作由P1003.12工作組(后改名為P1003.1g)起始于20世紀80年代后期。
開放團體(The Open Group)是由1984年成立的X/Open公司和1988年成立的開放軟件基金會(Open Software Foundation,OSF)于1996年合并成的阻止,它是廠商、工業界最終用戶、政府、學術機構共同參加的國際組織。
開放團體指定的標準的簡要背景:
1.X/Open公司于1989年出版了X/Open Portability Guide(X/Open移植性指南,XPG)第三期,即XPG3。
2.XPG第4期(XPG4)出版于1992年,其第二版出版于1994年,第二版也稱為Spec 1170,1170指系統接口數(926個)、頭文件數(70個)、命令數(174個)的總和。這組規范最終名字是X/Open Single Unix Specification(X/Open單一Unix規范),也稱為Unix95。
3.單一Unix規范第2版于1997年3月發行,符合這個規范的產品稱為Unix 98,Unix 98的接口數從1170個增長到1434個,而用于工作站的接口數達到3030個,因為它包含公共桌面環境(CDE,Common Desktop Environment),而公共桌面環境又需要X Windows系統和Motif用戶接口。Unix 98為套接字API和XTI API定義了網絡支持服務。這個規范與POSIX.1g幾乎相同。
X/Open稱它們的網絡標準為XNS(X/Open Networking Services),定義Unix98和XTI的文檔版本稱為XNS Issue 5(XNS第5期)。在網絡界,XNS已是Xerox Network System體系結構的簡稱,所以我們避免使用XNS,而稱這個X/Open文檔為Unix 98網絡API標準。
伴隨Austin CSRG發布單一Unix規范第3版,POSIX和開放團體達成了統一的標準。CSRG促成50多家公司就單一標準達成一致意見。如今大多Unix系統都符合POSIX.1和POSIX.2的某個版本,不少系統符合單一Unix規范第3版。
歷史上多數Unix系統或者源自Berkeley,或者源自System V,但這些差別在慢慢消失,因為大多廠商都開始采納這些標準。但在系統管理上兩者仍存在較大差別,這個領域目前還沒有標準。
本書焦點是單一Unix規范第3版,以其中套接字API為主。
因特網工程任務攻堅組(IETF,Internet Engineering Task Force)是一個關心因特網體系結構的發展及其順利運作的網絡設計者、操作員、廠商、研究人員聯合組成的開放的國際團體,它向任何感興趣的個人開放。
因特網標準處理過程在RFC 2026中說明。因特網標準一般處理協議問題而非編程API,但RFC 3493和RFC 3542說明了IPv6的套接字API,它們是信息性的RFC,不是標準,制定它們的目的是加速部署由多家從事IPv6工作較早的廠商所開發的可移植網絡應用程序。
20世紀90年代中期到末期開始出現向64位體系結構和64位軟件發展的趨勢,原因之一是每個進程內部可以使用更長的編址長度(即64位指針),從而可以尋址很大的內存空間。現有32位Unix系統上的編程模型為ILP32模型,表示整數、長整數、指針都用32位。64位Unix系統上最流行的模型為LP64模型,其長整數和指針占用64位。
LP64模型意味著我們不能假設一個指針能存放在一個整數中。我們還必須考慮LP64模型對現有API的影響。
ANSI C創造了size_t類型,它作為malloc函數的唯一參數(待分配字節數),或read和write函數的第3個參數(待讀或寫的字節數)。在32位系統中size_t是一個32位值,但在64位系統中它必須是一個64位值,以便發揮更大尋址模型的優勢。在64位系統中也許含有一個把size_t定義為unsigned long的typedef指令。聯網API有以下問題:POSIX.1g的某些草案規定,存放套接字地址結構大小的函數參數類型為size_t(如bind、connect函數的第3個參數),而某些XTI結構也含有類型為long的成員(如t_info、t_opthdr結構),如果不加以修改,當Unix系統從ILP32模型轉變為LP64模型時,size_t和long都將從32位值變為64位值,而套接字地址結構長度不需要使用64位值,它的長度最多也就幾百字節,而且給XTI的結構成員使用long類型則是個錯誤。
處理以上問題的方法是使用專門設計的數據類型,套接字API對套接字地址結構長度使用socklen_t類型,XTI則使用t_scalar_t和t_uscalar_t類型。不將這些值由32位改為64位的原因是為那些已在32位系統中編譯的應用提供在64位系統中的二進制代碼兼容性。
我們提供時間的服務器程序如果每次只發送一個字節,如果客戶和服務器運行在同一主機上,通常客戶只需要調用一次read就可以全部返回。然而如果客戶運行在Solaris 2.5上而服務器運行在BSD/OS 3.0上,那么客戶通常需要調用2次read,如果監視以太網上的分組,我們發現第一個字符自成一個分組發送,剩余25字節包含在下一個分組內發送(由于Nagle算法)。相反,如果客戶運行在BSD/OS 3.0上而服務器運行在Solaris 2.5上,那么客戶需要調用26次read,此時監視以太網上的分組,會發現每個字符自成一個分組發送。
總結
以上是生活随笔為你收集整理的UNIX网络编程卷一 学习笔记 第一章 简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: xss php漏洞扫描工具,XSpear
- 下一篇: 人工智能 一种现代方法 第9章 一阶逻辑