1. 程式人生 > >UNIX網絡編程入門——TCP客戶/服務器程序詳解

UNIX網絡編程入門——TCP客戶/服務器程序詳解

lap 它的 有時 quit 開始 初學者 圖片 索引 計算

前言

最近剛開始看APUE和UNP來學習socket套接字編程,因為網絡這方面我還沒接觸過,要等到下學期才上計算機網絡這門課,所以我就找了本教材啃了一兩天,也算是入了個門。
至於APUE和UNP這兩本書,書是好書,網上也說這書是給進入unix網絡編程領域初學者的聖經,這個不可置否,但這個初學者,我認為指的是接受過完整計算機本科教育的研究生初學者,需要具有完整計算機系統,體系結構,網絡基礎知識。基礎沒打好就上來啃書反而會適得其反,不過對於我來說也沒什麽關系,因為基礎課也都上得差不多了,而且如果書讀得沒有什麽阻礙的話又能收獲到什麽呢?
回到這篇文章來,寫這篇的目的,一來是梳理一下最近這幾天學到的知識,學而不思則罔。二來是希望通過自己的一點小小工作,能夠幫助更多的人輕松入門網絡編程,建立起對網絡編程的基本知識框架。

主要內容

文章的行文方式主要是通過講解《UNIX網絡編程 卷一》(UNP v1)第五章的TCP客戶/服務器程序,並以此為出發點來系統的介紹UNIX環境下的網絡編程。該程序雖然簡單,但是卻包含了unix環境下TCP需要用到的大部分函數,通過學習它,我們可以較快的熟悉相關環境。
學習過這本書的朋友可能知道,作者將大部分網絡程序都需要的系統頭文件及各種常值都包含在unp.h文件,並且如果我們要運行書裏的程序,需要先去下載書的源碼,編譯成靜態鏈接庫後再鏈接使用。這給我們學習源碼及探究裏面的流程帶來了麻煩和不必要的幹擾,為此我將TCP客戶/服務器程序需要到的函數及常量都提取了出來,放在了myunp.hmyunp.c

裏面,這樣我們要編譯運行該程序的時候,就不再需要去進行所謂的“搭建環境”了,這將大大減少不必要的幹擾。

面向讀者

此文主要面向初學或未接觸網絡編程的讀者,我希望你有計算機網絡的基礎,最好像我一樣正在學unp,沒有也關系,我盡量講得深入淺出一點,這也是對我能力的提升考驗。如有錯誤紕露之處,還請諸位多加包涵,私信或評論區指出。

一、TCP客戶/服務器程序直觀展現

人類都是視覺動物,如果先講原理再看實現容易讓人無所適從,不知所以然。所以這裏我們先編譯運行程序,來看一下程序的運行效果。
程序運行環境:Ubuntu 16.04,使用windows的讀者建議使用虛擬機或者安裝雙系統

  1. 將所有代碼放到同一文件夾內並打開終端切換至該文件夾,代碼詳見文章後半部分,總共四個文件,分別為myunp.h
    myunp.ctcpserver.ctcpclient.c
  2. 執行gcc tcpserver.c myunp.c -o tcpserver生成服務器程序tcpserver
  3. 執行gcc tcpclient.c myunp.c -o tcpclient生成客戶端程序tcpclient
  4. 執行./tcpserver &後臺運行服務器程序(&意為後臺運行)
  5. 執行./tcpclient 127.0.0.1運行客戶端程序連接到服務器(127.0.0.1為本地ip地址)
  6. 此時輸入任意字符按回車後將會發送給服務器,服務器會將字符完整的回送給客戶端,並在下一行顯示出來,因此該服務器也叫echo服務器。
  7. ctrl+c退出客戶端,輸入kill 4556關閉服務器(4556為服務端程序的進程id,參見下圖)
    技術分享圖片
  • 我們首先編譯好了兩個程序,分別是服務端和客戶端程序,然後將這兩個程序都在本機上運行,客戶端通過tcp協議與服務端連接後發送消息給服務端,服務端簡單的將消息回送給客戶端。

    二、計算機網絡概述

    上面我們展示了兩個程序間的通信,雖然為了方便測試他們都是在同一臺機子上運行,但我們可以將他們看成是不同地區的兩臺電腦之間的通信。
    這裏限於篇幅我也不想長篇大論各種概念,我們只要知道,在大多數情況下(除了連wifi和數據),兩臺能夠通信的電腦都是物理相連的,他們之間通信的內容轉化為電信號等在他們之間物理相連的線路上傳輸。那他們怎麽把電信號,也就是01010這些二進制轉化為可以讀的內容呢,這就需要雙方約定好怎樣去轉化,也就是協議,目前主要使用的是tcp/ip協議族,這並不是單個協議,而是由許多協議組成的。
    我們需要知道,一個通信過程並不是僅僅使用一個協議,而是需要各種協議同時使用,但是這裏我們並不關心這些,計算機網絡中有一個很重要的概念就是抽象,底層的設施封裝成可以直接使用的器件交付上層,對上層屏蔽了內部細節。這裏我們同樣要使用抽象方法,將網絡連接看成客戶端和服務器之間的一條連線,兩個程序在這條連線上使用TCP協議進行通訊。

    三、TCP協議簡介

    既然我們將復雜的現實連接抽象成了一條簡單的連線,那麽接下來就是在這條線上進行客戶端與服務器的連接。
    技術分享圖片
    TCP建立連接如上圖所示,也就是俗稱的三次握手,首先 B(服務器) 處於 LISTEN(監聽)狀態,等待客戶的連接請求。

    • A 向 B 發送連接請求報文段。
    • B 收到連接請求報文段,如果同意建立連接,則向 A 發送連接確認報文段
    • A 收到 B 的連接確認報文段後,還要向 B 發出確認。
    • B 收到 A 的確認後,連接建立。

