大学生寒假在家过于无聊,整理一下以前学过的知识,顺便复习一下,水平较低,专业性差,仅供参考,不喜勿喷(反正也没人看)。解封了,但是有点小失恋,我好难过呜呜呜呜。。。
一、五个IO模型
(1)阻塞I/O模型
最流行的I/O模型是阻塞I/O模型,缺省时,所有的套接口都是阻塞的
(2)非阻塞I/O模型
应用程序连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,一般只在专门提供某种功能的系统中才会用到
(3)I/O复用模型
有了I/O复用,我们就可以调用select或poll,在这两个系统调用的某一个上阻塞,而不是真正阻塞于真正的I/O系统调用
(4) 信号驱动I/O模型
我们也可以用信号,让内核在描述字准备好时用信号SIGIO通知我们,我们将此方法称为信号驱动I/O
(5)异步I/O模型
我们让内核启动操作,并在整个操作完成后通知我们
二、IO复用
(1)概念
之前的案例中服务器在每面对一个客户端的时候,会为它开辟一个进程,这就是最传统的多进程并发模型,但是如果客户端数量足够大,CPU的负荷也很大,显然这种方式不够合理。所以人们提出了I/O多路复用这个模型,单个线程,通过记录跟踪每个I/O流的状态,来同时管理多个I/O流 。
(2)方式
目前的常用的IO复用模型有三种:select,poll,epoll。
select | poll | epoll | |
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 红黑树 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。下面详细介绍epoll用法。
三、epoll
Linux中提供的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);
(1)epoll_create函数
作用
创建一个 epoll 对象,返回该对象的描述符,注意要使用 close 关闭该描述符。
原型
int epoll_create(int size)
参数
size:创建的红黑树的监听节点数量。(仅供内核参考)
返回值
成功:指向新创建的红黑树的根结点的fd。
失败:-1 errno
(2)epoll_ctl函数
作用
控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
参数
epfd:epoll_creat的句柄(即epoll_create函数返回值)
op:对该监听红黑树所做的操作
(1)EPOLL_CTL_ADD (添加fd到监听红黑树)
(2)EPOLL_CTL_MOD (修改fd在监听红黑树上的监听事件)
(3)EPOLL_CTL_DEL (将一个fd从监听红黑树上删除)
fd:待监听的fd
event:本质是struct epoll_event结构体的地址、
struct epoll_event {
__uint32_t events; /* Epoll events 【EPOLLIN,EPOLLOUT,EPOLLERR 等】*/
epoll_data_t data; /* User data variable */
};
//联合体
typedef union epoll_data {
void *ptr;//泛型指针(内核自动调回调函数)
int fd;//对应监听事件
uint32_t u32;
uint64_t u64;
} epoll_data_t;
返回值
成功:0
失败:-1 errno
(3)epoll_wait函数
原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
作用
等待所监控文件描述符上有事件的产生。
参数
events:用来存内核得到事件的集合。输出满足监听条件的fd结构体。【数组】
maxevents:【数组元素的总个数】。告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
timeout:超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定毫秒
返回值
大于0:有多少文件描述符就绪(满足监听的总个数,用作循环上限)
等于0:没有fd满足监听事件
等于-1:失败 ,errno
(4)代码实例
服务器
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/epoll.h>
#include <map>
using namespace std;
int main()//服务器
{
map<int, struct sockaddr_in> accept_map;
//网络信息结构体
struct sockaddr_in s_addr, c_addr;
int len = sizeof(struct sockaddr_in);
socklen_t client_len = sizeof(struct sockaddr_in);
//socket 标识符
int socket_fd;
//accept 标识符
int accept_fd;
/*
参数1 协议族 iPv4 iPv6
参数2 TCP/UDP
参数3 默认0
*/
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0)
{
perror("socket error");
return 0;
}
//需要使用协议族 AF_INET 表示 IPv4 地址 AF_INET6 表示 IPv6 地址
s_addr.sin_family = AF_INET;
//设置端口号 因为大小端的问题 字节顺序转换
s_addr.sin_port = htons(10086);
//IP 地址 INADDR_ANY泛指本机
s_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(socket_fd, (struct sockaddr*)&s_addr, len) < 0)//绑定
{
perror("bind error");
return 0;
}
//第二个参数 服务器可以同时接收几个客户端
if (listen(socket_fd, 10) < 0)
{
perror("listen error");
return 0;
}
int epollwaitfd;
int epoll_fd;
//epoll事件结构体
struct epoll_event epollEvent;
//epoll容器
struct epoll_event epollEventArray[30];
//初始化epoll事件结构体
bzero(&epollEvent, sizeof(epollEvent));
//绑定当前已经打通的网络通道标识
epollEvent.data.fd = socket_fd;
//捆绑事件为进入事件
epollEvent.events = EPOLLIN;
//创建epoll
epoll_fd = epoll_create(30);
//将打通的网络添加到epoll中
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &epollEvent);
char buffer[50] = {'\0'};
while (1)
{
//网络打通后 让epoll里面所有的socket都处于等待状态
epollwaitfd = epoll_wait(epoll_fd, epollEventArray, 30, -1);
if (epollwaitfd < 0)
{
perror("epoll_wait error");
}
for (int i = 0; i < epollwaitfd; i++)
{
//说明有客户端上线
if (epollEventArray[i].data.fd == socket_fd)
{
accept_fd = accept(socket_fd, (struct sockaddr*)&c_addr, &client_len);
printf("\n%s:%d 登录服务器!\n\n", inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port));
epollEvent.data.fd = accept_fd;
epollEvent.events = EPOLLIN;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accept_fd, &epollEvent);
accept_map[accept_fd] = c_addr;
}
else if (epollEventArray[i].events & EPOLLIN)//说明有数据发过来
{
bzero(buffer, sizeof(buffer));
int ret = read(epollEventArray[i].data.fd, buffer, sizeof(buffer));
if (ret <= 0)
{
cout << "客户端" << inet_ntoa(accept_map[epollEventArray[i].data.fd].sin_addr) << ":" << ntohs(accept_map[epollEventArray[i].data.fd].sin_port) << "下线" << endl;
//关闭socket
close(epollEventArray[i].data.fd);
//如果客户端掉线 就从epoll容器里面删除
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epollEventArray[i].data.fd, NULL);
//从map容器中删除
accept_map.erase(epollEventArray[i].data.fd);
}
else//ret>0
{
buffer[ret] = '\0';
cout << "客户端" << inet_ntoa(accept_map[epollEventArray[i].data.fd].sin_addr)<<":"<< ntohs(accept_map[epollEventArray[i].data.fd].sin_port )<< "说:" << buffer << endl;
}
}
else
{
continue;
}
}
}
close(socket_fd);
return 0;
}
客户端
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <string.h>
using namespace std;
int main()
{
//网络信息结构体
struct sockaddr_in s_addr;
int len = sizeof(struct sockaddr_in);
//socket 标识符
int client_fd;
/*
参数1 协议族 iPv4 iPv6
参数2 TCP/UDP
参数3 默认0
*/
client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd < 0)
{
perror("socket error");
return 0;
}
//需要使用协议族
s_addr.sin_family = AF_INET;
//设置端口号 因为大小端的问题 字节顺序转换
s_addr.sin_port = htons(10086);
//IP 地址
s_addr.sin_addr.s_addr = inet_addr("192.168.52.130");
if (connect(client_fd, (struct sockaddr*)&s_addr, len) < 0)
{
perror("connect error");
return 0;
}
cout << "连接服务器成功,可以开始发送消息" << endl;
while (1)
{
char msg[50] = { '\0' };
scanf("%s", msg);
//发送至服务器
if (write(client_fd, msg, sizeof(msg)) == -1)
{
perror("发送失败");
}
}
close(client_fd);
return 0;
}
执行结果