《UNIX环境高级编程》十六网络IPC:套接字读书笔记

1、套接字描述符

进程用套接字网络进程间通信接口能够和其他进程通信,无论它们是在同一台计算机上还是在不同的计算机上。
套接字是通信端点的抽象,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当作是一种文件描述符。

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

#include <sys/socket.h>
int socket(int domain,int type,int protocol);
//若成功,返回文件(套接字)描述符;若出错,返回-1

参数domain(域)确定通信的特性,包括地址格式:
这里写图片描述

参数type确定套接字的类型,进一步确定通信特征:
这里写图片描述

参数protocol通常是0,表示为给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用procotol选择一个特定协议:
这里写图片描述

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

#include <sys/socket.h>
int shutdown(int sockfd,int how);
//若成功,返回0;若出错,返回-1

参数how:
SHUT_RD(关闭读端):无法从套接字读取数据。
SHUT_WR(关闭写端):无法使用套接字发送数据。
SHUT_RDWR:既无法读取数据,又无法发送数据。
使用shudown函数的理由:
- 只有最后一个活动引用关闭时,close才释放网络端点。
- 可以很方便地关闭套接字双向传输中的一个方向。

2、寻址

进程标识由两部分组成:一部分是计算机的网络地址,它可以帮助标识网络上我们想与之通信的计算机;另一部分是该计算机上用端口号表示的服务,它可以帮助标识特定的进程。

2.1、字节序

如果处理器架构支持大端字节序:最大字节地址出现在最低有效字节(LSB)上。
如果处理器架构支持小端字节序:最小字节地址出现在最低有效字节(LSB)上。
这里写图片描述

网络协议指定了字节序,因此异构计算机能够交换协议信息而不会被字节序所混淆。TCP/IP协议栈使用大端字节序。

对于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);//返回以主机字节序表示的32位整数

2.2、地址格式

一个地址标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关。为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构sockaddr:

struct sockaddr {
sa_family_t   sa_family; /*address family(域)*/
char          sa_data[]; /*variable_length address*/
...
};

因特网地址定义在<netinet/in.h> 头文件中。在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];   /*IPv6address*/
};

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

注:尽管sockaddr_in与sockaddr_in6结构相差比较大,但它们均被强制转换称sockaddr结构输入到套接字例程中。

#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

函数inet_ntop将网络字节序的二进制地址转换称文本字符串格式。inet_pton将文本字符串格式转换成网络字节序的二进制地址。
参数domain仅支持两个值:AF_INET和AF_INET6。
参数size指定了保存文本字符串的缓冲区(str)的大小。两个常数用于简化工作:INET_ADDRSTRLEN定义了足够大的空间来存放一个表示IPv4地址的文本字符串;INET6_ADDRSTRLEN定义了足够大的空间来存放一个表示IPv6地址的文本字符串。

2.3、地址查询

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

#include <netdb.h>
struct hostent *gethostent(void);
//若成功,返回指针;若出错,返回NULL
void sethostent(int stayopen);
void endhostent(void);

如果主机数据库文件没有打开,gethostent会打开它。函数gethostent返回文件中的下一个条目。函数sethostent会打开文件,如果文件已经被打开,那么将其回绕。函数endhostent可以关闭文件。

struct hostent{
char    *h_name;      /*name of host*/
char   **h_aliases;   /*pointer to alternate host name array*/
int      h_adedrtype; /*address type*/
int      h_length;    /*length in bytes of address*/
char   **h_addr_list; /*pointer to array of network addresses*/
};
//注:返回的地址采用网络字节序

获得网络名字和网络编号:

#include <netdb.h>
struct netent *getnetbyaddr(uint32_t net,int type);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
//若成功,返回指针;若出错;返回NULL
void setnetent(int stayopen);
void endnetent(coid);
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*/
};
//网络编号按照网络字节序返回。地址类型是地址族常量之一(如AF_INET)。

我们可以用以下函数在协议名字和协议编号之间进行映射:

#include <netdb.h>
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
//若成功,返回指针;若出错;返回NULL
void setprotoent(int stayopen);
void endprotoent(void);
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);
//若成功,返回指针;若出错;返回NULL
void setservent(int stayopen);
void endservent(void);
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);

addrinfo结构:

