1.面向连接和无连接的通信模型
(1).面向连接的通信模型
服务端: 建立socket -> bind -> listen -> accept -> send/recv -> close
客户端: 建立socket -> connect -> send/recv -> close
(2).无连接的通信模型
服务端: 建立socket -> bind -> recvfrom/sendto -> close
客户端: 建立socket -> bind -> recvfrom/sendto -> close
2.需要的头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
1)int socket(int domain, int type,int protocol)
domain一般是AF_INET
type可以是:SOCK_STREAM,表示TCP
SOCK_DGRAM ,表示UDP
SOCK_RAW
Protocol一般是 :0
错误返回-1
关掉socket用: close(socket);
2)int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
my_addr:是一个指向sockaddr的指针. 在中有 sockaddr的定义
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};
不过由于系统的兼容性,我们一般不用这个结构体,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
sin_family一般为AF_INET,
sin_addr设置为INADDR_ANY表示可以和任何的主机通信,
sin_port是我们要监听的端口号.sin_zero[8]是用来填充的.
注意:sin_port 和 sin_addr 是网络字节。必须要转换:
Ex: sin_port = htons(port);
sin_addr.s_addr=htonl(ip); //sin_addr是一个union,是client的需要填server的ip
sin_addr.s_addr=INADDR_ANY; //INADDR_ANY不用转换,是server的时候用这个
字符串ip和网络/主机字节的转换:
Ip的字符串表示和二进制表示的总结:
Inet_addr(strIP): IP字符串转换为网络字节
Inet_network(strIP):IP字符串转换为主机字节
Inet_ntoa(addr): 网络字节转换为IP字符串
下面是一个server的举例:
sockaddr_in sockAddr;
memset((void*)&sockAddr,0,sizeof (sockaddr_in));
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.s_addr = INADDR_ANY;
sockAddr.sin_port = htons(3785);
bind(m_serSock,(sockaddr*)&sockAddr,sizeof (sockaddr_in));
3) int listen(int sockfd,int backlog)
backlog 表示可以有客户端的最大排队长度
4) int accept(int sockfd, struct sockaddr *addr,int *addrlen)
addr,是得到的客户端addr信息。
addrlen参数要注意;初始化值必须是 sockaddr的长度,在函数调用完之后它的值是实际的地址长度
Accept 将会阻塞程序,直到连接上一个客户端
注意(1):accept返回的是一个socket,是client的socket。以后在send和recv函数中都会用到。
注意(2) : accept实际上的功能和 read 的功能是一样的,它只是会从serverSocket的内核缓冲区里面读取cliSocket的sockaddr信息,如果serverSocket的缓冲区没有信息的话就会一直阻塞。而且在serverSocket的缓冲区里面只存sockaddr的信息,这些信息是由协议放进去的。协议是在listen之后就启动了。
server和client使用send/recv的信息是放在和sockaddr不同的缓冲区中。 从recv的参数就可以看出来,accept得到数据的缓冲区和 recv/send的缓冲区不一样。Recv/send有一个cliSocket参数,所以肯定有一个特定的缓冲区和每一个cliSocket相连。
所以,只要在服务器启动listen之后,下层的协议就会和client端建立连接。即使我们不使用accept读取serverSocket缓冲区中的clientSocket的sockaddr信息,client端也可以发送数据到服务端,协议也会收到这些数据并保存在clientSocket的缓冲区中。
accept 举例 :
socklen_t peerLen = sizeof(peeraddr);
int conn = accept(m_serSock,(sockaddr*)(&peeraddr),&peerLen) ;
5)int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
serv_addr 是客户端的信息
connect用于将一个流套接字连接到一个端口上。Connect的超时不是我们能设置的。一个连接的超时估计能够接受,多了估计就不行了。Connect花销比较大,因为三次握手上比较浪费时间。
6)send,recv, sendto,recvfrom 函数
这些函数默认都是阻塞函数。下面我们就说说阻塞的具体体现:
7)send 和 sendto 函数
无论是否是阻塞模式,send和sendto的作用都是把数据从应用缓冲区拷贝到内核缓冲区。需要注意的是,发送数据不是send和sendto的行为,它们只是把数据从一个缓冲区拷贝到内核缓冲区,发送数据时协议的行为。
阻塞模式的send(): 它会等待所有的数据都被拷贝到发送缓冲区之后才会返回。 比如,当前内核缓冲区的大小是8192,但是缓冲区里面已经有8000的数据了,如果我们要send的数据大小是2000,那么可能第一次只有192的数据被拷贝到内核缓冲区中,send这时仍然被阻塞。直到把剩下的所有数据都拷贝到内核缓冲区之后才返回。而且返回的大小一定是发送的数据大小。
非阻塞模式下的send(): 非阻塞模式下的send 会立即返回。还是用上面的例子来看,当缓冲区只有192字节剩余了,那么send()这时就只放192字节进去,然后就立即返回,返回值就是192.所以非阻塞模式下的send,需要一个while循环来发送,直到发送完毕。
如果非阻塞模式的send没有空间了,也会立即返回,但是会得到WSAWOULDBLOCK/E WOULDBLOCK的errno的错误。
Sendto函数:sendto函数在阻塞和非阻塞模式下的行为都是一样的,都是立即返回,因为UDP没有发送缓冲区,它所做的只是将应用缓冲区拷贝给下层协议栈,然后加上UDP,IP等报头,所以没有阻塞。
在阻塞模式下,recv/recvfrom会阻塞到至少有一个字节(TCP) 或者 有一个完整的UDP数据才返回。
在非阻塞模式下,recv/recvfrom 都会立即返回。如果有任何一个字节的数据(TCP) 或者 有一个完整的数据报(UDP), 那么将会返回数据的长度。 如果没有任何数据的话,也会立即返回,不过会得到WSAWOULDBLOCK/E WOULDBLOCK的errno的错误。
3.select 函数 和 poll函数
select编程模型
select 函数能够完成非阻塞(non-block)方式的通信.
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds 表示最大的文件描述符+1. 就是数字上最大的,注意这个一定要有,否则可能出错。
fd_set是一个集合,里面的内容是文件描述符。Socket也是文件,所以也是一个而文件描述符。
Readfds装的是文件描述符的集合,如果里面有任何一个文件可读,可读的文件描述符将被保留,其余的就被删除,然后select立即返回。
Writefds是被保留的可写的文件描述符;
Exceptfds是被保留的有异常的文件描述符。
Select的返回:超时之后立即返回;
或者发现了Readfds这个集合中有可读、或者writefds中有可写、或者exceptfds中有异常的描述符,那么select将立即返回,并且满足条件的文件描述符将被保留,其余的被删除。
特殊情况:
(1)readfds等这三个集合中是NULL的将不会被判断,如果三个都是NULL的话,select就无用。
(2)timeout 是NULL的话,那表示超时将是无穷大。
(3)timeout是(0,0)的话,将会立即返回,表示超时时间为0
返回值:
(1)如果超时将返回0
(2)找到有符合条件的话 ,将返回符合条件的文件描述符的个数
(3)出错将返回 SOCKET_ERROR
Select相关的几个函数:
FD_SET(s,*set); 向set集合添加套接口s
FD_CLR(s,*set); 向set集合删除套接口s
FD_ISSET(s,*set); 检查 s 是否为set集合中的一员,如果是则返回真(true)
FD_ZERO(*set); 将set集合初始化为空集
Select 和 阻塞accept编程模型的比较:
(1) Select的优势是:防止阻塞,可以做到多客户单线程或者单进程。但是如果单线程的话,如果没有客户端connect的话,accept就会一直阻塞线程。
真正的优势:一般大家都是在一个线程中进行select,之后,将收到的东西,发送给其他等待的任务线程去完成。这就是说,如果我有可能有20000个任务,如果按照你原来的做法是需要20000个线程来处理,但是这20000个任务并不是同时发生,或者说同时发生的概率不是很高。那么我可以用一个线程select,之后用20个线程处理任务,select到一个数据,就交给一个线程去处理,处理完了就等待。如果任务线程都忙,那么就需要策略了,是继续创建线程,还是让连接的任务等待,这就看工作的需要了。
(2)两种编程模型
//select 编程模型
serSocket = socket(...)
...设置 serSocket 为 非阻塞的
bind(serSocket,...);
listen(serSocket,...);
//创建并初始化select需要的参数(这里仅监视read),并把sock添加到fd_set中
fd_set readfds;
fd_set readfds_bak;
int maxfd = serSocket;
//
FD_ZERO(&readfds);
FD_ZERO(&readfds_bak);
FD_SET(sock, &readfds_bak);
//下面是轮询,查看那个socket 有任务了。
//现在这里处理的只是读的任务
while (1) {
readfds = readfds_bak;
//可能有新的socket加入进来了,
//所以要更新当前最大的socket的值,不过这里的跟新方法明显无效率
maxfd = updateMaxfd(readfds, maxfd);
//select(这里没有设置writefds和errorfds,如有需要可以设置)
res = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (res == -1) {//出错了 }
else if (res == 0) {
//没有有任务的socket,再次轮询
continue;
}
//检查readfds 里面的每个socket,对可读的socket进行处理。
for (i = 0; i <= maxfd; i++) {
if (!FD_ISSET(i, &readfds)) {
continue;
}
//如果是 serSocket, 说明有新连接来了,要去处理
if ( i == serSocket) {
new_sock = accept(serSocket, ...);
FD_SET(new_sock, &readfds_bak);
}
else {//有其他的socket要处理,这些socket都是client socket
...在这里面要处理 clientSocket的读数据
或者有些socket的连接已经断了,需要从 fd 集合中移除出去。
FD_CLR(i, &readfds_bak);//把socket为i值的,从readfds_bak移除。
}
}
}
//无select的阻塞accept的模型
serSocket = socket(...)
...设置 serSocket 为 非阻塞的
bind(serSocket,...);
listen(serSocket,...);
while(1)
{
clientSocket = accept(SerSocket,...);
...开一个线程,去处理clientSocket
}
用select可以很好地解决非阻塞connect这一问题.大致过程是这样的:
1).将打开的socket设为非阻塞的,可以用fcntl()完成
int sockfd = socket(...);
//得到socket的属性
flags = fcntl(sockfd,F_GETFL,0);
//设置socket的属性为非阻塞
fcntl(sockfd, F_SETFL, flags|O_NOBLOCK);
connect(sockfd,...);
2).发connect调用,这时返回-1,但是errno被设为EINPROGRESS,意即连接的三次握手仍旧在进行中还没有完成. 但是如果返回-1,但是错误不是EINPROGRESS,那么就是连接出问题了。
if( (n = connect(sockfd, saptr, salen)) < 0)
{
if(errno != EINPROGRESS) return -1;
}
3).将打开的socket用select进行监视,
因为在调用select时,可能连接已经建立了,也有肯能服务器已经发数据过来了。所以没有办法用select监视的可读可写集来判断connect是否成功。我们可以用getsockopt来判断连接是否建立成功。也可以用判断当前是否可写,来判断成功。
int error;
socklen_t errLen;
getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, &errLen); 来得到error的值,如果为零,则connect成功.
poll编程模型:
poll函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数1:结构体数组指针,struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
结构体中的fd 即套接字描述符,events 即这个fd感兴趣的事件,revents 即返回的事件。
events是一些枚举值的组合,下面是常用的值:
POLLIN 表示有数据到来,文件描述符可读
POLLOUT 表示文件描述符可写
POLLHUP 表示连接关闭
参数 nfds:表示有效的fd个数
参数 timeout: 表示超时事件,为 -1 表示超时时间无限长。
poll的基本逻辑:poll函数会监听fds里面的每个fd,当任何一个fd所感兴趣的任意一个事件发生的时候,poll函数就会把这个fd发生的事件放在 revents 里面,并且不会改变这个fd的events,然后poll函数就返回。如果没有任何感兴趣的事件发生,那么poll就一直阻塞。
注意:poll函数会把没有任何事件发生的 fd 的 revents 置为 0 。
下面是poll tcp协议的一个例子:
m_serSock = socket(AF_INET,SOCK_STREAM,0);
if(m_serSock == -1)
{
printf("server create failed.\n") ;
}
sockaddr_in sockAddr;
memset((void*)&sockAddr,0,sizeof (sockaddr_in));
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.s_addr = INADDR_ANY;
sockAddr.sin_port = htons(3785);
bind(m_serSock,(sockaddr*)&sockAddr,sizeof (sockaddr_in));
listen(m_serSock,50);
int maxFdsNum = 50;
int curFdsNum = 0;
pollfd fds[maxFdsNum];
int i = 0;
for(i = 0; i<maxFdsNum; i++)
{
fds[i].fd = -1;
}
fds[0].fd = m_serSock;
fds[0].events = POLLIN;
curFdsNum ++;
std::map<int,sockaddr_in> fdMap;
struct sockaddr_in peeraddr;
while(1)
{
int nReturn = poll(fds,curFdsNum,-1);
if(nReturn <= -1)
{
printf("poll error.\n") ;
break;
}
if(nReturn == 0)
{
continue;
}
if( fds[0].revents & POLLIN)
{
socklen_t peerLen = sizeof(peeraddr);
int conn = accept(m_serSock,(sockaddr*)(&peeraddr),&peerLen) ;
if(conn == -1)
{
printf("connect error.\n") ;
}
else
{
fdMap.insert(std::make_pair(conn,peeraddr));
for(i = 0; i<maxFdsNum; i++)
{
if(fds[i].fd < 0 )
{
fds[i].fd = conn;
fds[i].events = POLLIN|POLLHUP;
fds[i].revents = -1;
curFdsNum++;
break;
}
}
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
}
}
for(i = 1; fds[i].fd >0 ; i++)
{
if((fds[i].revents & POLLIN))
{
int bufLen = 1024;
char buf[bufLen];
int readCount = recv(fds[i].fd,buf,bufLen,0);
if(readCount >0)
{
buf[readCount] = 0;
printf("receive ip=%s data: %s\n",inet_ntoa(peeraddr.sin_addr),buf) ;
send(fds[i].fd,buf,strlen(buf),0);
}
else if(readCount==0){}//表示连接断开,看下面连接断开说明
else if(readCount==-1){}//表示出现了错误,
}
if( (fds[i].revents & POLLHUP))
{
printf("client %s closed.\n",inet_ntoa(fdMap[fds[i].fd].sin_addr));
fdMap.erase(fds[i].fd);
int j = 0;
for(j=i; fds[j+1].fd >0; j++)
{
fds[j].fd = fds[j+1].fd;
fds[j].fd = fds[j+1].fd;
fds[j].revents = fds[j+1].revents;
}
close(fds[j].fd);
fds[j].fd = -1;
}
}
}
close(m_serSock);
非阻塞模式下,socket的错误总结:
常用的errno可能为如下值:
EAGAIN: 意思是当前资源临时不可用,你可以稍候再试。
EBUSY:表示请求的资源部可用。
EINTR: 表示当前操作受到了中端操作。
在非阻塞模式下,recv/write返回值为-1,但是errno为EAGAIN, 这时EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。 表示当前没有数据可读,或者当前没有缓冲区可写。
在非阻塞模式下,recv/write返回值为-1,但是errno为EINTR, 表示当前的操作受到了中断影响,应该继续。
连接断开的判断方法(最好使用第六种方法):
1). 发送重试,由业务完成。
因为club_l5的send接口不会保留用户发送的内容,在recv失败的情况下,用户发送的数据已经丢失,所以只能由业务进行重试。
结论:否定。由于后端服务器有多台,每次发送的时候并不能不能保证连接的机器还是上次发送的那一台服务器,有可能后端所有的连接都被断开,虽然失败比例有所降低,还是不能解决问题。
2). 修改服务器端关闭连接的等待时间。
治标不治本,可以在紧急情况下使用。
经过和still、allan、robby、steven一起讨论,最佳的方案就是在发送的时候,就能感知到服务器端已经关闭连接,经过讨论,给出以下解决方案:
3). 在send之前对先对read进行selcect,并使用read检验连接的状态。
在send之前先对select函数read进行select,并设置时间参数设为0,select会立即返回,如果有FIN包可立刻知道,再进行read,如果read的返回值 <= 0,则说明连接有问题,或接收到了fin包,此时需关闭连接进行重连。
4). 使用poll()函数是否处于POLLRDHUP(套接字半关闭)状态。
如果是,则关闭连接进行重连,目前还不完全成熟,且不适用于内核版本较低的系统。
5). 使用系统调用getpeername函数加系统错误码的方式检查对端是否关闭连接。
用法如下:if(getpeername (sock, &addr, &len) < 0 && errno == ENOTCONN) 当此条件成立的时候,说明对端已经关闭连接。
6). 发送之前用MSG_PEEK的方式recv。
看recv的返回值是否0字节,如果是0字节,说明对方发送了fin包,已关闭了连接。allan给出TTC中验证连接是否有效的函数:
int CheckLinkStatus(void)
{
char msg[1] = {0};
int err = 0;
err = recv(netfd, msg, sizeof(msg), MSG_DONTWAIT|MSG_PEEK);
/* client already close connection. */
if(err == 0 || (err < 0 && errno != EAGAIN))
return -1;
return 0;
}
在知道怎样处理判断连接关闭后,下面给出一个更完善的模型:
select(...);
again:
ret = recv(fd);
if (ret == -1) {
if( errno == EAGAIN || errno == EWOULDBLOCK) {
...;
} else {
error here;
}
} else if (ret == 0) {
close(fd);
} else {
如果没有读完, goto again;
}
4.select 和 poll 编程模型优缺点比较
(1).select:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
#define __FD_SETSIZE 1024
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
(2).poll:
poll本质上和select没有区别,但是poll会突破1024这个限制。它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是"水平触发",如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
5.epoll编程方式
1.需要的头文件
#include <sys/epoll.h>
2.Epoll相关的函数(三个)
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_create:
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_ctl:
把一个 fd(文件描述符,一个sock也是一个fd) 注册到epoll_create 创建的句柄上去。
Op可以是:EPOLL_CTL_ADD,增加一个fd
EPOLL_CTL_DEL,删除一个fd
EPOLL_CTL_MOD, 修改一个fd
epoll_event的定义:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
结构体struct epoll_event成员events可以是以下几个宏的集合:
" EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
" EPOLLOUT:表示对应的文件描述符可以写;
" EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
" EPOLLERR:表示对应的文件描述符发生错误;
" EPOLLHUP:表示对应的文件描述符被挂断;
" EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。缺省是水平触发(Level Triggered)。
" EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
3.ET模式和LT模式:
LT,即水平触发,效率低于ET模式。尤其是在大流量、大并发情况下。但是LT模式的好处是,对代码要求低,只要数据没有被捕获,内核就会不断的通知你,你可以用 epoll_wait 捕获到。
ET,即边缘触发。效率很高。效率很高的原因是比 LT 用了更少的系统调用。但是对编程来说,容易丢失事件。因为ET模式下,内核只通知一次,如果你用epoll_wait捕获到了这个事件,但是并没有处理完这个事件,那么以后内核都不会再通知你这个事件了。比如,内核通知你,缓冲区可以读了,但是你并没有一次把数据读取完,那么即使缓冲区里面还剩有数据,内核也不会通知你了。
ET模式下对读缓冲区的操作:不断的读,直到读到缓冲区没有数据,recv返回-1,但是error的值是EAGAIN为止,才算读完。否则读事件会被丢失,知道下次读事件发生。
Ex:
while(rs)
{
buflen = recv(activeEvents[i].data.fd, buf, sizeof(buf),0);
if(buflen < 0)
{
//由于是非阻塞模式,所以当error为EAGAIN时,表示当前缓冲区已无数据可以读
//在这里就当作是该次事件已经处理
if(errno == EAGAIN)
break;
else
return;
}
else if(buflen == 0)
{
//这里表示对端的socket已正常关闭
}
if(buflen == sizeof(buf))
rs = 1;//需要再次读取
else
rs = 0;
}
ET模式下的send: 当缓冲区满了的时候send会返回-1, 也会报EAGAIN错误。这个时候就需要等待一段时间,从新发送数据。
Ex:
while(1)
{
tmp = send(sockfd, p, total, 0);
if(tmp < 0)
{
//当send收到信号时,可以继续写,但是这里返回-1
if(errno == EINTR)
return -1;
//当socket是非阻塞时,如返回此错误,表示缓冲队列已满
//在这里做延时后再重试
if(errno==EAGAIN)
{
usleep(1000);
continue;
}
return -1;
}
}
ET模式下的accept操作:不断的accept,直到accept返回-1,且error的值是EAGAIN为止,才算accept完成。
Ex:
//接收连接
{
TC_Socket s;
s.init(fd, false, AF_INET);
int iRetCode = s.accept(cs, (struct sockaddr *) &stSockAddr, iSockAddrSize);
if (iRetCode > 0)
{
//建立连接
}
else
{
//直到发生EAGAIN才不继续accept
if(errno == EAGAIN)
{
break;
}
}
}while(true);}
epoll_wait:
等待事件的产生,类似于select()调用。当epoll_wait返回成功时,参数events用来从内核得到所有的读写事件(从内核返回给用户),maxevents告之内核需要监听的所有的socket的句柄数(从用户传给内核),这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
epoll_wait 返回值: 0表示超时; -1表示错误; 大于0表示返回的需要处理的事件数目。
epoll_wait运行的原理是:等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。并 且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。
几乎所有的epoll程序都使用下面的框架:
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}
总结:
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用"事件"的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你"活跃"的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
三种模式的消息传递方式
select
内核需要将消息传递到用户空间,都需要内核拷贝动作
poll
同上
epoll
epoll通过内核和用户空间共享一块内存来实现的。
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善