之所以要進行第三次握手而不是簡單的兩次握手,主要原因還是現實網絡的不可靠性,它不可能保證你發送的任何一條消息都能被對方接收到,當然這是題外話,並不在我們的討論範圍內。

技術分享圖片
有建立連接自然就有關閉連接,TCP關閉連接稱為四次揮手,如上圖所示

  • A 發送連接釋放報文段。
  • B 收到之後發出確認,此時 TCP 屬於半關閉狀態,B 能向 A 發送數據但是 A 不能向 B 發送數據。
  • 當 B 不再需要連接時,發送連接釋放請求報文段。
  • A 收到後發出確認,進入 TIME-WAIT 狀態,等待 2 MSL(最大報文存活時間)後釋放連接。
  • B 收到 A 的確認後釋放連接。

這裏主要是簡單介紹一下TCP連接的過程,並不細究其中原理。

四、編碼實踐

現在我們已經了解了客戶端如何使用TCP跟服務器進行連接,是時候開始編寫代碼了。先說一下代碼組成,主要的代碼就是服務器程序tcpserver.c和客戶端程序tcpclient.c,這兩個的代碼比較簡潔,因為用到的函數大都進行了封裝。另外我將這兩個文件用到的函數都提取出來放到了myunp.c裏面,函數原型和宏以及用到的系統頭文件則在myunp.h裏。
這裏我就結合代碼將相關知識介紹一遍。

服務器程序

tcpserver.c
#include "myunp.h" 

int main(int argc, char **argv)
{
    int                 listenfd, connfd;
    pid_t               childpid;
    socklen_t           clilen;
    struct sockaddr_in  cliaddr, servaddr;

    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(SERV_PORT);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    for ( ; ; ) {
        clilen = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);

        if ( (childpid = Fork()) == 0) {    /* child process */
            Close(listenfd);    /* close listening socket */
            str_echo(connfd);   /* process the request */
            exit(0);
        }
        Close(connfd);          /* parent closes connected socket */
    }
}

第1行
包含所需要的頭文件myunp.h

第5行
聲明監聽描述符連接描述符

