sockt编程中的文件句柄 select poll epoll

socket编程的基本步骤:
socket()函数返回监听的socket句柄
listen()监听socket句柄

bind() 句柄和端口绑定
while(1){
accept() 返回连接的句柄
fork()子程序处理

dump2()将输入输出重定向到连接句柄
write(iConnSocket, szBuf, strlen(szBuf) + 1);
while(1){
read()
}

}



select poll 均不改变上述流程,只是将用户进程中的 accept() 返回连接的句柄加入到 ,fd数组中,while(1)中等待事件到来



socket()函数返回监听的socket句柄
listen()监听socket句柄

bind() 句柄和端口绑定
FD_SET(listenfd, &allset); //将监听socket加入select检测的描述符集合



while(1){
nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //调用select
rset=allset

if (FD_ISSET(listenfd, &rset))

{ //检测是否有新客户端请求

connectfd =accept() 返回连接的句柄
for (i = 0; i < FD_SETSIZE; i++)

client[i].fd = connectfd; //将新客户端的加入数组

}



			    for (i = 0; i < FD_SETSIZE; i++) 

// 有客户连接,检测是否有数据
if (FD_ISSET(sockfd, &rset))
{
recv(sockfd, recvbuf, MAXDATASIZE, 0)) FD_CLR(sockfd, &allset); //从监听集合中删除此socket连接
client[i].fd = -1; //数组元素设初始值,表示没客户端连接
}
} } }


socket()函数返回监听的socket句柄
listen()监听socket句柄

bind() 句柄和端口绑定
//5.对已连接的客户端的数据处理
while(1)
{
int ret = poll(client, maxi+1, -1);//对加入poll结构体数组所有元素进行监测
//5.1监测sockfd(监听套接字)是否存在连接
if((client[0].revents & POLLIN) == POLLIN )
{

//5.1.1 从tcp完成连接中提取客户端
connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
}



    if(client[i].revents & (POLLIN | POLLERR))
{
//5.2.1接受客户端数据
if((len = recv(client[i].fd, buf, sizeof(buf), 0)) < 0)
{
}
} }


epoll 流程完全不同,需要多消耗一个fd,通过epoll_ctl 将监听端口的句柄、连接句柄 和epoll的句柄关联



struct epoll_event ev,events[20];
//生成用于处理accept的epoll专用的文件描述符
epfd=epoll_create(256);



listenfd =socket()函数返回监听的socket句柄



//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

listen()监听socket句柄

bind() 句柄和端口绑定
while(1){
nfds=epoll_wait(epfd,events,20,0);
for(i=0;i<nfds;++i){
if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
{
connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);

else if (events[i].events&EPOLLIN){
//如果是已经连接的用户,并且收到数据,那么进行读入。
read(sockfd, BUF, MAXLINE)
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}else{
//写完后,这个sockfd准备读
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);



可以看出select和poll相似,都是将连接的句柄打包给系统,有事件的时候,遍历有事件的句柄处理。epoll则完全不同,新消耗一个句柄,事件都在新句柄上,用户比较新句柄和accept返回,确认是不是新的连接。


listen函数



摘要:listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。



listen函数在一般在调用bind之后-调用accept之前调用,它的函数原型是:



intlisten(int sockfd, int backlog)
参数sockfd



被listen函数作用的套接字,sockfd之前由socket函数返回。在被socket函数返回的套接字fd之时,它是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接,然后在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。



参数backlog



这个参数涉及到一些网络的细节。进程处理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。



毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。



accept函数



摘要:accept()用来接受参数s的socket连接,它的函数原型是:



intaccept(int s,struct sockaddr * addr,int * addrlen)



服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回 INVALID_SOCKET。该函数的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结构的长度。



accept()接受一个客户端的连接请求,并返回一个新的套接字。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的



再次调用accept()可以接受下一个客户端的连接请求,并再次返回一个新的套接字(与socket()返回的套接字、之前accept()返回的套接字都不同的新的套接字)。这个新的套接字用于与这次接受的客户端之间的通信。



epoll的接口非常简单,一共就三个函数:
1.创建epoll句柄
int epfd = epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。



2.将被监听的描述符添加到epoll句柄或从epool句柄中删除或者对监听事件进行修改。



int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个参数是epoll_create()的返回值,
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD: 注册新的fd到epfd中;
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;
第三个参数是需要监听的fd,
第四个参数是告诉内核需要监听什么事件,struct epoll_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队列里。



  1. 等待事件触发,当超过timeout还没有事件触发时,就超时。
    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表示已超时。
    返回的事件集合在events数组中,数组中实际存放的成员个数是函数的返回值。返回0表示已经超时。



EPOLL事件有两种模型:
Edge Triggered (ET) //高速工作方式,错误率比较大,只支持no_block socket (非阻塞socket)
Level Triggered (LT) //缺省工作方式,即默认的工作方式,支持block socket和no_block socket,错误率比较小。


Category linux