Socket

Linux系统支持套接字接口。你可以通过与使用管道类似的方法来使用套接字,但套接字还包括了计算机网络中的通信。一台机器上的进程可以使用套接字和另外一台机器上的进程通信,这样就可以支持分布在网络中的客户/服务器系统。同一台机器上的进程之间也可以使用套接字进行通信。

此外,微软的Windows系统也通过可公开获取的Windows Sockets技术规范(简称WinSock)实现了套接字接口。Windows系统的套接字服务是由系统文件winsock.dll来提供的。因此,Windows程序可以通过网络和Linux/Unix计算机进行通信来实现客户/服务器系统,反之亦然。虽然WinSock的编程接口和Unix套接字不尽相同,但它同样是以套接字为基础的。

下面介绍以下内容:

套接字连接的工作原理

套接字的属性、地址和通信

网络信息和互联网守护进程(xinetd)

客户和服务器


什么是套接字

套接字(socket)是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。

套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分开来。套接字机制可以实现多个客户连接到一个服务器。


213841431.jpg


实验:一个简单的本地客户

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch='A';
//为客户创建一个套接字
sockfd=socket(AF_UNIX,SOCK_STREAM,0);
//根据服务器的情况给套接字命名
address.sun_family=AF_UNIX;
strcpy(address.sun_path,"server_socket");
len=sizeof(address);
//将我们的套接字连接到服务器的套接字
result=connect(sockfd,(struct sockaddr *)&address,len);
if(result==-1)
{
perror("connect failed");
exit(1);
}
//现在就可以通过sockfd进行读写操作了
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
exit(0);
}
[root@localhost linux_C]# ./client1
connect failed: No such file or directory

运行这个程序时,它会失败,因为你还没有创建服务器端的命名套接字(具体的错误信息将随系统的不同而不同)。


实验:一个简单的本地服务器

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
//删除以前的套接字,为服务器创建一个未命名的套接字:
unlink("server_socket");
server_sockfd=socket(AF_UNIX,SOCK_STREAM,0);
//命名套接字:
server_address.sun_family=AF_UNIX;
strcpy(server_address.sun_path,"server_socket");
server_len=sizeof(server_address);
bind(server_sockfd,(struct sockaddr *)&server_address,server_len);
//创建一个连接队列,开始等待客户进行连接:
listen(server_sockfd,5);
while(1)
{
char ch;
printf("server waiting\n");
//接受一个连接:
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
//对client_sockfd套接字上的客户进行读写操作:
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
exit(0);
}


实验解析:

运行服务器程序时,它创建一个套接字并开始等待客户的连接。如果你在后台启动它,让它独立地运行,就可以在前台启动客户程序。

[root@localhost linux_C]# ./server1 &
[4] 26861
[root@localhost linux_C]# server waiting
./client1 1
server waiting
char from server = B

服务器在开始等待客户连接时会打印出一条消息。服务器等待的时是一个文件系统套接字,所以可以用普通的ls命令来看到它。

[root@localhost linux_C]# ls -lF server_socket
srwxr-xr-x. 1 root root 0 Aug 27 18:44 server_socket=

访问权限前面的字母s和这一行末尾的等号=表示该设备的类型是套接字。套接字的创建过程于普通文件一样,它的访问权限会被当前的掩码值所修改。如果使用ps命令,你可以看到服务器

正运行在后台。

[root@localhost linux_C]# ps lx | grep "server1" | grep -v "grep server1"
0     0 27089 27063  20   0   3920   388 skb_re S    pts/0      0:00 ./server1

它目前处于休眠状态,因此没有消耗CPU资源。


套接字属性

套接字的特性由3个属性确定,它们是:域(domain)、类型(type)和协议(protocol)。套接字还用地址作为它的名字。地址的格式随域的不同而不同。每个协议族又可以使用一个或多个地址族来定义地址格式。

1 套接字的域

域指定套接字通信中使用的网络介质。最常见的套接字域是AF_INET,它指的是Internet网络,许多Linux局域网使用的都是该网络,当然因特网自身用的也是它。其底层的协议-网际协议(IP)只有一个地址族,它使用一种特定的方式来指定网络中的计算机,即人们常说的IP地址。

虽然我们几乎总是用域名来指定因特网上的联网机器,但它们都会被转换为底层的IP地址。例如192.168.1.99就是一个地址。所有的IP地址都用4个数字来表示,每个数字都小于256。

第一个例子中的域是UNIX文件系统域AF_UNIX,即使是一台还未联网的计算机的套接字也可以使用这个域。这个域的底层协议就是文件输入/输出,而它的地址就是文件名。我们的服务器套接字的地址是server_socket,当我们运行服务器程序时,就可以在当前目录下看到这个地址。

其他可以使用的域还包括:基于ISO标准协议的网络所使用的AF_ISO域和用于Xerox网络系统的AF_XNS域。


