1. 主机字节序和网络字节序
下面以32位机为前提:CPU累加器一次能装载至少 4 字节,即一个整数。字节序分为:1.大端字节序(big endian)指一个整数的高位字节(23~32 bit )存储在内存的低地址处,低位字节(0~7 bit)存储在内存的高地址处。2.小端字节序(little endian)指整数的高位字节存储在内存的高地址处,低位字节存储在内存的低地址处。
现在大多PC采用小端字节序,因此小端字节序
又称为主机字节序
。
而大端字节序
又称为网络字节序
。
1.1 检查字节序
#include <stdio.h>void byteorder()
{union {short value;char union_bytes[sizeof(short)];}test;test.value = 0x0102;if(test.union_bytes[0] == 1 && test.union_bytes[1] == 2){printf("big endian.\n");}else if( test.union_bytes[0] ==2 && test.union_bytes[1] == 1 ){printf("little endian.\n");}else{printf("unknow...\n");}
}int main()
{byteorder();return 0;
}
1.2 字节序不统一的影响
若两台使用不同字节序的主机间传递格式化的数据时,接收端必然会错误解读。
1.3 统一字节序
解决字节序不统一的方法:发送端总是把要发送的数据转为 网络字节序(大端字节序)后再发送。接收端知道对方传送过来的数据是网络字节序(大端字节序),故接收端根据自身决定是否对接到的数据进行字节序转换。
1.4 Linux 字节序转换
Linux提供如下 4 个函数来完成主机字节序(小)和网络字节序(大)的转换:
#include <netinet/in.h>
unsigned long int htonl (unsigned long int hostlong);
unsigned short int htons (unsigned short int hostshort);
unsigned long int ntohl ( unsigned long int netlong );
unsigned short int ntohs (unsigned short int netshort);//htonl : host to network long,长整型主机字节序数据转化为网络字节序数据。//长整型函数(htonl、ntohl)常用来转换 IP 地址。
//短整型函数(htons 、ntohs )常用来转换 端口号。
2. socket 地址
2.1 通用 socket 地址(结构体)
注: 这一小节的 socket地址不常用 。
Linux通用 socket 地址结构体如下:
#include <bits/socket.h>
struct sockaddr
{sa_family_t sa_family; //地址族类型 通常对应协议族类型。char sa_data[14]; //用于存放 socket 地址。
}
sa_family 成员是 地址协议族(sa_family_t)的变量。
地址协议族类型 通常与 协议族类型对应
常见的协议族(protocol family,也称 domain)如下图:
sa_data 成员用于存放 socket 地址值。
2.2 专用 socket 地址(结构体)
**注:**这小节才是 实际编程常用的 socket 地址结构体。
通用不好用,所以就引申除了专用的。
2.2.1 UNIX 本地域协议族使用专用 socket 地址结构体
如下:
#include <sys/un.h>
struct sockaddr_un
{sa_family_t sin_family; //地址族:AF_UNIXchar sun_path[108]; //文件路径名
}
2.2.2 TCP/IP 协议族专用 socket 地址结构体
IPv4 专用结构体:
struct sockaddr_in
{sa_family_t sin_family; //地址族:AF_INETu_int16_t sin_port; //端口号,要用网络字节序表示struct in_addr sin_addr;//IPv4 地址结构体,见下面
}
struct in_addr
{u_int32_t s_addr; //IPv4地址,用网络字节序表示(大端)
}IPv6 专用结构体:
struct sockaddr_in6
{sa_family_t sin6_family; //地址族:AF_INET6u_int16_t sin6_port; //端口号,网络字节序表示(大端)u_int32_t sin6_flowinfo; //流信息,应设置为 0struct in6_addr sin6_addr; //IPv6 地址结构体,见下面u_int32_t sin6_scope_id; //scope id,尚处于实验阶段
}
struct in6_addr
{unsigned char sa_addr[16]; //IPv6 地址,用网络字节序表示(大端)
}
所有专用 socket 地址
(以及类型 sockaddr-storage)类型的变量在实际使用时都需要转化为 通用 socket 地址类型 sockaddr
(强制转换即可)。
2.2.2.1 IP地址转换
专用地址结构体中有 IP 地址的使用。通常,人们用好记的字符串表示IP,如
IPv4 地址: 点分十进制字符串表示
IPv6 地址:十六进制字符串 表示。但 Linux网络编程的地址结构体(见上一小节)中使用整型类型表示 IPv4地址。
下面3 个函数 点分十进制 IPv4 地址和网络字节序整数 IPv4地址间的转换#include <arpa/inet.h>
in_addr_t inet_addr(const char * strptr); //将点分十进制字符串的IPv4地址转为网络字节序整数的 IPv4 地址。失败返回 INADDR_NONE.
int inet_aton(const char * cp,struct in_addr * inp);//完成和 inet_addr 一样的功能,但是将转化结果存储于 inp 指向的地址结构中
char * inet_ntoa(struct in_addr in); //将网络字节序整数表示的IPv4地址转换为点分十进制字符串表示的IPv4地址注意: inet_ntoa 内部 应用的是一个 静态变量存储转化结果,函数返回值指向该静态内存。 故inet_ntoa 是不可重入的。
下面 2 个函数可同时完成 IPv4 和 IPv6的 地址 字符串到 网络字节序类型的转换 :#include <arpa/inet.h>
//src:点分十进制字符串IPv4或十六进制字符串IPv6地址
//dst:网络字节序整数的内存地址
//af 指定协议族。 AF_INET 、AF_INET6
//功能:将字符串表示的 IP 地址 src,转换为 网络字节序整数的 IP地址,并把转换结果存储到 dst 指向的内存中。
//成功返回 1。失败 0,并设置 errno.
int inet_pton(int af,const char * src,void *dst); //执行与inet_pton相反的转换。
//cnt:指定目标存储单元大小,下面两个宏帮助指定大小
//成功返回目标存储单元地址,失败 返回 NULL,并设置errno
const char* inet_ntop( int af,const void *src,char *dst,socklen_t cnt);#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
3. socket 相关
3.1 创建 socket
//服务端、客户端都要用到
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);domain: 使用哪个底层协议族对TCP/IP协议族该参数设置为:IPv4:PF_INET //(Protocol Family of Internet)IPv6:PF_INET6 对于 UNIX 本地协议族而言,该参数设置为: PF_UNIX其他详见 man手册。type:指定服务类型。服务类型有:1.流服务:SOCK_STREAM对应 TCP2.数据报服务:SOCK_UGRAM 对应 UDP扩展:Linux 内核版本2.6.17起,type参数可以接收上述服务类型与下面两个重要标志 相与 的值:1.SOCK_NONBLOCK将新建的 socket 设为非阻塞的2.SOCK_CLOEXEC用 fork 调用创建子进程时在子进程中关闭该 socket。Linux 内核版本2.6.17前,上诉两个重要标志需使用额外的系统调用。protocol:在前两个参数构成的协议集合下,再选择一个具体的协议。这个值通常是唯一的(前两个参数已经完全决定了它的值)。一般我们把此值设为 0,表示使用默认协议。成功返回 socket 文件描述符,失败返回 -1 并设置errno.
3.2 命名 (bind)socket
//用于服务端和客户端#include <sys/types.h>
#include <sys/socket.h>
//将 myaddr 所指的地址绑定到创建的 sockfd 上。
//成功返回 0,失败返回-1 并设置 errno。
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);两种常见的 errno:1.EACCES绑定的地址是受保护地址,仅超级用户能够访问。如 普通用户绑定 socket到 知名服务端口(0~1023),即返回此报错。2.EADDRINUSE被绑定的地址正在使用。如将 socket 绑定到一个处于 TIME_WAIT状态的 socket 地址。
3.3 监听(listen)socket
//用于服务端
#inlcude <sys/socket.h>
int listen( int sockfd,int backlog);sockfd:指定被监听的 socket。
backlog:提示内核监听队列的最大长度。典型值为 5.监听队列的长度如果超过 backlog,服务器将不受理新的客户连接,客户端也将收到 ECONNREFUSED 错误信息。扩展:内核版本2.2之前:backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的 socket 上限。内核版本2.2之后:指处于完全连接状态的 socket 上线。处于半连接状态的 socket 上限则由 /proc/sys/net/ipv4/tcp_max_syn_backlog 内核参数定义。成功时返回 0,失败返回 -1 并设置errno。
3.4 接收(accept)连接请求
//用于服务端#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);sockfd:执行过 listen 系统调用的监听 socket。
addr:用来获取被接受连接的远端 socket 地址。
addrlen:指定 addr 的长度。成功;返回一个新的 socket,此socket唯一的标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。失败:返回 -1 并设置errno。扩展:accept只是从监听队列汇总取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。例:1.服务端开启监听2.客户端发起连接,然后 20s 后断开此连接,3.服务端 还是会在 accept 中拿到该客户端请求。
3.5 发起连接(connect)
//用于客户端
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);serv_addr:服务器监听的 socket 地址
addrlen:指定这个地址的长度。成功: 返回 0。一旦成功建立连接,sockfd就唯一标识了这个连接,客户端可以通过读写 sockfd 来与服务器通信。
失败:返回 -1,并设置 errno:常见 errno:1.ECONNREFUSED目标端口号不存在,连接被拒绝。2.ETIMEDOUT连接超时。
3.6 关闭连接(close、shutdown)
#include <unistd.h>
int close(int fd);fd:待关闭的socket。扩展:close 关闭只能同时关闭 socket 读写。close调用并不是立即关闭一个连接,而是将fd引用计数减 1。只有当 fd 引用计数为 0 时,才真正关闭连接。 例:对进程程序中,一次 fork 调用默认将使父进程中打开的 socket 引用计数加 1.故,必须在父进程和子进程中都对该 socket执行 close 才能将连接关闭。
立即终止连接,而不是和close一样将 socket 引用计数减 1.#include <sys/socket.h>
int shutdown(int sockfd,int howto);sockfd:待关闭的 socket。
howto:此取值决定 shutdown 行为。可取值如下图 5-3。
成功:0
失败:-1,并设置errno.扩展:可指定关闭读或写。
3.7 TCP 数据读写
3.7.1 TCP 读
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd,void *buf,size_t len,int flags);sockfd:读该socket连接上的数据buf:指定读缓存区的位置len:指定读缓存区的长度,它可能小于我们期望的长度。因此可能需要多次执行 recv()才能读到完整数据。flags:详见后文返回: 0,意味对方已关闭连接。-1, 意味出错并设置errno。
3.7.2 TCP 写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd,const void *buf,size_t len,int flags);sockfd:往 该连接上 写数据。buf:指定写缓冲区的位置len:指定写缓冲区大小成功:返回实际写入数据长度失败:返回-1,并设置errno。
3.7.3 参数 flags详解
3.8 UDP 数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd,void * buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t * addrlen);sockfd:recvfrom 读取 sockfd上的数据buf:指定读缓冲区的位置len:指定读缓冲区大小src_addr:因为UDP通信没有连接概念,故每次读取都需要获取发送端 socket 地址,即 src_addr 的内容。addrlen:指 src_addr 的长度flags:同上面TCP flags详解。ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t * addrlen)sockfd:sendto 向 sockfd上写入数据buf:指定写缓冲区的位置len:指定写缓冲区大小dest_addr:因为UDP通信没有连接概念,故每次写入都需要指定接收端 socket 地址,即 dest_addr 的内容。addrlen:指 dest_addr的长度flags:同上面TCP flags详解。扩展:recvfrom/sendto 系统调用也可以用于面向连接(STREAM)的socket数据读写,只需要把最后两个参数设置为 NULL 即可。
3.9 通用数据读写
socket 编程接口还提供不仅能用于TCP流数据,也可用于UDP数据报的系统调用:
#include <sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr* msg,int flags);
参数详解:
sockfd:读写的socket连接
flags:同上面TCP send/recv 的flags 参数。msghdr:struct msghdr{void * msg_name; //socket地址socklen_t msg_namelen; //socket地址长度struct iovec* msg_iov; //分散内存块,详见下文int msg_iovlen; //分散内存块数量void * msg_control; //指向辅助数据的起始位置socklen_t msg_controllen;//辅助数据大小int msg_flags; //复制函数中的 flags 参数,并在调用过程中更新}参数详解:msg_name:指向一个 socket 地址结构变量。指定通信对方的 socket 地址。对于TCP而言,必须设为NULL。msg_namelen:指定 msg_name 所指地址长度。msg_iov:iovec结构体定义如下,iovec结构体封装了一块内存的起始位置和长度:struct iovec{void * iov_base; //内存起始地址size_t iov_len; //这块内存长度}msg_iovlen:指定 iovec 这样的结构对象有多少个。 msg_control、msg_controllen:用于辅助数据的传送msg_flags:无需设定。它会复制 recvmsg/sendmsg 的 flags 参数内容。recvmsg 还会在调用结束前,将某些更新后的标志设置到 msg_flags 中。
3.10 带外数据(紧急数据)的读取
实际开发中无法预料带外数据何时到来。
Linux内核检测到 TCP紧急标志时,将通知应用程序有带外数据需接收。
内核通知应用程序带外数据到达两种常见方式:1.I/O复用产生的异常事件2.SIGURG 信号应用程序得到信号后,还要知道带外数据在数据流中的位置,通过如下系统调用实现:
#include <sys/socket.h>
int sockarmark(int sockfd);判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据,
若是,返回 1,此时可利用带 MSG_OOB标志的 recv调用来接收带外数据。
若不是,返回 0.
3.10 获取 socket 连接 地址信息
#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr * address,socklen_t * address_len);sockfd:获取sockfd对应的本端 scoket 地址,address:将获取到的地址存储于指向的内存中address_len:存储address地址长度。若实际socket地址长度大于address_len所指大小,那么 address 将被截断。成功:返回 0失败:返回 -1并设置 errno。int getpeername(int sockfd,struct sockaddr * address,socklen_t * address_len);获取对端地址信息。 参数同上。
3.11 socket 选项详解
下面两个系统调用专门用来读取和设置 socket 文件描述符属性;
#include <sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,void * option_value,socklen_t * restrict option_len);
int setsockopt(int sockfd,int level,int option_name,const void *option_value,socklen_t option_len);上面两个函数参数和返回值含义基本一致,如下: sockfd:指定被操作的目标 socket。level:指定要操作哪个协议的选项(即属性),如 IPv4、IPv6、TCP等。option_name:指定选项名。如下图 5-5 列举了常用的选项。option_value:被操作选项的值。不同的选项有不同的值,参照 图5-5 数据类型列。option_len:被操作选项的长度。成功返回 0失败返回-1,并设置errno。
扩展:对于服务器:1.部分 socket 选项只能在调用 listen 系统调用前针对监听 socket 设置才有效。因为连接 socket 只能由 accept 返回,而 accept 从 listen 监听队列中接受的连接至少已经完成了 TCP 三次握手的前两步(因为 listen 监听队列的连接至少进入了 SYN_RCVD状态),这说明服务器已经往被接受连接上发送了 TCP 同步报文段。2.有的 socket 选项却应该在 TCP 报文段中设置。如:TCP最大报文段。对这种情况Linux提供的解决方案:对监听 socket 设置这些 socket 选项,那么 accept 返回的连接 socket 自动继承这些选项。这种能继承的 socket 选项包括:图 5-5 画红√的选项。对于客户端: 这些 socket 选项应在调用 connect 之前设置。因为 connect 调用成功返回后,TCP三次握手已完成。
下面详解部分重要 socket 选项。
3.11.1 SO_REUSEADDR 选项
通过设置 socket 选项 SO_REUSEADDR 来强制使用处于 TIME_WAIT 状态的连接占用的 socket 地址。实现如下代码:
int sock = socket(PF_INET,SOCK_STREAM,0);
assert(sock >= 0);
int reuse = 1;
setsockopt(sock, SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse)); //重点,注意此代码所处位置,创建socket后就绑定。struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port = htons(port);
int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
扩展:也可以通过修改内核参数 /proc/sys/net/ipv4/tcp_tw_recycle 来快速回收被关闭的 socket,从而使TCP连接根本就不进入 TIME_WAIT 状态,进而允许应用程序立即重用本地 socket 地址。
3.11.2 SO_RCVBUF 和 SO_SNDBUF 选项
SO_RCVBUF 选项:用来设置 TCP 接收缓冲区大小 SO_SNDBUF 选项:用来设置 TCP 发送缓冲区大小 当我们使用 setsockopt 设置如上两个选项时,系统都会将该值加倍,并且不小于某个最小值。系统自动设置不小于最小值的目的:主要是确保一个 TCP 连接拥有足够空闲缓冲区来处理拥塞(如快速重传算法期望TCP接收缓冲区至少容纳 4 个大小为SSMS的TCP报文段)。一般情况下(不同系统会有不同值):TCP 接收缓冲区最小值是 256字节,TCP 发送缓冲区最小值是 2048字节。扩展:可通过修改内核参数来强制TCP接收缓冲区和发送缓冲区大小没有限制:/proc/sys/net/ipv4/tcp_rmem/proc/sys/net/ipv4/tcp_wmenm示例如下图:
3.11.3 SO_RCVLOWAT 和 SO_SNDLOWAT 选项
SO_RCVLOWAT 选项:用来表示 TCP 接收缓冲区的低水位标记
SO_SNDLOWAT 选项: 用来表示 TCP 发送缓冲区的低水位标记一般被 I/O 复用系统调用用来判断 socket 是否可读或可写。1.当TCP接收缓冲区可读数据的总数 大于 其低水位标记时,I/O复用系统调用将通知 应用程序可以从对应的 socket 上读取数据。2.当TCP接收缓冲区可写入数据的空间 大于 其低水位标记时,I/O复用系统调用将通知 应用程序可以向对应的 socket 上写入数据。默认情况下 TCP 接收缓冲区低水位标记 和 TCP 发送缓冲区低水位标记 均为 1 字节。
3.11.4 SO_LINGER 选项
此选项 控制 close 系统调用在 关闭 TCP 连接时的行为。1.默认情况,当利用 close 系统调用关闭一个 socket 时,close 将立即返回,TCP 模块负责将该 socket 对应的 TCP 发送缓冲区中残留的数据发送给对方。2.设置 SO_LINGER 选项,会产生三种行为设置该选项时,需给 setsockopt(getsockopt)系统调用传递一个 linger 类型结构体(详见下面)根据 linger 结构体 成员变量不同的值,会产生如下 3 种行为:1. l_onoff 等于 0.此时 SO_LINGER选项不起作用,close使用默认行为关闭 socket。2. l_onoff 不为 0 ,同时 l_linger 等于 0.此时 close 系统调用立即返回,TCP模块将丢弃被关闭的 socket 对应的 TCP 缓冲区的残余数据,同时给对方发送一个 复位报文段。3. l_onoff 不为 0,同时 l_linger 大于 0.此时 close 行为取决于两个条件:1. 被关闭的 socket 对应的 TCP 发送缓冲区中是否还有残留的数据2. 该 socket 是阻塞的,还是非阻塞的。①对于阻塞:close 将等待一段长为 l_linger 的时间,直到 TCP 模块发送完所有残余数据并得到对方确认。若 l_linger 时间内 TCP模块没有发送完残留数据并得到对方确认,那么 close 将返回 -1 并设置 errno 为 EWOULDBLOCK。②对于非阻塞:close 将立即返回,此时根据其返回值和 errno 来判断残留数据是否已发送完毕。
#include <sys/socket.h>
struct linger
{int l_onoff; //开启:非0,关闭: 0int l_linger; //滞留时间
}
3.12 socket 网络信息 API
3.12.1 根据主机名 或 IP获取主机信息
gethostbyname : 根据主机名获取主机完整信息 查找流程:1.通产先在本地 /etc/hosts 配置文件中查找主机,若没有找到再到 DNS 服务器
gethostbyaddr :根据 IP 获取主机完整信息
注:这两个函数是不可重入的,即非线程安全的,Linux还给出了线程安全版本的相应函数,即 原函数名后 加 _r(re-entrant)#include <netdb.h>
struct hostent * gethostbyname(const char * name);
struct hostent * gethostbyaddr(const void * addr,size_t len,int type);name:指定主机名addr:指定目标主机IPlen:指定 addr 所指 IP 地址长度。type:指定 addr 所指 IP地址类型取值示例:IPv4:AF_INETIPv6:AF_INET6hosent 结构体定义:
#include <netdb.h>
struct hostent
{char * h_name; //主机名char ** h_aliases; //主机别名列表,可能有多个int h_addrtype; //地址类型(地址族)int h_length; //地址长度char ** h_addr_list; //网络字节序(大端)列出的主机 IP 地址列表
}
3.12.2 根据名称 或 端口号获取某个服务完整信息
getservbyname:根据名称获取某个服务完整信息
getservbyport:根据端口号获取某个服务完整信息实际上两者都是通过读取 /etc/services 文件来获取服务信息的。
注:这两个函数是不可重入的,即非线程安全的,Linux还给出了线程安全版本的相应函数,即 原函数名后 加 _r(re-entrant)#include <netdb.h>
struct servent * getservbyname(const char * name,const char * proto);
struct servent * getservbyport(int port,const char * proto);name:指定目标服务名。port;指定目标服务端口号proto:指定服务类型。传递 tcp 表示获取流服务传递 udp 表示获取数据服务,传递 NULL 表示获取全部服务。servent结构体定义如下:
#include <netdb.h>
struct servent
{char * s_name; //服务名称char ** s_aliases; //服务别名列表,可能有多个int s_port; //服务端口号char * s_proto; //服务类型,通常为 tcp 或 udp
}
3.12.3 getaddrinfo
getaddrinfo 函数即可通过主机名获得 IP(内部使用gethostbyname函数),亦可通过服务名获得端口号(内部使用getservbyname函数)。
#include <netdb.h>
int getaddrinfo(const char * hostname,const char * service,const struct addrinfo * hints,struct addrinfo ** result);hostname:1.可以接收主机名2.也可以接收 字符串表示的IP地址(点分十进制IPv4,16进制字符串IPv6)service:1.可以接收服务名2.也可接收字符串表示的十进制端口号hints:是应用程序给 getaddrinfo 的一个提示,以对输出进行更精确控制。可以设置为 NULL,表示允许 getaddrinfo 反馈任何可用结果。result:指向一个链表,该链表存储 getaddrinfo 返回结果。成功:返回 0失败:返回错误码,错误码列表如下表 5-8 所示。 addrinfo结构体定义如下:
struct addrinfo
{int ai_flags; //可以取 表 5-6 中的标志 按位或int ai_family; //地址族int ai_socktype; //服务类型, SOCK_STREAM 或 SOCK_DGRAMint ai_protocol; //指具体的网络协议,其含义好socket系统调用的第三个参数相同,通常默认设置为 0socklen_t ai_addrlen; //socket 地址 ai_addr 的长度char * ai_canonname; //主机别名struct sockaddr * ai_addr; //指向 socket 地址struct addrinfo * ai_next; //指向下一个 sockinfo 结构对象
}注意:当使用 hits参数时,可以设置 ai_flags、 ai_family、ai_socktype、ai_protocol四个字段,其它字段必须设置为 NULL。
例子:
3.12.4 getnameinfo
getnameinfo 能通过 socket 地址同时获得 以字符串表示的主机名(内部使用 gethostbyaddr 函数)和 服务名(内部使用 getservbyport 函数)。
#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr,socklen_t addrlen,char * host,socklen_t hostlen,char * serv,socklen_t servlen,int flags);host:将返回的主机名存储在 host 指向的内存中。hostlen:指定 host 指向的内存 长度serv:将返回的服务名存储在 serv 指向的内存中。servlen:指定 serv 指向的内存 长度flags:控制 getnameinfo 的行为,接收下表 5-7 中的选项。成功:返回 0失败 返回错误码,错误码列表如下表 5-8 所示。
使用如下函数将数值错误码 errno 转换为可读的字符串形式:
#include <netdb.h>
const char * gai_strerror(int error);
4.高级I/O函数
4.1 创建文件描述符的函数
4.1.1 pipe 函数
用于创建一个管道,以实现进程间通信
#include <unistd.h>
int pipe(int fd[2]);参数:两个 int 型整数指针。返回值:成功:返回 0.并将一对打开的文件描述符填入其参数指向的数组失败:返回 -1,并设置errno。详解:pipe 函数创建的两个文件描述符 fd[0] 和 fd[1]分别构成管道两端,管道内传输的是 字节流,和TCP 字节流有差异:大小区别:TCP连接:往 TCP连接 写入数据多少取决于 对方接收通告窗口大小和本端的拥塞窗口大小。管道:管道 本身有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节数据。Linux 2.6.11内核起,管道最大容量默认为 65536 字节,可以使用 fcntl 函数来修改容量。假设 fd[0] 为 读端,fd[1] 为 写端:读端只能读,写端只能写。故fd[0]只能读, fd[1] 只能写,不能反过来用。若要双向传输,应该使用两个管道。默认情况下,这一对描述符都是阻塞的:例:若调用 write 往一个满的管道中写入数据,则 write 将被阻塞,直到管道有足够多的空闲空间可用。若设置 创建的两个文件描述符 fd[0] 和 fd[1] 都为非阻塞的,此时 read 和 write 会有不同行为。如果管道 写端 文件描述符的引用计数减少至 0,即没有任何数据往管道中写,-> 则相应的读端的 read 操作将返回 0,即读到了文件结束标记 (EOF,End Of File).如果管道 读端 文件描述符引用计数减少至 0,即没有任何数据需要从管道读取,-> 则 相应的 管道 写端的 write 操作将失败,并引发 SIGPIPE 信号。
扩展:
创建 双向管道函数:
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain,int type,int protocol,int fd[2]);参数:1. 前三个参数和socket系统调用参数完全相同,但 domain 只能使用 UNIX 本地协议族 AF_UNIX.2. fd[2]参数含义和 pipe相同,但区别于 pipe 参数的是这对描述符都是 既可读又可写的。返回:成功 0失败 -1 并设置 errno。注意:仅能在本地使用这个 双向管道。
4.1.2 dup 和 dup2 函数
#include <unistd.h>
int dup(int file_descriptor);创建一个新的文件描述符,该文件描述符和原有文件描述符 file_descriptor 指向相同的文件、管道或网络连接,返回:返回的文件描述符总是取系统当前可用的最小整数值。失败 -1 并设置 errno。int dup2(int file_descriptor_one , int file_descriptor_two);返回:返回的文件描述符为 第一个不小于 file_descriptor_two 的整数值。失败 -1 并设置 errno。
例:
4.1.3 readv 和 writev 函数
readv:将数据从文件描述符读到分散的内存块中
writev:将多块分散内存数据一并写入文件描述符中相当于简化版的 recvmsg 和 sendmsg函数。
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector,int count);
ssize_t writev(int fd, const struct iovec* vector,int count);参数:fd:备操作的文件描述符。iovec:此结构体描述一块内存。 详见下文。count:是vector数组长度,即有多少块内存数据从fd读出或写入。返回:成功 返回读出/写入 fd 的字节数。失败 返回 -1,并设置 errno。iovec结构体定义如下,iovec结构体封装了一块内存的起始位置和长度:struct iovec{void * iov_base; //内存起始地址size_t iov_len; //这块内存长度}
4.1.4 sendfile 函数
两个文件描述符间传递数据(完全在内核中操作)
,从而避免了内核缓冲区和用户缓冲区间的数据拷贝,这被称为零拷贝
。
sendfile 函数:是零拷贝。
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t * offset,size_t count);out_fd:待写入内容的文件描述符。注意,此参数必须是一个 socket。in_fd:待读出内容的文件描述符。注意:此参数必须是指向真实的文件,不能是socket和管道。offset:指定从读入文件流(in_fd)的哪个位置开始读。若为空,使用读入文件默认的起始位置。count:指定in_fd 和 out_fd 间传输的字节数。返回:成功: 返回传输的字节数。失败:-1 并设置 errno。由上可知,sendfile几乎是专门为网络上传输文件而设计的。
例:
4.1.5 mmap 函数 和 munmap 函数
mmap 函数用于申请一段内存空间,
munmap 用于释放这段内存空间。这段内存的作用;1.进程间通信的共享内存。2.将文件直接映射到其中。
#include <sys/mman.h>
void * mmap(void * start,size_t length,int prot, int flags, int fd, off_t offset);
int mummap(void * start, size_t length);start:允许用户使用某个特定的地址作为这段内存的起始地址。若被设为 NULL,则系统自动分配一个地址。length:指定 start指向内存段的长度。prot:设置 start指向内存段访问权限。可以取以下几个值得按位或:PROT_READ: 内存段可读。PROT_WRITE: 内存段可写。PROT_EXEC: 内存段可执行。PROT_NONE: 内存段不能被访问。flags:控制内存段内容被修改后程序的行为。可以被设置为 如下图6-1 (只有常用的,不是全部)中的某些值的 按位或。注意(MAP_SHARED 和 MAP_PRIVATE 是互斥的,不能同时指定。)fd:被映射文件对应的文件描述符。一般通过 open 系统调用获得。offset:设置从文件的何处开始映射(对于不需要读入整个文件的情况)返回:成功:返回指向目标内存区域的指针失败:返回 -1,并设置errno。
4.1.6 splice 函数
splice 函数 用于在两个文件描述符间移动数据。是零拷贝操作。
注意:使用此函数时,fd_in 或 fd_out 必须有一个是管道文件描述符。#include <fcntl.h>
ssize_t splice(int fd_in, loff_t * off_in, int fd_out, loff_t * off_out, size_t len, unsigned int flags);fd_in:待输入数据的文件描述符。off_in:若 fd_in 是一个管道文件描述符:off_in 必须设置为 NULL若 fd_in 不是一个管道文件描述符:如 是一个 socket,此时 off_in 表示从输入数据流的何处开始读取数据。此时:若 off_in 设置为 NULL,表示从输入流当前位置读入若 off_in 设置不为 NULL,将指出具体偏移位置。fd_out:输出数据的文件描述符。off_out(同 off_in,但表示 输出流):若 fd_out 是一个管道文件描述符:off_out 必须设置为 NULL若 fd_out 不是一个管道文件描述符:如 是一个 socket,此时 off_out 表示从输入数据流的何处开始读取数据。此时:若 off_out 设置为 NULL,表示从输入流当前位置读入若 off_out 设置不为 NULL,将指出具体偏移位置。len:指定移动数据长度。flags:控制数据如何移动。可以被设置为 表 6-2 中某些值的 按位或。返回:成功:返回移动字节数量。返回 0,代表没有数据移动。失败:-1,并设置errno。常见 errno 见表 6-3.
例:
4.1.7 tee 函数
在两个管道文件描述符之间复制数据。
是零拷贝。
源文件描述符上的数据仍然可以用于后续 读操作。
#include <fcntl.h>
ssize_t tee(int fd_in ,int fd_out,size_t len, unsigned int flags);fd_in :必须是管道文件描述符。fd_out:必须是管道文件描述符。len:指定移动数据长度。flags:控制数据如何移动。可以被设置为 表 6-2 中某些值的 按位或。返回:成功:返回两个文件描述符间复制的数据数量(字节数)。0,表示没有复制任何数据。失败:-1,并设置 errno。
例:
4.1.8 fcntl 函数
fcntl : file control缩写。
提供对文件描述符的各种控制操作。
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);fd:被操作的文件描述符。cmd:指定执行何种类型的操作。根据此类型的不同,可能还需要第三个可选参数表。支持的类型,详见 表 6-4....:根据 cmd 类型 才回存在此参数。返回:成功:详见 表 6-4 最后一列。失败:-1,并设置 errno。
例: