Linux Socket 编程深入
一、网络信息
对于一个更通用的服务器和客户程序来说,可以通过网络信息函数来决定应该使用的地址和端口。如果你有足够的权限,也可以将自己的服务添加到 /etc/services 文件中的已知服务列表中,并在这个文件中为端口号分配一个名字,使用户可以使用符号化的服务名而不是端口号的数字。类似地,如果给定一个计算机的名字,你可以通过调用解析地址的主机数据库函数来确定它的IP地址。这些函数通过查询网络配置文件来完成这一工作,如 /etc/hosts 文件或网络信息服务。常用的网络信息服务有 NIS(Network Information Service,网络信息服务,以前称为Yellow Pages,黄页服务)和 DNS(Domain Name Service,域名服务)。
主机数据库函数在接口头文件netab.h中声明,如下所示:
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, size_t len,int type);
struct hostent*gethostbyname(const char *name);
这些函数返回的结构中至少会包含以下几个成员:
struct hostent {
char *h_name; /* name of the host */
char **h_aliases; /* list of aliases (nicknames) */
int h_addrtype; /* address type */
int h_length; /* length in bytes of the address */
char **h_addr_ist /* list of address (network order) */
};
如果没有与我们查询的主机或地址相关的数据项,这些信息函数将返回一个空指针。类似地,与服务及其关联端口号有关的信息也可以通过一些服务信息函数来获取。如下所示:
#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
proto参数指定用于连接该服务的协议,它的两个取值是 tcp 和 udp,前者用于 SOCK_STREAM 类型的 TCP 连接,后者用于 SOCK_DGRAM 类型的 UPD 数据报。
结构 servent 至少包含以下几个成员:
struct servent {
char *s_name; /* name of the service */
char **s_aliases; /* list of aliases (alternative names) */
int s_port; /* The IP port number */
char *s_proto; /* The service type,usually "tcp" or "udp" */
}
如果想获得某台计算机的主机数据库信息,可以调用 gethostbyame 函数并且将结果打印出来注意,要把返回的地址列表转换为正确的地址类型,并用函数 inet_ntoa 将它们从网络字节序转换为可打印的字符串。函数inet_ntoa的定义如下所示:
#include <arpa/inet.h>
char *inet_ntoa(struct in addr in)
这个函数的作用是,将一个因特网主机地址转换为一个点分四元组格式的字符串。它在失败时返回-1,但 POSIX 规范并未定义任何错误。其他可用的新函数还有 gethostname,它的定义如下所示:
#include <unistd.h>
int gethostname(char *name, int namelength);
这个函数的作用是,将当前主机的名字写入 name 指向的字符串中。主机名将以 null 结尾。参数 namelength 指定了字符串 name 的长度,如果返回的主机名太长,它就会被截断。gethostname 在成功时返回0,失败时返回-1,但 POSIX 规范中没有定义任何错误。
实例:下面这个程序用来获取一台主机的有关信息(getname.c)
/* As usual, make the appropriate includes and declare the variables. */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *host, **names, **addrs;
struct hostent *hostinfo;
/* Set the host in question to the argument supplied with the getname call,
or default to the user's machine. */
if(argc == 1) {
char myname[256];
gethostname(myname, 255);
host = myname;
}
else
host = argv[1];
/* Make the call to gethostbyname and report an error if no information is found. */
hostinfo = gethostbyname(host);
if(!hostinfo) {
fprintf(stderr, "cannot get info for host: %s\n", host);
exit(1);
}
/* Display the hostname and any aliases it may have. */
printf("results for host %s:\n", host);
printf("Name: %s\n", hostinfo -> h_name);
printf("Aliases:");
names = hostinfo -> h_aliases;
while(*names) {
printf(" %s", *names);
names++;
}
printf("\n");
/* Warn and exit if the host in question isn't an IP host. */
if(hostinfo -> h_addrtype != AF_INET) {
fprintf(stderr, "not an IP host!\n");
exit(1);
}
/* Otherwise, display the IP address(es). */
addrs = hostinfo -> h_addr_list;
while(*addrs) {
printf(" %s", inet_ntoa(*(struct in_addr *)*addrs));
addrs++;
}
printf("\n");
exit(0);
}
此外,你也可以用 gethostbyaddr 函数来查出哪个主机拥有给定的IP地址。你可以在服务器上用这个函数来查找连接客户的来源。
实验解析
程序通过调用 gethostbyname 从主机数据库中提取出主机的信息。它打印出主机名、它的别名(这台计算机的其他名字) 和该主机在它的网络接口上使用的IP地址。运行这个示例程序并指定主机名 tilde 时,程序给出了以太网和调制解调器两个网络接口的信息。如下所示:
$ ./getname tilde
results for host tilde:
Name: tilde.localnet
Aliases: tilde192.168.1.1 158.152.x.x
当我们使用主机名localhost时,程序只给出了回路网络的信息。如下所示:
$ ./getname localhost
results forhost localhost:
Name: localhost
Aliases:
127.0.0.1
现在可以改进我们的客户程序,使它可以连接到任何有名字的主机。这次不是连接到我们的示例服务器,而是连接到一个标准服务,这样就可以演示端口号的提取操作了。大多数 UNIX 和一些 Linux 系统都有一项标准服务daytime,它提供系统的日期和时间。客户可以连接到这个服务来查看服务器的当前日期和时间。
实例二、连接到标准服务(getdate.c)
/* Start with the usual includes and declarations. */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *host;
int sockfd;
int len, result;
struct sockaddr_in address;
struct hostent *hostinfo;
struct servent *servinfo;
char buffer[128];
if(argc == 1)
host = "localhost";
else
host = argv[1];
/* Find the host address and report an error if none is found. */
hostinfo = gethostbyname(host);
if(!hostinfo) {
fprintf(stderr, "no host: %s\n", host);
exit(1);
}
/* Check that the daytime service exists on the host. */
servinfo = getservbyname("daytime", "tcp");
if(!servinfo) {
fprintf(stderr,"no daytime service\n");
exit(1);
}
printf("daytime port is %d\n", ntohs(servinfo -> s_port));
/* Create a socket. */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* Construct the address for use with connect... */
address.sin_family = AF_INET;
address.sin_port = servinfo -> s_port;
address.sin_addr = *(struct in_addr *)*hostinfo -> h_addr_list;
len = sizeof(address);
/* ...then connect and get the information. */
result = connect(sockfd, (struct sockaddr *)&address, len);
if(result == -1) {
perror("oops: getdate");
exit(1);
}
result = read(sockfd, buffer, sizeof(buffer));
buffer[result] = '\0';
printf("read %d bytes: %s", result, buffer);
close(sockfd);
exit(0);
}
你可以用 getdate 获取任一已知主机的日期和时间。
$ ./getdate localhost
daytime port is 13
read 26 bytes: 24 JUN 2007 06:03:03 BST
如果你看到如下所示的一条错误信息:
oops: getdate: Connection refused
或是:
oops: getdate: No such file or directory
这可能是因为你正在连接的计算机没有启用daytime服务。最新版本的 Linux 系统在默认情况下都没有启用该服务。实验解析运行这个程序时,你可以指定要连接的主机。daytime 服务的端口号是通过网络数据库函数getservbyname来确定的,该函数以与返回主机信息类似的方法返回和网络服务相关的信息。程序getdate尝试连接到指定主机返回的地址列表中的第一个地址,如果成功,它就读取daytime服务返回的信息:一个表示UNIX日期和时间的字符串。
二、因特网守护进程(xineta/ineta)
UNIX系统通常以超级服务器的方式来提供多项网络服务。超级服务器程序(因特网守护进程 xineta或inetd)同时监听许多端口地址上的连接。当有客户连接到某项服务时,守护程序就运行相应的服务器。这使得针对各项网络服务的服务器不需要一直运行着,它们可以在需要时启动。
因特网守护进程在现代 Linux 系统中是通过 xinetd 来实现的。xinetd 实现方式取代了原来的 UNIX 程序 inetd,尽管你仍然会在一些较老的 Linux 系统中以及其他的类 UNIX 系统中看到 ineta 的应用。
我们通常是通过一个图形用户界面来配置 xineta 以管理网络服务,但我们也可以直接修改它的配置文件。它的配置文件通常是 /etc/xinetd.conf 和 /etc/xineta.a 目录中的文件。
每一个由xineta提供的服务都在 /etc/xinetd.d 目录中有一个对应的配置文件。xineta 将在其启动时或被要求的情况下读取所有这些配置文件。
下面是一些 xineta 配置文件的例子,首先是 daytime 服务的配置:
#default: off
# description:A daytime server. This is the tcp version.
service daytime
{
socket_type = stream
protocol = tcp
wait = no
user = root
type = INTERNAL
id = daytime-stream
FLAGS = IPv6 IPv4
}
然后是文件传输服务的配置:
# default:off
# description:
# The vsftpd FTP server serves FTP connections,It uses
# normal,unencrypted usernames and passwords for authentication
# vsftpd is designed to be secure.
#
# NOTE: This file contains the configuration for xinetd to start vsftpd
# the configuration file for vsftp itself is in /etc/vsftpd.conf
service ftp
{
# server args =
# log_on_success += DURATION USERID
# log_on_failure += USERID
# nice = 10
socket_type = stream
protocol = tcp
wait = no
user = root
server = /usr/sbin/vsftpd
}
我们的 getdate 程序连接的 daytime 服务实际上就是由 xinetd 自身负责处理的(它被标记为 internal,即内部),它同时支持 SOCK_STREAM(tcp) 和 SOCK_DGRAM (udp) 套接字。
ftp文件传输服务只支持 SOCK_STREAM 套接字,并且是由一个外部程序来提供服务的。在本例中这个程序是 vsftpd,当有客户连接到 ftp 的端口时,守护进程就会启动它。
为了激活服务配置的修改,你需要编辑xineta的配置文件,然后发送一个挂起信号给守护进程,但我们建议你使用一种更加友好的方式来配置服务。为了允许time-of-day客户进行连接,你可以使用Linux系统提供的工具来启用 daytime 服务。对于 SUSE 和 openSUSE 系统来说,你可以通过 SUSE 控制中心来配置服务。Red Hat的版本(包括企业版Linux和Fedora)也有一个类似的配置界面。对于使用 inetd 而不是 xineta 的系统来说,下面是从 ineta 的配置文件 /etc/inetd.conf 中提取的完成相同功能的配置,ineta 使用该配置文件来决定运行哪些服务器:
#
# <service name> <sock type> <proto> <flags> <user> <server_path> <args>
#
# Echo, discard, daytime, and chargen are used primarily for testing.
#
daytime stream tcp nowait root internal
daytime dgram udp wait root internal
#
# These are standard services.
#
ftp stream tcp nowait root /usr/sbin/tcpd /usr/sbin/wu.ftpd
telnet stream udp nowait root /usr/sbin/tcpd /usr/sbin/in.telnetd
#
# End of inetd.conf
注意,在本例中,ftp服务是由外部程序 wu.ftpd 提供的。如果你的系统运行着 inetd 进程,你可以通过编辑文件 /etc/inetd.conf (一行开头的#号表示这是一个注释行)再重新启动 inetd 进程的方法来改变提供的服务。你可以用 kill 命令向 ineta 进程发送一个挂起信号来重启该进程。为了方便执行这个操作,有的系统会配置成让 ineta 将它的进程号写入一个文件中。此外,你还可以使用 killall 命令,如下所示:
# killall -HUP inetd
三、Socket 选项
你可以用许多选项来控制套接字连接的行为,这些选项的数目众多,我们不可能在这里对它们一一解释。setsockopt 函数用于控制这些选项,它的定义如下所示:
#include <sys/socket.h>
int setsockopt(int socket,int level, int option nameconst void *option_value, size t option_len);
你可以在协议层次的不同级别对选项进行设置。如果想要在套接字级别设置选项,就必须将 level 参数设置为 SOL_SOCKET。如果想要在底层协议级别(如TCP、UDP等)设置选项,就必须将 level 参数设置为该协议的编号(可以通过头文件 netinet/in.h或函数 getprotobyname 来获得)。
option_name 参数指定要设置的选项; option_value参数的长度为option_len字节,它用于设置选项的新值,它被传递给底层协议的处理函数,并且不能被修改。在头文件 sys/socket.h 中定义的套接字级别选项,如下所示:
- SO_DEBUG 打开调试信息
- SO_KEEPALIVE 通过定期传输保持存活报文来维持连接
- SO_LINGER 在close调用返回之前完成传输工作
SO_DEBUG 和 SO_KEEPALIVE 用一个整数的 option_value 值来设置该选项的开(1)或关(0)。SO_LINGER需要使用一个在头文件 sys/socket.h 中定义的 linger 结构,来定义该选项的状态以及套接字关闭之前的拖延时间。
setsockopt 在成功时返回0,失败时返回-1。它的手册页介绍了更多的选项和错误。
四、多客户
用套接字来实现本地的和跨网络的客户/服务器系统,一旦连接建立,套接字连接的行为就类似于打开的底层文件描述符,而且在很多方面类似于双向管道。现在来考虑有多个客户同时连接一个服务器的情况。服务器程序在接受来自客户的一个新连接时,会创建出一个新的套接字,而原先的监听套接字将被保留以继续监听以后的连接。如果服务器不能立刻接受后来的连接,它们将被放到队列中以等待处理。原先的套接字仍然可用并且套接字的行为就像文件描述符,这一事实给我们提供了一种同时服务多个客户的方法。如果服务器调用 fork 为自己创建第二份副本,打开的套接字就将被新的子进程所继承。新的子进程可以和连接的客户进行通信,而主服务器进程可以继续接受以后的客户连接。这些改动对我们的服务器程序来说是非常容易的。因为我们创建子进程,但并不等待它们的完成,所以必须安排服务器忽略 SIGCHLD 信号以避免出现僵尸进程”。
实例:可以同时服务多个客户的服务器
/* This program, server4.c, begins in similar vein to our last server,
with the notable addition of an include for the signal.h header file.
The variables and the procedure of creating and naming a socket are the same. */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
/* Create a connection queue, ignore child exit details and wait for clients. */
listen(server_sockfd, 5);
signal(SIGCHLD, SIG_IGN);
while(1) {
char ch;
printf("server waiting\n");
/* Accept connection. */
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
/* Fork to create a process for this client and perform a test to see
whether we're the parent or the child. */
if(fork() == 0) {
/* If we're the child, we can now read/write to the client on client_sockfd.
The five second delay is just for this demonstration. */
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
exit(0);
}
/* Otherwise, we must be the parent and our work for this client is finished. */
else {
close(client_sockfd);
}
}
}
在处理客户请求时插入的5秒延迟是为了模拟服务器的计算时间或数据库访问时间。如果在前面的服务器中这样做,client3的每次运行都将花费5秒钟的时间。而新服务器可以同时处理多个 client3 程序,所花费的总时间将只有5秒钟多一点。
$ ./server4 &
[1] 26566
server waiting
$ ./client3 & ./client3 & ./client3 & ps x
[2] 26581
[3] 26582
[4] 26583
server waiting
server waiting
server waiting
... ...
【1】select系统调用
在编写Linux应用程序时,我们经常会遇到需要检查好几个输入的状态才能确定下一步行动的情况。例如,像终端仿真器这样的通信程序,需要有效地同时读取键盘和串行口。如果是在一个单用户系统中,运行一个“忙等待”循环还是可以接受的,它不停地扫描输入设备看是否有数据,如果有数据到达就读取它。但这种做法很消耗CPU的时间。
select 系统调用允许程序同时在多个底层文件描述符上等待输入的到达(或输出的完成)。这意味着终端仿真程序可以一直阻塞到有事情可做为止。类似地,服务器也可以通过同时在多个打开的套接字上等待请求到来的方法来处理多个客户。
select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合。有一组定义好的宏可以用来控制这些集合:
#include <sys/types.h>
#include <sys/time.h>
void FD_ZERO(fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
顾名思义,FD_ZERO 用于将 fd_set 初始化为空集合,FDSET 和 FD_CLR 分别用于在集合中设置和清除由参数 fd 传递的文件描述符。如果 FD_ISSET 宏中由参数指向的文件描述符是由参数 fdset 指向的 fd_set 集合中的一个元素,FD_ISSET 将返回非零值。fd_set 结构中可以容纳的文件描述符的最大数目由常量 FDSETSIZE 指定。select 函数还可以用一个超时值来防止无限期的阻塞。这个超时值由一个 timeval 结构给出。这个结构定义在头文件 sys/time.h 中,它由以下几个成员组成:
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
}
类型 time_t 在头文件 sys/types.h 中被定义为一个整数类型。
select系统调用的原型如下所示:
#include <sys/types.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
select 调用用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态,它将阻塞以等待某个文件描述符进入上述这些状态。
参数 nfds 指定需要测试的文件描述符数目,测试的描述符范围从 0 到 nfds-1。3个描述符集合都可以被设为空指针,这表示不执行相应的测试。
select函数会在发生以下情况时返回:readfds集合中有描述符可读、writefds集合中有描述符可写或errorfds集合中有描述符遇到错误条件。如果这3种情况都没有发生,select 将在 timeout 指定的超时时间经过后返回。如果 timeout 参数是一个空指针并且套接字上也没有任何活动,这个调用将一直阻塞下去。
当select返回时,描述符集合将被修改以指示哪些描述符正处于可读、可写或有错误的状态。可以用 FD_ISSET 对描述符进行测试,来找出需要注意的描述符。可以修改 timeout 值来表明剩余的超时时间,但这并不是在 X/Open 规范中定义的行为。如果 select 是因为超时而返回的话,所有描述符集合都将被清空。
select 调用返回状态发生变化的描述符总数。失败时它将返回 -1 并设置 errno 来描述错误。可能出现的错误有:EBADF(无效的描述符)、EINTR(因中断而返回)、EINVAL(nfds 或 timeout 取值错误)。
虽然Linux系统会把参数 timeout 指向的结构修改为剩余的超时时间,但大多数 UNIX 版本不会这样做。许多现有的使用 select 函数的代码在初始化 timeval 结构后,就一直使用它而不会重新初始化它的内容。但这些代码在 Linux 系统上可能会工作不正常,因为 Linux 会在每次 select 调用返回时修改 timeval 结构。如果你正在编写或移植使用 select 函数的代码,就需要注意这一区别,并且总是重新初始化 timeout。注意,这两种行为都是正确的,但它们确实不同!
实例:select系统调用
下面这个程序(select.c)演示了 select 函数的使用方法。这个程序读取键盘(即标准输入一文件描述符为),超时时间设为25秒。它只有在输入就绪时才读取键盘。它可以很容易地通过添加其他描述符(如串行线、管道、套接字等)进行扩展,具体做法取决于应用程序的需要。
/* Begin as usual with the includes and declarations
and then initialize inputs to handle input from the keyboard. */
#include <sys/types.h>
#include <sys/time.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
char buffer[128];
int result, nread;
fd_set inputs, testfds;
struct timeval timeout;
FD_ZERO(&inputs);
FD_SET(0,&inputs);
/* Wait for input on stdin for a maximum of 2.5 seconds. */
while(1) {
testfds = inputs;
timeout.tv_sec = 2;
timeout.tv_usec = 500000;
result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, &timeout);
/* After this time, we test result. If there has been no input, the program loops again.
If there has been an error, the program exits. */
switch(result) {
case 0:
printf("timeout\n");
break;
case -1:
perror("select");
exit(1);
/* If, during the wait, we have some action on the file descriptor,
we read the input on stdin and echo it whenever an <end of line> character is received,
until that input is Ctrl-D. */
default:
if(FD_ISSET(0,&testfds)) {
ioctl(0,FIONREAD,&nread);
if(nread == 0) {
printf("keyboard done\n");
exit(0);
}
nread = read(0,buffer,nread);
buffer[nread] = 0;
printf("read %d from keyboard: %s", nread, buffer);
}
break;
}
}
}
运行这个程序时,它会每隔2.5秒打印一个 timeout。如果在键盘上敲入字符,它就会从标准输入读取数据并报告敲入的内容。对大多数 shell 来说,输入会在用户按下回车键或某个控制序列时被发送给程序,所以这个程序将在你按下回车键时把输入内容显示出来。注意,回车键本身也像其他字符一样被读取和处理(你可以尝试不按下回车键,而是在敲入几个字符后按下组合键Ctl+D,看看会怎么样)。
$ ./select
timeout
hello
read 6 from keyboard: hello
fred
read 5 from keyboard: fred
timeout
^D
keyboard done
实验解析
这个程序用 select 调用来检查标准输入的状态。程序通过事先安排的超时时间每隔2.5秒打印一个 timeout 信息,这是通过 select 调用返回0来判断的。在文件的结尾,标准输入描述符被标记为可读,但没有字符可以读取。
【2】多客户
我们的简单服务器程序可以从 select 调用中获得益处,通过用 select 调用来同时处理多个客户就无需再依赖于子进程了。但在把这个技巧应用到实际的应用程序中时,你必须要注意,不能在处理第一个连接的客户时让其他客户等太长的时间。
服务器可以让select调用同时检查监听套接字和客户的连接套接字。一旦select调用指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是哪个上面有活动发生。
如果是监听套接字可读,这说明正有一个客户试图建立连接,此时就可以调用accept而不用担心发生阻塞的可能。如果是某个客户描述符准备好,这说明该描述符上有一个客户请求需要我们读取和处理。如果读操作返回零字节,这表示有一个客户进程已结束,你可以关闭该套接字并把它从描述符集合中删除。
实例:一个改进的多客户/服务器
/* For our final example, server5.c,
we include the sys/time.h and sys/ioctl.h headers in place of signal.h
in our last program and declare some extra variables to deal with select. */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
int result;
fd_set readfds, testfds;
/* Create and name a socket for the server. */
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
/* Create a connection queue and initialize readfds to handle input from server_sockfd. */
listen(server_sockfd, 5);
FD_ZERO(&readfds);
FD_SET(server_sockfd, &readfds);
/* Now wait for clients and requests.
Since we have passed a null pointer as the timeout parameter, no timeout will occur.
The program will exit and report an error if select returns a value of less than 1. */
while(1) {
char ch;
int fd;
int nread;
testfds = readfds;
printf("server waiting\n");
result = select(FD_SETSIZE, &testfds, (fd_set *)0,
(fd_set *)0, (struct timeval *) 0);
if(result < 1) {
perror("server5");
exit(1);
}
/* Once we know we've got activity,
we find which descriptor it's on by checking each in turn using FD_ISSET. */
for(fd = 0; fd < FD_SETSIZE; fd++) {
if(FD_ISSET(fd,&testfds)) {
/* If the activity is on server_sockfd, it must be a request for a new connection
and we add the associated client_sockfd to the descriptor set. */
if(fd == server_sockfd) {
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
FD_SET(client_sockfd, &readfds);
printf("adding client on fd %d\n", client_sockfd);
}
/* If it isn't the server, it must be client activity.
If close is received, the client has gone away and we remove it from the descriptor set.
Otherwise, we 'serve' the client as in the previous examples. */
else {
ioctl(fd, FIONREAD, &nread);
if(nread == 0) {
close(fd);
FD_CLR(fd, &readfds);
printf("removing client on fd %d\n", fd);
}
else {
read(fd, &ch, 1);
sleep(5);
printf("serving client on fd %d\n", fd);
ch++;
write(fd, &ch, 1);
}
}
}
}
}
}
在实际应用的程序中,最好用一个变量来专门保存已连接套接字的最大文件描述符号(它不一定是最新连接的套接字文件描述符号)。这可以避免循环检查数千个其实并未连接的套接字,它们根本不可能处于可读状态。出于简洁和让代码易于理解的目的,我们在这里没有这样做。
运行服务器的这个版本时,它将在一个进程中对多个客户依次进行处理。
$ ./server5 &
[1] 26686
server waiting
$ ./client3 & ./client3 & ./client3 & ps x
[2] 26689
[3] 26690
adding client on fd 4
server waiting
[4] 26691
... ...
五、数据报
编写与客户之间维持连接的应用程序,用面向连接的TCP套接字来完成这一工作。但在有些情况下,在程序中花费时间来建立和维持一个套接字连接是不必要的。比如程序 getdate.c 中所使用的 daytime 服务就是一个很好的例子,我们首先创建一个套接字,然后建立连接,读取一个响应,最后关闭连接。在这一过程中,我们使用了很多操作步骤,仅仅为了获取一个日期。
daytime服务还可以用数据报通过UDP来访问。为了访问它,发送一个数据报给该服务,然后在响应中获取一个包含日期和时间的数据报。这一过程非常简单。当客户需要发送一个短小的查询请求给服务器,并且期望接收到一个短小的响应时,我们一般就使用由UDP提供的服务。如果服务器处理客户请求的时间足够短,服务器就可以通过一次处理一个客户请求的方式来提供服务,从而允许操作系统将客户进入的请求放入队列。这简化了服务器程序的编写。
因为 UDP 提供的是不可靠服务,所以你可能发现数据报或响应会丢失。如果数据对于你来说非常重要,就需要小心编写 UDP 客户程序,以检查错误并在必要时重传。实际上,UDP数据报在局域网中是非常可靠的。为了访问由UDP提供的服务,你需要像以前一样使用套接字和 close 系统调用,但你需要用两个数据报专用的系统调用 sendto 和 recvfrom 来代替原来使用在套接字上的 read 和 write 调用。下面是一个修改过的 getdate.c 版本,它通过 UDP 数据报服务来获取数据。对先前版本的改动将以阴影显示。
/* Start with the usual includes and declarations. */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *host;
int sockfd;
int len, result;
struct sockaddr_in address;
struct hostent *hostinfo;
struct servent *servinfo;
char buffer[128];
if(argc == 1)
host = "localhost";
else
host = argv[1];
/* Find the host address and report an error if none is found. */
hostinfo = gethostbyname(host);
if(!hostinfo) {
fprintf(stderr, "no host: %s\n", host);
exit(1);
}
/* Check that the daytime service exists on the host. */
servinfo = getservbyname("daytime", "udp");
if(!servinfo) {
fprintf(stderr,"no daytime service\n");
exit(1);
}
printf("daytime port is %d\n", ntohs(servinfo -> s_port));
/* Create a UDP socket. */
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
/* Construct the address for use with sendto/recvfrom... */
address.sin_family = AF_INET;
address.sin_port = servinfo -> s_port;
address.sin_addr = *(struct in_addr *)*hostinfo -> h_addr_list;
len = sizeof(address);
result = sendto(sockfd, buffer, 1, 0, (struct sockaddr *)&address, len);
result = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&address, &len);
buffer[result] = '\0';
printf("read %d bytes: %s", result, buffer);
close(sockfd);
exit(0);
}
如你所见,需要改动的地方非常少。像以前一样,我们用 getservbyname 来查找 daytime 服务,但通过请求 UDP 协议来指定数据报服务。我们使用带有 SOCK_DGRAM 参数的 socket 调用来创建一个数据报套接字。我们还是采用与以前一样的方式来构建目标地址,但现在需要发送一个数据报而不是仅仅从套接字上读取数据。
因为我们并没有明确地建立一条到指定 UDP 服务的连接,所以必须用某些方式让服务器知道你需要接收一个响应。在本例中,给服务器发送一个数据报(在这里,从准备接收响应的缓存区中发送一个字节的数据),它返回包含日期和时间的响应。sendto 系统调用从 buffer 缓存区中给使用指定套接字地址的目标服务器发送一个数据报。它的原型如下所示;
int sendto(int sockfd,void *buffer,size_t len,int flags,struct sockaddr *to,socklen_t tolen);
在正常应用中,flags参数一般被设置为0。
recvfrom系统调用在套接字上等待从特定地址到来的数据报,并将它放入buffer缓存区。它的原型如下所示:
int recvfrom(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
同样,在正常应用中,flags参数一般被设置为0。为了让示例程序变得简短,我们省略了错误处理。当错误发生时,sendto 和 recvfrom 都将返回 -1 并设置 errno。可能的错误如下:
- EBADF 传递了一个无效的文件描述符
- EINTR 产生一个信号
除非用 fcnt1 将套接字设置为非阻塞方式,否则 recvfrom 调用将一直阻塞。我们可以用与前面的面向连接服务器一样的方式,通过select调用和超时设置来判断是否有数据到达套接字。此外,还可以用 alarm 时钟信号来中断一个接收操作。
六、总结
了解了另一种进程间通信的方法:套接字。通过它可以开发出真正可以跨网络运行的分布式客户/服务器应用程序。我们简要介绍了一些主机数据库信息函数以及Linux是如何使用因特网守护进程来处理标准系统服务的。我们开发了几个客户/服务器示例程序来演示网络和多客户处理方法。
最后,我们介绍了select系统调用,它允许一个程序同时在多个打开的文件描述符和套接字上等待输入和输出活动的发生。