2 套接字类型

一个套接字域可能有多种不同的通信方式,而每种通信方式又有其不同的特性。但AF_UNIX域的套接字没有这样的问题,它们提供了一个可靠的双向通信路径。在网络域中,我们就需要注意底层网络的特性,以及不同的通信机制是如何受到它们的影响的。

因特网协议提供了两种通信机制:流(stream)和数据报(datagram)。它们有着截然不同的服务层次。


流套接字(在某些方面类似于标准的输入/输出流)提供的是一个有序、可靠、双向字节流的连接。因此,发送的数据可以确保不会丢失、复制或乱序到达,并且在这一过程中发生的错误也不会显示出来。大的消息将被分片、传输、再重组。这很像一个文件流,它接收大量的数据,然后以小数据块的形式将它们写入底层磁盘。流套接字的行为是可预见的。

流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现的。它们也是AF_UNIX域中常用的套接字类型。它们在编写网络程序时是最常用的。


数据报套接字与流套接字相反,由类型SOCK_DGRAM指定的数据报套接字不建立和维持一个连接。它对可以发送的数据报的长度有限制。数据报作为一个单独的网络消息被传输,它可能会丢失、复制或乱序到达。

数据报套接字是在AF_INET域中通过UDP/IP连接实现的,它提供的是一种无序的不可靠服务。但从资源的角度来看,相对来说它们开销比较小,因为不需要维持网络连接。而且因为无需花费时间来建立连接,所以它们的速度也很快。

数据报适用于信息服务中的“单次”查询,它主要用来提供日常状态信息或执行低优先级的日志记录。它的有点是服务器的崩溃不会给客户造成不便,也不会要求客户重启,因为基于数据报的服务器通常不会保留连接信息,所以它们可以在不打扰其他客户的前提下停止并重启。


3 套接字协议

只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议。下面重点讨论UNIX网络套接字和文件系统套接字,它们不需要你选择一个特定的协议,只需要使用其默认值即可。


创建套接字

socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain,int type,int protocol);

创建的套接字是一条通信线路的一个端点。domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议。

domain参数可以指定的协议族如下表所示:

域 说明

AF_UNIX UNIX域协议(文件系统套接字)

AF_INET ARPA因特网协议(UNIX网络套接字)

AF_ISO ISO标准协议

AF_NS Xerox网络系统协议

AF_IPX Novell IPX协议

AF_APPLETALK Appletalk DDS



最常用的套接字域是AF_UNIX和AF_INET,前者用于通过UNIX和Linux文件系统实现的本地套接字,后者用于UNIX网络套接字。AF_INET套接字可以用于通过包括因特网在内的TCP/IP网络进行通信的程序。微软Windows系统的Winsock接口也提供了对这个套接字域的访问功能。

socket函数的参数type指定用于新套接字的通信特性。它的取值包括SOCK_STREAM和SOCK_DGRAM。

SOCK_STREAM是一个有序、可靠、面向连接的双向字节流。对AF_INET域套接字来说,它默认是通过一个TCP连接来提供这一特性的,TCP连接在两个流套接字端点之间建立。数据可以通过套接字连接进行双向传递。TCP协议所提供的机制可以用于分片和重组长消息,并且可以重传可能在网络中丢失的数据。

SOCK_DGRAM是数据报服务。我们可以用它来发送最大长度固定(通常比较小)的消息。但消息是否会被正确传递或消息是否不会乱序到达并没有保证。对于AF_INET域套接字来说,这种类型的通信是由UDP数据报来提供的。

通信所用的协议一般由套接字类型和套接字域来决定,通常不需要选择。只有当需要选择时,我们才会用到protocol参数。将该参数设置为0表示使用默认协议。

socket系统调用返回一个描述符,它在许多方面都类似于底层的文件描述符。当这个套接字连接到另一端的套接字后,我们就可以用read和write系统调用,通过这个描述符来在套接字上发送和接收数据了。close系统调用用于结束套接字连接。


套接字地址

每个套接字域都有自己的地址格式。对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述,该结构定义在头文件sys/un.h中。

struct sockaddr_un{

sa_family_t sun_family; /* AF_UNIX */

char sun_path[]; /* pathname */

}

因此,对套接字进行处理的系统调用可能需要接受不同类型的地址,每种地址格式都使用一种类似的结构来描述,它们都以一个指定地址类型(套接字域)的成员开始。

在当前linux系统中,由X/Open规范定义的类型sa_family_t在头文件sys/un.h中声明,它是短整数类型。此外sun_path指定的路径名长度也是有限制的(Linux规定的是108个字符,其他系统可能使用的是更清楚的常量,如UNIX_MAX_PATH)。因为地址结构的长度不一致,所以许多套接字调用需要用到一个用来复制特定地址结构的长度变量或将它作为一个输出。

