- 论坛徽章:
- 0
|
第十六章 网络IPC:套接字
1、概述
套接字的原始版本是
BSD
套接字
,它是通信端点的抽象。可用于同一机器上的进程间通信,典型应用为
UNIX
域套接字
;也可用于通信网络上任何体系结构的计算机之间的通信,典型应用为
互联网套接字
及在此基础上的
TCP/IP
协议栈
实现。
1983年,4.2 BSD发布了基于套接字技术的第一个TCP/IP协议栈API实现,它成为此后其它系统TCP/IP实现的基础。POSIX的socket(7)标准是在4.4 BSD的基础上制定,微软则于1990年代初期在成功移植BSD套接字的基础上开发了
winsock
,此外使用TCP/IP技术进行通信的各种嵌入式系统也有诸多基于Socket API的移植版本。
套接字是在文件I/O机制的基础上实现的,包括匿名和有名两种文件形式。典型的有名套接字是/dev/log,它使用的是UNIX域套接字,守护进程syslogd(8)使用它和使用系统日志服务的客户进程通信。下面的内容除非特别注明,否则“套接字”特指匿名套接字。
用于分析TCP/IP协议的经典UNIX工具包括netcat(1)和tcpdump(1)。前者被称为网络瑞士军刀,可以建立任意基于TCP/IP的网络连接并进行输入输出;后者可以把所在网络上的数据流转储到当前的标准输出,这些输出可通过管道线连接到一些文本过滤器之类的程序进行分析。
本章只讲述套接字的建立、设置、数据收发等基本接口。关于套接字机制与TCP/IP实现细节可参考:
TCP/IP Illustrated Volume 2: The Implementation
中文译名《TCP/IP详解卷2:实现》。基于4.4 BSD-Lite的套接字机制讲述TCP/IP实现;
The Design and Implementation of 4.4BSD
中文译名《4.4 BSD 设计与实现》。讲述包括Sockets机制在内的4.4 BSD设计原理与实现细节;
Understanding Linux Network Internals
中文译名《深入理解Linux网络技术内幕》。包括Linux环境的网络实现细节及解决方案。暂无简体中文版。
关于TCP/IP协议及应用可参考:
TCP/IP Illustrated Volume 1: The Protocols
中文译名为《TCP/IP详解卷1:协议》。讲述TCPIP协议族的体系结构及细节;
中文译名为《UNIX网络编程》。其中第二版分为两卷,第一卷The Sockets Networking API(中文译名:《套接口API》)讲述了Sockets编程的细节;
Internetworking With TCP/IP Vol Ⅲ:Client-Server Programming And Applications
中文译名为《用TCP/IP进行网际互联第三卷:客户-服务器编程与应用》。讲述C/S程序设计的典型模型与应用;
RFC
是互联网技术的文献资料集,这些
文件
通过编号排定,也有译为
中文的
RFC
文档
;
2、套接字的创建和关闭
套接字是UNIX基本文件类型的一种,可以使用文件I/O的大部分函数,fchdir(2)只适用于目录文件,而以下是否可以与实现有关,通常不允许使用:fchmod(2), ftruncate(2), lseek(2), mmap(2);(我在linux下,用root身份对有名套接字文件/dev/log执行chmod(1)是成功的,但cp(1)操作失败);
socket(2)函数用于创建并打开一个套接字:
#include
int socket(int domain, int type, int protocol);
domain为通信域选项,即地址族(Address Family)。主要的地址族包括:
AF_UNIX(AF_LOCAL) UNIX域
AF_INET ipv4因特网域
AF_INET6 ipv6因特网域
type为套接字类型选项,主要包括
SOCK_STREAM 面向流的套接字,默认为TCP协议;
SOCK_DGRAM 面向数据报的套接字,默认为UDP协议;
SOCK_RAW 访问IP层的数据报接口,用户自行构造协议,此选项需要root权限;
protocol为协议选项,为0时使用type的默认值。可以通过getprotoent(3)转换协议名为协议值。
以下函数用于设置和获取指定套接字的选项
#include
int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t len);
int setsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict len);
level表明option应用在哪个协议上,例如:
SOL_SOCKET 套接字通用选项;
IPPROTO_TCP TCP协议选项;
IPPROTO_IP IP协议选项;
option则为level对应的可选项,这些选项可能是一个on/off开关,也可能是一个数值;
如果option是一个开关,val表示是否勾选此选项的功能;如果option是数值选项,val表示其取值;
套接字可以用close(2)来关闭,或者用shutdown(2)关闭套接字的一端:
#include
int shutdown(int sockfd, int how);
how包括SHUT_RD、SHUT_WR、SHUT_RDWR;
3、计算机字节顺序
网络上通信的双方可能是异构主机,这意味着可能存在
字节顺序
的不同。
例如Motorola 68K系列、早期的SPARC等采用的是大端(或称高地址优先)字节顺序,即在一个机器字的存储单元上,低字节存在高地址,高字节存在低地址上;而Interl X86等则采用小端(或称低地址优先)字节顺序,即在一个机器字的存储单元上,低字节存在低地址,高字节存在高地址;而ARM, SPARC V9, MIPS等体系结构可以选择使用大端还是小端模式。它们之间直接通信会得到错误的数据。
以下接口函数提供了主机字节顺序和网络地址顺序的转换。用户不必关心网络字节顺序是什么,只要数据从主机发送到网络上或者从网络上接收数据时,使用这些函数进行转换,就不用担心字节顺序错误的问题:
#include
uint32_t htonl(uint32_t hostint32);
uint32_t htons(uint32_t hostint16);
uint32_t ntohl(uint32_t netint32);
uint32_t ntohs(uint32_t netint16);
异种体系结构的不同字节顺序同时也带来带有位操作的程序的可移植性问题,移植时需要特别注意。
4、套接字与进程地址标识的关联
一个套接字绑定的进程,在网络上主要以该进程的主机(或IP地址,网络层的标识)、协议(传输层的标识)、端口(应用层的标识)等信息来标识。这些信息在ipv4因特网域中,以结构sockaddr_in来描述,并封装到套接字的sockaddr结构。
结构sockaddr_in包括sin_family、sin_port、sin_addr三个成员;结构sockaddr包括sa_family和sa_data两个成员;
bind(2)将套接字绑定到指定的地址
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
对addr的成员有如下限制:
必须使用本地地址;
必须与socket(2)创建时的domain格式匹配;
只有root进程的端口号可以小于1024;
对于AF_INET,如果IP地址为INADDR_ANY,则sockfd绑定到本地系统的全部链路层接口;
以下两个函数可以通过套接字获取本地或者对端所绑定sockfd的地址标识:
#include
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
查找和转换进程地址标识信息的传统API包括:
gethostbyname(3), gethostbyaddr(3), getnetbyaddr(3), getnetbyname(3), getprotobyname(3), getprotobynumber(3), getservbyname(3), getservbyport(3)。
它们返回相应的hostent/netent/protoent/servent结构的指针。也可以通过gethostent(3), getnetent(3), getservent(3)直接取当前进程的相关结构的指针。它们的信息取自hosts(5), services(5), protocols(5)等文件。
而POSIX定义了一些新的函数代替上述函数的功能,包括
#include
#include
int getaddrinfo(const char *restrict host,
const char *restrict service,
const struct addrinfo *restrict hint,
struct addrinfo **restrict res);
int getnameinfo(const struct sockaddr *restrict addr,
socklen_t alen, char *restrict host,
socklen_t hostlen, char *restrict service,
socklen_t servlen, unsigned int flags);
getaddrinfo(3)通过给定的参数host, service, hint填充结构res。其中
host的字符串可以是主机名或者点分十进制IP地址;
service为标准的服务名;
hint只使用ai_flags, ai_family, ai_socktype, ai_protocol这几个成员,其它成员必须设为0或者NULL;
res是指向一个addrinfo结构的链表首址,它的后趋结点的指针为ai_next;释放这个链表的函数为freeaddrinfo(3);
getnameinfo(3)通过给定的套接字地址标识addr以flags标志设置host或service的值;
这两个函数的返回值可以通过gai_strerror(3)转换为字符串:
#include
const char *gai_strerror(int error);
5、基于套接字的数据传输
面向流的协议如TCP(7),需要建立连接才能进行数据传输。在C/S模型中,建立连接的请求通常为客户机向服务器提出。请求建立连接的函数为
#include
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
该函数请求套接字sockfd连接到地址标识addr端对应的套接字。
而服务器则只需要监听端口,在请求到来的时候决定是否接受这个连接即可。
#include
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
listen将套接字设置为监听模式,backlog指定了等待队列的最大长度,超过此长度的连接请求将直接被拒绝;
accept将使进程阻塞,直到sockfd的监听队列非空,此时取出最早一个连接请求,并将此请求的相关地址标识存到addr指向的缓冲区,创建一个新的套接字描述符(与监听的sockfd有相同的sockaddr标识)与此请求建立连接,并返回这个套接字描述符。
connect和accept都是低速系统调用,即它们在资源不可用下将永远阻塞直到被一个信号打断;根据第十四章,可以用ioctl(2)或者fcntl(2)设置对应的套接字描述符为非阻塞模式,或者使用有多路转接与等待超时功能的select(2)和poll(2),描述符可读表示有连接请求在等待处理,描述符可写表示连接建立成功。
对于数据发送,可以直接用write(2),或者使用下面3个专门针对sockets机制设计的函数:
#include
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
send(2)除了带一个标志参数外,其它用法同write(2)。flags有4个选项,包括
MSG_DONTROUTE 该套接字仅在本地使用
MSG_DONTWAIT 使套接字非阻塞
MSG_EOR 表明发送记录结束
MSG_OOB 表明发送带外数据(若协议支持)
sendto(2)可以指定一个目标地址,常用于UDP(7)等无连接的协议;
sendmsg(2)类似wrtiev(2),可以设定多个缓冲区;
对于数据接收,可以直接用read(2),或者使用下面3个专门针对sockets机制设计的函数:
#include
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv(2)可用的标志包括
MSG_OOB 表明接收带外数据(若协议支持)
MSG_PEEK 只查看数据而不取出队;
MSG_TRUNC 要求返回报文的实际长度而不是实际收到的长度;
MSG_WAITALL 等待到数据都已可用(对于可靠连接的SOCK_STREAM而言)
recvfrom(2)则可以拿到数据发送者的地址标识存到指定缓冲区;recvmsg(2)类似readv(2)。
6、书中实例的程序模型
下面为书中给出的几个客户机/服务器模型的程序结构,会引起进程阻塞的低速系统调用用橙色表示。
![]()
程序清单16-4 面向连接的客户端
![]()
程序清单16-5 面向连接的服务器
![]()
程序清单16-6 另一个面向连接的服务器
![]()
程序清单16-7 无连接客户端
注意里面的函数print_uptime调用了alarm(2)。如果其它地方先使用了alarm(2)的话,原来的定时可能会被覆盖并带来bug。我给它打了个补丁(APUE2
源程序
的sockets/ruptime-dg.c):
--- ruptime-dg.c
+++ ruptime-dg.c.mod
@@ -15,18 +15,22 @@
print_uptime(int sockfd, struct addrinfo *aip)
{
int n;
+ int remain;
char buf[BUFLEN];
buf[0] = 0;
if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen)
err_sys("sendto error");
- alarm(TIMEOUT);
+ remain = alarm(TIMEOUT);
if ((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL))
- if (errno != EINTR)
- alarm(0);
+ if (errno != EINTR) {
+ remain = remain - alarm(0);
+ if (remain > 0) alarm(remain);
+ }
err_sys("recv error");
}
- alarm(0);
+ remain = remain - alarm(0);
+ if (remain > 0) alarm(remain);
write(STDOUT_FILENO, buf, n);
}
![]()
程序清单16-8 面向无连接的服务器
7、带外数据及异步IO
带外数据(也称紧急数据)为传输队列上优先被传输的数据。TCP(7)支持带外数据,UDP(7)不支持。
调用send(2)的标志参数flags为MSG_OOB时即为发送带外数据。recv(2)的flags为MSG_OOB时优先接收带外数据。在整个TCP队列中,只允许出现一个字节的带外数据。如果send(2)设置了MSG_OOB不止一个字节时,最后一个字节为带外数据。对接收方来说,数据队列中只有最新的带外数据有效。
收到带外数据时产生信号SIGURG。可以通过fcntl(2)设置接收指定套接字产生的SIGURG信号的接收者。进程就可以通过在SIGURG的信号捕捉函数中处理sockfd的I/O来处理这个带外数据:
fcntl(sockfd, F_SETOWN, pid);
还可以使用fcntl(2)实现基于套接字的异步I/O:
fcntl(sockfd, F_SETFL, O_ASYNC);
在sockfd的I/O可用时,进程将收到SIGIO信号。这样,进程就可以在SIGIO的信号捕捉函数中处理sockfd的I/O。
关于实时扩展的异步I/O机制已经在第14章说明,另可参考aio.h(7)。
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u2/82556/showart_1794854.html |
|