socket的设计目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信。socket接口可采用许多不同的网络协议进行通信,本章讨论限制在因特网事实上的通信标准:TCP/IP协议栈。
套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中是一种文件描述符,许多处理文件描述符的函数(如read、write函数)都可用于处理套接字描述符。
创建一个套接字:
domain(域)参数确定通信的特性,包括地址格式。POSIX.1指定的各个域如下,每个域都有自己表示地址的格式,表示各个域的常数都以AF_开头,含义是地址族(address family):
大多系统还定义了AF_LOCAL域,它是AF_UNIX的别名。AF_UNSPEC域可代表任何域。有些平台支持其他网络协议,如AF_IPX域代表NetWare协议族,但它们没有被POSIX.1标准定义。
参数type确定套接字的类型,进一步确定通信特征,以下是POSIX.1定义的套接字类型,实现中还可增加其他类型的支持:
protocol参数通常是0,表示为给定的域和套接字类型的组合选择默认协议。当一对域和套接字类型支持多个协议时,可用protocol参数选择一个特定协议。AF_INET通信域和SOCK_STREAM套接字类型的默认协议是TCP。AF_INET通信域和SOCK_DGRAM套接字类型的默认协议是UDP。以下是protocol参数的可选值:
对于数据报接口SOCK_DGRAM,两个对等进程间不需要逻辑连接,只需要向对等进程使用的套接字送出一个报文。数据报提供的是无连接服务。而字节流SOCK_STREAM要求在交换数据前,在本地套接字和通信的对等进程的套接字之间建立一个逻辑连接。
数据报是独立的报文,可同时发送多个数据报,但不保证传递的次序,且可能丢失。每个数据报都包含接收者地址,因此每个数据报可能送给不同的接收进程。
面向连接的协议通信前需要先建立一个连接,建立好后能双向通信,每个连接是端到端的通信链路,对话中不包含地址信息,连接本身暗示特定的源和目的地。
SOCK_STREAM套接字提供字节流服务,应用分辨不出报文的界限,从SOCK_STREAM读数据时,可能不会返回由发送进程写的字节数,可能需要通过多次读函数调用才能得到对端写的所有数据。
SOCK_SEQPACKET套接字与SOCK_STREAM套接字类似,但从该套接字得到的是基于报文的服务而不是字节流的服务,从SOCK_SEQPACKET套接字上接收的数据量与对方发送时的一致。流控制传输协议(Stream Control Transmission Protocol,SCTP)提供了因特网域上的顺序数据包服务。
SOCKET_RAW套接字提供一个数据报接口,用于直接访问IP层,应用程序负责构造自己的协议头部,绕过了传输协议(如TCP和UDP),创建原始套接字时,需要超级用户特权,这样可防止恶意应用绕过内建的安全机制来创建报文。
调用socket与调用open类似,都可获得用于IO的文件描述符,不再需要该文件描述符时,都可调用close关闭对文件或套接字的访问,从而释放该描述符以便重新使用。
不是全部接受文件描述符为参数的函数都支持套接字,如lseek函数,套接字不支持文件偏移量的概念。以下是将套接字用于接受文件描述符的函数时的各函数行为:
套接字通信是双向的,禁用一个套接字的IO:
how参数为SHUT_RD时候,无法从套接字读取数据;SHUT_WR时,无法使用套接字发送数据;SHUT_RDWR时,不能读数据也不能写数据。
使用shutdown函数而非close函数的原因:
1.只有最后一个活动引用关闭时,close函数才释放网络端点,如果复制一个套接字(如用dup函数),会直到关闭了最后一个引用该套接字的文件描述符后才会释放这个套接字。而shutdown函数使一个套接字处于不活动状态,不管引用它的套接字有多少。
2.shuwdown函数可关闭一个方向的传输,如让通信对方决定什么时候传输结束时,可关闭套接字的写端,此时还可通过读端接收数据。
标识一个通信进程需要两部分:计算机的网络地址和该计算机上的端口号。
与同一台计算机上的进程通信时,不用考虑字节序,字节序是一个处理器架构特性,用于指示数据类型内部的字节如何排序,以下是一个32位整数的字节排序方式:
上图中n表示内存地址。
如果处理器架构支持大端字节序,则最大字节地址出现在最低有效字节上(Least Significant Byte,LSB)上。小端字节序是最低有效字节在最小字节地址上。
如一个整数0x04030201,将其地址转换成字符指针cp时,在小端字节序的处理器上,cp[0]指向最低有效字节(即1);在大端字节序的处理器上,cp[0]指向最高有效字节(即4)。
有些处理器可以配置成大端,也可配置成小端。
TCP/IP协议栈使用大端字节序来交换协议信息,称其为网络字节序,应用有时需要在处理器字节序与网络字节序之间转换它们。
对TCP/IP应用,有4个用来在处理器字节序与网络字节序之间转换的函数:
函数名中的h表示主机字节序,n表示网络字节序,l表示长整数(4字节),s表示短整数(2字节)。这些函数使用时需要包含的头文件是arpa/inet.h,但实现经常在其他头文件中声明这些函数,只是这些头文件都包含在arpa/inet.h中。这些函数常实现为宏。
一个地址标识一个特定通信域的套接字端点,地址格式与特定通信域相关。为使不同格式地址都能传入到套接字函数,地址会被强制转换成一个通用地址结构sockaddr:
套接字的实现可自由地添加额外成员,并定义sa_data成员的大小。Linux中,sockaddr结构的定义为:
FreeBSD中,sockaddr结构的定义为:
因特网地址结构定义在头文件netinet/in.h中,在IPv4因特网域(AF_INET)中,套接字地址用sockaddr_in结构表示:
in_port_t类型被定义为uint16_t,in_addr_t类型被定义为uint32_t。这些整数类型的名字就说明了该类型中的字节数,它们在头文件stdint.h中定义。
IPv6因特网域(AF_INET6)套接字地址用sockaddr_in6结构表示:
以上sockaddr_in、sockaddr_in6结构都是SUS要求的定义,每个实现可自由添加更多字段,如Linux中,sockaddr_in定义如下:
sin_zero字段为填充字段,应全部置0。
sockaddr_in和sockaddr_in6结构都能被强制转换成sockaddr结构,从而输入到套接字例程中。
BSD网络软件包中包含inet_addr和inet_ntoa函数,可将二进制地址格式与点分十进制表示之间相互转换,仅适用于IPv4地址,而新函数inet_ntop和inet_pton功能类似,但能同时用于IPv4和IPv6地址:
inet_ntop函数将网络字节序的二进制地址转换成文本字符串格式。inet_pton函数将文本字符串格式转换成网络字节序的二进制地址。参数domain仅支持:AF_INET、AF_INET6。
对于inet_ntop函数,参数size指定了保存文本字符串的缓冲区str的大小。参数size有两个常数用于简化工作:INET_ADDRSTRLEN定义了足够大的空间来存放一个IPv4地址的文本字符串;INET6_ADDRSTRLEN定义了足够大的空间来存放一个IPv6地址的文本字符串。
对于inet_pton函数,如果domain参数是AF_INET,则缓冲区addr需要足够大的空间存放一个32位地址;如果domain参数是AF_INET6,需要足够大的空间存放一个128位地址。
理想情况下,应用不需要了解一个套接字地址的内部结构。如果一个应用仅仅将套接字地址以sockaddr结构传递且不依赖于任何特定协议的特性,则应用可在不同的协议下工作。
获取给定计算机系统的主机信息的函数返回的网络配置信息可能是从多个地方获取的,如可从静态文件(如/etc/hosts、/etc/serices)中取得,也可从被名字服务处取得(如域名系统(DNS,Domain Name System)、网络信息服务(NIS,Network Information Service))。以下函数gethostent可获取给定计算机系统的主机信息:
如果主机信息数据库文件(/etc/hosts)没有打开,则gethostent函数会打开它。gethostent函数返回文件中的下一个条目。
sethostent函数会打开主机信息数据库文件,如果文件已经被打开,则rewind它。当stayopen参数设为非0值时,调用gethostent后,文件将保持打开。
endhostent函数可关闭主机信息数据库文件。
当gethostent函数返回时,会得到一个指向hostent结构的指针,该结构可能包含一个静态的数据缓冲区,每次调用gethostent时,缓冲区都会被覆盖,hostent结构至少需要包含以下成员,且返回的地址是网络字节序:
gethostbyname和gethostbyaddr函数最初是和以上三个函数一起引入的,但现在被认为是过时的,SUSv4中删除了它们。
类似的一套获得网络名和网络号的函数:
netent结构至少包含以下字段:
网络号按网络字节序返回。地址类型是地址族常量之一(如AF_INET)。
用以下函数在协议名和协议号之间映射:
POSIX.1定义的protoent结构至少包含以下字段:
每个服务由一个唯一的众所周知端口号来表示,可用getservbyname函数将一个服务名映射到一个端口号;getserbyport函数将一个端口号映射到一个服务名;getservent函数顺序扫描服务数据库(/etc/services):
servent结构至少包含以下成员:
POSIX.1定义了一些新函数,允许应用将一个主机名和一个服务名映射到一个套接字地址结构,或者反之,这代替了较老的gethostbyname和gethostbyaddr函数。
将主机名和服务名映射到地址的函数:
主机名和服务名要么同时都提供,要么只提供一个,另一个传空指针。主机名可以是一个节点名或点分格式的主机地址。
getaddrinfo函数通过参数res返回addrinfo结构。可用freeaddrinfo函数释放一个或多个这种结构,这取决于ai_next字段链接起来的结构有多少。
addrinfo结构至少包含以下成员:
可提供一个可选的hint参数选择符合特定条件的地址,仅使用了ai_family、ai_flags、ai_protocol、ai_socktype字段来过滤,剩下的整型字段必须设为0,且剩下的指针字段必须设为空指针。
hints结构的ai_flags字段的可选值如下,这些值可用于定制地址和服务名如何被对待:
如果getaddrinfo函数失败(即返回了非0错误码),可调用以下函数将返回的错误码转换成错误消息,该函数也适用于下面的getnameinfo函数的非0错误返回值:
getnameinfo函数将一个地址转换成主机名和服务名:
如果host参数非空,则它应该指向一个长度为参数hostlen字节的缓冲区用于存放返回的主机名。如果service参数非空,则它指向一个长度为参数servlen字节的缓冲区用于存放返回的主机名。
flags参数可对转换过程提供一些控制:
使用getaddrinfo函数打印主机和服务信息:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
#if defined(SOLARIS)
#include <netinet/in.h>
#endif
#if defined(BSD)
#include <sys/socket.h>
#include <netinet/in.h>
#endif
void print_family(struct addrinfo *aip) {
printf(" family ");
switch (aip->ai_family) {
case AF_INET:
printf("inet");
break;
case AF_INET6:
printf("inet6");
break;
case AF_UNIX:
printf("unix");
break;
case AF_UNSPEC:
printf("unspecified");
break;
default:
printf("unknown");
}
}
void print_type(struct addrinfo *aip) {
printf(" type ");
switch (aip->ai_socktype) {
case SOCK_STREAM:
printf("stream");
break;
case SOCK_DGRAM:
printf("datagram");
break;
case SOCK_SEQPACKET:
printf("seqpacket");
break;
case SOCK_RAW:
printf("raw");
break;
default:
printf("unknown (%d)", aip->ai_socktype);
}
}
void print_protocol(struct addrinfo *aip) {
printf(" protocol ");
switch (aip->ai_protocol) {
case 0:
printf("default");
break;
case IPPROTO_TCP:
printf("TCP");
break;
case IPPROTO_UDP:
printf("UDP");
break;
case IPPROTO_RAW:
printf("raw");
break;
default:
printf("unknown (%d)", aip->ai_protocol);
}
}
void print_flags(struct addrinfo *aip) {
printf("flags");
if (aip->ai_flags == 0) {
printf(" 0");
} else {
if (aip->ai_flags & AI_PASSIVE) {
printf(" passive");
}
if (aip->ai_flags & AI_CANONNAME) {
printf(" conon");
}
if (aip->ai_flags & AI_NUMERICHOST) {
printf(" numhost");
}
if (aip->ai_flags & AI_NUMERICSERV) {
printf(" numserv");
}
if (aip->ai_flags & AI_V4MAPPED) {
printf(" v4mapped");
}
if (aip->ai_flags & AI_ALL) {
printf(" all");
}
}
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
struct sockaddr_in *sinp;
const char *addr;
int err;
char abuf[INET_ADDRSTRLEN];
if (argc != 3) {
printf("ussage: %s nodename service\n", argv[0]);
exit(1);
}
// getaddrinfo函数返回的addrinfo结构中,第一个addrinfo结构的ai_canonname字段需要设置为规范名(FQDN)
// 在这种情况下,如果getaddrinfo函数的第一个参数host为NULL,则getaddrinfo函数会返回EAI_BADFLAGS错误,因为如果要解析规范名称,则必须提供主机名或ip地址作为查询的目标
hint.ai_flags = AI_CANONNAME;
hint.ai_family = 0;
hint.ai_socktype = 0;
hint.ai_protocol = 0;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(argv[1], argv[2], &hint, &ailist)) != 0) {
printf("getaddrinfo error: %s\n", gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
print_flags(aip);
print_family(aip);
print_type(aip);
print_protocol(aip);
printf("\n\thost %s", aip->ai_canonname ? aip->ai_canonname : "-");
if (aip->ai_family == AF_INET) {
sinp = (struct sockaddr_in *)aip->ai_addr;
addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN);
printf(" address %s", addr ? addr : "unknown");
printf(" port %d", ntohs(sinp->sin_port));
}
printf("\n");
}
exit(0);
}
运行它:
由上图,如果有多个协议为指定的主机提供指定的服务,程序会打印出多条信息。上图中协议号132是SCTP协议。
客户端socket关联的地址不是很重要,我们可以让系统为我们选择默认地址。然而,对于服务器,我们需要将一个众所周知的地址与服务器的socket关联起来,以便客户端请求到达。客户端可从/etc/services文件和名字服务中获取服务器的端口和地址。
关联地址和套接字:
地址参数addr有以下限制:
1.不能指定一个其他机器的地址,在进程正在运行的机器上,指定的地址必须有效。
2.地址必须和创建sockfd表示的套接字时的地址族所支持的格式相匹配。
3.地址中的端口号不能小于1024,除非进程具有root权限。
4.一般只能将一个套接字端点绑定到一个地址上,不过有些协议允许多重绑定。
对于因特网域,如果指定IP地址为INADDR_ANY,套接字端点相当于绑定到所有的接口上,这意味着可以接收这个系统所安装的任何一个网卡的数据包。如果调用connect或listen,但没有将地址绑定到套接字上,系统会选一个地址绑定到套接字。
获取绑定到套接字上的地址:
alenp参数是一个指向整数的指针,我们需要在调用函数前将该指针指向一个整数,该整数指定addr参数表示的缓冲区的长度。如果要获取的地址的大小比提供的缓冲区长度大,地址会被自动截断而不报错。如果当前没有地址绑定到该套接字,则其结果是未定义的。
如果套接字已经和另一方连接,可调用getpeername找到对方地址:
除了返回的是连接另一端的地址,getpeername和getsockname函数相同。
如果需要处理一个面向连接的网络服务(SOCK_STREAM或SOCK_SEQPACKET),在开始交换数据前,需要在请求服务的进程套接字和提供服务的进程套接字之间建立一个连接。使用connect函数建立连接:
参数addr是我们想与之通信的服务器地址。如果sockfd参数表示的本地套接字没有绑定到一个地址(只调用了socket没有调用bind),connect函数会将本地套接字绑定到一个默认地址。
连接另一端时,可能会失败,要想连接请求成功,要连接的计算机必须正在运行,服务器必须绑定到了我们想连接的地址上,且服务器的等待连接队列有足够空间。应用需要处理connect函数返回的错误,有些错误可能是由一些瞬时条件引起的。
指数退避尝试重连接:
#include <sys/socket.h>
#include <unistd.h>
#define MAXSLEEP 128
int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen) {
int numsec;
// try to connect with exponential backoff
for (numsec = 1; numsec <= MAXSLEEP; numsec <<= 1) {
if (connect(sockfd, addr, alen) == 0) {
// connect accepted
return 0;
}
// delay before trying again
if (numsec <= MAXSLEEP / 2) {
sleep(numsec);
}
}
return -1;
}
以上代码是不可移植的,在Linux和Solaris上可以正常工作,但在FreeBSD和Mac OS X上不能,原因在于,基于BSD的套接字实现中,如果第一次连接尝试失败,那么在TCP中继续使用同一个套接字描述符仍旧会失败。因此SUS警告,如果connect函数失败,套接字的状态会变成未定义的,在这些机器上,如想重试连接,需要关闭套接字,并用一个新的套接字调用connect。
以下是可迁移的支持重试的连接代码:
#include <sys/socket.h>
#include <unistd.h>
#define MAXSLEEP 128
int connect_retry(int domain, int type, int protocol, const struct sockaddr *addr, socklen_t alen) {
int numsec, fd;
// try to connect with exponential backoff
for (numsec = 1; numsec <= MAXSLEEP; numsec <<= 1) {
if ((fd = socket(domain, type, protocol)) < 0) {
return -1;
}
if (connect(fd, addr, alen) == 0) {
// connection accepted
return fd;
}
close(fd);
// delay before trying again
if (numsec <= MAXSLEEP / 2) {
sleep(numsec);
}
}
return -1;
}
以上代码不会给connect_retry函数传递一个套接字,因为可能要建立一个新套接字,该函数会返回一个已连接的套接字描述符给调用者。
如果套接字描述符处于非阻塞模式,在连接不能立即建立时,connect函数会返回-1,并将errno设为EINPROGRESS。程序可用poll或select函数判断套接字描述符何时可写,如果可写,表示连接完成。
connect函数还可用于无连接的网络服务(SOCK_DGRAM),此时传送的报文的目标地址会设置成connect调用中指定的地址,每次传送报文时就不需要再提供地址,且只能接收来自指定地址的报文。
服务器调用listen函数宣告它愿意接受连接请求:
参数backlog是一个提示,提示系统还未完成的连接请求队列容量,队列容量的实际数值由系统决定,但上限由头文件sys/socket.h中的SOMAXCONN决定(而Solaris系统忽略了SOMAXCONN,具体的最大值取决于每个协议的实现,对于TCP,默认值是128)。
如果队列满,系统会拒绝多余的连接请求,因此backlog参数的值应基于服务器期望负载和处理量来选择。
服务器调用listen后,sockfd参数表示的套接字就能接收连接请求,使用accept函数获取一个新连接:
accept函数返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端,这个新的套接字描述符和sockfd参数表示的套接字描述符具有相同的套接字类型和地址族。accept函数返回后,传给accept函数的套接字参数sockfd继续保持可用状态并接收其他连接请求。
如果不关心客户端标识,可将参数addr和len都设为NULL,否则,调用accept前,addr参数需要有足够大的缓冲区来存放地址,且将len参数指向的整数设为这个缓冲区的字节大小。accept函数返回时,会在缓冲区填充客户端地址,且更新len指向的整数以指示该地址的实际大小。
如果没有连接请求在等待,accept函数会阻塞直到一个请求到来,如果参数sockfd处于非阻塞模式,accept函数会返回-1,并将errno设为EAGAIN或EWOULDBLOCK。
在很多情况下,EAGAIN和EWOULDBLOCK都是可以互换使用的,因为它们通常表示相同的错误条件。EAGAIN是在非阻塞I/O操作中使用的错误码,表示当前没有数据可用。在这种情况下,应该重试该操作,直到数据可用或达到了预定的超时时间;而EWOULDBLOCK则通常在阻塞I/O操作中使用,表示该操作会阻塞,但是套接字被设置为非阻塞模式。在这种情况下,应该等待一段时间并重新尝试操作。在实践中,许多系统实现将这两个错误码视为相同的,并且在返回对应错误时返回两者之一。
服务器可用poll或select函数等待一个连接请求的到来,此时,监听套接字会以可读的方式出现。
初始化一个套接字端点供服务器进程使用:
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen) {
int fd;
int err = 0;
if ((fd = socket(addr->sa_family, type, 0)) < 0) {
return -1;
}
if (bind(fd, addr, alen) < 0) {
goto errout;
}
if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
if (listen(fd, qlen) < 0) {
goto errout;
}
}
return fd;
errout:
err = errno;
close(fd);
errno = err;
return -1;
}
TCP还有一些地址复用规则(SO_REUSEADDR),以上代码还有不足。
只要建立了连接,就能对套接字描述符调用read或write等原先为处理本地文件而设计的函数,而且还能将套接字描述符传递给子进程,而该子进程exec的程序可能不了解这些套接字都是什么套接字。
尽管可通过read和write函数交换数据,如果想指定选项,如从多个客户端接收数据包,或发送带外数据,需要使用为数据传递而设计的套接字函数,其中有3个函数用来发送数据,3个函数用来接收数据。
最简单的发送数据的函数是send,可指定标志来改变处理传输数据的方式:
调用send时套接字必须已连接。参数buf、nbytes的含义与write函数中的一致。flags参数的可选值:
send函数成功返回也不表示连接另一端的进程收到了数据,能保证的只是数据已被无错误地发送到网络驱动程序上。
对于支持报文边界的协议,如果尝试发送的单个报文长度超过协议支持的最大长度,那么send函数会失败,并将errno设为EMSGSIZE。对于字节流协议,send函数会阻塞直到整个数据都被传输。
sendto函数类似于send函数,但它可以在无连接的套接字上指定一个目标地址:
对于面向连接的套接字,目标地址是被忽略的,因为连接中隐含了目标地址。对于无连接的套接字,除非先调用connect设置了目标地址,否则不能使用send函数。
sendmsg函数类似于writev函数,发送数据时可指定多个缓冲区:
POSIX.1定义了msghdr结构,它至少有以下成员:
recv函数与read函数类似,但可指定标志控制如何接收数据:
flags参数的可选值:
指定了MSG_PEEK标志时,可查看下一个要读取的数据但不真正取走它,再次调用read或一个recv函数时,会返回刚查看的数据。
对于SOCK_STREAM套接字,接收的数据可能比预期的少,MSG_WAITALL标志会等到请求的数据全部收到再返回。对于SOCK_DGRAM和SOCK_SEQPACKET套接字,此标志无意义,因为这些基于报文的套接字类型一次读取就返回整个报文。
如果发送者已经调用shutdown来结束传输,或发送端已经关闭,当所有数据接收完毕后,recv函数返回0。
recvfrom函数可得到发送者的地址:
如果addr参数非空,它将包含数据发送者的套接字端点地址,参数addrlen指向一个整数,该整数是addr参数指向的缓冲区的字节长度。函数返回时,addrlen参数被设为该地址的实际字节长度。
因为可获得发送者的地址,recvfrom函数常用于无连接的套接字。
将收到的数据送入多个缓冲区或想接收辅助数据时可用recvmsg函数:
recvmsg函数与sendmsg函数一样,用msghdr结构指定接收数据的输入缓冲区,函数返回时,msghdr结构的msg_flags字段被设为所接收数据的各种特征(进入recvmsg函数时,msg_flags字段会被忽略),recvmsg函数返回时,msg_flags标志的可能值:
从一个提供ruptime服务的服务器获取服务器的远程正常运行时间(remote uptime,简写ruptime)的程序:
#include <netdb.h>
#include <unistd.h>
#include <memory.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFLEN 128
extern int connect_retry(int, int, int, const struct sockaddr *, socklen_t);
void print_uptime(int sockfd) {
int n;
char buf[BUFLEN];
// 从服务器获取内容并打印到标准输出
// 由于使用的是SOCK_STREAM套接字,不能保证一次recv调用就读取全部服务器发送内容,因此需要重复调用recv,直到返回0
while ((n = recv(sockfd, buf, BUFLEN, 0)) > 0) {
write(STDOUT_FILENO, buf, n);
}
if (n < 0) {
printf("recv error\n");
}
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err;
if (argc != 2) {
printf("usage: ruptime hostname\n");
exit(1);
}
memset(&hint, 0, sizeof(hint));
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0) {
printf("getaddrinfo error: %s\n", gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = connect_retry(aip->ai_family, SOCK_STREAM, 0, aip->ai_addr, aip->ai_addrlen)) < 0) {
err = errno;
} else {
print_uptime(sockfd);
exit(0);
}
}
printf("can't connect to %s\n", argv[1]);
exit(1);
}
以上代码中,如果服务器支持多重网络接口或多重网络协议,getaddrinfo函数可能会返回多个候选地址,需要尝试连接每个地址,直到连接成功。
以下是提供ruptime服务的服务器程序:
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <syslog.h>
#include <sys/socket.h>
#include <fcntl.h>
#define BUFLEN 128
#define QLEN 10
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif
extern int initserver(int, const struct sockaddr *, socklen_t, int);
int set_cloexec(int fd) {
int val;
if ((val = fcntl(fd, F_GETFD, 0)) < 0) {
return -1;
}
val |= FD_CLOEXEC; // enable close-on-exec
return fcntl(fd, F_SETFD, val);
}
void serve(int sockfd) {
int clfd;
FILE *fp;
char buf[BUFLEN];
set_cloexec(sockfd);
for (; ; ) {
if ((clfd = accept(sockfd, NULL, NULL)) < 0) {
syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
exit(1);
}
set_cloexec(clfd);
if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
sprintf(buf, "error: %s\n", strerror(errno));
send(clfd, buf, strlen(buf), 0);
} else {
while (fgets(buf, BUFLEN, fp) != NULL) {
send(clfd, buf, strlen(buf), 0);
}
pclose(fp);
}
close(clfd);
}
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err, n;
char *host;
if (argc != 1) {
printf("usage: ruptimed\n");
exit(1);
}
if ((n = sysconf(_SC_HOST_NAME_MAX)) < 0) {
n = HOST_NAME_MAX; // best guess
}
if ((host = malloc(n)) == NULL) {
printf("malloc error\n");
exit(1);
}
// get local hostname
if (gethostname(host, n) < 0) {
printf("gethostname error\n");
exit(1);
}
daemonize("ruptimed\n");
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN)) >= 0) { // 简单选择第一个成功建立连接的地址作为服务器套接字端点
serve(sockfd);
exit(0);
}
}
exit(1);
}
为找到此服务的地址,服务器需要获得其运行时主机名。最大主机名长度如不确定,可用HOST_NAME_MAX代替,如果系统没有定义它,我们将其定义为256,原因是POSIX.1要求主机名的最大长度至少为255,这不包括终止null字符,因此定义HOST_NAME_MAX为256字符。
可将uptime命令的标准输出和标准错误直接连接到服务端的套接字端点:
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define QLEN 10
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif
extern int initserver(int, const struct sockaddr *, socklen_t, int);
void serve(int sockfd) {
int clfd, status;
pid_t pid;
set_cloexec(sockfd);
for (; ; ) {
if ((clfd = accept(sockfd, NULL, NULL)) < 0) {
syslog(LOG_ERR, "ruptime: accept error: %s", strerror(errno));
exit(1);
}
if ((pid = fork()) < 0) {
syslog(LOG_ERR, "ruptime: fork error: %s", strerror(errno));
exit(1);
} else if (pid == 0) { // child
// The parent called daemonize , so
// STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO
// are already open to /dev/null. Thus, the call to
// close doesn't need to be protected by checks that
// clfd isn't already equal to one of these values.
if (dup2(clfd, STDOUT_FILENO) != STDOUT_FILENO
|| dup2(clfd, STDERR_FILENO) != STDERR_FILENO) {
syslog(LOG_ERR, "ruptimed: unexpected error");
exit(1);
}
close(clfd);
execl("/usr/bin/uptime", "uptime", (char *)0);
syslog(LOG_ERR, "ruptimed: unexpected return from exec: %s", strerror(errno));
} else { // parent
close(clfd);
waitpid(pid, &status, 0); // 子进程运行时间较短,父进程可等待子进程完成再接受下一个连接请求
}
}
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err, n;
char *host;
if (argc != 1) {
printf("usage: ruptimed\n");
exit(1);
}
if ((n = sysconf(_SC_HOST_NAME_MAX)) < 0) {
n = HOST_NAME_MAX; // best guess
}
if ((host = malloc(n)) == NULL) {
printf("malloc error\n");
exit(1);
}
if (gethostname(host, n) < 0) {
printf("gethostname error\n");
exit(1);
}
daemonize("ruptimed");
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN)) >= 0) {
serve(sockfd);
exit(0);
}
}
exit(1);
}
对于无连接的套接字,数据包到达时可能已经没有次序(应用中需要关心数据包的到达次序);并且可能数据包会丢失,如果丢失,有两种选择:
1.如果想和对等方可靠通信,就必须对数据包编号,发现数据包丢失时,请求另一方重传,还需要标识出重复的数据包并丢弃它们(重复出现可能是由于数据包延迟,我方请求对端重传后,数据包才到达)。
2.对于简单程序,可让用户再次运行程序。
采用数据报套接字的ruptime客户端:
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#define BUFLEN 128
#define TIMEOUT 20
void sigalrm(int signo) { }
void print_uptime(int sockfd, struct addrinfo *aip) {
int n;
char buf[BUFLEN];
buf[0] = 0;
if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen) < 0) { // 发送1字节的数据表示向服务器请求服务,即使服务器没有在运行也会发送成功
printf("sendto error\n");
exit(1);
}
alarm(TIMEOUT); // 避免调用recvfrom时无限阻塞
if ((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL)) < 0) {
if (errno != EINTR) {
alarm(0); // cancel alarm
}
printf("recv error\n");
exit(1);
}
alarm(0);
write(STDOUT_FILENO, buf, n);
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err;
struct sigaction sa;
if (argc != 2) {
printf("usage: ruptime hostname\n");
exit(1);
}
sa.sa_handler = sigalrm;
sa.sa_flags = 0;
// 处理SIGALRM信号时不阻塞其他信号
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) < 0) {
printf("sigaction error\n");
exit(1);
}
memset(&hint, 0, sizeof(hint));
hint.ai_socktype = SOCK_DGRAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0) {
printf("getaddrinfo error: %s\n", gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0)) < 0) {
err = errno;
} else {
print_uptime(sockfd, aip);
exit(0);
}
}
fprintf(stderr, "can't contact %s: %s\n", argv[1], strerror(err));
exit(1);
}
以上代码运行时可能会出现以下错误:
这是由于找不到ruptime服务对应的端口,我们需要手动在/etc/services文件中添加ruptime服务对应的端口:
此时再运行程序即可。
以下是无连接的ruptime服务器:
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFLEN 128
#define MAXADDRLEN 256
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif
extern int initserver(int, const struct sockaddr *, socklen_t, int);
void serve(int sockfd) {
int n;
socklen_t alen;
FILE *fp;
char buf[BUFLEN];
char abuf[MAXADDRLEN];
struct sockaddr *addr = (struct sockaddr *)abuf;
set_cloexec(sockfd);
for (; ; ) {
alen = MAXADDRLEN;
if ((n = recvfrom(sockfd, buf, BUFLEN, 0, addr, &alen)) < 0) { // 在此处等待接收服务请求
syslog(LOG_ERR, "ruptimed: recvfrom error: %s", strerror(errno));
exit(1);
}
if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
sprintf(buf, "error: %s\n", strerror(errno));
sendto(sockfd, buf, strlen(buf), 0, addr, alen);
} else {
if (fgets(buf, BUFLEN, fp) != NULL) {
sendto(sockfd, buf, strlen(buf), 0, addr, alen); // 地址参数是从recvfrom函数获取的
}
pclose(fp);
}
}
}
int main(int argc, char *argv[]) {
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err, n;
char *host;
if (argc != 1) {
printf("usage: ruptimed\n");
exit(1);
}
if ((n = sysconf(_SC_HOST_NAME_MAX)) < 0) {
n = HOST_NAME_MAX; // best guess
}
if ((host = malloc(n)) == NULL) {
printf("malloc error\n");
exit(1);
}
if (gethostname(host, n) < 0) {
printf("gethostname error\n");
exit(1);
}
daemonize("ruptimed");
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_DGRAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err));
exit(1);
}
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = initserver(SOCK_DGRAM, aip->ai_addr, aip->ai_addrlen, 0)) >= 0) {
serve(sockfd);
exit(0);
}
}
exit(1);
}
套接字选项机制可控制套接字行为,有两个相关函数,一个用来设置选项,一个查询选项。
设置套接字选项:
参数level标识了选项应用的协议,如果option参数是通用的套接字选项,则level设置成SOL_SOCKET,否则就要设置成这个选项应用的协议的编号,对于TCP选项,应设为IPPROTO_TCP;对于IP选项,应设为IPPROTO_IP。以下是SUS定义的通用套接字选项:
参数val根据选项参数option的不同指向一个整数或一个数据结构。一些选项是开关,对应的val参数是一个整数,如果整数非0,则启用选项,如果整数为0,则禁止选项。参数len指定了参数val指向的对象的大小。
getsockopt函数查看选项的当前值:
参数lenp是一个指向整数的指针,调用getsockopt函数前,将该整数设为存放该选项值副本的缓冲区(即val参数)的长度,如果选项的实际长度大于此值,则选项会被截断,如果长度小于此值,则函数返回时将此值更新为实际长度。
以上ruptime服务器在终止并尝试重启时,无法正常工作,因为TCP的实现不允许在几分钟内绑定同一个地址,但可用套接字选项SO_REUSEADDR绕过此限制:
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen) {
int fd, err;
int reuse = 1;
if ((fd = socket(addr->sa_family, type, 0)) < 0) {
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)) < 0) {
goto errout;
}
if (bind(fd, addr, alen) < 0) {
goto errout;
}
if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
if (listen(fd, qlen) < 0) {
goto errout;
}
}
return fd;
errout:
err = errno;
close(fd);
errno = err;
return -1;
}
带外数据是一些通信协议支持的可选功能,与普通数据相比,它允许更高优先级的数据传输。带外数据先行传输,即使传输队列已经有数据(即使传输队列已经有数据,发送带外数据时,TCP也会发一个报文段,其中首部的紧急数据标志位打开,相当于先于队列中的数据传输了)。TCP支持带外数据,但UDP不支持。
TCP将带外数据称为紧急数据,TCP仅支持1个字节的紧急数据,且允许紧急数据在普通数据传递机制之外传输。为产生紧急数据,可在3个send函数中指定MSG_OOB标志,此时如果要发送的字节超过1个,最后一个字节会被视为紧急数据字节。
如果我们安排了接收套接字信号的进程,则紧急数据被接收时,会发送SIGURG信号。可通过以下函数安排接收套接字的信号的进程:
fcntl(sockfd, F_SETOWN, pid);
F_SETOWN命令设置一个套接字的所有权,如果pid参数是一个正值,那么它指定的就是进程ID,如果是一个负值,那么它代表的就是进程组ID。F_GETOWN命令用来获取当前套接字的所有权,其返回值是上述调用的pid值:
owner = fcntl(sockfd, F_GETOWN, 0);
TCP支持紧急标记的概念,即紧急数据在普通数据流中的位置。如果使用了套接字选项SO_OOBINLINE,那么可在普通数据中接收紧急数据(即将带外数据放在接收缓冲区的普通数据队列中,因此可以与普通数据一起读取。这样做可以简化应用程序的编写,但会使带外数据和普通数据混合在一起,需要应用程序自行处理区分)。可用sockatmark函数判断是否已经到紧急标记:
当下一个要读取的字节在紧急标志处时,sockatmark函数返回1。
带外数据出现在套接字读取队列时,select函数返回一个待处理的异常条件描述符。可以在普通数据流上接收紧急数据,也可在某个recv函数中用MSG_OOB标志在输入队列的其他数据之前接收紧急数据。TCP仅有一个字节的紧急数据,如果在接收当前紧急数据字节前又有新紧急数据到来,则已有的会被废弃。
recv函数在没有数据可接收时会阻塞等待;send函数在套接字输出队列没有足够空间时也会阻塞。在套接字非阻塞模式下,以上情况会导致函数调用失败而非阻塞,并将errno设为EWOULDBLOCK或EAGAIN。非阻塞情况下可用poll或select函数判断能否接收或传输数据。
套接字机制有自己的处理异步IO的方式,但这在SUS中没有标准化。有些文献将基于套接字的异步IO机制称为基于信号的IO,以区别SUS中的通用异步IO机制。
基于套接字的异步IO中,当可以从套接字中读取数据时,或当套接字写队列中空间可用时,可安排发送信号SIGIO到某个进程,启用它需要两步:
1.确定套接字所有权,信号会被传送到拥有套接字所有权的进程。
2.通知套接字在IO操作不会被阻塞时发送信号。
有三种方式完成上述步骤1:
1.在fcntl函数中使用F_SETOWN命令。
2.在ioctl函数中使用FIOSETOWN命令。
3.在ioctl函数中使用SIOCSPGRP命令。
有两种方式完成上述步骤2:
1.在fcntl函数中使用SETFL命令启用文件标志O_ASYNC。
2.在ioctl函数中使用FIOASYNC命令。
以上多种完成步骤的方式并不总是全被支持的:
判断系统的字节序:
#include <stdio.h>
int main(int argc, char *argv[]) {
union {
short s;
char c[sizeof(short)];
} un;
un.s = 0x0102;
if (sizeof(short) == 2) {
if (un.c[0] == 1) {
printf("big-endian\n");
} else if (un.c[0] == 2) {
printf("little-endian\n");
} else {
printf("unknown\n");
}
} else {
printf("size of short is: %s\n", sizeof(short));
}
}
另一种方法:
#include <stdio.h>
#include <inttypes.h>
int main() {
uint32_t i = 0x04030201;
unsigned char *cp = (unsigned char *)&i;
if (*cp == 1) {
printf("little-endian\n");
} else if (*cp == 4) {
printf("big-endian\n");
} else {
printf("who knows?\n");
}
}
两个库例程:一个在套接字上启用异步IO,一个在套接字上不使用异步IO,保证能在所有平台上运行:
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#if defined(BSD) || defined(MACOS) || defined(SOLARIS)
#include <sys/filio.h>
#endif
int setasync(int sockfd) {
int n;
if (fcntl(sockfd, F_SETOWN, getpid()) < 0) {
return -1;
}
n = 1;
if (ioctl(sockfd, FIOASYNC, &n) < 0) {
return -1;
}
return 0;
}
int cleasync(int sockfd) {
int n;
n = 0;
if (ioctl(sockfd, FIOASYNC, &n) < 0) {
return -1;
}
return 0;
}