在AF_INET域中,套接字地址由结构sockaddr_in来指定,该结构定义在头文件netinet/in.h中,它至少包含以下几个成员:

struct sockaddr_in{

short int sin_family; /*AF_INET*/

unsigned short int sin_port; /*Port number*/

struct in_addr sin_addr; /*Internet address*/

};

IP地址结构in_addr被定义为:

struct in_addr{

unsigned long int s_addr;

}

一个AF_INET套接字由它的域、IP地址和端口号来完全确定。从应用程序的角度来看,所有套接字的行为就像文件描述符一样,并且通过一个唯一的整数值来区分。


命名套接字

要想让通过socket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名。这样,AF_UNIX套接字就会关联到一个文件系统的路径名,正如你在server1例子中所看到的。AF_INET套接字就会关联到一个IP端口号。

#include <sys/socket.h>

int bind(int socket,const struct sockaddr * address,size_t address_len);

bind系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字。地址结构的长度由参数address_len传递。

地址的长度和格式取决于地址族。bind调用需要将一个特定的地址结构指针转换为指向通用地址类型(struct sockaddr *)。

bind调用在成功时返回0,失败时返回-1并设置errno为下表中的值

errno值
说明
EBADF
文件描述符无效
ENOTSOCK
文件描述符对应的不是一个套接字
EINVAL
文件描述符对应的是一个已命名的套接字
EADDRNOTAVAIL
地址不可用
EADDRINUSE
地址已经绑定了一个套接字

AF_UNIX域套接字还有其他一些错误代码,如下表所示

errno值
说明
EACCESS
因为权限不足,不能创建文件系统的路径名
ENOTDIR、ENAMETOOLONG
表明选择的文件名不符合要求


创建套接字队列

为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。它用listen系统调用来完成这一工作。

#include <sys/socket.h>

int listen(int socket,int backlog);

Linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制。为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值。在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字。再往后的连接将被拒绝,导致客户的连接请求失败。listen函数提供的这种机制允许当服务器程序正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理。backlog参数常用的值是5。

listen函数在成功时返回0,失败时返回-1。错误代码包括EBADF、EINVAL和ENOTSOCK,其含义与上面bind系统调用中说明的一样。


接受连接

一旦服务器程序创建并命名了套接字之后,它就可以通过accept系统调用来等待客户建立对该套接字的连接。

#include <sys/socket.h>

int accept(int socket,struct sockaddr * address,size_t * address_len);

accept系统调用只有当有客户程序试图连接到由socket参数指定的套接字上时才返回。这里的客户是指,在套接字队列中排在第一个的未处理连接。accept函数将创建一个新套接字来与该客户进行通信,并且返回套接字的描述符。新套接字的类型和服务器监听套接字类型是一样的。

套接字必须事先由bind调用命名,并且由listen调用给它分配一个连接队列。连接客户的地址将被放入address参数指向的sockaddr结构中。如果我们不关心客户的地址,也可以将address参数指定为空指针。

参数address_len指定客户结构的长度。如果客户地址的长度超过这个值,它将被截断。所以在调用accept之前,address_len必须被设置为预期的地址长度。当这个调用返回时,address_len将被设置为连接客户地址结构的实际长度。
如果套接字队列中没有未处理的连接,accept将阻塞(程序将暂停)直到有客户建立连接为止。我们可以通过对套接字文件描述符设置O_NONBLOCK标志来改变这一行为,使用的函数是fcntl,如下所示:

int flags=fcntl(socket,F_GETFL,0);

fcntl(socket,F_SETFL,O_NONBLOCK|flags);

当由未处理的客户连接时,accept函数返回一个新的套接字文件描述符。发生错误时,accept函数返回-1。可能的错误情况大部分与bind、listen调用类似,其他的错误有EWOULDBLOCK和EINTR。前者是当指定了O_NONBLOCK标志,但队列中没有未处理连接时产生的错误。后者是当进程阻塞在accept调用时,执行被中断而产生的错误。

请求连接

客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器。它们通过connect调用来完成这一工作。

#include <sys/socket.h>

int connect(int socket,const struct sockaddr * address, size_t address_len);

参数socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度由参数address_len指定。参数socket指定的套接字必须是通过socket调用获得的一个有效的文件描述符。

成功时,connec调用返回0,失败时返回-1。 可能的错误代码如下:

errno值 说明
EBADF 传递给socket参数的文件描述符无效
EALREADY 该套接字上已经有一个正在进行中的连接
ETIMEDOUT 连接超时
ECONNREFUSED 连接请求被服务器拒绝

如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间。一旦这个超时时间到达,连接将被放弃,connect调用失败。但如果connect调用被一个信号中断,而该信号又得到了处理,connect调用还是会失败(errno被设置为EINTR),但连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立。

