本节书摘来自异步社区《UNIX网络编程 卷1:套接字联网API(第3版)》一书中的第1章,第1.2节,作者:【美】W. Richard Stevens , Bill Fenner , Andrew M. Rudoff著,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.2 一个简单的时间获取客户程序
让我们考虑一个具体的例子,引入将在本书中遇到的许多概念和说法。图1-5所示的是TCP当前时间查询客户程序的一个实现。该客户与其服务器建立一个TCP连接后,服务器以直观可读格式简单地送回当前时间和日期。
这就是本书用于展示所有源代码的格式。每个非空行都被编排行号。如稍后所示,代码正文讲解部分一开始标注该段代码起始与结束的行号。有的段落会以一个简短的、描述性的醒目标题起头,对所讲解代码段进行概要说明。
每个源代码段起始与结束处的水平线标出了该代码段所在的源代码文件名,对于本例就是intro目录下的daytimetcpcli.c文件(intro/daytimetcpcli.c)。本书所有例子的源代码都可免费获得(见前言),在此标注它们的文件名便于读者找到其源文件。在阅读本书期间,编译、运行特别是修改这些程序是学习网络编程概念的好方法。
整本书中我们随时会插入缩进的小字号段落(如此处所示)来说明实现的细节和历史上的观点。
如果编译该程序生成默认的a.out可执行文件后执行它,我们会得到如下结果:
solaris % a.out 206.168.112.96 我们的输入
Mon May 26 20:58:40 2003 程序的输出
当我们展示交互的输入和输出时,输入总是采用加粗的等宽字体,而计算机的输出总是采用不加粗的等宽字体。注释用宋体字加在右边。作为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语句:
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
但是把这两行合并成一行却是常见的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字节字符串:
Mon May 26 20:58:40 2003\r\n
其中,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套接字就此被关闭。
刚才已提过,本书后面会对刚才讲述的所有概念深入进行探讨。