這裏我們要先了解類unix系統的一個核心思想,“一切皆文件”,這裏的“文件”不僅僅是我們通常所指的文件,在linux和unix中它代表的更為寬泛。目錄、字符設備、塊設備、 套接字、進程、線程、管道等都被視為是一個“文件”。當我們打開一個文件時,就得到了一個文件描述符(file descriptor),文件描述符通常是非負整數,代表了一個文件的索引,通過這個索引我們就可以去操作文件。
現在我們知道了描述符是用來當做文件的索引,那麽上面的監聽描述符和連接描述符又是什麽呢?別急,我們先認識一下socket,英文原義是“插座”,我們通常把它翻譯作“套接字”,socket本質是編程接口(API),對TCP/IP的封裝,我們可以先將它簡單理解成一個插口,在客戶端和服務器兩邊分別都打開一個socket,然後兩個插口相連,這就形成了一條可以通信的線路了。兩個socket接口分別返回一個文件描述符,這個描述符就是對方主機的索引,比如客戶端通過得到的文件描述符可以讀取到服務器寫過來的內容。
那麽現在就有一個問題了,我們這個服務器程序為什麽有兩個描述符呢,不是一個就可以跟客戶端通信了嗎?實際上,上面只是簡化了的敘述,現在我們來詳細了解socket的工作流程:
技術分享圖片

  • 客戶端和服務器使用socket函數來創建一個套接字描述符(socket descriptor),但這個描述符是半打開的,還不能用於讀寫。
  • 對於客戶端,使用connect函數與服務器建立連接,如果連接成功,套接字描述符將會完全打開,可以進行讀寫了。
  • 對於服務器,需先使用bind函數套接字描述符與本服務器ip地址和端口綁定,然後使用listen函數套接字描述符轉換為一個監聽描述符(listening)。這時再使用accept函數等待客戶端的連接請求到達監聽描述符,一旦接收到來自客戶端的連接請求,就返回一個新的已連接描述符(connected descriptor),這個描述符就可以用來與客戶端通信了。

如上圖,當連接建立後,客戶端和服務器就可以進行通信了。很明顯,服務器做的工作比客戶端多得多,監聽描述符和已連接描述符之間的區別也容易讓人感到迷惑。這裏解釋一下,監聽描述符只被創建一次,只有一個,它是作為服務器接受請求的一個窗口。而只要服務器在這個窗口每接受一次連接請求,就創建一個已連接描述符,它則是客戶端和服務器連接的端點。如下圖所示,監聽描述符作為一個端口接受來自客戶端的連接請求,當服務器允許連接就生成一個已連接描述符,客戶端和服務器之間的連接就是建立在這個端點之上。(下面圖片的過程對應於上面連接建立的過程)
技術分享圖片
技術分享圖片
好了,解釋到這裏你也應該基本明白了監聽描述符連接描述符是幹什麽的,可能你還會有疑問,為什麽就只聲明一個已連接描述符,不是說每接受一個連接請求就要生成一個新的已連接描述符嗎?這個問題我們先放著。

第6行
聲明子進程id,這裏類型後綴為_t的都是系統數據類型。

我們這個程序是並發的,意味著能夠同時接受多個客戶端的請求。這裏是使用fork函數來實現的,該函數是UNIX中派生新進程的唯一方法。每次有客戶端發生連接請求到來時,該程序都會派生出一個子進程來處理客戶端的請求。子進程是父進程的一個副本,也就是好像是把這個服務器程序復制了一份一樣,但子進程和父進程裏面fork函數的返回值是不一樣的,父進程中返回的是子進程的進程id,而子進程裏面返回的是0,我們就可以利用這一點,編寫一個判別語句,根據fork函數返回值來確定這是哪個進程,要執行什麽操作。
如下圖所示,在上面連接已建立的連接之上,為了實現並發的能力,我們使用fork函數復制了一個子進程,這個子進程跟父進程的內容是一樣的,這就導致了連接形成的分叉,怎麽辦呢?前面我們不是說過子進程和父進程裏面fork函數的返回值是不一樣的嘛,我們根據這個返回值作為判別條件,在父進程裏面就把connfd(已連接描述符)關閉,在子進程裏面就把listenfd(監聽描述符)關閉。這樣,父進程仍舊作為服務器接受連接請求的窗口等待請求的到來,而子進程就為剛才請求連接的客戶端服務,達到了並發的目的。
技術分享圖片
技術分享圖片
作為背景知識,這裏我們再提一下:父進程和子進程共享相同的內存空間,也就是說他們所引用的描述符是同一個東西,那麽為什麽子進程關閉了listenfd卻不會使父進程的listenfd也被關閉呢?熟悉c++ 11標準的朋友可能已經猜到了,這裏的原理跟c++ 智能指針的原理差不多,即采用了引用計數。對於一個描述符,我們使用一個引用計數值來表示有多少個對這個描述符的使用,比如這裏的listenfd有父進程和子進程兩個引用者,所以他的引用計數值為2,當子進程關閉它的listenfd時,所做的僅僅是將這個引用計數值減1而已,只有當引用計數值變成0時才會真正關閉這個描述符。