与accept调用一样,connect调用的阻塞特性可以通过设置该文件描述的O_NONBLOCK标志来改变。此时,如果连接不能立刻建立,connect将失败并把errno设置为EINPROGRESS,而连接将以异步方式继续进行。

虽然异步连接难于处理,但我们可以在套接字文件描述符上,用select调用来检查套接字是否已处于写就绪状态。


关闭套接字

你可以通过调用close函数来终止服务器和客户上的套接字连接,就如同对底层文件描述符进行关闭一样。你应该总是在连接的两端都关闭套接字。对于服务器来说,应该在read调用返回0时关闭套接字,但如果套接字是一个面向连接的,并且设置了SOCK_LINGER选项,close调用会在该套接字还有为传输数据时阻塞。


套接字通信

文件系统套接字的缺点是,除非程序员使用一个绝对路径名,否则套接字将创建在服务器程序的当前目录下。为了让他更具通用型,你需要将它创建在一个服务器及其客户都认可的可全局访问的目录(如/tmp目录)中。而对网络套接字来说,你只需要选择一个未被使用的端口号即可。

小于1024的端口号是为系统使用保留的。其它端口号及通过他们提供的服务通常都列在系统文件/etc/services中。编写基于套接字的应用程序时,请注意总要选择没有列在该配置文件中的端口号。

我们将在局域网中运行我们的客户和服务器,但网络套接字不仅可用于局域网,任何带有因特网连接(即使是一个调职解调器拨号连接)的及其都可以使用网络套接字来彼此通信。甚至可以在一台UNIX单机上运行基于网络的程序,因为UNIX计算机通常会配置了一个只包含它自身的回路(loopback)网络。回路网络对调试网络应用程序也很有用,因为它排除了任何外部网络问题。

回路网络中只包含一台计算机,传统上被称为localhost,它有一个标准的IP地址127.0.0.1。你可以在网络主机文件/etc/hosts中找到它的地址,在该文件中还列出了在共享网络中的其他主机的名字和对应的地址。

每个与计算机进行通信的网络都有一个与之关联的硬件接口。一台计算机可能在每个网络中都有一个不同的网络名,当然也就会有几个不同的IP地址。


实验:网络客户

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv) {
     int sockfd;
     int len;
     struct sockaddr_in address;
     int result;
     char ch='A';
     /*为客户创建一个套接字*/
     sockfd=socket(AF_INET,SOCK_STREAM,0);
     /*命名套接字,与服务器保持一致*/
     address.sin_family=AF_INET;
     address.sin_addr.s_addr=inet_addr("127.0.0.1");
     address.sin_port=8888;
     len=sizeof(address);
     /*将我们的套接字连接到服务器的套接字*/
     result=connect(sockfd,(struct sockaddr *)&address,len);
     if(result==-1)
     {
         perror("connect failed");
         exit(1);
     }
     /*可以通过sockfd进行读写操作了*/
     write(sockfd,&ch,1);
     read(sockfd,&ch,1);
     printf("char from server = %c\n",ch);
     close(sockfd);
     exit(0);
}


运行:

[root@localhost linux_C]# ./client2
connect failed: Connection refused

因为还没有服务器运行在这台计算机的8888端口上。


实验解析:

客户程序用在头文件netinet/in.h中定义的sockaddr_in结构指定了一个AF_INET地址。它试图连接到IP地址为127.0.0.1的主机上的服务器。它用inet_addr函数将IP地址的文本表示方式转换为符合套接字地址要求的格式。



实验:网络服务器

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv) {
    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=inet_addr("127.0.0.1");
    server_address.sin_port=8888;
    server_len=sizeof(server_address);
    bind(server_sockfd,(struct sockaddr *)&server_address,server_len);
    /*创建一个连接队列,开始等待客户进行连接*/
    listen(server_sockfd,5);
    while(1)
    {
       char ch;
       printf("server waiting\n");
       /*接受一个连接*/
       client_len=sizeof(client_address);
       client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
       /*对client_sockfd套接字上的客户进行读写操作*/
       read(client_sockfd,&ch,1);
       ch++;
       write(client_sockfd,&ch,1);
       close(client_sockfd);
    }
}


编译:

[root@localhost linux_C]# gcc -o server2 shm2.c



运行:

[root@localhost linux_C]# ./server2 &
[3] 19512
[root@localhost linux_C]# server waiting
./client2
server waiting
char from server = B


实验解析:

服务器程序创建一个AF_INET域的套接字,并安排在它之上接受连接。这个套接字被绑定到你选择的端口。指定的地址决定了允许建立连接的计算机。通过指定像客户程序中一样的回路地址,你就把通信限制在本地主机上。

如果想允许服务器和远程客户进行通信,就必须指定一组你允许连接的IP地址。你可以用特殊值INADDR_ANY来表示,你将接受来自计算机任何网络接口的连接。如果你喜欢,还可以通过分离内部局域网和外部广域网连接的方式来区分不同的网络接口。INADDR_ANY是一个32位的整数值,它可以用在地址结构的sin_addr.s_addr域中。



