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_UNIX
char sun_path[108]; //文件路径名
}
2.2.2 TCP/IP 协议族专用 socket 地址结构体
IPv4 专用结构体:
struct sockaddr_in
{
sa_family_t sin_family; //地址族:AF_INET
u_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_INET6
u_int16_t sin6_port; //端口号,网络字节序表示(大端)
u_int32_t sin6_flowinfo; //流信息,应设置为 0
struct 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
对应 TCP
2.数据报服务: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.EINPROGRESS
这个错误发生在对非阻塞的 socket 调用 connect,而连接又没有立即建立时。
这种情况下,可以用 select、poll 等函数来监听这个连接失败的 socket 上的 可写事件。
当select、poll 等函数返回后,再利用 getsockopt 来读取错误码并清除该 socket 上的错误。
如果错误码是 0,表示连接成功建立,否则 连接失败。
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。
详解常用 errno:
EAGAIN
它的另一个名字叫做EWOULDBLOCK ,这两个宏定义在GNU的c库中永远是同一个值。
大多数系统下是EWOULDBLOCK 和 EAGAIN 是同一个东西。
对非阻塞socket而言,EAGAIN不是一种错误。
下次循环接着recv即可。
在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
例:
如果使用fork创建进程,如果资源不足,也会返回EAGAIN。
重新读即可,资源可用就会正常返回。
EINTR
如果在读的过程中遇到了中断则read()应该返回-1,同时置errno为EINTR。
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,关闭: 0
int 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:
指定目标主机IP
len:
指定 addr 所指 IP 地址长度。
type:
指定 addr 所指 IP地址类型
取值示例:
IPv4:AF_INET
IPv6:AF_INET6
hosent 结构体定义:
#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_DGRAM
int ai_protocol; //指具体的网络协议,其含义好socket系统调用的第三个参数相同,通常默认设置为 0
socklen_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。
例: