UNP_Chapter01_Introduction_筆記總結

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter01: UNP入門介紹

1.1 簡介

當我們寫一個程序要想在網絡中進行傳輸的時候,就必須考慮到需要一個協議能夠對網絡中傳輸的細節問題做統一的約束.
比如說,Web服務器一般都是一個永久運行,不down機或者說是一個守護進程在服務器端. 再比如我們所熟知的CS模型,
即client/server模型,誰負責起初始化,誰負責請求,誰負責迴應等等. 當然在絕大多數的CS模型中,都是客戶端向服務器
端發送初始化請求,服務器端進行迴應的,這也簡化了好多協議和程序本身.但是也有一些模型是需要相互的初始化,相互的
請求與迴應.
一般情況下我們客戶端在同一時間只能和一個服務器端進行交互,但是服務器端,同一時間處理多個客戶端是司空見慣的.
這裏寫圖片描述
從淺層來看,客戶端程序和服務器段程序是通過網絡中的協議進行交流的,但是其中涉及到了很多網絡協議. 但是我們這裏只
主要討論TCP/IP套裝,比如說我們Web客戶端訪問服務器端的時候,傳輸層經TCP(Transmission control protocol)然後轉給
網絡層也就是IP層(Intenet protocol),之後再轉給數據鏈路層.如果客戶端和服務器端在一個以太網內,那麼就是如下這種模式:
這裏寫圖片描述
在這裏,應用層的客戶端和服務器端就是典型的用戶空間的進程,傳輸層入TCP和網絡層如IP協議作爲協議棧的一部分就存在於
內核中了.
傳輸層中我們不僅討論TCP,我們也會討論UDP(User Datagram Protocol).網絡層中我們通常所說的IP,其實是1980s的IP version
4(IPv4), 我們也會討論IPv6的. 當然交互的兩端不僅僅能在局域網(LAN)中,也是可以存在在WAN中的,當今最大的WAN就是Internet:
這裏寫圖片描述
許多公司都自己搭建字節的WAN, 並且這些私有WAN可能並沒有接入到Internet中.
本章主要就是概述以後會詳解的知識點,我們全程都會以服務器客戶端模型引入,雖然簡單,但是其中涉及到了很多很多的知識點.

1.2 Daytime客戶端

接下來我們來簡單寫個讓服務器端返回日期的CS模型熱熱手,在這裏我用的是Richard的代碼,但是他自己還寫了個unp.h,我會將每個代碼
中用到的被unp.h封起的宏或函數都摔出來,也就是我不用unp.h,這樣更會有助於理解知識點:
參考: socket(2), bzero(3), ip(7), htons(3), inet_pton(3), connect(2))

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#define SA      struct sockaddr
#define MAXLINE 4096
int main(int argc, char *argv[])
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr;

    if(argc != 2)
    {
        perror("usage: dateCS <IP Address>");
    }
    /*int socket(int domain, int type, int protocol);*/
    /*tcp_socket = socket(AF_INET, SOCK_STREAM, 0);*/
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0) < 0))
    {
        perror("socket err");
    }
/*void bzero(void *s, size_t n);*/
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(13);

    /*int inet_pton(int af, const char *src, void *dst);*/
    if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        perror("inet_pton error");
    }