主机字节序和网络字节序

当在基于Intel处理器的linux机器上运行新版本的服务器和客户程序时,我们可以用netstat命令来查看网络连接状况。它显示了客户/服务器连接正在等待关闭。连接将在一小段超时时间之后关闭。


[root@localhost linux_C]# ./server2 & ./client2
[4] 19825
server waiting
server waiting
char from server = B
[root@localhost linux_C]# netstat -A inet
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address               Foreign Address             State
tcp        1      0 192.168.146.146:49150       a96-7-54-98.deploy.aka:http CLOSE_WAIT
tcp        0      0 localhost:47138             localhost:36630             TIME_WAIT

可以看到这条连接对应的服务器和客户的端口号,local address一栏显示的是服务器,而foreign address一栏显示的是远程客户(即使是在同一台机器上,它仍然是通过网络连接的)。为了确保所有套接字都是不同的,这些客户端口一般都与服务器监听套接字不同,并且在这台计算机上是唯一的。

可是,显示的本地地址(服务器套接字)端口是1574(或者你可能会看到显示的是一个服务名),而我们选择的端口是8888。为什么会不一样呢?答案是,通过套接字接口传递的端口号和地址都是二进制数字。不同的计算机使用不同的字节序来表示整数。例如,Intel处理器将32位的整数分为4个连续的字节,并以字节序1-2-3-4存储到内存中,这里的1表示最高位的字节。而IBM PowerPC处理器是以字节序4-3-2-1的方式来存储整数。如果保存整数的内存只是以逐个字节的方式来复制,两个不同的计算机得到的整数值就会不一致。

为了使不同类型的计算机可以就通过网络传输的多字节整数的值达成一致,你需要定义一个网络字节序。客户和服务器程序必须在传输之前,将它们的内部整数表示方式转换为网络字节序。它们通过定义在头文件netinet/in.h中的函数来完成这一工作。这些函数如下所示:

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);

unsigned short int htons(unsigned short int hostshort);

unsigned long int ntohl(unsigned long int netlong);

unsigned short int ntohs(unsigned short int netshort);

这些函数将16位和32位整数在主机字节序和标准的网络字节序之间进行转换。函数名是与之对应的转换操作的简写形式。例如:“host to network,long”(htonl,长整数从主机字节序到网络字节序的转换)和“host to network,short”(htons,短整数从主机字节序到网络字节序的转换)。如果计算机本身的主机字节序与网络字节序相同,这些函数的内容实际上就是空操作。

为了保证16位的端口号有正确的字节序,你的服务器和客户端要用这些函数来转换端口地址。新服务器程序server3.c中的改动是:

server_address.sin_addr.s_addr=htonl(INADDR_ANY);

server_address.sin_port=htons(8888);

你不需要对函数调用inet_addr("127.0.0.1")进行转换,因为inet_addr已被定义为产生一个网络字节序的结果。新客户程序client3.c中的改动是:

address.sin_port=htons(8888);

服务器也做了改动,通过用INADDR_ANY来允许到达服务器任一网络接口的连接。

现在,运行server3和client3时,你将看到本地连接使用的是正确的端口。



网络信息

我们的客户和服务器程序一直是把地址和端口号编译到它们自己的内部。对于一个更通用的服务器和客户程序来说,我们可以通过网络信息函数来决定应该使用的地址和端口。

如果你有足够的权限,也可以将自己的服务添加到/etc/services文件中的已知服务列表中,并在 这个文件中为端口号分配一个名字,使用户可以使用符号化的服务名而不是端口号的数字。

如果给定一个计算机的名字,你可以通过调用解析地址的主机数据库函数来确定它的IP地址。这些函数通过查询网络配置文件来完成这一工作,如/etc/hosts文件或网络信息服务。常用的网络信息服务有NIS(Network Information Service,网络信息服务)和DNS(Domain Name Service,域名服务)。

主机数据库函数在接口头文件netdb.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 aliased(nicknames)*/

int h_addrtype; /*address type*/

int h_length; /*length in bytes of the address*/

char ** h_addr_list /*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类型的UDP数据报。

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*/

}

如果想获得某台计算机的主机数据库信息,可以调用gethostbyname函数并且将结果打印出来。注意,要把返回的地址列表转换为正确的地址类型,并用函数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用来获取一台主机的有关信息。

