1.5 一个简单的时间获取服务器程序
我们可以编写一个简单的TCP时间获取服务器程序,它和1.2节中的客户程序一道工作。图1-9给出了这个服务器程序,它使用了上一节中讲过的包裹函数。
创建TCP套接字
10 TCP套接字的创建与客户程序相同。
把服务器的众所周知端口捆绑到套接字
11~15 通过填写一个网际套接字地址结构并调用bind函数,服务器的众所周知端口(对于时间获取服务是13)被捆绑到所创建的套接字。我们指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户连接。以后我们将了解怎样限定服务器进程只在单个网络接口上接受客户连接。
把套接字转换成监听套接字
16 调用listen函数把该套接字转换成一个监听套接字,这样来自客户的外来连接就可在该套接字上由内核接受。socket、bind和listen这3个调用步骤是任何TCP服务器准备所谓的监听描述符(listening descriptor,本例中为listenfd)的正常步骤。
常值LISTENQ在我们的unp.h头文件中定义。它指定系统内核允许在这个监听描述符上排队的最大客户连接数。我们将在4.5节详细说明客户连接的排队。
接受客户连接,发送应答
17~21 通常情况下,服务器进程在accept调用中被投入睡眠,等待某个客户连接的到达并被内核接受。TCP连接使用所谓的三路握手(three-way handshake)来建立连接。握手完毕时accept返回,其返回值是一个称为已连接描述符(connected descriptor)的新描述符(本例中为connfd)。该描述符用于与新近连接的那个客户通信。accept为每个连接到本服务器的客户返回一个新描述符。
本书全文采用的无限循环采用以下风格:
for ( ; ; ) {
. . .
}
当前时间和日期是由库函数time返回的,它实际上返回的是自Unix纪元即0点0分0秒(国际标准时间)以来的秒数。下一个库函数ctime把该整数值转换成直观可读的时间格式,例如:
Mon May 26 20:58:40 2003
snprintf函数在这个字符串末尾添加一个回车符和一个回行符,随后write函数把结果字符串写给客户。
如果你尚不习惯改用snprintf代替较早的sprintf函数,那么现在是学习的时候了。调用sprintf无法检查目的缓冲区是否溢出。相反,snprintf要求其第二个参数指定目的缓冲区的大小,因此可确保该缓冲区不溢出。
snprintf相对较晚才加到ANSI C标准中,在称为ISO C99的版本中引入。不过几乎所有厂商都把它作为标准C函数库的一部分提供,而且另有许多免费可得的版本可用。我们贯穿全书使用snprintf,也推荐你出于可靠性考虑在自己的程序中改用它来代替sprintf。
值得注意的是,许多网络入侵是由黑客通过发送数据,导致服务器对sprintf的调用使其缓冲区溢出而发生的。必须小心使用的函数还有gets、strcat和strcpy,通常应分别改为调用fgets、strncat和strncpy。更好的替代函数是后来才引入的strlcat和strlcpy,它们确保结果是正确终止的字符串。编写安全的网络程序的更多技巧参见[Garfinkel, Schwartz, and Spafford 2003]的第23章。
终止连接
22 服务器通过调用close关闭与客户的连接。该调用引发正常的TCP连接终止序列:每个方向上发送一个FIN,每个FIN又由各自的对端确认。2.6节将详细讲述TCP的三路握手和用于终止一个TCP连接的4个TCP分组。
与上节查看客户程序一样,本节查看服务器程序也非常简略,具体细节留待本书以后论述。有以下几点需要注意。
与其客户程序一样,这一服务器程序也与IPv4协议相关。我们将在图11-13中给出使用getaddrinfo函数实现的一个协议无关的版本。
本服务器一次只能处理一个客户。如果多个客户连接差不多同时到达,系统内核在某个最大数目的限制下把它们排入队列,然后每次返回一个给accept函数。本服务器只需调用time和ctime这两个库函数,运行速度很快。然而如果服务器需用较多时间(譬如说几秒钟或一分钟)服务每个客户,那么我们必须以某种方式重叠对各个客户的服务。
图1-9中所示的服务器称为迭代服务器(iterative server),因为对于每个客户它都迭代执行一次。同时能处理多个客户的并发服务器(concurrent server)有多种编写技术。最简单的技术是调用Unix的fork函数(4.7节),为每个客户创建一个子进程。其他技术包括使用线程代替fork(26.4节),或在服务器启动时预先fork一定数量的子进程(30.6节)。
如果从shell命令行启动本例这样的一个服务器,我们也许想要它运行很长时间,因为服务器往往在系统工作期间一直运行。这要求我们往服务器程序中添加代码,以便它能够作为一个Unix守护进程(daemon)——能在后台运行且不跟任何终端关联的进程——运行。我们将在13.4节讨论守护进程。