/*int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);*/
    if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect err");
    }
    while((n = read(sockfd, recvline, MAXLINE)) > 0)
    {
        recvline[n] = 0;//null terminate
        if(fputs(recvline, stdout) == EOF)
        {
            perror("fputs err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    exit(0);
}

這裏涉及到很多的知識點,看不大懂也沒關係,這些函數都很重要,並且我們在之後都會一一介紹到. 在這裏socket函數創造了
一個Internet流socket,通過參數AF_INET表示Internet(這裏說到Internet就默認是IPV4), SOCKET_STREAM表示是一種流協議,
其實就是TCP. 這個函數返回的 一個整數描述符就是我們之後要用到識別socket的.在這裏我們還用到一個Internet的socket地址
結構體: sockaddr_in. 我們先 通過bzero用0將這個結構體清空, 然後塞入AF_INET,port和IP. 並且在這個結構體中IP和port都得
用指定的格式進行傳輸, 所以我們用htons(主機字節序轉換成網絡字節序)轉換二進制端口號,inet_pton轉換ASCII的命令行參數.
讀者可能會有疑問,爲什麼要用bzero而不用memset?是因爲bzero要比memset方便記憶,基本上每個Unix廠商都會有bzero方法
的. 他們的效果是一樣一樣的.
我們定義了一個宏#define SA struct sockaddr, 是一個通用socket地址結構體.就是任何的socket函數要求指向socket地址結構
體的時候都要強制轉換成通用結構體.這是因爲socket函數要早於ANSI C標準, 所以void*是不允許的.
在這裏需要注意的一點是TCP是字節流協議,是沒有記錄邊界的,也就是並不會知道你想要在哪裏截斷這波傳輸.
在這個實例中, 整個傳輸的結束是由服務器端在傳輸完成後直接斷開的.這很類似於HTTP/1.0. 但比如SMTP中,這波傳輸的結束是
服務器端傳輸回兩個字節的序列號緊跟這換行(\n, linefeed). RPC和DNS會將返回文本的二進制長度添加到記錄前面用當用TCP
進行傳輸的時候.這裏要說明的就是TCP本身是沒有給你任何的結束或開始標誌的,什麼\r\n都是沒有的.如果應用程序想要達到
在指定位置結束這波傳輸,就必須在傳輸文本前後做些手腳.

1.3 協議的獨立性

這裏做一下對比,如果我們不用Ipv4而是用ipv6:

int main(int argc, char *argv[])
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in6 servaddr;

    if(argc != 2)
    {
        perror("usage: dateCS <IP Address>");
    }
    /*int socket(int domain, int type, int protocol);*/
    /*tcp_socket = socket(AF_INET, SOCK_STREAM, 0);*/
    if((sockfd = socket(AF_INET6, SOCK_STREAM, 0) < 0))
    {
        perror("socket err");
    }
/*void bzero(void *s, size_t n);*/
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin6_family = AF_INET6;
    servaddr.sin6_port = htons(13);

    /*int inet_pton(int af, const char *src, void *dst);*/
    if(inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0)
    {
        perror("inet_pton error");
    }
/*int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);*/
    if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect err");
    }
    while((n = read(sockfd, recvline, MAXLINE)) > 0)
    {
        recvline[n] = 0;//null terminate
        if(fputs(recvline, stdout) == EOF)
        {
            perror("fputs err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    exit(0);
}

這裏我們發現其實僅僅修改了幾個宏而已,因爲不同的協議會產生不同的方式去調用,所有在我們以上的兩端代碼中就顯示出了弊端,僅僅因爲
修改幾行代碼就重新打個c文件.在之後我們會引入一個函數,getaddrinfo(3),這個函數的解釋是這樣的:
這裏寫圖片描述
返回一個或多個addrinfo結構體, 包括網絡地址,不管是v4還是v6,返回的結果都能直接用bind(), connect()直接使用.
我們的代碼中的另外一個弊端就是用戶通常是不願意輸入IP地址的,而更喜歡輸入域名.在之後的章節中,我們會講解如何進行地址的轉換的.e

1.4 錯誤處理:封裝方法

在我們寫代碼的時候,檢查每個返回值是一定要做的.所以在UNP這本書中作者就封裝了一些錯誤處理的方法,絕大多數對程序的執行效率並
不會有實質性的改變,但是對操作很方便.比如socket(2)函數,作者就定義了一個Socket函數,其中這個函數還直接進行了返回值的判斷.這
樣做的好處再比如,線程函數中pthread_錯誤返回時,會返回錯誤碼,但是並不會自動的給errno賦值,所以我們要想打印出準確的錯誤信息,
我們必須每次調用這個函數的時候定義一個變量接到這個返回的錯誤碼然後手動賦值給errno,然後再調用perror等函數.但是我會盡量給大家
還原最原始的代碼.

1.5 Daytime 服務器

  1. 創建TCP socket,這個是和client一樣的
  2. 綁定服務器的端口給創建的socket.這裏調用的函數是bind(2). 並且我們制定IP地址是INADDR_ANY,表示我們向全世界開放.
  3. 將創建的普通socket轉換成監聽socket.這裏調用函數listen(2),這樣從客戶端進來的連接都會交給kernel處理了.LISTENQ是說kernel接收
    客戶端監聽隊列的最大數量.我們在之後會詳解介紹.
#include <stdio.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#define MAXLINE         4096    /* max text line length */
#define LISTENQ         1024    /* 2nd argument to listen() */
#define SA      struct sockaddr
/*Richard老爺爺定義的這些函數我就都寫出來了*/
/* include Socket */
int
Socket(int family, int type, int protocol)
{
        int             n;

        if ( (n = socket(family, type, protocol)) < 0)
/*                err_sys("socket error");*/
//我們爲了方便,就不寫這樣的錯誤方法了
        perror("socket error");
        return(n);
}
/* end Socket */
void
Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
        if (bind(fd, sa, salen) < 0)
                perror("bind error");
}
/* include Listen */
void
Listen(int fd, int backlog)
{
        char    *ptr;

                /*4can override 2nd argument with environment variable */
        if ( (ptr = getenv("LISTENQ")) != NULL)
                backlog = atoi(ptr);

        if (listen(fd, backlog) < 0)
                perror("listen error");
}
/* end Listen */
int
Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
        int             n;
again:
        if ( (n = accept(fd, sa, salenptr)) < 0) {
#ifdef  EPROTO
                if (errno == EPROTO || errno == ECONNABORTED)
#else
                if (errno == ECONNABORTED)
#endif
                        goto again;
                else
                        perror("accept error");
        }
        return(n);
}
void
Write(int fd, void *ptr, size_t nbytes)
{
        if (write(fd, ptr, nbytes) != nbytes)
                perror("write error");
}
void
Close(int fd)
{
        if (close(fd) == -1)
                perror("close error");
}
int main(int argc, char **argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buf[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(8888);
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);
    for(;;)
    {
        connfd = Accept(listenfd, (SA *)NULL, NULL);
        ticks = time(NULL);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buf, strlen(buf));
        Close(connfd);
    }
}

