UNIX环境高级编程——网络IPC:套接字

16.1 引言

本章将考察不同计算机(通过网络相连)上的进程相互通信的机制:网络进程间通信(network IPC)。

16.2 套接字描述符

为创建一个套接字,调用socket函数:

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
										// 返回值:若成功,返回文件(套接字描述符);若出错,返回-1
  • 参数domain(域)确定通信的特征,包括地址格式,POSIX.1指定的各个域如下:
    在这里插入图片描述
  • 参数type确定套接字的类型,进一步确认通信特征,POSIX.1定义的套接字类型如下:
    在这里插入图片描述
  • 参数protocol通常为0,表示为给定的域和套接字类型选择默认协议,因特网域套接字定义的协议如下:
    在这里插入图片描述

套接字通信是双向的,可以采用shutdown来禁止一个套接字的I/O:

#include <sys/socket.h>

int shutdown(int sockfd, int how);
										// 返回值:若成功,返回0;若出错,返回-1
  • 参数how的可能值有3种:
    • SHUT_RD(关闭读端),那么无法从套接字读取数据;
    • SHUT_WR(关闭写端),那么无法使用套接字发送数据;
    • SHUT_RDWR(关闭读写端),既无法读取数据,又无法发送数据。

16.3 寻址

进程标识由两部分组成:

  • 网络地址:标识网络上想与之通信的计算机;
  • 端口号:计算机上用端口号表示服务,用于标识特定的进程。

16.3.1 字节序

  • 大端字节序:最大字节地址出现在最低有效字节(Least Significant Byte,LSB)上;
  • 小端字节序:最大字节地址出现在最高有效字节(Most Significant Byte,MSB)上。

对于TCP/IP应用程序,有4个用来在处理器字节序网络字节序之间实施转换的函数:

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostint32);
										// 返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostint16);
										// 返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netint32);
										// 返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netint16);
										// 返回值:以主机字节序表示的16位整数
  • h表示“主机”字节序,n表示“网络”字节序;
  • l表示“长”(即4字节)整数,s表示“短”(即2字节)整数。

16.3.2 地址格式

为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构 sockaddr

struct sockaddr {
	sa_family_t		sa_family;	/* address family */
	char			sa_data[];	/* variable-length address */
	...
};

在IPv4因特网域(AF_INET)中,套接字地址用结构 sockaddr_in 表示:

struct in_addr {
	in_addr_t		s_addr;		/* IPv4 address */
};

struct sockaddr_in {
	sa_family_t		sin_family;	/* address family */
	in_port_t		sin_port;	/* port number */
	struct in_addr	sin_addr;	/* IPv4 address */
};

IPv6因特网域(AF_INET6)套接字地址用结构 sockaddr_in6 表示:

struct in6_addr {
	uint8_t			s6_addr[16];	/* IPv6 address */
};

struct sockaddr_in6 {
	sa_family_t		sin6_family;	/* address family */
	in_port_t		sin6_port;		/* port number */
	uint32_t		sin6_flowinfo;	/* traffic class and flow info */
	struct in6_addr sin6_addr;		/* IPv6 address */
	uint32_t		sin6_scope_id;	/* set of interfaces for scope */
};

inet_ntopinet_pton用于二进制地址格式与点分十进制字符(a.b.c.d)之间的相互转换:

#include <arpa/inet.h>

const char *inet_ntop(int domain, const void *restrict addr,
					  char *restrict str, socklen_t size);
										// 返回值:若成功,返回地址字符串指针;若出错,返回NULL
int inet_pton(int domain, const char *restrict str,
			  void *restrict addr);
										// 返回值:若成功,返回1;若格式无效,返回0;若出错,返回-1
  • 参数 domain 仅支持两个值:AF_INETAF_INET6
  • 对于inet_ntop,参数size指定了保存文本字符串的缓冲区(str)的大小,INET_ADDRSTRLENINET6_ADDRSTRLEN分别定义了足够大的空间来存放一个表示IPv4和IPv6地址的文本字符串;

16.3.3 地址查询

通过调用gethostent,可以找到给定计算机系统的主机信息:

#include <netdb.h>

struct hostent *gethostent(void);
										// 返回值:若成功,返回指针;若出错,返回NULL
void sethostent(int stayopen);
void endhostent(void);
  • 如果主机数据库文件没有打开,gethostent会打开它,函数gethostent返回文件中的下一个条目,得到一个指向 hostent 结构的指针,该结构至少包含以下成员:
struct hostent {
	char	*h_name;		/* name of host */
	char	**h_aliases;	/* pointer to alternate host name array */
	int		h_addrtype;		/* address type */
	int		h_length;		/* length in bytes of address */
	char	**h_addr_list;	/* pointer to array of network addresses */
};
  • 函数sethostent会打开文件,如果文件已经被打开,那么将其回绕;当stayopen参数设置成非0值时,调用gethostent之后,文件将依然是打开的;
  • 函数endhostent可以关闭文件。