第7~8行
聲明客戶端地址結構長度
聲明客戶端和服務器套接字地址結構

在unix中,每個協議族都定義了它自己的套接字地址結構,這個結構裏面包含ip地址、端口號、所使用的協議等信息。這裏的服務器套接字地址結構保存服務器的ip地址和端口等信息,客戶端地址結構則在每一次接受連接請求時保存客戶端的信息。客戶端地址結構長度的意義在於當連接請求到來時,內核要將客戶端的信息寫入客戶端地址結構中,長度限制了內核寫入的大小,避免產生越界。

第10行
創建套接字描述符
Socket(AF_INET, SOCK_STREAM, 0)
第一個參數指IPv4協議族,第二個為字節流套接字,第三個為使用的協議(protocol),0的意思是選擇前兩個參數組合的系統默認值,這裏默認是TCP協議。

第12~15行
填充服務器地址結構

bzero函數把整個結構清零,這是一種慣例寫法。
而後填寫所使用的協議族、ip地址及端口。這裏之所以使用servaddr.sin_addr.s_addr是因為sin_addr是一個結構(struct),而不是一個簡單的字段,具體的ip地址字段在這個結構的s_addr中,這是因為一些歷史上的原因(關於地址分類),這裏我們也不深究了。
後面使用的htonlhtons函數意為Host to Network LongHost to Network Short,分別是將一個長整數或短整數從主機字節順序轉換成網絡字節順序。
字節順序指的是一個字節存放在內存中的順序,主要有大端法和小端法。網絡字節順序一般是大端順序,而主機字節順序則依賴於具體的操作系統和cpu芯片。學過匯編的同學應該就清楚,我們內存中一個單元為一個字節,有8bit,那麽這個單元就可以存放用十六進制表示的兩個數了,比如一個內存單元,可以存放16進制的0x12,也就是二進制的0001 0010。
如下圖所示,一個16進制數0x01234567存放在一段連續的內存單元中,如果是大端法,存放的順序就是01 23 45 67,而小端法就是67 45 23 01。

技術分享圖片
因為不同的字節順序不能進行通信,所以必須進行轉換。其中的參數INADDR_ANY表示通配地址,一些服務器可能會有多個網絡接口,因而有了多個ip地址,通配地址表示訪問任一個ip地址都能訪問到這個程序。參數SERV_PORT定義在myunp.h中,值為9877。

第17行
將套接字描述符與服務器地址結構進行綁定

這裏第二個參數(SA *) &servaddr把servaddr強制轉換為SA類型,SA為struct sockaddr的縮寫,定義在myunp.h中,僅僅是為了方便書寫而已,而sockaddr是通用地址結構,為什麽要強轉為通用地址結構呢?因為不同的協議族有不同的套接字地址結構,但bind函數要求必須要有一個確切的參數類型,所以就將參數類型設置為struct sockaddr,需要使用bind函數時就將具體的結構類型強轉一下就行。(額外提一下,之所以采用這種方法是由於歷史所限,如今已經有了通用的指針類型void *了。)
細心的朋友可能已經註意到了,這裏代碼寫的是Bind而不是bind,這是作者的一種編程風格,也稱做包裹函數,因為bind函數會有時會返回一些錯誤,如果將錯誤處理代碼都寫在主程序裏面就顯得很繁雜,因此作者就將這些錯誤處理和原函數寫在一個函數裏面,使用時調用這個外層的函數就行了。後面還有許多包裹函數,它們統一的特征就是函數名首字母大寫,這些函數的定義都在myunp.c中。

第19行
將套接字描述符轉換為一個監聽描述符
第二個參數為監聽隊列的最大排隊數,具體實現原理有些復雜,這裏不去詳解,LISTENQ值為1024,定義在myunp.h中。

第21~31行
等待來自客戶端的連接請求,當空閑時就阻塞在Accept函數處,一旦有請求到了就fork出一個子進程,25~29行為子進程的處理內容,它先關閉listenfd,接著調用str_echo直接把客戶端發送過來的內容回送回去,處理完了就退出子進程。30行為父進程的內容,它僅僅簡單的關閉connfd然後就繼續等待連接請求了,周而復始。註意,上述兩個過程是同時發生的,父子進程的執行是分開的。

客戶端程序