#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;
    /*把host变量设置为getname程序所提供的命令行参数,或默认设置为用户主机的主机名*/
   if(argc==1)
   {
      char myname[256];
      gethostname(myname,255);
      host=myname;
   }
   else {
     host=argv[1];
}
   /*调用gethostbyname,如果未找到相应的信息就报告一条错误:*/
   hostinfo=gethostbyname(host);
   if(!hostinfo)
   {
      fprintf(stderr,"cannot get info for host:%s\n",host);
      exit(1);
   }
   /*显示主机名和它可能有的所有别名*/
   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");
   /*如果查询的主机不是一个IP主机,就发出警告并退出:*/
   if(hostinfo->h_addrtype!=AF_INET)
   {
      fprintf(stderr,"not an IP host!\n");
      exit(1);
   }
   /*否则,显示它的所有IP地址*/
   addrs=hostinfo->h_addr_list;
   while(*addrs)
   {
      printf("%s",inet_ntoa(*(struct in_addr *)*addrs));
      addrs++;
   }
  printf("\n");
  exit(0);
}

可以用gethostdbyaddr函数来查出哪个主机拥有给定的IP地址。你可以服务器上用这个函数来查找连接客户的来源。


实验解析:

getname程序通过调用gethostbyname从主机数据库中取出主机的信息。它打印出主机名、它的别名和该主机在它的网络接口上使用的IP地址。



运行:

[root@localhost linux_C]# ./getname localhost
results for host localhost:
Name:localhost
Aliases:localhost.localdomainlocalhost4localhost4.localdomain4localhost.localdomainlocalhost6localhost6.localdomain6
127.0.0.1127.0.0.1


现在可以改进客户程序,使它可以连接到任何有名字的主机。这次不是连接到我们的示例服务器,而是连接到一个标准服务,这样就可以演示端口号的提取操作了。

大多数UNIX和一些Linux系统都有一项标准服务daytime,它提供系统的日期和时间。客户可以连接到这个服务来查看服务器的当前日期和时间。


实验:连接到标准服务

#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];
}
  /*查找主机的地址,如果找不到,就报告一条错误*/
    hostinfo=gethostbyname(host);
    if(!hostinfo)
    {
        fprintf(stderr,"no host %s\n",host);
        exit(1);
    }
    /*检查主机上是否有daytime服务*/
   servinfo=getservbyname("daytime","tcp");
   if(!servinfo)
   {
       fprintf(stderr,"no daytime service\n");
       exit(1);
   }
   printf("daytime port is %d\n",ntohs(servinfo->s_port));
   /*创建一个套接字*/
   sockfd=socket(AF_INET,SOCK_STREAM,0);
   /*构造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);
  /*然后建立连接并取得有关信息*/
   result=connect(sockfd,(struct sockaddr *)&address,len);
   if(result==-1)
   {
      perror("connect failed");
      exit(1);
   }
   result=read(sockfd,buffer,sizeof(buffer));
   buffer[result]='\0';
   printf("read %d bytes:%s",result,buffer);
   close(sockfd);
   exit(0);
}


运行:

[root@localhost linux_C]# gcc -o getdate shm2.c
[root@localhost linux_C]# ./getdate
daytime port is 13
connect failed: Connection refused

这里是因为你的计算机没有启用daytime服务。



因特网守护进程(xinetd/inetd)

UNIX系统通常以超级服务器的方式来提供多项网络服务。超级服务器程序(因特网守护进程xinetd或inetd)同时监听许多端口地址上的连接。当有客户连接到某项服务时,守护程序就运行相应的服务器。这使得针对各项网络服务的服务器不需要一直运行着,它们可以在需要时启动。

因特网守护进程在现代linux系统中是通过xinetd来实现的。xinetd实现方式取代了原来的UNIX程序inetd。

我们通常是通过一个图形用户界面来配置xinetd以管理网络服务,但我们也可以直接修改它的配置文件。它的配置文件通常是/etc/xinetd.conf和/etc/xinetd.d目录中的文件。

每一个由xinetd提供的服务都在/etc/xinetd.d目录中有一个对应的配置文件。xinetd将在其启动时或被要求的情况下读取所有这些配置文件。


套接字选项

setsockopt函数用于控制这些选项,它的定义如下所示:

#include <sys/socket.h>

int setsockopt(int socket,int level,int option_name,const 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信号以避免出现僵尸进程。


实验:可以同时服务多个客户的服务器

#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 argc, char **argv) {
  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(8888);
  server_len=sizeof(server_address);
  bind(server_sockfd,(struct sockaddr *)&server_address,server_len);
  /*创建一个连接队列,忽略子进程的退出细节,等待客户的到来*/
  listen(server_sockfd,5);
  signal(SIGCHLD,SIG_IGN);
  while(1)
  {
      char ch;
      printf("server waiting\n");
      /*接受连接*/
      client_len=sizeof(client_address);
      client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
      /*通过fork调用为这个客户创建一个子进程,然后测试你是在父进程中还是在子进程中:*/
      if(fork()==0)
      {
         /*如果你是在子进程中,就可以对client_sockfd上的客户执行读/写操作,5秒的延迟*/
         read(client_sockfd,&ch,1);
         sleep(5);
         ch++;
         write(client_sockfd,&ch,1);
         close(client_sockfd);
         exit(0);
      }
      /*否则,你一定是在父进程中,你只需关闭这个客户*/
      else {
       close(client_sockfd);
    }
  }
}