以下接口用于获得网络名字和网络编号:

#include <netdb.h>

struct netent *getnetbyaddr(uint32_t net, int type);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
										// 3个函数的返回值:若成功,返回指针;若出错,返回NULL
void setnetent(int stayopen);
void endnetent(void);

netent 结构至少包含以下字段:

struct netent {
	char		*n_name;		/* network name */
	char		**n_aliases;	/* alternate network name array pointer */
	int			n_addrtype;		/* address type */
	uint32_t	n_net;			/* network number */
	...
};

下列函数在协议名字协议编号之间进行映射:

#include <netdb.h>

struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
										// 3个函数的返回值:若成功,返回指针;若出错,返回NULL
void setprotoent(int stayopen);
void endprotoent(void);

POSIX.1定义的 protoent 结构至少包含以下成员:

struct protoent {
	char	*p_name;		/* protocol name */
	char	**p_aliases;	/* pointer to altername protocol name array */
	int		p_proto;		/* protocol number */
	...
};

服务是由地址的端口号部分表示的,每个服务由一个唯一的众所周知的端口号来支持。函数getservbyname将一个服务名映射到一个端口号,函数getservbyport将一个端口号映射到一个服务名,函数getservent顺序扫描服务数据库:

#include <netdb.h>

struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
struct servent *getservent(void);
										// 3个函数的返回值:若成功,返回指针;若出错,返回NULL
void setservent(int stayopen);
void endservent(void);

servent 结构至少包含以下成员:

struct servent {
	char	*s_name;		/* service name */
	char	**s_aliases;	/* pointer to alternate service name array */
	int		s_port;			/* port number */
	char	*s_proto;		/* name of protocol */
	...
};

getaddrinfo函数允许将一个主机名和一个服务名映射到一个地址:

#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *restrict host,
				const char *restrict service,
				const struct addrinfo *restrict hint,
				struct addrinfo **restrict res);
										// 返回值:若成功,返回0;若出错,返回非0错误码
void freeaddrinfo(struct addrinfo *ai);
  • 需要提供主机名、服务名,或者两者都提供;如果仅提供一个名字,另一个必须是一个空指针;主机名可以是一个节点名或点分格式的主机地址;
  • getaddrinfo函数返回一个链表结构 addrinfo,可以用freeaddrinfo来释放一个或多个这种结构,这取决于用 ai_next 字段链接起来的机构由多少;addrinfo 结构的定义至少包含以下成员:
struct addrinfo {
	int				ai_flags;		/* customize behavior */
	int				ai_family;		/* adddress family */
	int 			ai_sockettype;	/* socket type */
	int				ai_protocol;	/* protocol */
	socklen_t		ai_addrlen;		/* length in bytes of address */
	struct sockaddr	*ai_addr;		/* address */
	char			*ai_canonname;	/* canonical name of host */
	struct addrinfo	*ai_next;		/* next in list */
	...
};
  • 可以提供一个可选的hint来选择符合特定条件的地址,hint是一个用于过滤地址的模板,包括ai_familyai_flagsai_protocolai_socktype字段;剩余的整数字段必须设置为0,指针字段必须为空;下图总结了ai_flags字段中的标志:
    在这里插入图片描述

如果getaddrinfo失败,不能使用perrorstrerror来生成错误消息,而是要调用gai_strerror将返回的错误码转换成错误消息:

#include <netdb.h>

const char *gai_strerror(int error);
										// 返回值:指向描述错误的字符串的指针

getnameinfo函数将一个地址转换成一个主机名和一个服务名:

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen,
				char *restrict host, socklen_t hostlen,
				char *restrict service, socklen_t servlen, int flags);
										// 返回值:若成功,返回0;若出错,返回非0值
  • flags参数提供了一些控制翻译的方式,支持的标志如下:
    在这里插入图片描述

16.3.4 将套接字与地址关联

对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址;使用bind函数来关联地址和套接字:

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
										// 返回值:若成功,返回0;若出错,返回-1

可以调用getsockname函数来发现绑定到套接字上的地址:

#include <sys/socket.h>

int getsockname(int sockfd, struct sockaddr *restrict addr,
				socklen_t *restrict alenp);
										// 返回值:若成功,返回0;若出错,返回-1

如果套接字已经和对等方连接,可以调用getpeername函数来找到对方的地址:

#include <sys/socket.h>

int getpeername(int sockfd, struct sockaddr *restrict addr
				socklen_t *restrict alenp);
										// 返回值:若成功,返回0;若出错,返回-1

16.4 建立连接

