目录
3 redis,ngnix,memcached reactor具体使用
1 梳理reactor网络编程
2 编程细节 (返回值以及错误码)
连接的建立
分为两种:服务端处理接收客户端的连接,服务端作为客户端连接第三方服务;
int clientfd = accept(listenfd, addr, sz);
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
errno = EINPOGRESS 正在建立连接
errno = EISCONN 连接建立成功
clientfd == -1 && errno == EWOULDBLOCK(全链接队列没有数据)
在linux中 listen(fd,listenblock);中的listenblock设置的是全链接队列的大小
mac中listenblock设置的是全链接队列和半链接队列的和
int ret = connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
errno = EINPOGRESS 正在建立连接
errno = EISCONN 连接建立成功
连接的断开
分为两种:主动断开和被动断开;
// 主动关闭
close(fd);
shutdown(fd, SHUT_RDWR);
// 主动关闭本地读端,对端写段关闭
shutdown(fd, SHUT_RD);
// 主动关闭本地写端,对端读段关闭
shutdown(fd, SHUT_WR);
// 被动:读端关闭
int n = read(fd, buf, sz);
if (n == 0) {
close_read(fd);
// close(fd);
}
// 被动:写端关闭
//有的网络编程需要支持半关闭状态
int n = write(fd, buf, sz);
if (n == -1 && errno == EPIPE) {
close_write(fd);
// close(fd);
}
主动:close shutdown
被动:
关闭读端 read = 0
关闭写端 write = -1 && errno = EPIPE(写端关闭了)
消息的到达
从读缓冲区中读取数据;
int n = read(fd, buf, sz);//从读缓冲区中的数据从内核态读取到用户态中来
if (n < 0) { // n == -1 EINTR是被信号打断了 EINTR和EWOULDBLOCK都是正向积极的错误
if (errno == EINTR || errno == EWOULDBLOCK)
break;
close(fd);
} else if (n == 0) {
close(fd);
} else {
// 处理 buf
}
消息发送完毕
往写缓冲区中写数据;
int n = write(fd, buf, dz); //n == -1 或者 n==dz
if (n == -1) {//EWOULDBLOCK 写缓冲区满了 EINTR :信号打断 两者都是正向积极的
if (errno == EINTR || errno == EWOULDBLOCK) {
return;
}
close(fd);
}
网络 IO 职责
检测 IO
io 函数本身可以检测 io 的状态;但是只能检测一个 fd 对应的状态;io 多路复用可以同时检测多 个io的状态;区别是:io函数可以检测具体状态;io 多路复用只能检测出可读、可写、错误、断开 等笼统的事件;
操作 IO
只能使用 io 函数来进行操作;分为两种操作方式:阻塞 io 和非阻塞 io;
阻塞 IO 和 非阻塞 IO
阻塞在网络线程;
连接的 fd 阻塞属性决定了 io 函数是否阻塞;
具体差异在:io 函数在数据未到达时是否立刻返回;
// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;
//拿到fd的属性
int flag = fcntl(fd, F_GETFL, 0);
//设置fd的属性
fcntl(fd, F_SETFL, flag | O_NONBLOCK)
图形描述
IO多路复用
IO多路复用只负责检测IO,不负责操作IO
epoll_wait(epfd,evs,sz,timeout);
timeout == -1 IO多路复用是永久阻塞的
timeout = 0 非阻塞 立刻返回
timeout == 一个具体的值 IO多路复用就是阻塞的
epoll的部分源码
struct eventpoll {
// ...
struct rb_root rbr; // 管理 epoll 监听的事件 //红黑树
struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件 //双向链表
// ...
};
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
// ...
};
struct epoll_event { //重点关注************
__uint32_t events;
epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
int epoll_create(int size);
/**
op:
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL
event.events:
EPOLLIN 注册读事件
EPOLLOUT 注册写事件
EPOLLET 注册边缘触发模式,默认是水平触发
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/**
events[i].events:
EPOLLIN 触发读事件
EPOLLOUT 触发写事件
EPOLLERR 连接发生错误
EPOLLRDHUP 连接读端关闭
EPOLLHUP 连接双端关闭
*/
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int
timeout);
epoll原理图
调用 epoll_create 会创建一个 epoll 对象;调用 epoll_ctl 添加到 epoll 中的事件都会与网 卡驱动程序建立回调关系,相应事件触发时会调用回调函数 ( ep_poll_callback ),将触发的 事件拷贝到 rdlist 双向链表中;调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态中
epoll编程
连接的建立
// 一、处理客户端的连接
// 1. 注册监听 listenfd 的读事件
struct epoll_event ev;
ev.events |= EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev);
// 2. 当触发 listenfd 的读事件,调用 accept 接收新的连接
int clientfd = accept(listenfd, addr, sz);
struct epoll_event ev;
ev.events |= EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, clientfd, &ev);
// 二、处理连接第三方服务
// 1. 创建 socket 建立连接
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
// 2. 注册监听 connectfd 的写事件
struct epoll_event ev;
ev.events |= EPOLLOUT;
epoll_ctl(efd, EPOLL_CTL_ADD, connectfd, &ev);
// 3. 当 connectfd 写事件被触发,连接建立成功
if (status == e_connecting && e->events & EPOLLOUT) {
status == e_connected;
}
连接的断开
epoll_wait();
if (e->events & EPOLLRDHUP) {
// 读端关闭
close_read(fd);
// close(fd);
}
if (e->events & EPOLLHUP) {
// 读写端都关闭
close(fd);
}
数据到达
//rector 为什么要用非阻塞IO
//数据到达如果经过一个错误的校验,epoll发送一个数据可读的信号,
但实际上读缓冲区是没有数据可以读的,那么如果用的是阻塞IO,
则会一直阻塞在IO的操作函数上面,
会对业务的处理或者说的代码的正常执行上面造成效率降低的影响,
如果用的是非阻塞的IO 那么就不会存在这样的问题。
epoll_wait();
if (e->events & EPOLLIN) {
wjile(1)
int n = read(fd, buf, sz);
if (n < 0) {
if (errno == EINTR)
continue;
if (errno == EWOULDBLOCK)
break;
close(fd);
} else if (n == 0) {
close_read(fd);
// close(fd);
}
//具体的业务逻辑
}
数据发送完毕
int n = write(fd, buf, dz);
if (n == -1) {
if (errno == EINTR)
continue;
if (errno == EWOULDBLOCK) {
struct epoll_event ev;
ev.events = EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); //注册写事件 然后写成功后,就可以把这个写事件给删除
}
close(fd);
}
// ...
if (e->events & EPOLLOUT) {
int n = write(fd, buf, sz);
//...
if (n > 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
}
reactor的应用
组成:IO多路复用+非阻塞IO
事件方式通知,事件循环
将对IO的处理转换成对事件的处理
while(1) {
int n = epoll_wait();
for (int i = 0;i < n;i++) {
}
}
3 redis,ngnix,memcached reactor具体使用
单reactor(redis)
redis 网络的封装
redis优化方法:
将 int n = read(fd,buf,sz)
cmd args = decode(buf,sz)
加入队列当中通过负载均衡的方式分派给IO多线程去执行。
out = logic(cmd,args);再回归到主线程执行。
得出的out结果加入队列再次交给IO多线程去处理encode write
注意
read 只能用单线程去读,而不能用多线程去读,否则会出现读取的数据出现错乱的情况。
write 可以多线程 也可以单线程的去写。
skynet的优化方法:
将后面除了读以外的四个步骤加入到多线程上面去执行
memcached
CPU的核数有多少个就分配多少哥reactor,可以充分的利用reactor
(nginx)
nginx 的worker的共享内存当中有一把锁,当有连接进来时候,哪个worker拿到锁,就accept 接收这一条连接,加入epoll红黑树当中
redis memcached采用的是水平触发,nginx采用的是边沿触发
nginx 为什么采用边沿触发?
nginx用来做反向代理,主要是用来转发的,nginx收到数据后就直接往后台转发,直接用边沿触发用while把数据一次性读出来,这样的效率是最高的。
而redis memcached 用水平触发的原因是用户层不知道读缓冲区有多少数据,需要界定数据包,首先读一个数据包然后做相应的业务逻辑。所以用水平触发。
结论:一般而言,需要做业务处理 和数据量小的时候用水平触发。 对数据包不用太在意时就用边沿触发。
worker进程都是监听的同一个端口,master进程执行的listen,然后再开始fork出子进程。
然后一条连接进来后,今后造成所有的work会产生惊群,nginx处理惊群的方法是,worker进程间的共享内存当中会有一把锁,当哪个worker拿到锁之后就执行accept建立好连接,然后注册读事件