tcpclient.c
#include    "myunp.h"

int
main(int argc, char **argv)
{
    int                 sockfd;
    struct sockaddr_in  servaddr;

    if (argc != 2)
        err_quit("usage: tcpcli <IPaddress>");

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

    str_cli(stdin, sockfd);     /* do it all */

    exit(0);
}

第6~7行
聲明套接字描述符和服務器地址結構

第9~10行
驗證命令行參數數量,我們執行客戶端程序時要使用命令./tcpclient.c 127.0.0.1,這裏面就是兩個參數了,第一個參數是程序的名字,第二個就是後面的127.0.0.1,當參數數量不正確時,就會報錯並退出。這裏的err_quit函數也是包裹函數,它輸出錯誤信息並終止程序。

第12行
生成套接字描述符

第14~17行
填寫服務器地址結構信息
其中Inet_pton函數把服務器地址從表達(presentation)形式轉換為數值(numeric)形式,並保存在servaddr.sin_addr中,比如把127.0.0.1轉換為01111111 00000000 00000000 00000001。它同樣也包裹函數,具體實現比較復雜,因為它既可以用於IP v4地址也可以用於IP v6地址。

第19行
連接到服務器

第12行
該函數將用戶在命令行中輸入的字符串發送給服務器,而後將收到的來自服務器的消息顯示在屏幕上。


到這裏服務器和客戶端程序基本講解完畢,註意這只是一個簡單的程序,缺乏應對各種極端情況的能力,但這其中的原理已經足夠我們一探網絡編程的世界了。剩下的就是一些提取出來的函數和定義了,這裏我就直接貼代碼,不再單獨講了,對程序實現底層細節有興趣的朋友可以研讀下下面的代碼。

myunp.h
#ifndef MY_UNP_H 
#define MY_UNP_H

#include    <sys/socket.h>  /* basic socket definitions */
#include    <netinet/in.h>  /* sockaddr_in{} and other Internet defns */
#include    <string.h>      /* bzero(); */
#include    <stdlib.h>      /* exit(); */
#include    <stdio.h>       /* snprintf(); */
#include    <errno.h>
#include    <unistd.h>
#include    <stdarg.h>      /* ANSI C header file */
#include    <syslog.h>      /* for syslog() */
#define SERV_PORT   9877    /* TCP and UDP */
#define LISTENQ     1024    /* 2nd argument to listen() */
#define MAXLINE     4096    /* max text line length */

#ifndef AF_INET6
#define AF_INET6    AF_MAX+1    /* just to let this compile */
#endif
/* Following shortens all the typecasts of pointer arguments: */
#define SA          struct sockaddr
#define IN6ADDRSZ   16
#define INADDRSZ     4
#define INT16SZ      2

/* 套接字連接相關函數 */
int      Socket(int, int, int);
void     Bind(int, const SA *, socklen_t);
void     Listen(int, int);
int      Accept(int, SA *, socklen_t *);
void     Connect(int, const SA *, socklen_t);
void     Close(int);

/* 派生進程相關 */
pid_t    Fork(void);

/* 發送、回送相關 */
void     str_echo(int);
void     str_cli(FILE *, int);

/* 錯誤處理相關 */
void     err_sys(const char *, ...);
void     err_quit(const char *, ...);

/* 字符串輸入輸出相關 */
void     Writen(int, void *, size_t);
char    *Fgets(char *, int, FILE *);
void     Fputs(const char *, FILE *);
ssize_t  Readline(int, void *, size_t);

/* 地址轉換相關 */
void     Inet_pton(int, const char *, void *);

#endif /* MY_UNP_H */
myunp.c
#include "myunp.h" /* for implicit declaration of function ‘socket’ */

/* 套接字連接相關函數 */

int Socket(int family, int type, int protocol)
{
    int     n;
    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (bind(fd, sa, salen) < 0)
        err_sys("bind error");
}

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)
        err_sys("listen error");
}


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
            err_sys("accept error");
    }
    return(n);
}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (connect(fd, sa, salen) < 0)
        err_sys("connect error");
}

void Close(int fd)
{
    if (close(fd) == -1)
        err_sys("close error");
}


/* 派生進程相關 */

pid_t Fork(void)
{
    pid_t   pid;
    if ( (pid = fork()) == -1)
        err_sys("fork error");
    return(pid);
}