如果要处理一个面向连接的网络服务(SOCK_STREAMSOCK_SEQPACKET),那么在开始交换数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之前建立一个连接;使用connect函数来建立连接:

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
										// 返回值:若成功,返回0;若出错,返回-1
  • connect中指定的地址是我们想与之通信的服务器地址;
  • 如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。

服务器调用listen函数来宣告它愿意接受连接请求:

#include <sys/socket.h>

int listen(int sockfd, int backlog);
										// 返回值:若成功,返回0;若出错,返回-1
  • 参数backlog提示系统该进程所要入队的未完成连接请求数量。

服务器使用accept函数获得连接请求并建立连接:

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *restrict addr,
		   socklen_t *restrict len);
										// 返回值;若成功,返回文件(套接字)描述符;若出错,返回-1
  • 如果不关心客户端标识,可以将参数 addrlen 设置为NULL

16.5 数据传输

send函数用于发送数据,它可以指定标志来改变处理传输数据的方式:

#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
										// 返回值:若成功,返回发送的字节数;若出错,返回-1
  • flags参数指定了套接字调用标志,如下:
    在这里插入图片描述

sendto函数在无连接的套接字上指定一个目标地址:

#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
			   const struct sockaddr *destaddr, socklen_t destlen);
										// 返回值:若成功,返回发送的字节数;若出错,返回-1

可以调用带有msghdr结构的sendmsg来指定多重缓冲区传输数据:

#include <sys/socket.h>

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
										// 返回值:若成功,返回发送的字节数;若出错,返回-1

POSIX.1定义了msghdr结构,它至少有以下成员:

struct msghdr {
	void			*msg_name;		/* optional address */
	socklen_t		msg_namelen;	/* address size in bytes */
	struct iovec	*msg_iov;		/* array of I/O buffers */
	int				msg_iovlen;		/* number of elements in array */
	void			*msg_control;	/* ancillary data */
	socklen_t		msg_controllen;	/* number of ancillary bytes */
	int				msg_flags;		/* flags for received message */
	...
};

函数recv可以指定标志来控制如何接收数据:

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
										// 返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
  • 下图总结了这些标志:
    在这里插入图片描述

可以使用recvfrom来得到数据发送者的源地址:

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
				 struct sockaddr *restrict addr,
				 socklen_t *restrict addrlen);
										// 返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
  • 如果addr非空,它将包含数据发送者的套接字端点地址。

为了将接收到的数据送入多个缓冲区,类似于readv,或者想接收辅助数据,可以使用recvmsg

#include <sys/socket.h>

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
										// 返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
  • msghdr结构指定接收数据的输入缓冲区;
  • 可以设置flags来改变recvmsg的默认行为;返回时,msghdr结构中的msg_flags字段被设为所接收数据的各种特征(进入recvmsgmsg_flags被忽略);recvmsg中返回的各种可能值如下:
    在这里插入图片描述

16.6 套接字选项

可以使用setsockopt函数来设置套接字选项:

#include <sys/socket.h>

int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);
										// 返回值:若成功,返回0;若出错,返回-1
  • 参数level标识了选项应用的协议;如果选项是通用的套接字层次选项,则level设置成SOL_SOCKET;否则,设置成控制这选项的协议编号,对于TCP选项,levelIPPROTO_TCP,对于IP,levelIPPROTO_IP;下图总结了Single UNIX Specification中定义的通用套接字选项:
    在这里插入图片描述
  • 参数val根据选项的不同指向一个数据结构或者一个整数;一些选项是on/off开关,如果整数非0,则启用选项,如果整数为0,则禁止选项;
  • 参数len指定了val指向对象的大小。

可以使用getsockopt函数来查看选项的当前值:

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict len);

16.7 带外数据

带外数据(out-of-band data)是一些通信协议所支持的可选功能,与普通数据相比,它允许更高优先级的数据传输;带外数据先行传输,即使传输队列已经有数据。TCP支持带外数据,但是UDP不支持。

TCP将带外数据称为紧急数据。TCP仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。为了产生紧急数据,可以在3个send函数中的任何一个里指定MSG_OOB标志。如果带MSG_OOB标志发送的字节数超过一个时,最后一个字节将被视为紧急数据字节。

如果通过套接字安排了信号的产生,那么紧急数据被接收时,会发送SIGURG信号。

TCP支持紧急标记(urgent mark)的概念,即在普通数据流中紧急数据所在的位置。如果采用套接字选项SO_OOBINLINE,那么可以在普通数据中接收紧急数据。为帮助判断是否已经到达紧急标记,可以使用函数sockatmark

#include <sys/socket.h>

int sockatmark(int sockfd);
										// 返回值:若在标记处,返回1;若没在标记处,返回0;若出错,返回-1
  • 当下一个要读取的字节在紧急标志处时,sockatmark返回1。

16.8 非阻塞和异步I/O

实例代码

chapter16

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MinBadGuy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值