在处理客户请求时插入的5秒延迟是为了模拟服务器的计算时间或数据库访问时间。如果在前面的服务器中这样做,client3的每次运行都将花费5秒钟的时间。而新服务器可以同时处理多个client3程序,所花费的总时间将只有5秒钟多一点。


运行:

[root@localhost C_test]# ./server4
server waiting
server waiting
server waiting
server waiting


[root@localhost C_test]# time ./client2 & ./client2 & ./client2 &
[1] 43919
[2] 43920
[3] 43921
[root@localhost C_test]# char from server = B
char from server = B
char from server = B
real    0m5.004s
user    0m0.000s
sys 0m0.001s
[1]   Done                    time ./client2
[2]-  Done                    ./client2
[3]+  Done                    ./client2


实验解析:

服务器程序现在将创建一个新的子进程来处理每个客户,所以你将看到好几个服务器在等待消息,而主进程将继续等待新的连接。

服务器程序用fork函数来处理多个客户。但在数据库应用程序中,这可能不是最佳的解决方案。因为服务器程序可能会相当大,而且在数据库访问方面还存在者需要协调多个服务器副本的问题。事实上,我们需要的是如何让单个服务器进程在不阻塞、不等待客户请求到达的前提下处理多个客户。这个问题的解决方案涉及如何同时处理多个打开的文件描述符,并且它不仅仅局限于套接字应用程序,

请看下面的select系统调用。


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初始化为空集合,FD_SET和FD_CLR分别用于在集合设置和清除由参数fd传递的文件描述符。如果FD_ISSET宏中由参数fd指向的文件描述符是由参数fdset指向的fd_set集合中的一个元素,FD_ISSET将返回非零值。fd_set结构中可以容纳的文件描述符的最大数目由常量FD_SETSIZE指定。

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取值错误)。


实验:select系统调用

这个程序读取键盘(即标准输入--文件描述符为0),超时时间为2.5秒。它只有在输入就绪时才读取键盘。它可以很容易地通过添加其他描述符(如串行线、管道、套接字等)进行扩展,具体做法取决于应用程序的需要。

