15.2.3 套接字地址
每个套接字域都有自己的地址格式,对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述,该结构定义在头文件sys/un.h中.struct sockaddr_un {因此,对套接字进行处理的系统调用可能需要接受不同类型的地址,每种地址格式都使用一种类似的结构来描述,它们都以一个指定地址类型(套接字域)的成员(在本例中是sun_family)开始.在AF_UNIX域中,套接字地址由结构中的sun_path成员中的文件名所指定.
sa_family_t sun_family; /* AF_UNIX */
char sun_path[]; /* pathname */
};
在当前的linux系统中,由X/Open规范定义的类型sa_family_t在头文件sys/un.h中声明,它是短整数类型.此外,sun_path指定的路径名长度也是有限制.因为地址结构的长度不一致,所以许多套接字调用需要用到一个用来复制特定地址结构的长度变量或将它作为一个输出.
在AF_INET域中,套接字地址由结构sockaddr_in来指定,该结构定义在头文件netinet/in.h中,它至少包含以下几个成员:
struct sockaddr_in {IP地址结构in_addr被定义为:
short int sin_family; /* AF_INET */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};
struct in_addr{IP地址中的4个字节组成的一个32位的值,一个AF_INET套接字由它的域,IP地址和端口号完全确定.从应用程序的角度来看,所有套接字的行为就像文件描述符一样,并且通过一个唯一的整数值来区分.
unsigned long int s_addr;
};
15.2.4 命名套接字
函数作用
要想让通过socket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名,这样,AF_UNIX套接字就会关联到一个文件系统的路径名,正如在server1例子,AF_INET套接字就会关键到IP端口号.函数原型
#include <sys/socket.h>bind系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字.地址结构的长度由参数address_len传递.
int bind(int socket, const struct sockaddr *address, size_t address_len);
地址的长度和格式取决于地址族,bind调用需要将一个特定的地址结构指针转换为指向通用地址类型(struct sockaddr *).
函数返回值
bind调用在成功时返回0,失败时返回-1并设置errno为下表的一个值:errno值 说明
EBADF 文件描述符无效
ENOTSOCK 文件描述符对应的不是一个套接字
EINVAL 文件描述符对应的是一个已命名的套接字
EADDRNOTAVAIL 地址不可用
EADDRINUSE 地址已经绑定了一个套接字
EACCESS 因为权限不足,不能创建文件系统中的路径名
ENOTDIR,ENAMETOOLONG 表明选择的文件名不符合要求
15.2.5 创建套接字队列
函数作用
为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列保存未处理的请求.它用listen系统调用完成这一工作.函数原型
#include <sys/socket.h>linux系统可能对队列中可以容纳的未处理连接的最大数目做出限制,为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值.在套接字队列中,等待处理的进入连接的数目最多不超过这个数字.再往后的连接将被拒绝,导致客户的连接请求失败.listen函数提供的这种机制允许服务器正忙于前一个客户请求的时候,将后续的客户连接放入队列等待处理.backlog参数常用的值是5.
int listen(int socket, int backlog);
函数返回值
listen函数在成功时返回0,失败时返回-1.错误代码包括EBADF,EINVAL和ENOTSOCK,其含义与上面bind系统调用说明的一样.15.2.6 接收连接
函数作用
一旦服务器程序创建并命名了套接字之后,它就可以通过accept系统调用来等待客户建立对该套接字的连接.函数原型
#include <sys/socket.h>accept系统调用只有当客户程序试图连接到由socket参数指定的套接字时才返回.这里的客户是指,在套接字队列中排在第一个的未处理连接.accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符.新套接字的类型和服务器监听套接字类型是一样的.
int accept(int socket, struct sockaddr *address, size_t *address_len);
函数参数
第一个参数是套接字socket,它必须事先由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调用时,执行被中断而产生的错误.15.2.7 请求连接
函数作用
客户程序通过一个未命名套接字和服务器监听套接字之间建立连接方法来连接到服务器,它们通过connect调用来完成这一工作.函数原型
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, size_t address_len);
函数参数
第一个参数socket指定的套接字将连接到参数address指定的服务器套接字第三个参数address_len指定address指向的结构的长度
参数socket指定的套接字必须通过socket调用获得一个有效的文件描述符
函数返回值
成功时,connect调用返回0.失败时,返回-1.可能的错误代码如下所示:errno值 说明
EBADF 传递给socket参数的文件描述符无效
EALREADY 该套接字上已经有一个正在进行中的连接
ETIMEDOUT 连接超时
ECONNREFUSED 连接请求被服务器拒绝
如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间.一旦这个超时时间到达,连接将被放弃,connect调用失败.但如果connect调用被一个信号中断,而该信号又得到了处理,connect调用还是会失败(errno被设置为EINTR),但连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立.
与accept调用一样,connect调用的阻塞特性可以通过设置该文件描述符的O_NONBLOCK标志来改变.此时,如果连接不能立刻建立,connect将失败并把errno设置为EINPROGRESS,而连接将以异步方式继续进行.
虽然异步连接难于处理,但可以在套接字文件描述符上,用select调用来检查套接字是否已处于就绪状态.
15.2.8 关闭套接字
可以通过调用close函数来终止服务器和客户上的套接字连接,就如同对底层文件描述符进行关闭一样,应该总是在连接的两端都关闭套接字.对于服务器来说,应该在read调用返回0时关闭套接字,但如果套接字是一个面向连接类型的,并且设置了SOCK_LINGER选项,close调用会在该套接字还有未传输数据时阻塞.15.2.9 套接字通信
下面看几个示例程序,将尽量使用网络套接字而不是文件系统套接字.文件系统套接字的缺点时,除非程序使用一个绝对路径名,否则套接字将创建在服务器程序的当前目录下.为了让它更具有通用型,需要将它创建一个服务器及其客户都认可的可全局访问的目录(如/tmp目录)中.而对网络套接字来说,只需要选择一个未被使用的端口号即可.将在局域网中运行客户和服务器,但网络套接字不仅可用于局域网,任何带有因特网连接的机器都可以使用网络套接字来彼此通信.甚至可以在一台UNIX单机上运行基于网络的程序,因为UNIX计算机通常会配置了一个只包含它自身的回路(loopback)网络.
回路网络中只包含一台计算机,传统上它被称为localhost,它有一个标准的IP地址127.0.0.1.这是本地主机,可以在网络主机地址上/etc/hosts中找到它的地址,在该文件中还列出了在共享网络中的其他主机的名字和对应的地址.
每个与计算机通信的网络都有一个与之关联的硬件接口,一台计算机可能在每个网络中都有一个不同的网络名,当然也就会有几个不同的IP地址.例如,Neil的机器就有3个网络接口,因此就有3个IP地址,它们被记录在文件/etc/hosts中,如下所示:
127.0.0.1 localhost # Loopback
192.168.1.1 tilde.localnet # Local, private Ethernet
158.152.x.x tilde.demon.co.uk # Modem dial-up
第一个就是简单的回路网络,第二个是通过一块以太网卡来访问的局域网.第三个就是一个因特网接入服务提供上的调制解调器连接.
编写client2.c
/*************************************************************************运行这个版本的客户程序时,它将连接失败,因为还没有服务器运行在这条计算机的9734端口上.如下所示:
> File Name: client2.c
> Description: client2.c
> Author: Liubingbing
> Created Time: 2015年07月21日 星期二 23时24分16秒
> Other: client2.c
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
int main()
{
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 = 9734;
len = sizeof(address);
result = connect(sockfd, (struct sockaddr *)&address, len);
if (result == -1) {
perror("oops: client2");
exit(1);
}
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf("char from server = %c\n", ch);
close(sockfd);
exit(0);
}
编写server2.c
/*************************************************************************client2.c客户程序用在头文件netinet/in.h中定义的sockaddr_in结构指定了一个AF_INET地址.它试图连接到IP地址为127.0.0.1的主机上的机器.它用inet_addr函数将IP地址的文本表示方式转换为符合套接字地址要求的格式.
> File Name: server2.c
> Description: server2.c
> Author: Liubingbing
> Created Time: 2015年07月21日 星期二 23时30分19秒
> Other: server2.c
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.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 = inet_addr("127.0.0.1");
server_address.sin_port = 9734;
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);
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}
运行client2和server2如下所示:
程序解析
服务器程序创建一个AF_NET域的套接字,并安排在它之上接受连接.这个套接字被绑定在端口号9734上. 指定的地址决定了允许建立连接的计算机.通过指定像客户程序中一样的回路地址,就把通信限制在本地主机上.如果想允许服务器和远程客户进行通信,就必须指定一组想要允许连接的IP地址,可以用特殊值INADDR_ANY来表示,将接受来自计算机任何网络接口的连接.如果愿意,还可以通过分离如内部局域网和外部广域网连接的方式区分不同的网络接口.