這樣的話我們就能夠與客戶端執行了:
這裏寫圖片描述
注意我這裏將端口號改成了8888,是因爲13是被系統服務佔用這的.
在這波代碼中我將作者的封裝代碼扣了出來,然後我錯誤處理函數使用的是perror,當然大家在做實驗的時候可以模仿作者的錯誤處理函數自己
封裝一下,作者錯誤源碼lib/error.c:
這裏寫圖片描述
這裏寫圖片描述
在之後的代碼中我將封裝代碼的位置會告知大家,自行可以去查看實現,都是很簡單的,基本都是多打包了一層錯誤處理.不然的話,代碼太長了.
通常狀態下,服務器端執行到accept方法的時候會睡眠,等待客戶端的到達並且被接收.TCP用三次握手的方式建立連接.當握手結束的時候,
accept返回,並且返回一個新的fd是鏈接成功的fd,這個fd就是正兒八經和客戶端傳輸數據的fd. time這個函數返回的自1970年開始的UTC秒數,所以我們用ctime(3)轉換成人類可讀的字符串形式.
另外一點,在這裏我們用到的是snprintf函數而不是很多人喜歡用的sprintf函數.大家應該更加習慣於用snprintf,放棄sprintf,因爲用sprintf
你是檢測不出buf是否overflow了,但是用snprintf你必須在第二個參數傳入buf_size. 在顯示生活中,有很多的黑客就是通過發送數據致使服務器的sprintf函數overflow.同樣的類似於gets, strcat, strcpy 都要替換成fgets, strncat, strncpy甚至strlcat和strlcpy,確保程序的健壯.
服務器端最後通過調用close(2)函數觸發了終止序列:FIN我們會在下一章主要介紹.
在之後我們還會涉及到很多很多的知識點,比如多併發,守護進程, 監聽等待隊列等.

1.6 OSI模型

想必有人也會混淆OSI,ISO吧,我們網絡最標準的定義組織是ISO(International Organization for Standardization)其中的OSI(open systems interconnection)模型.
這裏寫圖片描述
最下面的兩層其實是硬件和驅動的領域,我們基本不會操作那塊兒,除了會MTU的修改.網絡層就是我們經常用到的V4和V6, 傳輸層我們通常用到的就是TCP和UDP,但是圖中有個空隙表明有個叫raw socket的能繞過使用TCP和UDP和網絡層直接讀取數據鏈路層的數據.我們會在之後討論.
那麼爲什要提供這個叫socket的東西呢?有兩點原因:
1. 上層處理着所有應用層的任務並且基本上是不考慮交互的細節的.而下四層基本不清楚上層的應用,但是處理着交流的所有細節,比如發送數據, 等待ACK, 數據序列以保證數據的順序性, J計算和驗證checksum等.
2. 應用層就是處在我們通常所說的用戶進程中,下四層處在內核進程中,所有提供這樣的socket就是爲了提供API.

1.7 BSD網絡的歷史

4.2BSD到4.4BSD都是由伯克利的CSRG(Computer Systems Research Group)研發而出的.但是由於ATT掌握着服務層的源碼,所以BSD在1989年的時候研發出了第一款BSD網絡操作系統. 並且開源.BSD的最後一個版本是1995年的4.4BSD-Lite2,它與4.4BSD-Lite就是構成了當今的BSD/OS, FreeBSD, NetBSD, OpenBSD. 這些延伸品不斷的在更新加強中.好多的Unix系統都是開始於BSD網絡操作系統的某個版本,包括socket API. 許多商業用途的Unix是基於System V Release 4(SVR4).
這裏有一篇其他大哥寫的文章相比較了BSD和System V:BSD VS SVR4,但是我們需要知道Linux這塊Unix衍生品並不屬於BSD領域的.它的網絡代碼和Socket API是白手起家的(from scratch).
這裏寫圖片描述

1.8 測試網絡和主機