/* 發送、回送相關 */

void str_echo(int sockfd)
{
    ssize_t     n;
    char        buf[MAXLINE];
again:
    while ( (n = read(sockfd, buf, MAXLINE)) > 0)
        Writen(sockfd, buf, n);

    if (n < 0 && errno == EINTR)
        goto again;
    else if (n < 0)
        err_sys("str_echo: read error");
}

void str_cli(FILE *fp, int sockfd)
{
    char    sendline[MAXLINE], recvline[MAXLINE];
    while (Fgets(sendline, MAXLINE, fp) != NULL) {
        Writen(sockfd, sendline, strlen(sendline));

        if (Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");

        Fputs(recvline, stdout);
    }
}



/* 錯誤處理相關 */

int     daemon_proc;        /* set nonzero by daemon_init() */
static void err_doit(int, int, const char *, va_list);

void err_sys(const char *fmt, ...)
{
    va_list     ap;
    va_start(ap, fmt);
    err_doit(1, LOG_ERR, fmt, ap);
    va_end(ap);
    exit(1);
}

void err_quit(const char *fmt, ...)
{
    va_list     ap;
    va_start(ap, fmt);
    err_doit(0, LOG_ERR, fmt, ap);
    va_end(ap);
    exit(1);
}

static void err_doit(int errnoflag, int level, const char *fmt, va_list ap)
{
    int     errno_save, n;
    char    buf[MAXLINE + 1];

    errno_save = errno;     /* value caller might want printed */
#ifdef  HAVE_VSNPRINTF
    vsnprintf(buf, MAXLINE, fmt, ap);   /* safe */
#else
    vsprintf(buf, fmt, ap);                 /* not safe */
#endif
    n = strlen(buf);
    if (errnoflag)
        snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
    strcat(buf, "\n");

    if (daemon_proc) {
        syslog(level, buf);
    } else {
        fflush(stdout);     /* in case stdout and stderr are the same */
        fputs(buf, stderr);
        fflush(stderr);
    }
    return;
}




/* 字符串輸入輸出相關 */

ssize_t writen(int fd, const void *vptr, size_t n)/* Write "n" bytes to a descriptor. */
{
    size_t      nleft;
    ssize_t     nwritten;
    const char  *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;       /* and call write() again */
            else
                return(-1);         /* error */
        }

        nleft -= nwritten;
        ptr   += nwritten;
    }
    return(n);
}

void Writen(int fd, void *ptr, size_t nbytes)
{
    if (writen(fd, ptr, nbytes) != nbytes)
        err_sys("writen error");
}

char * Fgets(char *ptr, int n, FILE *stream)
{
    char    *rptr;

    if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream))
        err_sys("fgets error");

    return (rptr);
}

void Fputs(const char *ptr, FILE *stream)
{
    if (fputs(ptr, stream) == EOF)
        err_sys("fputs error");
}

static int  read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];

static ssize_t
my_read(int fd, char *ptr)
{

    if (read_cnt <= 0) {
again:
        if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
            if (errno == EINTR)
                goto again;
            return(-1);
        } else if (read_cnt == 0)
            return(0);
        read_ptr = read_buf;
    }

    read_cnt--;
    *ptr = *read_ptr++;
    return(1);
}

ssize_t readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n, rc;
    char    c, *ptr;

    ptr = vptr;
    for (n = 1; n < maxlen; n++) {
        if ( (rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == ‘\n‘)
                break;  /* newline is stored, like fgets() */
        } else if (rc == 0) {
            *ptr = 0;
            return(n - 1);  /* EOF, n - 1 bytes were read */
        } else
            return(-1);     /* error, errno set by read() */
    }

    *ptr = 0;   /* null terminate like fgets() */
    return(n);
}

ssize_t readlinebuf(void **vptrptr)
{
    if (read_cnt)
        *vptrptr = read_ptr;
    return(read_cnt);
}

ssize_t Readline(int fd, void *ptr, size_t maxlen)
{
    ssize_t     n;

    if ( (n = readline(fd, ptr, maxlen)) < 0)
        err_sys("readline error");
    return(n);
}



/* 地址轉換相關 */

static int  inet_pton4(const char *src, u_char *dst);
static int  inet_pton6(const char *src, u_char *dst);