struct addrinfo {
int              ai_flags;     /*customize behavior*/
int              ai_family;    /*address family*/
int              ai_socktype;  /*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_family、ai_flags、ai_protocol和ai_socktype字段。剩余的整数字段必须设置为0,指针字段必须为空。
ai_flags字段中的标志,用来自定义如何处理地址和名字:
这里写图片描述

如果getaddrinfo失败,不能使用perror或strerror来生成错误信息,而是要调用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参数提供了一些控制翻译的方式:
这里写图片描述

2.4、将套接字与地址关联

使用bind函数来关联地址和套接字:

#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t len);
//若成功,返回0;若出错,返回-1
  • 在进程正在运行的计算机上,指定的地址必须有效。
  • 地址必须和创建套接字时的地址族所支持的格式相匹配。
  • 地址中的端号必须不小于1024,除非该进程具有相应的特权(即超级用户)。
  • 一般只能将一个套接字端点绑定到一个给定地址上。

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

#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr *restrict addr,
                socklen_t *restrict alenp);
//若成功,返回0;若出错,返回-1

调用getsockname之前,将alenp设置为一个指向整数的指针,该整数指定缓冲区sockaddr的长度。返回时,该整数会被设置成返回地址的大小。

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

#include <sys/socket.h>
int getpeername(int sockfd,struct sockaddr *restrict addr,
                socklen_t *restrict alenp);
//若成功,返回0;若出错,返回-1

3、建立连接

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

#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t len);
//若成功,返回0;若出错,返回-1

在connect中指定的地址是我们想与之通信的服务器地址。

如果套接字描述符处于非阻塞模式,那么在连接不能马上建立时,connect将会返回-1并且将errno设置为特殊的错误码EINPROGRESS。

connect函数还可以用于无连接的网络服务。如果用SOCK_DGRAM套接字调用connect,传送的报文的目标地址会设置成connect调用中所指定的地址,这样每次传送报文时就不需要再提供地址。

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

#include <sys/socket.h>
int listen(int sockfd,int backlog);
//若成功,返回0;若出错,返回-1

参数backlog提供了一个提示,提示系统该进程所要入队的未完成连接请求数量。一旦队列满,系统就会拒绝多余的连接请求。

一旦服务器调用了listen,所用的套接字就能接受连接请求。使用accept函数获得连接请求并建立连接:

#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *restrict addr,
           socklen_t *restrict len);
//若成功,返回文件(套接字)描述符;若出错,返回-1

函数accept所返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字描述符(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持可用状态并接受其他连接请求。
如果不关心客户端标识,可以将参数addr和len设为NULL。
如果没有连接请求在等待,accept会阻塞直到一个请求道来。如果sockfd处于非阻塞模式,accept会返回-1,并将errno设置为EAGAIN或WEOULDBLOCK。

4、数据传输

用于发送数据的函数:
send和write相似,但是可以指定标志来改变处理传输数据的方式:

#include <sys/socket.h>
ssize_t send(int sockfd,const void *buf,size_t nbytes,int flags);
//若成功,返回发送的字节数;若出错,返回-1

使用send时套接字必须已经连接。
参数flags:
这里写图片描述

即使send成功返回,也并不表示连接的另一端的进程就一定接收了数据。我们所能保证的只是当send成功返回时,数据已经被无错误地发送到网络驱动程序上。

函数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

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

#include <sys/socket.h>
ssize_t sendmsg(int sockfd,const struct msghdr *msg,int flags);
//若成功,返回发送的字节数;若出错,返回-1

msghdr结构:

#struct msghdr {
void         *msg_name;
socklen_t     msg_namelen;
struct iovec *msg_iov;
int           msg_iovlen;
void         *msg_control;
socklen_t     msg_controllen;
int           msg_flags;
};

函数recv与read相似,但是recv可以指定标志来控制如果接受数据:

#include <sys/socket.h>
ssize_t recv(int sockfd,const void *buf,size_t nbytes,int flags);
//若成功,返回数据的字节长度;若无可用数据或对等放已经按序结束,返回0;若出错,返回-1

flags标志:
这里写图片描述

当指定MST_PEEK标志时,可以查看下一个要读取的数据但不真正取走它。当再次调用read或其中一个recv函数时,会返回刚才查看的数据。

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

#include <sys/socket.h>
ssize_t recvfrom(int sockfd,const void *buf,size_t nbytes,
                 int flags,
                 struct sockaddr *restrict addr,
                 socklen_t *restrict addrlen);
//若成功,返回数据的字节长度;若无可用数据或对等放已经按序结束,返回0;若出错,返回-1

如果addr非空,它将包含数据发送者的套接字端点地址。

因为可以获得发送者的地址,recvfrom通常用于无连接的套接字。

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

##include <sys/socket.h>
ssize_t recv(int sockfd,struct msghdr *msg,int flags);
//若成功,返回数据的字节长度;若无可用数据或对等放已经按序结束,返回0;若出错,返回-1

这里写图片描述

5、套接字选项

套接字机制提供了两个套接字选项接口来控制套接字行为。一个接口用来设置选项,另一个接口可以查询选项的状态。可以获取或设置以下3中选项:
(1)通用选项,工作在所有套接字类型上。
(2)在套接字层次管理的选项,但是依赖于下层协议的支持。
(3)特定于某协议的选项,每个协议独有的。

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

#include <sys/socket.h>
int setsockopt(int sockfd,int level,int option,const void *val,
               socklen_t len);
//若成功,返回0;若出错,返回-1

参数level:
SOL_SOCKET:通用的套接字层次选项
IPPROTO_TCP:TCP选项
IPPROTE_IP:IP选项

通用套接字层次选项:
这里写图片描述

参数val根据选项的不同指向一个数据结构或者一个整数。一些选项是on/off开关。如果整数非0,则启用选项,如果整数为0,则禁用选项。

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

#include <sys/socket.h>
int setsockopt(int sockfd,int level,int option,void *restrict val,
               socklen_t *restrict lenp);
//若成功,返回0;若出错,返回-1

6、带外数据

与普通数据相比,带外数据先行传输,即使传输队列已经有数据。TCP支持带外数据,但是UDP不支持。

TCP将带外数据称为紧急 数据。TCP仅支持一个字节的紧急数据。为了产生紧急数据,可以在3个send函数中的任何一个里指定MSG_OOB标志。
如果通过套接字安排了信号的产生,那么紧急数据被接收时,会发送SIGURG信号。

7、非阻塞和异步I/O

通常,recv函数没有数据可用时会阻塞等待。同样地,当套接字输出队列没有足够空间来发送消息时,send函数会阻塞。在套接字非阻塞模式下,这些函数不会阻塞而是会失败,将errno设置为EWOULDBLOCK或者EAGAIN。当这种情况发生时,可以使用poll或select来判断能否接收或者传输数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值