這裏寫圖片描述
在這套的學習中,我們只要就是使用這套網絡拓撲圖, 雖然在不同額網絡下有不同的系統也有着不同的硬件,但是在顯示的虛擬網絡(VPN)和阿全shell(SSH)下,使得他們之間相連.
我們所要知道的就是兩個基本的命令:netstat(8)和ifconfig(8)來獲取我們的網絡拓撲. 有的Unix產商會將這兩個命令放置在管理員目錄下/sbin或者/usr/sbin下而不是/usr/bin/,所有有可能在全全局的環境變量中沒法直接執行這兩命令.
netstat -i 提供了網絡接口的信息 -n打印出地址.
NetBSD:
這裏寫圖片描述
Ubuntu:
這裏寫圖片描述
lo是表示迴環接口,一般真機上會有以太網接口就是eth0. 我的是虛擬機.netstat -r是列出路由表.
NetBSD:
這裏寫圖片描述
Ubuntu:
這裏寫圖片描述
接下來給定接口的名字,我們就能通過ifconfig來查看接口的具體信息ifconfig eth0

1.9 Unix標準

Unix標準大部分是由奧斯丁大學的CSRG(Common Standards Revision Group)整理編輯. 大概有4000頁覆蓋了1700個接口函數.這些大多都在
Posix上指定了. 所以我們主要都回去討論POSIX,除非有特殊的需求,纔會看一些POSIX上沒有,更古老的標準.
現在就解開POSIX的神祕面紗: POSIX(Protable Operating System Interface).它是IEEE的家族成員.POSIX標準也被ISO和IEC(ISO/IEC)採納.
IEEE的1003.1-1988(317頁)就是第一個POSIX標準.它指定了類Unix內核中的C語言接口, 並且覆蓋了進程原語(fork, exec, signal和timers),進程的環境,文件和目錄(所有的IO函數),終端IO, 系統的數據庫, tar和cpio解壓形式.
IEEE的1003.1-1990(356頁), 也是ISO/IEC9945-1:1990. 細微的改變從88年版本到90年版本.
IEEE的1003.2-1992出來兩卷,標題包含Shell和工具.這個部分定義了shell基於system V的sh和大約100個工具.我們稱爲POSIX.2
IEEE的1003.1b-1993就是IEEE P1003.4. 增加了文件的同步異步IO,信號量,內存管理, 調度操作, 時鐘和timers, 還有消息隊列.
IEEE的1003.1 1996版也叫做ISO/IEC 9945-1:1996. 增加了三章線程,線程的同步和鎖還有線程的調度.我們稱爲POSIX.1.
IEEE的1003.1g增加了協議獨立接口(PII)是2000年的標準.這個版本就是我們今後要主打的.
雖然現在的系統都是BSD和System V的衍生版本,但是隨着POSIX.1, POSIX.2, Single Unix Specification Version3的普遍使用與各個產商,這樣系統之間的差距也慢慢再縮小.主要的差異還存在於系統管理, 因爲這塊兒還沒有出現標準化的規定.
IETF(The Internet Engineering Task Force)是一個大型的,開源的國際性的網絡開發商,設計商,研究者,針對於網絡的架構協議架構,網絡流暢度等操作,他們處理的是協議問題而不是API.
在90年代開始就流行起了64位操作系統和64位軟件, 其中一個原因是有着更大的進程空間地址.在32位機上的通用程序模型叫做ILP32, 64位機上的叫ILP64.
這裏寫圖片描述
從我們編程者的角度看,在64位操作系統上,我們就不能再把指針當做整型存儲了.我們必須考慮LP64再API上的影響了.
ANSI C開發出了size_t數據類型, 比如malloc中就會用到這樣的參數: void *malloc(size_t size). 在32位系統上,size_t是個32位的值,但是在64位系統上,它必須是64位的值來處理更大的地址模型. 這就是意味着64位的操作系統必須要有一個typedef將size_t定義爲unsigned long.
其中有個問題就是網絡的API中的一些POSIX.1g指定一些函數參數有socket地址結構體大大小size_t(比如bind和connect的第三個參數). 一些XTI的結構中有一些成員有數據類型long.它們都需要從32位轉成64位. 在這兩個實例中,它們都不需要64位的數據類型,因爲socket的地址結也就幾百字節最多,所以XTI使用long類型就是個錯誤.解決這些問題的方式就是特殊指定對應特殊情形,socket API用socklen_t數據類型指定socket地址結構, XTI用t_scalar_t和t_uscalar_t數據類型.不將它們的值從32位轉換成64位的原因是爲了二進制兼容性在32和64位操作系統上.


聯繫方式: [email protected]

發佈了19 篇原創文章 · 獲贊 2 · 訪問量 4154
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章