int inet_pton(af, src, dst)
    int af;
    const char *src;
    void *dst;
{
    switch (af) {
    case AF_INET:
        return (inet_pton4(src, dst));
    case AF_INET6:
        return (inet_pton6(src, dst));
    default:
        errno = EAFNOSUPPORT;
        return (-1);
    }
    /* NOTREACHED */
}


static int inet_pton4(src, dst)
    const char *src;
    u_char *dst;
{
    static const char digits[] = "0123456789";
    int saw_digit, octets, ch;
    u_char tmp[INADDRSZ], *tp;

    saw_digit = 0;
    octets = 0;
    *(tp = tmp) = 0;
    while ((ch = *src++) != ‘\0‘) {
        const char *pch;

        if ((pch = strchr(digits, ch)) != NULL) {
            u_int new = *tp * 10 + (pch - digits);

            if (new > 255)
                return (0);
            *tp = new;
            if (! saw_digit) {
                if (++octets > 4)
                    return (0);
                saw_digit = 1;
            }
        } else if (ch == ‘.‘ && saw_digit) {
            if (octets == 4)
                return (0);
            *++tp = 0;
            saw_digit = 0;
        } else
            return (0);
    }
    if (octets < 4)
        return (0);
    /* bcopy(tmp, dst, INADDRSZ); */
    memcpy(dst, tmp, INADDRSZ);
    return (1);
}


static int inet_pton6(src, dst)
    const char *src;
    u_char *dst;
{
    static const char xdigits_l[] = "0123456789abcdef",
              xdigits_u[] = "0123456789ABCDEF";
    u_char tmp[IN6ADDRSZ], *tp, *endp, *colonp;
    const char *xdigits, *curtok;
    int ch, saw_xdigit;
    u_int val;

    memset((tp = tmp), 0, IN6ADDRSZ);
    endp = tp + IN6ADDRSZ;
    colonp = NULL;
    /* Leading :: requires some special handling. */
    if (*src == ‘:‘)
        if (*++src != ‘:‘)
            return (0);
    curtok = src;
    saw_xdigit = 0;
    val = 0;
    while ((ch = *src++) != ‘\0‘) {
        const char *pch;

        if ((pch = strchr((xdigits = xdigits_l), ch)) == NULL)
            pch = strchr((xdigits = xdigits_u), ch);
        if (pch != NULL) {
            val <<= 4;
            val |= (pch - xdigits);
            if (val > 0xffff)
                return (0);
            saw_xdigit = 1;
            continue;
        }
        if (ch == ‘:‘) {
            curtok = src;
            if (!saw_xdigit) {
                if (colonp)
                    return (0);
                colonp = tp;
                continue;
            }
            if (tp + INT16SZ > endp)
                return (0);
            *tp++ = (u_char) (val >> 8) & 0xff;
            *tp++ = (u_char) val & 0xff;
            saw_xdigit = 0;
            val = 0;
            continue;
        }
        if (ch == ‘.‘ && ((tp + INADDRSZ) <= endp) &&
            inet_pton4(curtok, tp) > 0) {
            tp += INADDRSZ;
            saw_xdigit = 0;
            break;  /* ‘\0‘ was seen by inet_pton4(). */
        }
        return (0);
    }
    if (saw_xdigit) {
        if (tp + INT16SZ > endp)
            return (0);
        *tp++ = (u_char) (val >> 8) & 0xff;
        *tp++ = (u_char) val & 0xff;
    }
    if (colonp != NULL) {
        /*
         * Since some memmove()‘s erroneously fail to handle
         * overlapping regions, we‘ll do the shift by hand.
         */
        const int n = tp - colonp;
        int i;

        for (i = 1; i <= n; i++) {
            endp[- i] = colonp[n - i];
            colonp[n - i] = 0;
        }
        tp = endp;
    }
    if (tp != endp)
        return (0);
    /* bcopy(tmp, dst, IN6ADDRSZ); */
    memcpy(dst, tmp, IN6ADDRSZ);
    return (1);
}

void Inet_pton(int family, const char *strptr, void *addrptr)
{
    int     n;
    if ( (n = inet_pton(family, strptr, addrptr)) < 0)
        err_sys("inet_pton error for %s", strptr);  /* errno set */
    else if (n == 0)
        err_quit("inet_pton error for %s", strptr); /* errno not set */
    /* nothing to return */
}

UNIX網絡編程入門——TCP客戶/服務器程序詳解