#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(int argc, char **argv) {
    char buffer[128];
    int result,nread;
    fd_set inputs,testfds;
    struct timeval timeout;
    FD_ZERO(&inputs);
    FD_SET(0,&inputs);
    /*在标准输入stdin上最多等待输入2.5秒*/
    while(1)
    {
          testfds=inputs;
          timeout.tv_sec=2;
          timeout.tv_usec=500000;
         result=select(FD_SETSIZE,&testfds,(fd_set *)NULL,(fd_set *)NULL,&timeout);
         /*经过这段时间后,我们对result进行测试。如果没有输入,
          * 程序将再次循环。如果出现一个错误,程序将退出运行*/
         switch(result)
         {
         case 0:
             printf("timeout\n");
             break;
         case -1:
             printf("select failed");
             exit(1);
         /*如果在等待期间,你对文件描述符采取了一些动作,程序就将读取标准输入stdin上的输入
          * 并在接收到行尾字符后把它们都回显到屏幕上,当你输入的字符是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来说,输入会在用户按下回车键或某个控制序列时被发送给程序,所以这个程序将在你按下回车键时把输入内容显示出来。回车键本身也像其他字符一样被读取和处理(你可以尝试不按下回车键,而是在敲入几个字符后按下组合键Ctrl+D,看看会怎么样)。


[root@localhost linux_C]# gcc -o select shm2.c
[root@localhost linux_C]# ./select
timeout
sdf
read 4 from keyboard:sdf
timeout
timeout
timeout
timeout
^C


实验解析:

这个程序用select调用来检查标准输入的状态。程序通过事先安排的超时时间每隔2.5秒打印一个timeout信息,这是通过select调用返回0来判断的。在文件的结尾,标准输入描述符被标记为可读,但没有字符可以读取。



多客户

我们的简单服务器程序可以从select调用中获得益处,通过用select调用来同时处理多个客户就无需再依赖于子进程了。但在把这个技巧应用到实际的应用程序中时,你必须要注意,不能在处理第一个连接的客户时让其他客户等太长的时间。

服务器可以让select调用同时检查监听套接字和客户的连接套接字。一旦select调用指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是那个上面有活动发生。

如果是监听套接字可读,这说明正有一个客户试图建立连接,此时就可以调用accept而不用担心发生阻塞的可能。如果是某个客户描述符准备好,这说明该描述符上有一个客户请求需要我们读取和处理。如果读操作返回零字节,这表示有一个客户进程已结束,你可以关闭该套接字并把它从描述符集合中删除。


实验:一个改进的多客户/服务器

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv) {
      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;
      server_sockfd=socket(AF_INET,SOCK_STREAM,0);
      server_address.sin_family=AF_INET;
      server_address.sin_port=htons(8888);
      server_address.sin_addr.s_addr=htonl(INADDR_ANY);
      server_len=sizeof(server_address);
      bind(server_sockfd,(struct sockaddr *)&server_address,server_len);
      listen(server_sockfd,5);
      FD_ZERO(&readfds);
      FD_SET(server_sockfd,&readfds);
      while(1)
      {
           char ch;
           int fd;
           int nread;
           testfds=readfds;
           printf("server waiting\n");
           result=select(FD_SETSIZE,&testfds,NULL,NULL,(struct timeval *)0);
           if(result<1)
           {
               perror("server5 failed");
               exit(1);
           }
           for(fd=0;fd<FD_SETSIZE;fd++)
           {
               if(FD_ISSET(fd,&testfds))
               {
                   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);
                   }
                   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);
                    }
                }
               }
           }
      }
}


注:在实际应用的程序中,最好用一个变量来专门保存已连接套接字的最大文件描述符号(它不一定 是最新连接的套接字文件描述符号)。这可以避免循环检查数千个其实并未连接的套接字,它们根本不可能处于可读状态。


运行:

[root@localhost C_test]# time ./client2 & ./client2 & ./client2 &
[2] 52636
[3] 52637
[4] 52639
[root@localhost C_test]# char from server = B
char from server = B
char from server = B
real    0m0.003s
user    0m0.000s
sys 0m0.001s
[2]   Done                    time ./client2
[3]-  Done                    ./client2
[4]+  Done                    ./client2


[root@localhost C_test]# ./server5
server waiting
adding client on fd 4
server waiting
serving client on fd 4
server waiting
adding client on fd 5
removing client on fd 4
server waiting
serving client on fd 5
server waiting
adding client on fd 4
server waiting
removing client on fd 5
server waiting
serving client on fd 4
server waiting
removing client on fd 4
server waiting


下面的比喻更易懂:

电话 网络套接字
给公司打电话,号码是555-0828 连接到IP地址127.0.0.1
连接员接听电话 建立起到远程主机的连接
要求转到财务部 转到指定端口(8888)
财务主管接听电话 服务器从select调用返回
电话转给免费帐号经理 服务器调用accept,在456编号上创建新的套接字


数据报

daytime服务还可以用数据报通过UDP来访问。为了访问它,发送一个数据报给该服务,然后在响应中获取一个包含日期和时间的数据报。

当客户需要发送一个短小的查询请求给服务器,并且期望接收到一个短小的响应时,我们一般就使用由UDP提供的服务。如果服务器处理客户请求的时间足够短,服务器就可以通过一次处理一个客户请求的方式来提供服务,从而允许操作系统将客户进入的请求放入队列。这简化了服务器程序的编写。

因为UDP提供的是不可靠服务,所以你可能发现数据报或响应会丢失。如果数据对于你来说非常重要,就需要小心编写UDP客户程序,以检查错误并在必要时重传。实际上,UDP数据报在局域网中是非常可靠的。

为了访问由UDP提供的服务,你需要像以前一样使用套接字和close系统调用,但你需要用两个数据报专用的系统调用sendto和recyfrom来代替原来使用在套接字上的read和write调用。

下面是一个修改过的getdate.c版本,它通过UDP数据报服务来获取数据。

#include <sys/socket.h>
#include <sys/types.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];
    }
   hostinfo=gethostbyname(host);
    if(!hostinfo)
    {
        fprintf(stderr,"no host %s\n",host);
        exit(1);
    }
    servinfo=getservbyname("daytime","udp");
    if(!servinfo)
    {
        fprintf(stderr,"no daytime service \n");
        exit(1);
    }
    printf("ssh port is %d\n",ntohs(servinfo->s_port));
    sockfd=socket(AF_INET,SOCK_DGRAM,0);
    address.sin_family=AF_INET;
    address.sin_port=htons(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 tolen);

flags参数一般设置为0.


recvfrom系统调用在套接字上等待从特定地址到来的数据报,并将它放入buffer缓冲区中,它的原型如下:

同样,flags参数一般设置为0.


这里省略了错误处理。当错误发生时,sendto和recvfrom都将返回-1并设置errno。可能的错误如下表:

errno值 说明
EBADF 传递了一个无效的文件描述符
EINTR 产生一个信号

除非用fcntl将套接字设置为非阻塞方式,否则recvfrom调用将一直阻塞。通过select调用和超时设置来判断是否有数据到达套接字。此外,还可以用alarm时钟信号来中断一个接收操作。




你可能感兴趣的:(socket)