(1) 阻塞模型
没有数据到达时候程序会一直阻塞直到有数据到达 如tcp 的recv 函数,当然也可以设置超时。
(2)非阻塞I/O模型
通过fcntl函数设置socket 非阻塞模型 fcntl(sockfd, F_SETFL, O_NONBLOCK);如果没有数据到达会返回一个错误码。一般对于一般都对非阻塞I/O模型进行轮询,就是一直在检查这个状态,查看有无数据到达。
(3)3. I/O复用模型 select模型 (重点)
利用select函数,判断套接字上是否存在数据,或者能否向一个套接字写入数据。目的是防止应用程序在套接字处于锁定模式时,调用recv(或send)从没有数据的套接字上接收数据,被迫进入阻塞状态。
select参数和返回值意义如下:
int select (
IN int nfds, //0,无意义
IN OUT fd_set* readfds, //检查可读性
IN OUT fd_set* writefds, //检查可写性
IN OUT fd_set* exceptfds, //例外数据
IN const struct timeval* timeout); //函数的返回时间
返回值:错误:-1,超时:0, 执行成功则返回文件描述词状态已改变的个数。
参数nfds代表最大的文件描述词加1,
参数 timeout为结构timeval,用来设置select()的等待时间,有三种情况,1永远等待,超时参数设置为NULL,2等待一定时间,时间自己设定。3不等待,超时设置为0。
参数readfds、writefds 和exceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况。
fd_set是一个SOCKET队列,以下宏可以对该队列进行操作:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.
Select工作流程:
1> 用FD_ZERO宏来初始化我们感兴趣的fd_set。
2> 用FD_SET宏来将套接字句柄分配给相应的fd_set。
3> 调用select函数。
4:用FD_ISSET对套接字句柄进行检查。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
//这个服务器同时监听7777和7778两个端口
//绑定监听7779端口的fd
int listenfd1;
struct sockaddr_in serv_addr1;
listenfd1 = socket(AF_INET, SOCK_STREAM, 0);
bzero((char *) &serv_addr1, sizeof(serv_addr1));
serv_addr1.sin_family = AF_INET;
serv_addr1.sin_port = htons(7777);
serv_addr1.sin_addr.s_addr = INADDR_ANY;
bind(listenfd1, (struct sockaddr *) &serv_addr1, sizeof(serv_addr1));
listen(listenfd1, 5);
//绑定监听7778端口的fd
int listenfd2;
struct sockaddr_in serv_addr2;
listenfd2 = socket(AF_INET, SOCK_STREAM, 0);
bzero((char *) &serv_addr2, sizeof(serv_addr2));
serv_addr2.sin_family = AF_INET;
serv_addr2.sin_port = htons(7778);
serv_addr2.sin_addr.s_addr = INADDR_ANY;
bind(listenfd2, (struct sockaddr *) &serv_addr2, sizeof(serv_addr2));
listen(listenfd2, 5);
int maxfd;
//为什么这里设置两个fd_set?每次select的时候函数会把没有事件发生的描述字清零,所以需要两个集合
fd_set allset, rset;
maxfd = listenfd1;
if(listenfd2 > maxfd) {
maxfd = listenfd2;
}
FD_ZERO(&allset);
FD_SET(listenfd1, &allset);
FD_SET(listenfd2, &allset);
int clifd, clilen;
struct sockaddr_in cli_addr;
char buffer[256];
for(;;) {
rset = allset;
select(maxfd + 1, &rset, NULL, NULL, NULL);
//如果是listenfd1 获取消息
if(FD_ISSET(listenfd1, &rset)) {
clilen = sizeof(cli_addr);
clifd = accept(listenfd1, (struct sockaddr *) &cli_addr, &clilen);
bzero(buffer, 256);
read(clifd, buffer, 255);
printf("Listenfd1 Message is:%s
", buffer);
}
//如果是listenfd1 获取消息
if(FD_ISSET(listenfd2, &rset)) {
clilen = sizeof(cli_addr);
clifd = accept(listenfd2, (struct sockaddr *) &cli_addr, &clilen);
bzero(buffer, 256);
read(clifd, buffer, 255);
printf("Listenfd2 Message is:%s
", buffer);
}
close(clifd);
}
close(listenfd1);
close(listenfd2);
return 0;
}
(4)信号驱动I/O模型
当socket 有数据到达,内核就会发送 SIGIO 信号,可以调用 sigaction 安装 SIGIO 信号的处理函数,这个时候就可以在 SIGIO 信号处理函数中进行 recv函数来接受数据。
(5)异步I/O模型 (poll模型 和 epoll模型) (重点)
注: 这里只讲epoll poll模型 查阅函数int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
Epoll 可是当前在 Linux 下开发大规模并发网络程序的热门人选, Epoll 在 Linux2.6 内核中正式引入,和 select 相似,其实都 I/O 多路复用技术而已 。
epoll 模型优点:
Epoll 没有最大并发连接的限制
效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关.
内存拷贝, Epoll 在这点上使用了“共享内存"
使用epoll:
typedef union epoll_data{void *ptr;int fd; /*Socket*/ __uint32_t u32; __uint64_t u64; } epoll_data_t;
struct epoll_event {__uint32_t events; /*程序关注的事件 */ epoll_data_t data; };
其中events可以用以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
1> 生成一个 Epoll 专用的文件描述符 int epoll_create(int size); size 就是你在这个 Epoll fd 上能关注的最大 socket数.
2> 控制某个 Epoll 文件描述符上的事件:注册、修改、删除。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event );
参数 epfd 是 epoll_create() 创建 Epoll 专用的文件描述符。
参数op操作类型,有如下取值: EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除
参数fd socket
参数event 指向epoll_event的指针
3> 等待 I/O 事件的发生 int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);
参数epfd:由epoll_create 生成的epoll专用的文件描述符;
参数epoll_event:用于回传等待处理的事件数组;
参数maxevents:每次能处理的事件数;
参数timeout:等待I/O事件发生的超时值(ms);-1永不超时,直到有事件产生才触发,0立即返回
工作方式:
1> LT(level triggered):水平触发,缺省方式,同时支持block和no-block socket,在这种做法中,内核告诉我们一个文件描述符是否被就绪了,如果就绪了,你就可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错的可能性较小。
2>ET(edge-triggered):边沿触发,高速工作方式, 只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪状态时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如:你在发送、接受或者接受请求,或者发送接受的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fs做IO操作(从而导致它再次变成未就绪状态),内核不会发送更多的通知。
区别:LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读取,则不断的通知你。而ET则只在事件发生之时通知。
#include stdio.h>
#include string.h>
#include unistd.h>
#include fcntl.h>
#include errno.h>
#include sys/types.h>
#include sys/socket.h>
#include sys/epoll.h>
#include netinet/in.h>
#include pthread.h>
#include stdlib.h>
#define SERV_PORT 5358
#define MAX_CONN 1024
#define EVENT_NUM 1024
#define EPOLL_SIZE 1024
#define BUF_LEN 1024
int setnonblocking(int fd)
{
int opts;
if((opts = fcntl(fd, F_GETFL)) 0)
{
return -1;
}
opts |=O_NONBLOCK;
if(fcntl(fd, F_SETFL, opts) 0)
{
return -1;
}
return 0;
}
void *str_echo(void *arg)
{
int sockfd;
ssize_t nread;
char buf[BUF_LEN] = {0};
pthread_detach(pthread_self());
sockfd = *(int *)arg;
while(1)
{
bzero(buf, BUF_LEN);
if((nread = read(sockfd, buf, BUF_LEN)) == -1)
{
if(errno == EINTR)
{
continue;
}
else
{
printf("read error: %s
", strerror(errno));
continue;
}
}
else if (nread == 0)
{
break;
}
else
{
//fputs(buf, stdout);
write(sockfd, buf, nread);
}
}
return NULL;
}
int main(int argc, char **argv)
{
int listenfd, connfd, epfd, nfds;
socklen_t addrlen;
struct sockaddr_in cliaddr, servaddr;
struct epoll_event ev, events[EVENT_NUM];
pthread_t tid;
//create socket fd
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("Create socket error!
");
return 0;
}
if (setnonblocking(listenfd) == -1)
{
printf("setnonblicking error!
");
close(listenfd);
return 0;
}
//bind
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("Socket bind error!
");
close(listenfd);
return 0;
}
//listen
if(listen(listenfd, MAX_CONN) == -1)
{
printf("listen error
");
close(listenfd);
return 0;
}
//create epoll
if((epfd = epoll_create(EPOLL_SIZE)) == -1)
{
printf("Create epoll error!
");
close(listenfd);
return 0;
}
//register epoll event
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET;
if ((epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev)) == -1)
{
printf("epoll_ctl error!
");
close(listenfd);
return 0;
}
while(1)
{
if((nfds = epoll_wait(epfd, events, EVENT_NUM, -1)) ==-1)
{
if(errno == EINTR){
printf("%s
", strerror(errno));
continue;
}
else{
printf("epoll_wait error!
");
continue;
}
}
int i;
for(i=0;infds;i++)
{
if(events.data.fd == listenfd)
{
addrlen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &addrlen);
if(connfd == -1)
{
printf("%s
", strerror(errno));
continue;
}
printf("New Connection %d
", connfd);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
if((epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev)) == -1)
{
printf("connect failed!
");
}
}
else
{
if((pthread_create(&tid, NULL, str_echo, &events.data.fd)) == -1)
{
exit(0);
}
}
}
}
return 0;
}
附加: epoll_data.ptr 可以设置成socket对应的回掉函数,当事件发生时候可以调用该函数处理。 也可设置成其它参数。