套接字(socket)是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。Linux所提供的功能(如打印服务、连接数据库和提供Web页面)和网络工具(如用于远程登录的rlogin和用于文件传输的ftp)通常都是通过套接字来进行通信的。
套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分开来。套接字机制可以实现将多个客户连接到一个服务器。
域指定套接字通信中使用的网络介质。最常见的套接字域是AF_INET,它指的是Internet网络,许多Linux局域网使用的都是该网络,当然,因特网自身用的也是它。其底层的协议——网际协议(IP)只有一个地址族,它使用一种特定的方式来指定网络中的计算机,即人们常说的IP地址。
一个套接字域可能有多种不同的通信方式,而每种通信方式又有其不同的特性。但AF_UNIX域的套接字没有这样的问题,它们提供了一个可靠的双向通信路径。在网络域中,我们就需要注意底层网络的特性,以及不同的两种通信机制:流(stream)和数据报(datagram)。它们有着截然不同的服务层次。
流套接字(在某些方面类似于标准的输入/输出流)提供的是一个有序、可靠、双向字节流的连接。因此,发送的数据可以确保不会丢失、复制或乱序到达,并且在这一过程中发生的错误也不会显示出来。大的消息将被分片、传输、再重组。这很像一个文件流,它接收大量的数据,因为它们在编写网络程序时是最常用的。
TCP/IP代表的是传输控制协议/网际协议。IP协议是针对数据包的底层协议,它提供从一台计算机通过网络到达另一台计算机的路由。TCP协议提供排序,流控和重传,以确保大数据的传输可以完整地到达目的地或报告一个适当的错误条件。
与流套接字相反,由类型SOCK_DGRAM指定的数据报套接字不建立和维持一个连接。它对可以发送的数据报的长度有限制。数据报作为一个单独的网络消息被传输,它可能会丢失、复制或乱序到达。
数据报套接字是在AF_INET域中通过UDP/IP连接实现的,它提供的是一种无序的不可靠服务(UDP代表的是用户数据报协议)。但从资源的角度来看,相对来说它们开销比较小,因为不需要维持网络连接。而且因为无需花费时间来建立连接,所以它们的速度也很快。
数据报适用于信息服务中“单次”(singl-shot)查询,它主要用来提供日常状态信息或执行低优先级的日志记录。它的优点是服务器的崩溃不会给客户造成不便,也不会要求客户重启,因为基于数据报的服务器通常不保留连接信息,所以它们可以在不打扰其客户的前提下停止并重启。
只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议。那么这里提供了UNIX网络套接字和文件系统套接字,它们不需要你选择一个特定的协议,只需要使用其默认值即可。(这里将不详细讨论这两种)
socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。
#include
#include
int socket(int domain,int protocol);
创建的套接字是一条通信线路的一个端点,domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议。
domain参数可以指定的协议族如下图:
最常见的套接字域是AF_UNIX和AF_INET,前者用于通过UNIX和Linux文件系统实现的本地套接字,后者用于UNIX网络套接字。AF_INET套接字可以用于通过包括因特网在内的TCP/IP网络进行通信的程序。微软Windowns系统的Windowns接口也提供了对这个套接字域的访问功能。
socket函数的参数type指定用于新套接字的通信特性。它的取值包括SOCK_STREAM和SOCK_DGRAM。
SOCK_STREAM是一个有序、可靠、面向连接 的双向字节流,对AF_INET域套接字来说,它默认是通过一个TCP连接来提供这一特性的,TCP连接在两个流套接字端点之间建立。数据可以通过套接字连接进行双向传递。TCP协议所提供的机制可以用于分片和重组长消息,并且可以重传可能在网络中丢失的数据。
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 */
};
因此,对套接字进行处理的系统调用可能需要接受不同类型的地址,每种地址格式都使用一种类似的结构来描述,它们都以一个指定地址类型(套接字域)的成员(在本例中是sun_family)开始。在AF_UNIX域中,套接字地址由结构中的sun_path成员中的文件名所指定。
在当前的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_INEX */
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;
};
IP地址中的4个字节组成一个32位的值。一个AF_INET套接字由它的域、IP地址和端口号来完全确定。从应用程序的角度来看,所有套接字的行为就像文件描述符一样,并且通过一个唯一的整数值来区分。
要想让通过sockket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名。这样,AF_UNIX套接字就会关联到一个文件系统的命名。
#include
int bind(int socket, const struct sockaddr *address, size_t address_len);
bind系统调用把参数address中的地址分配给文件描述符socket关联的未命名套接字。地址结构的长度由参数address_len传递。
地址的长度和格式取决于地址族。bind调用需要将一个特定的地址结构指针转换为指向通用地址类型(struct sockaddr *)。
bind调用在成功时返回0,失败时返回-1并设置errno为下表中的一个值。
AF_UNIX域套接字还有其他一些错误代码,如下表。
为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。它用listen系统调用来完成这一工作。
#include
int listen(int socket, int backlog);
Linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制。为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值。在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字。再往后的连接将被拒绝,导致客户的连接请求失败。listen函数提供的这种机制允许当服务器程序正忙处理前一个客户请求的时候,将后续的客户连接放入队列等待处理。backlog参数常用的值为5.
listen函数在成功时返回0,失败时返回-1.错误代码包括EBADF、EINVAL和ENOTSOCK,其含义与上面bind系统调用中说明的一样。
一旦服务器程序创建并命名了套接字之后,它就可以通过accept系统调用来等待客户建立对该套接字的连接。
#include
int accept(int socket, struct sockaddr *address, size_t *address_len);
accept系统调用只有当有客户程序试图连接到由socket参数指定的套接字上时才返回。这里的客户是值在套接字队列中排第一个的未处理连接。accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符。新套接字的类型和服务器监听套接字类型一样的。
套接字必须事先由bind调用命名,并且由listen调用给它分配一个连接队列。连接客户的地址将被放入address参数指向的sockaddr结构中。如果我们不关心客户的地址,也可以将address参数指定为空指针。
参数addr_len指定客户结构的长度。如果客户地址的长度超过这个值,它将被截断。所以在调用accept之前,address_len必须被设置为预期的地址长度。当这个调用返回时,address_len将被设置为连接客户地址结构的实际长度。
如果套接字队列中没有未处理的连接,accept将阻塞(程序将1暂停)直到有客户建立连接为止。我们可以通过对套接字文件描述符设置O_NONBLOCK标志来改变这一行为,使用的函数是fcnt1,如下:
int flags = fcnt1(socket, F_GETFL, 0);
fcnt1(socket, F_SETFL, O_NONBLOCK|flags);
当有未处理的客户连接时,accept函数将返回一个新的套接字文件描述符。发生错误时,accept函数将返回-1,可能的错误情况大部分与bind、listen调用类似,其他的错误有EWOULDBLOCK和EINTR。前者是当指定了O_NONBLOCK标志,但队列中没有未处理连接时产生的错误。后者是当进程阻塞在accept调用时,执行被中断而产生的错误。
客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器。它们通过connect调用来完成这一工作。
#include
int connect(int socket, const struct sockaddr *address, size_t address_len);
参数socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度由参数address_len指定。参数socket指定的套接字必须是通过socket调用获得的一个有效的文件描述符。
成功时,connect调用返回0,失败时返回-1.可能的错误代码见下表。
如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间。一旦这个超时时间到达,连接将被放弃,connect调用失败。但如果connect调用被一个信号中断,而该信号又得到了处理,connect调用还是会失败(errno被设置为EINTR),但连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立。
与accept调用一样,connect调用的阻塞特性可以通过设置该文件描述符的O_NONBLOCK标志来改变。此时,如果连接不能立刻建立,connect将失败并把errno设置为EINPROGRESS,而连接将以异步方式继续进行。
虽然异步连接难于处理,但我们可以在套接字文件描述符上用select调用来检查套接字是否已处于就绪状态。(此处先不将select调用)
你可以通过调用close函数来终止服务器和客户上的套接字连接,就如同对底层文件描述符进行关闭一样。你应该总是在连接的两端都关闭套接字。对于服务器来说,应该在read调用返回0时关闭套接字,但如果套接字是一个面向连接类型的,并且设置了SOCK_LINGER选项,close调用会在改套接字还有未传输数据时阻塞。(这里就涉及到如何设置套接字选项,这里不细讲)