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 512

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr;    // 此结构位于头文件netinet/in.h

    if (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_INET
    servaddr.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将其简化为SA
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {    // sockaddr结构位于头文件sys/socket.h
        printf("connect error\n");
		perror("connect");    // perror函数将其参数和errno所对应的错误一起输出到标准错误
		exit(1);
    }

    // 此处需要while循环,因为服务器可能会将TCP分节,我们一次只能读取单个分节,需要一直读取,直到read函数返回0(对端关闭连接)或负数(发生错误),此程序中,服务器关闭连接表示记录接收结束
    while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    // null terminate
        if (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 512

int 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时间获取服务器程序,与上面的时间获取客户程序一道工作(图1.9):

#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,此时监视以太网上的分组,会发现每个字符自成一个分组发送。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值