1、网络编程概述
1.1 套接字socket
网络程序设计主要依靠套接字接受和发送信息来实现。Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,每一个Socket都用一个半相关描述:
{协议,本地地址,本地端口}
一个完整的Socket则用一个相关描述:
{协议,本地地址,本地端口,远程地址,远程端口}
每一个Socket有一个本地的唯一Socket号,由操作系统分配。
1.2 套接字的三种类型
套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字。
1.流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺序的数据:“1”、“2”。那么数据到达远程时候的顺序也是“1”、“2”。
流式套接字使用了TCP(Transmission Control Protocol)协议,保证数据传输是正确的,并且是顺序的。
2.数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。
数据报套接字使用使用者数据报协议UDP(User Datagram Protocol)协议。
3.原始套接字
原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
1.3 套接字的相关知识
1.3.1 struct in_addr结构体
该结构体用于存储IP地址,其定义如下:
/* 因特网地址 (a structure for historical reasons) /
struct in_addr
{
unsigned long s_addr; /4个字节的IP 地址(按网络字节顺序排放)/
};
该结构体因为历史原因,出现在下一节的struct sockaddr_in结构体中。
1.3.2 struct sockaddr结构体
这个结构用来存储套接字地址,定义为:
struct sockaddr
{
unsigned short sa_family; / address族, AF_xxx /
char sa_data[14]; / 14 bytes的协议地址 /
};
l sa_family 一般来说,都是 “AFINET”。
l sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一切的。
为了处理struct sockaddr,
程序员建立了另外一个相似的结构struct sockaddr_in:
struct sockaddr_in (“in” 代表 “Internet”)
struct sockaddr_in
{
short int sin_family; / Internet地址族 /
unsigned short int sin_port; / 端口号 /
struct in_addr sin_addr; / Internet地址 / //见1.3.1节
unsigned char sin_zero[8]; / 添0(和struct sockaddr一样大小)*/
};
一个指向struct sockaddr_in 的指针可以声明指向一个struct sockaddr 的结构。所以虽然socket() 函数需要一个struct sockaddr *,你也可以给他一个sockaddr_in *。
注意:在struct sockaddr_in中,sin_family 相当于在struct sockaddr中的sa_family,需要设成 “AF_INET”。
1.3.3 常用的转换函数
1、字节序转换
每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后),网络传输的数据大家是一定要统一顺序的。
套接字字节转换程序的列表:
l htons() ——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作4 bytes)
l htonl() ——“Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作8 bytes)
l ntohs() ——“Network to Host Short” 网络字节顺序转换为主机字节顺序(对无符号短型进行操作4 bytes)
l ntohl() ——“Network to Host Long” 网络字节顺序转换为主机字节顺序(对无符号长型进行操作8 bytes)
2、地址转换
IP地址既可以用点分十进制的字符串来表示,也可以用一个长整形数字来表示。
l inet_addr() ――“Address” 点分十进制的表示IP地址的字符串转换成无符号长整型。
l inet_ntoa() ――“Network to ASCII” 把一个无符号长整型的IP地址转换成点分十进制的字符串。
例如: ina.sin_addr.s_addr = inet_addr(“166.111.69.52”);
printf(“%s”, inet_ntoa(ina.sin_addr));
注意:(1)inet_addr() 返回的地址已经是网络字节顺序了
(2)如果inet_addr() 函数执行错误,它将会返回–1,相当于255.255.255.255广播用的IP地址;
(3)inet_ntoa() 返回一个字符指针,它指向一个定义在函数inet_ntoa() 中的static 类型字符串。
1.3.4 头文件即函数声明
#include <netinet/in.h>
struct sockaddr_in;
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain , int type , int protocol);
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
int send(int sockfd, const void *msg, int len, int flags);
int recv(int sockfd, void *buf, int len, unsigned int flags);
int sendto(int sockfd, const void *msg, int len, unsigned int flags,const struct sockaddr *to, int tolen);
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen);
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int accept(int sockfd, void *addr, int *addrlen);
int shutdown(int sockfd, int how);
void close(int sockfd);
1.3.5 socket库的错误
问题描述:Linux所提供的socket 库含有一个错误(bug)。此错误表现为你不能为一个套接字重新启用同一个端口号,即使在你正常关闭该套接字以后。例如,比方说,你编写一个服务器在一个套接字上等待的程序.服务器打开套接字并在其上侦听是没有问题的。无论如何,总有一些原因(不管是正常还是非正常的结束程序)使你的程序需要重新启动。然而重启动后你就不能把它绑定在原来那个端口上了。从bind()系统调用返回的错误代码总是报告说你试图连接的端口已经被别的进程所绑定。
解决办法:当套接字已经打开但尚未有连接的时候用setsockopt()系统调用在其上设定选项(options)。即:
opt = 1; len = sizeof(opt); /* 设定参数数值 /
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len); / 设置套接字属性 */
1.3.6 其他常用函数
(1)setsockopt()与getsockopt()
setsockopt()调用设置选项,getsockopt()从给定的套接字取得选项。
#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int name, char *value, int *optlen);
int setsockopt(int sockfd, int level, int name, char *value, int *optlen);
(2)getpeername()
取得一个已经连接上的套接字的远程信息(比如IP 地址和端口)。
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
(3)gethostname()
取得正在执行它的计算机的名字。返回的这个名字可以被gethostbyname()函数使用,由此可以得到本地主机的IP地址。
#include <unistd.h>
int gethostname(char *hostname, size_t size);
(4)gethostbyname()
取得本地主机的信息.
#include <netdb.h>
struct hostent
{
char *h_name; //主机的正式名称
char **h_aliases; //主机的备用名称
int h_addrtype; //返回地址的类型,一般来说是“AF_INET”。
int h_length; //地址的字节长度
char **h_addr_list; //一个以0结尾的数组,存储了主机的网络地址
};
#define h_addr h_addr_list[0]
struct hostent *gethostbyname(const char *name);
注意:使用gethostbyname()函数,你不能使用perror()来输出错误信息(因为错误代码存储在h_errno中而不是errno中。所以,你需要调用herror()函数。
1.4 五种I/O模式
在Linux/UNIX下,有下面这五种I/O 操作方式:
l 阻塞I/O
l 非阻塞I/O
l I/O多路复用
l 信号驱动I/O(SIGIO)
l 异步I/O
1.4.1阻塞I/O
缺省的,一个套接字建立后所处于的模式就是阻塞I/O模式。
1.4.2非阻塞I/O
可以使用fcntl()函数将一个套接字设置为非阻塞模式,如:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, long arg);
int ret = fcntl(sockfd, F_SETFL, fcntl(sockfd,F_GETFD, 0)|O_NONBLOCK);
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做polling)。应用程序不停的polling 内核来检查I/O操作是否已经就绪。
polling
英['pəʊlɪŋ]
美['poʊlɪŋ]
n.
投票;轮询
v.
获得…张票数(poll的ing形式);对…进行调查;投票
1.4.3 I/O多路复用
在使用I/O 多路技术的时候,我们调用select()函数和poll()函数,在调用它们的时候阻塞,而不是我们来调用accept()、recv()等函数的时候阻塞。
多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()/poll()函数就可以返回。
此部分具体见第2、3、4节。
1.4.4信号驱动I/O(SIGIO)
信号驱动I/O模式是使用信号,让内核在文件描述符就绪的时候使用SIGIO信号来通知我们。
1.4.5异步I/O(AIO)
当我们运行在异步I/O 模式下时,我们如果想进行I/O操作,只需要告诉内核我们要进行I/O 操作,然后内核会马上返回。具体的I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的I/O 操作和数据拷贝后,内核将通知我们的程序。
2、I/O多路复用——select模型
select系统调用时可以让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变。
1.1 函数声明
关于文件句柄,其实就是一个整数, socket函数的声明如:
int socket(int domain, int type,int protocol);
我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。
select函数就是用来监视某个或某些句柄的状态变化的。select函数原型如下:
int select (int nfds, fd_set *readfds, fd_set writefds, fd_setexceptfds, struct timeval *timeout);
函数参数:
◆ 参数nfds是需要监视的最大的文件描述符值+1;
◆ 第2、3、4三个参数是一样的类型;fd_set *,即我们在程序里要申请几个fd_set类型的变量,比如rdfds,wtfds,exfds,然后把这个变量的地址&rdfds,&wtfds,&exfds传递给select函数。这三个参数都是一个句柄的集合,第一个rdfds是用来保存这样的句柄的:当句柄的状态变成可读时系统就告诉select函数返回,同理第二个函数是指向有句柄状态变成可写时系统就会告诉select函数返回,同理第三个参数exfds是特殊情况,即句柄上有特殊情况发生时系统会告诉select函数返回。
◆ 函数的最后一个参数timeout是一个超时时间值。其类型是struct timeval *,即一个struct timeval结构的变量的指针,所以我们在程序里要声明一个struct timeval tv;然后把变量tv的地址&tv传递给select函数。该结构体的定义如下:
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
};
返回值:
执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
下面的宏提供了处理这三种描述词组的方式:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_setset);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set);用来清除描述词组set的全部位
1.2 示例程序
/**
-
FileName:main.cpp
-
Description:练习使用select模型
*/
#include <stdio.h>
#include <assert.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include
using std::vector;
int main(int argc,char *argv[])
{
int sfd=0;
int ret=0;
sfd=socket(AF_INET,SOCK_STREAM,0);
assert(-1!=sfd);
int optValue=1;
setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&optValue,sizeof(optValue));
struct sockaddr_in myAddr;
myAddr.sin_family=AF_INET;
myAddr.sin_addr.s_addr=htonl(INADDR_ANY);
myAddr.sin_port=htons(4000);
bzero(&(myAddr.sin_zero),sizeof(myAddr.sin_zero));//string.h
ret=bind(sfd,(struct sockaddr *)&myAddr,sizeof(myAddr));
assert(-1!=ret);
ret=listen(sfd,SOMAXCONN);
assert(-1!=ret);
vector<int> acceptList;
int maxfd=sfd+1;
fd_set sockets;
fd_set readfds,writefds;
FD_ZERO(&sockets);
FD_SET(sfd,&sockets);
vector<int>::iterator it;
while(1)
{
FD_ZERO(&readfds);
FD_ZERO(&writefds);
readfds=sockets;
writefds=sockets;
ret=select(maxfd,&readfds,&writefds,NULL,NULL);
assert(-1!=ret);
if(FD_ISSET(sfd,&readfds))
{//accept client connection
struct sockaddr_in clientAddr;
socklen_t addrlen=sizeof(clientAddr);
int cfd=accept(sfd,(struct sockaddr *)&clientAddr,&addrlen);
assert(-1!=cfd);
FD_SET(cfd,&sockets);
maxfd=cfd+1;
acceptList.push_back(cfd);
}
else
{
//check read && write
for(it=acceptList.begin();it!=acceptList.end();++it)
{
int s=*it;
//check recieve
if(FD_ISSET(s,&readfds))
{//recieve data from client
char buffer[1024];
int buflen=1024;
int recvlen=recv(s,buffer,buflen,0);
if(-1==recvlen) //error accurred
continue;
else if(0==recvlen) //client disconnect
{
close(s);
it=acceptList.erase(it);
it--;
FD_CLR(s,&sockets);
}
else // print recieve data
{
buffer[buflen]='\0';
printf(buffer);
}
}
//check send
if(FD_ISSET(s,&writefds))
{
//char sendbuf[128]="hello client";
//int sendlen=send(s,sendbuf,strlen(sendbuf),0);
}
}
}
}
close(sfd);
return 0;
}
3、I/O多路复用——poll模型
和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。poll实现的效率要比select高。
3.1函数声明
include < sys/poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
函数参数:
◆ pollfd结构体定义如下:
include < sys/poll.h>
struct pollfd
{
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递结构体数组,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSG SIGPOLL消息可用。
此外,revents域中还可能返回下列事件:
POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL 指定的文件描述符非法。
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。使用poll()和select()不一样,你不需要显式地请求异常情况报告。POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
◆ nfds参数用于标记数组fds中的结构体元素的总数量;
◆ timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULT fds指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVAL fds参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。
3.2示例程序
/**
-
File:main.cpp
-
Description:练习使用poll模型
*/
#include <stdio.h>
#include <assert.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>
#include
#include
using std::vector;
int main(int argc,char *argv[])
{
int sfd=0;
int ret=0;
sfd=socket(AF_INET,SOCK_STREAM,0);
assert(-1!=sfd);
int optValue=1;
setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&optValue,sizeof(optValue));
struct sockaddr_in myAddr;
myAddr.sin_family=AF_INET;
myAddr.sin_addr.s_addr=htonl(INADDR_ANY);
myAddr.sin_port=htons(4000);
bzero(&(myAddr.sin_zero),sizeof(myAddr.sin_zero));//string.h
ret=bind(sfd,(struct sockaddr *)&myAddr,sizeof(myAddr));
assert(-1!=ret);
ret=listen(sfd,SOMAXCONN);
assert(-1!=ret);
vector<int> acceptList;
vector<int>::iterator it;
acceptList.push_back(sfd);
struct pollfd *fds=NULL;
while(1)
{
int pollsize=acceptList.size();
if(fds) delete []fds;
fds=new struct pollfd[pollsize];
memset(fds,0,pollsize*sizeof(struct pollfd));
size_t index=0;
for(it=acceptList.begin();it!=acceptList.end();++it)
{
fds[index].fd=*it;
fds[index].events |=POLLIN|POLLOUT;
index++;
}
ret=poll(fds,index,-1);
assert(-1!=ret);//0 means timeout
for(size_t i=0;i<index;++i)
{
if(fds[i].revents & POLLIN)//can read
{
if(fds[i].fd==sfd) //can accept
{
struct sockaddr_in clientAddr={0};
socklen_t addrlen=sizeof(clientAddr);
int cfd=accept(sfd,(struct sockaddr *)&clientAddr,&addrlen);
assert(-1!=cfd);
acceptList.push_back(cfd);
}
else // can receive
{
char buffer[1024];
int recvLen=recv(fds[i].fd,buffer,sizeof(buffer),0);
if(recvLen<0)
continue;
else if(0==recvLen)
{
close(fds[i].fd);
it=std::find(acceptList.begin(),acceptList.end(),fds[i].fd);
acceptList.erase(it);
}
else
{
buffer[recvLen]='\0';
printf(buffer);
}
}
}
if(fds[i].revents & POLLOUT) //can send
{
//char sendbuf[128]="hello client";
//int sendlen=send(s,sendbuf,strlen(sendbuf),0);
}
}
}
acceptList.clear();
close(sfd);
return 0;
}
4、I/O多路复用——epoll模型
epoll是Linux内核为处理大批句柄而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著的减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一个原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提供应用程序的效率。
LT(level triggered):水平触发,缺省方式,同时支持block和no-block socket,在这种做法中,内核告诉我们一个文件描述符是否被就绪了,如果就绪了,你就可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错的可能性较小。传统的select\poll都是这种模型的代表。
ET(edge-triggered):边沿触发,高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪状态时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如:你在发送、接受或者接受请求,或者发送接受的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fs做IO操作(从而导致它再次变成未就绪状态),内核不会发送更多的通知。
区别:LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读取,则不断的通知你。而ET则只在事件发生之时通知。
4.1、使用流程
1、创建一个epoll句柄
#include <sys/epoll.h>
int epoll_create(int size)
int epoll_create1(int flag)
2、注册epoll事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
函数参数
epfd为epoll的句柄;
◆ op表示动作,用3个宏来表示:
EPOLL_CTL_ADD(注册新的fd到epfd)
EPOLL_CTL_MOD(修改已经注册的fd的监听事件)
EPOLL_CTL_DEL(从epfd删除一个fd);
◆ fd为需要监听的标示符;
◆ event告诉内核需要监听的事件,event的结构如下:
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中events可以用以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3、等待事件的产生
int epoll_wait(int epfd, struct epoll_event * events, int maxevents,int timeout)
类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size(此次待定,具体需要查相关文档),参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
4.2、示例程序
/*
-
FileName:main.cpp
-
Description:练习使用epoll模型
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <assert.h>
#include
using std::list;
int main(int argc,char *argv[])
{
int listener;
int epfd;
int ret;
list<int> clients;
listener=socket(AF_INET,SOCK_STREAM,0);
assert(-1!=listener);
struct sockaddr_in myAddr;
myAddr.sin_family=AF_INET;
myAddr.sin_addr.s_addr=htonl(INADDR_ANY);
myAddr.sin_port=htons(4000);
bzero(&(myAddr.sin_zero),sizeof(myAddr.sin_zero));
ret=bind(listener,(struct sockaddr *)&myAddr,sizeof(myAddr));
assert(-1!=ret);
ret=listen(listener,SOMAXCONN);
assert(-1!=ret);
epfd=epoll_create1(0);
assert(-1!=epfd);
struct epoll_event event;
event.events=EPOLLIN | EPOLLOUT;
event.data.fd=listener;
ret=epoll_ctl(epfd,EPOLL_CTL_ADD,listener,&event);
assert(-1!=ret);
struct epoll_event events[1024];
int maxsize=1024;
while(1)
{
ret=epoll_wait(epfd,events,maxsize,-1);
assert(-1!=ret);
assert(ret>0);
int event_counts=ret;
for(int i=0;i<event_counts;++i)
{
if(events[i].data.fd==listener) //can accept
{
struct sockaddr_in clientAddr;
socklen_t addrLen=sizeof(clientAddr);
int cfd=accept(listener,(struct sockaddr *)&clientAddr,&addrLen);
assert(-1!=cfd);
event.events=EPOLLIN | EPOLLOUT;
event.data.fd=cfd;
ret=epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
assert(-1!=ret);
clients.push_back(cfd);
}
else if(events[i].events & EPOLLIN)//can receive
{
char buffer[1024];
int recvLen=recv(events[i].data.fd,buffer,sizeof(buffer),0);
if(-1==recvLen)
{//error accurred!
perror("error when recv()\n");
}
else if(0==recvLen)
{//client disconnect
ret=epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&events[i]);
assert(-1!=ret);
clients.remove(events[i].data.fd);
close(events[i].data.fd);
}
else
{//print receive data
buffer[recvLen]='\0';
printf(buffer);
}
}
else if(events[i].events & EPOLLOUT) //can send
{
//char sendBuf[]="hello client";
//int sendLen=send(events[i].data.fd,sendBuf,strlen(sendBuf)+1,0);
}
}
}
close(epfd);
close(listener);
clients.clear();
return 0;
}
5、select/poll/epoll比较
5.1 参数及实现对比
1. select的第一个参数nfds为fdset集合中最大描述符值加1,fdset是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位代表其对应的描述符是否需要被检查。
select的第二三四个参数表示需要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需要重新初始化fdset。
timeout参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。
select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被SET的描述符调用进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回;当轮询一遍后没有任何事件发生时,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行一次轮询,并将临时结果写到用户空间,然后返回。
select返回后,需要逐一检查关注的描述符是否被SET(事件是否发生)。
2. poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。
3. epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait检查事件,epoll_wait的第二个参数用于存放结果。
epoll与select、poll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。
epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像poll、select那样进行轮询检查。
5.2 性能对比
(无)。
5.3 连接数对比
对于select,感触最深的是linux下select最大数目限制(windows下似乎没有限制),每个进程的select最多能处理FD_SETSIZE个FD(文件句柄),
如果要处理超过1024个句柄,只能采用多进程了。
常见的使用slect的多进程模型是这样的: 一个进程专门accept,成功后将fd通过unix socket传递给子进程处理,父进程可以根据子进程负载分派。曾经用过1个父进程+4个子进程承载了超过4000个的负载。
这种模型在我们当时的业务运行的非常好。epoll在连接数方面没有限制,当然可能需要用户调用API重现设置进程的资源限制。
6、信号驱动I/O(SIGIO)
信号驱动I/O是让内核在描述符就绪时发送SIGIO信号通知我们。首先开启套接口的信号驱动1/O功能,sigaction系统调用安装一个信号处理函数,当内核数据包准备好时,会为该进程产生一个SIGIO信号。应用可以在信号处理时接收数据。
具体步骤:
1,建立SIGIO信号处理函数。
2,设置该套接口的属主,通常使用fcntl的F_SETOWN命令设置。
3,开启该套接口的信号驱动I/O,通常使用fcntl的F_SETFL命令打开O_ASYNC标志完成。
6.1 UDP套接口上的SIGIO信号
UDP上使用信号驱动I/O是简单的。当下述事件发生时产生SIGIO信号:
-
数据报到达套接口
-
套接口上发生异步错误
6.2 TCP套接口上的SIGIO信号
不幸的是,信号驱动I/O对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。
下列条件均可在TCP套接口上产生SIGIO信号(假设信号驱动I/O是使能的):
-
在监听套接口上有一个连接请求已经完成
-
发起了一个连接拆除请求
-
一个连接拆除请求已经完成
-
一个连接的一半已经关闭
-
数据到达了套接口
-
数据已从套接口上发出(即输出缓冲区有空闲时间)
-
发生了一个异步错误
inet_ntoa(sockaddr_in addr)包含在arpa/inet.h
int fcntl(int nFd,int nCmd,…),该函数包含在头文件fcntl.h中
信号处理函数的设置采用函数sighandler_t signal(int nSig,void(*DealFun)(intnSig)),该函数包含在头文件signal.h中
7、异步非阻塞 I/O(AIO)
Linux aio是Linux下的异步读写模型。Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强。它是 2.6 版本内核的一个标准特性。
在深入介绍 AIOAPI 之前,让我们先来探索一下 Linux 上可以使用的不同 I/O 模型。下图给出了同步和异步模型,以及阻塞和非阻塞的模型。
异步非阻塞 I/O 模型是一种处理I/O 重叠的模型。读请求会立即返回,说明read/write请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当 read/write 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。
linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本)、一套是由linux内核实现的,并由libaio来封装调用接口(以下称之为linux版本)。
7.1 glibc版本AIO接口
7.1.1 接口介绍
函数声明
函数说明
int aio_read(struct aiocb *aiocbp);
提交一个异步读
int aio_write(struct aiocb *aiocbp);
提交一个异步写
int aio_cancel(int fildes, struct aiocb *aiocbp);
取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL)
int aio_error(const struct aiocb *aiocbp);
查看一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错
ssize_t aio_return(struct aiocb *aiocbp);
查看一个异步请求的返回值(跟同步读写定义的一样)
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout);
阻塞等待请求完成
其中,struct aiocb主要包含以下字段:
struct aiocb
{
int aio_fildes; /* 要被读写的fd */
void * aio_buf; /* 读写操作对应的内存buffer */
__off64_t aio_offset; /* 读写操作对应的文件偏移 */
size_t aio_nbytes; /* 需要读写的字节长度 */
int aio_reqprio; /* 请求的优先级 */
structsigevent aio_sigevent; /* 异步事件,定义异步操作完成时的通知信号或回调函数 */
}
7.1.2 接口使用
要使用aio的功能,需要include头文件aio.h,在编译连接的时候需要加入POSIX实时扩展库rt.
(1) int aio_read(struct aiocb *aiocbp);
异步读操作,向内核发出读的命令,传入的参数是一个aiocb的结构,比如
struct aiocb myaiocb;
memset(&aiocb , 0x00 , sizeof(myaiocb));
myaiocb.aio_fildes = fd;
myaiocb.aio_buf = new char[1024];
myaiocb.aio_nbytes = 1024;
if (aio_read(&myaiocb) != 0)
{
printf("aio_read error:%s/n" , strerror(errno));
return false;
}
(2)int aio_write(structaiocb *aiocbp);
异步写操作,向内核发出写的命令,传入的参数仍然是一个aiocb的结构,当文件描述符的O_APPEND标志位设置后,异步写操作总是将数据添加到文件末尾。如果没有设置,则添加到aio_offset指定的地方,比如:
struct aiocb myaiocb;
memset(&aiocb , 0x00 , sizeof(myaiocb));
myaiocb.aio_fildes = fd;
myaiocb.aio_buf = new char[1024];
myaiocb.aio_nbytes = 1024;
myaiocb.aio_offset = 0;
if (aio_write(&myaiocb) != 0)
{
printf(“aio_read error:%s/n” , strerror(errno));
return false;
}
(3) int aio_error(const struct aiocb *aiocbp);
如果该函数返回0,表示aiocbp指定的异步I/O操作请求完成。
如果该函数返回EINPROGRESS,表示aiocbp指定的异步I/O操作请求正在处理中。
如果该函数返回ECANCELED,表示aiocbp指定的异步I/O操作请求已经取消。
如果该函数返回-1,表示发生错误,检查errno。
(4)ssize_t aio_return(struct aiocb *aiocbp);
这个函数的返回值相当于同步I/O中,read/write的返回值。只有在aio_error调用后才能被调用。
(5)int aio_cancel(int fd, struct aiocb *aiocbp);
取消在文件描述符fd上的aiocbp所指定的异步I/O请求。
如果该函数返回AIO_CANCELED,表示操作成功。
如果该函数返回AIO_NOTCANCELED,表示取消操作不成功,使用aio_error检查一下状态。
如果返回-1,表示发生错误,检查errno.
(6)int lio_listio(int mode, struct aiocb *restrict constlist[restrict],int nent, struct sigevent *restrict sig);
使用该函数,在很大程度上可以提高系统的性能,因为再一次I/O过程中,OS需要进行用户态和内核态的切换,如果我们将更多的I/O操作都放在一次用户太和内核太的切换中,减少切换次数,换句话说在内核尽量做更多的事情。这样可以提高系统的性能。
用户程序提供一个structaiocb的数组,每个元素表示一次AIO的请求操作。需要设置struct aiocb中的aio_lio_opcode数据成员的值,有LIO_READ,LIO_WRITE和LIO_NOP。nent表示数组中元素的个数。最后一个参数是对AIO操作完成后的通知机制的设置。
(7)设置AIO的通知机制,有两种通知机制:信号和回调
(a).信号机制
首先我们应该捕获SIGIO信号,对其作处理:
struct sigaction sig_act;
sigempty(&sig_act.sa_mask);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = aio_handler;
struct aiocb myaiocb;
bzero( (char *)&myaiocb, sizeof(struct aiocb) );
myaiocb.aio_fildes = fd;
myaiocb.aio_buf = malloc(BUF_SIZE+1);
myaiocb.aio_nbytes = BUF_SIZE;
myaiocb.aio_offset = next_offset;
myaiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
myaiocb.aio_sigevent.sigev_signo = SIGIO;
myaiocb.aio_sigevent.sigev_value.sival_ptr = &myaiocb;
ret = sigaction( SIGIO, &sig_act, NULL );
信号处理函数的实现:
void aio_handler( int signo, siginfo_t *info, void *context )
{
struct aiocb *req;
if (info->si_signo == SIGIO) {
req = (struct aiocb *)info->si_value.sival_ptr;
if (aio_error( req ) == 0) {
ret = aio_return( req );
}
}
return;
}
(b). 回调机制
需要设置:
myaiocb.aio_sigevent.sigev_notify= SIGEV_THREAD
my_aiocb.aio_sigevent.notify_function= aio_handler;
回调函数的原型:
typedef void (*FUNC_CALLBACK)(sigval_t sigval);
7.2 linux的AIO接口
7.2.1 接口介绍
linux下有aio封装的接口API(也成为libaio)通常以io_开头,libaio提供下面五个主要API函数:
int io_setup(int maxevents, io_context_t *ctxp);
int io_destroy(io_context_t ctx);
int io_submit(io_context_t ctx, long nr, struct iocb *ios[]);
int io_cancel(io_context_t ctx, struct iocb iocb, struct io_eventevt);
int io_getevents(io_context_t ctx_id, long min_nr, long nr, structio_event *events, struct timespec *timeout);
和五个宏定义:
void io_set_callback(struct iocb *iocb, io_callback_t cb);
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_tcount, long long offset);
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_tcount, long long offset);
void io_prep_pwritev(struct iocb iocb, int fd, const struct ioveciov, int iovcnt, long long offset);
void io_prep_preadv(struct iocb iocb, int fd, const struct ioveciov, int iovcnt, long long offset);
这五个宏定义都是操作structiocb的结构体。struct iocb是libaio中很重要的一个结构体,用于表示IO,但是其结构略显复杂,为了保持封装性不建议直接操作其元素而用上面五个宏定义操作。
7.2.2 接口使用
(1)libaio的初始化和销毁
观察libaio五个主要API,都用到类型为io_context的变量,这个变量为libaio的工作空间。不用具体去了解这个变量的结构,只需要了解其相关操作。创建和销毁libaio分别用到io_setup(也可以用io_queue_init,区别只是名字不一样而已)和io_destroy。
int io_setup(int maxevents, io_context_t *ctxp);
int io_destroy(io_context_t ctx);
(2)libaio读写请求的下发和回收
a). 请求下发
libaio的读写请求都用io_submit下发。下发前通过io_prep_pwrite和io_prep_pread生成iocb的结构体,做为io_submit的参数。这个结构体中指定了读写类型、起始扇区、长度和设备标志符。
libaio的初始化不是针对一个具体设备进行初始,而是创建一个libaio的工作环境。读写请求下发到哪个设备是通过open函数打开的设备标志符指定。
b). 请求返回
读写请求下发之后,使用io_getevents函数等待io结束信号:
int io_getevents(io_context_t ctx_id, long min_nr, long nr, structio_event *events, struct timespec *timeout);
io_getevents返回events的数组,其参数events为数组首地址,nr为数组长度(即最大返回的event数),min_nr为最少返回的events数。timeout可填NULL表示无等待超时。io_event结构体的声明为:
struct io_event
{
PADDEDptr(void *data,__pad1);
PADDEDptr(struct iocb*obj, __pad2);
PADDEDul(res, __pad3);
PADDEDul(res2, __pad4);
};
其中,res为实际完成的字节数;res2为读写成功状态,0表示成功;obj为之前下发的structiocb结构体。这里有必要了解一下struct iocb这个结构体的主要内容:
iocbp->iocb.u.c.nbytes 字节数
iocbp->iocb.u.c.offset 偏移
iocbp->iocb.u.c.buf 缓冲空间
iocbp->iocb.u.c.flags 读写
c). 自定义字段
struct iocb除了自带的元素外,还留有供用户自定义的元素,包括回调函数和void *的data指针。如果在请求下发前用io_set_callback绑定用户自定义的回调函数,那么请求返回后就可以显示的调用该函数。回调函数的类型为:
void callback_function(io_context_t ctx, struct iocb *iocb, longres, long res2);
另外,还可以通过iocbp->data指针挂上用户自己的数据。
注意:实际使用中发现回调函数和data指针不能同时用,可能回调函数本身就是使用的data指针。
7.2.2 示例程序
#include <stdlib.h>
#include <stdio.h>
#include <libaio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <libaio.h>
int srcfd=-1;
int odsfd=-1;
#define AIO_BLKSIZE 1024
#define AIO_MAXIO 64
static void wr_done(io_context_t ctx, struct iocb *iocb, long res, long res2)
{
if(res2 != 0)
{
printf(“aio write error\n”);
}
if(res != iocb->u.c.nbytes)
{
printf( “write missed bytes expect %d got %d\n”, iocb->u.c.nbytes, res);
exit(1);
}
free(iocb->u.c.buf);
free(iocb);
}
static void rd_done(io_context_t ctx, struct iocb *iocb, long res, long res2)
{
/*library needs accessors to look at iocb*/
int iosize = iocb->u.c.nbytes;
char *buf = (char *)iocb->u.c.buf;
off_t offset = iocb->u.c.offset;
int tmp;
char *wrbuff = NULL;
if(res2 != 0)
{
printf(“aio read\n”);
}
if(res != iosize)
{
printf( “read missing bytes expect %d got %d”, iocb->u.c.nbytes, res);
exit(1);
}
/*turn read into write*/
tmp = posix_memalign((void **)&wrbuff, getpagesize(), AIO_BLKSIZE);
if(tmp < 0)
{
printf(“posix_memalign222\n”);
exit(1);
}
snprintf(wrbuff, iosize + 1, “%s”, buf);
printf(“wrbuff-len = %d:%s\n”, strlen(wrbuff), wrbuff);
printf(“wrbuff_len = %d\n”, strlen(wrbuff));
free(buf);
io_prep_pwrite(iocb, odsfd, wrbuff, iosize, offset);
io_set_callback(iocb, wr_done);
if(1!= (res=io_submit(ctx, 1, &iocb)))
printf(“io_submit write error\n”);
printf(“\nsubmit %d write request\n”, res);
}
void main(int args,void * argv[])
{
int length = sizeof(“abcdefg”);
char * content = (char * )malloc(length);
io_context_t myctx;
int rc;
char * buff=NULL;
int offset=0;
int num,i,tmp;
if(args<3)
{
printf(“the number of param is wrong\n”);
exit(1);
}
if((srcfd=open(argv[1],O_RDWR))<0)
{
printf(“open srcfile error\n”);
exit(1);
}
printf(“srcfd=%d\n”,srcfd);
lseek(srcfd,0,SEEK_SET);
write(srcfd,”abcdefg”,length);
lseek(srcfd,0,SEEK_SET);
read(srcfd,content,length);
printf(“write in the srcfile successful,content is %s\n”,content);
if((odsfd=open(argv[2],O_RDWR))<0)
{
close(srcfd);
printf(“open odsfile error\n”);
exit(1);
}
memset(&myctx, 0, sizeof(myctx));
io_queue_init(AIO_MAXIO, &myctx);
struct iocb *io = (struct iocb*)malloc(sizeof(struct iocb));
int iosize = AIO_BLKSIZE;
tmp = posix_memalign((void **)&buff, getpagesize(), AIO_BLKSIZE);
if(tmp < 0)
{
printf(“posix_memalign error\n”);
exit(1);
}
if(NULL == io)
{
printf( “io out of memeory\n”);
exit(1);
}
io_prep_pread(io, srcfd, buff, iosize, offset);
io_set_callback(io, rd_done);
printf(“START…\n\n”);
rc = io_submit(myctx, 1, &io);
if(rc < 0)
printf(“io_submit read error\n”);
printf(“\nsubmit %d read request\n”, rc);
//m_io_queue_run(myctx);
struct io_event events[AIO_MAXIO];
io_callback_t cb;
num = io_getevents(myctx, 1, AIO_MAXIO, events, NULL);
printf(“\n%d io_request completed\n\n”, num);
for(i=0;i<num;i++)
{
cb = (io_callback_t)events[i].data;
struct iocb *io = events[i].obj;
printf(“events[%d].data = %x, res = %d, res2 = %d\n”, i, cb, events[i].res, events[i].res2);
cb(myctx, io, events[i].res, events[i].res2);
}
}