C++后端开发(2.1.1)——网络io模型中的阻塞与非阻塞
本节提纲
0.简介
1.基本代码框架
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
//使用socket套接字创建listenfd
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//设置sockaddr结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//绑定地址
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//开启listen
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
#if
// 在这个区域分别使用阻塞,多线程,select,poll,epoll等多种方式实现连接
#endif
close(listenfd);
return 0;
}
2.阻塞——单线程
2.1 accept位于whlie循环之前:只能连接一个客户端
//首先是最基本的方案,在while循环之前accept
//只能实现单个客户端的连接
//定义客户端地址
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("=================wait for client===================\n");
while (1)
{
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
//加上字符串的尾部,以便显示和转发
buff[n] = '\0';
printf("recv msg from clientL %s \n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
}
}
2.2 accept位于whlie循环之中:能连接多个客户端,但只能接收一条消息
//将accept放入while循环,可以实现多个客户端连接,
//但是每次只能进行对话一次
printf("========waiting for client's request========\n");
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
}
// close(connfd);
}
3.阻塞——thread 多线程
3.1 多线程实现方法
3.2 多线程优缺点
// 4G / 8m = 512 假设运行内存只有4g,每个客户端连接占8m,只能支持512个连接
// C10K
void *client_routine(void *arg)
{
int connfd = *(int *)arg;
char buff[MAXLNE];
while (1)
{
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
break;
}
}
return NULL;
}
//开线程,可以实现多个客户端连接,
//但是数量有限,开销大
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, (void *)&connfd);
}
4 多路复用——select
4.1 select 执行流程
4.2 select 优缺点
4.2.1优点
4.2.2 缺点
//使用select实现多路复用
//分别声明select中的读集合,写集合,读操作,写操作
fd_set rset, wset, rfds, wfds;
//全都置0
FD_ZERO(&rfds);
//将listen_fd加入读集合
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int max_fd = listenfd;
while (1)
{
//将新的读写操作更新读写集合
rset = rfds;
wset = wfds;
//登记读写集合
int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);
//监听到新的套接字
if (FD_ISSET(listenfd, &rset))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//加入到写集合中
FD_SET(connfd, &rfds);
//更新max_fd
if (connfd > max_fd)
max_fd = connfd;
//返回的套接字里面啥也没有
if (--nready == 0)
continue;
}
//循环读操作里面的
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++)
{
//在读操作里
if (FD_ISSET(i, &rset))
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
//设置为可写
FD_SET(i, &wfds);
// reactor
// send(i, buff, n, 0);
}
else if (n == 0)
{
FD_CLR(i, &rfds);
close(i);
}
if (--nready == 0)
break;
}
//在写操作里
else if (FD_ISSET(i, &wset))
{
send(i, buff, n, 0);
FD_CLR(i, &wfds);
//设置为可读
FD_SET(i, &rfds);
}
}
}
3 多路复用——poll
3.1 pollfd结构体
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* 文件描述符 */
short int events; /* poller所对应关心的事件,如果是读事件就是POLLIN,如果是写事件就是POLLOUT */
short int revents; /* 对events的反馈,开始时为0,当有数据可读时就为POLLIN,类似select的rset */
};
3.2 poll使用实例
// poll 先把listenfd放进去,关注读事件
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
int i = 0;
for (i = 1; i < POLL_SIZE; i++)
{
fds[i].fd = -1;
}
while (1)
{
int nready = poll(fds, max_fd + 1, -1);
// listenfd发生读事件
if (fds[0].revents & POLLIN)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//接收此次收到的客户端文件描述符
printf("accept \n");
fds[connfd].fd = connfd;
fds[connfd].events = POLLIN;
if (connfd > max_fd)
max_fd = connfd;
if (--nready == 0)
continue;
}
//遍历fds内部,读取revent确定是否收到数据
for (i = listenfd + 1; i <= max_fd; i++)
{
if (fds[i].revents & POLLIN)
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
//把刚刚收到的发出去
send(i, buff, n, 0);
}
//客户端断开连接,回收资源
else if (n == 0)
{
fds[i].fd = -1;
close(i);
}
if (--nready == 0)
break;
}
}
}
3.3 poll执行流程
3.4 对于select提升的地方
3.5 poll弊端
4.多路复用——epoll
4.1 epoll 所使用的函数与解析
#include <sys/epoll.h>
int epoll_create(int size);
/*
epoll_create() 返回一个文件描述符,
该文件描述符“描述”的是内核中的一块内存区域,size现在不起任何作用。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
epoll_ctl()用来操作内核事件表,用于向内核注册新的描述符或者是改变某个文件描述符的状态。
已注册的描述符在内核中会被维护在一棵红黑树上,
通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
/*
进程调用epoll_wait() 便可以得到事件完成的描述符。
*/
4.1.1 参数解析
4.2 epoll 关键结构体epoll_event *event
struct epoll_event{
_uint32_t events; //epoll事件,读、写、异常三种
epoll_data_t data; //用户数据
}
struct epoll_data{
void* prt;
int fd;
_uint32_t u32;
_uint64_t u64;
}epoll_data_t;
epoll_wait()该函数返回就绪文件描述符的个数
4.3 epoll优点
4.3 epoll 使用实例
// poll/select -->
// epoll_create
// epoll_ctl(ADD, DEL, MOD)
// epoll_wait
//创建一个空间,假设是一个大房子
int epfd = epoll_create(1);
// epoll针对的是事件,创建一个事件数组用来管理需要关注的事件
struct epoll_event events[POLL_SIZE] = {0};
//某次接收到的新事件
struct epoll_event ev;
//先把listenfd先塞进去
ev.events = EPOLLIN;
ev.data.fd = listenfd;
// 添加listenfd,持续监听,添加到了内核管理的一棵红黑树上面
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1)
{
//非阻塞,这里是从内核中的链表中提取出来放进events数组里
//查看events数组里面关注的事件描述符有没有置1,最后一个是超时时间
//没有则返回-1,有则返回事件的数量
int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
if (nready == -1)
{
continue;
}
int i = 0;
for (i = 0; i < nready; i++)
{
//读取获取的fd
int clientfd = events[i].data.fd;
//如果是listenfd,则收到的是客户端的fd
if (clientfd == listenfd)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept\n");
//通过epoll_clt添加到内核的红黑树里
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 获取到的消息
else if (events[i].events & EPOLLIN)
{
n = recv(clientfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
//这里是直接发送回去,并不规范
send(clientfd, buff, n, 0);
}
else if (n == 0)
{
//传输完成删除该fd
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}