预备知识
1、操作系统的用户态和内核态:
用户态指的是用户自己定义工作空间,自己申请变量、定义函数的操作。
内核态指把一些工作交给操作系统去玩成,用户本身看不到执行过程,只能获取操作系统最后执行完成的结果。其中,在网络编程中,accept recv send等函数均在内核态实现的。
计算机之间的通信,就是内核态在操作系统里实现的,用户本身是感觉不到的,除非使用ping等函数,能查到和其他计算机进程通信的结果。
所以,当有计算机远程连接或传送数据时,操作系统内核首先建立连接,并且开辟一段缓冲区把对方的信息放入缓冲区。如果需要用户去读取,则需要通过特定函数(listen,accept)将首先得到该连接在用户态层面的地址,再使用(recv)函数将存在于内核去的数据读取出来。
阻塞状态:如何内核函数没有相应的结果,会阻塞在这个状态,直到有新的结果出现才会返回。
2、实现tcp server
IO复用
select函数
select函数 也是一个内核态的函数,调用该函数可以使操作系统内核检查其中建立的连接,并把结果进行返回。
//需要首先初始化两个数组
fd_set fdset, rset;
FD_ZERO(&fdset);
//后续需要对fdset数组进行监听,所以需要提前将该数组初始化,即把bind的文件描述符放入其中
FD_SET(servfd, &fdset);int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
当调用了select函数后,内核就会遍历这个数组,挨个判断这个数组里的文件描述符是否达到可读状态,如果达到可读状态,就把对应文件描述符放入rset数组中,随后用户就可以对rset里的文件进行相应的操作。
注:什么是可读状态?
对于bind端口的文件描述符而言,内核监听到有新的连接建立,就是可读了;
对于普通文件描述符,内核监听到缓冲区有数据了,就是可读状态。
因此在调用select函数后,需要写两段逻辑,分别处理这两种可读状态:
int mready = select(maxfd + 1, &rset, NULL, NULL, NULL);//第一段逻辑,处理是否有新的连接建立,如果有新的建立,就拿到文件描述符//加入到fd_set数组中,参与下一轮的监听if(FD_ISSET(servfd, &rset)){ //acceptint clientfd = accept(servfd, (struct sockaddr*)&clientaddr, &client_len);printf("%d had connected!\n", clientfd);FD_SET(clientfd, &fdset);if(clientfd > maxfd){maxfd = clientfd;}}//第二段逻辑,处理是否有数据发送过来。//如果有,就把数据缓冲区的内容读取出来for(int i = servfd + 1; i <= maxfd; i++){if(FD_ISSET(i, &rset)){char buffer[1024] = {0};int count = recv(i, buffer, 1024, 0);if(count == 0){printf("%d disconnected!\n", i);close(i);FD_CLR(i, &fdset);continue;}printf("receive: %s \n", buffer);send(i, buffer, count, 0);} }
poll函数
select的弊端:fd_set这个数组的大小是固定的,对于高并发而言效率过低。
select参数过多,监听可读需要一个数组,监听可写需要一个数组,监听错误也需要一个数组,拷 贝到内核态的东西太多了。
poll基于select,建立了新的数组,其实是新的结构体数组,结构体(数组)如下:
相较于select的数组而言,该结构体不只是包含了文件描述符,还还包含了想要监听的事件状态(events)所以可以同时做到监听可读、可写、错误等,这样就避免了select使用过多的数组作为参数传给内核。
//初始化fd数组,准备把该数组传给poll
struct pollfd fds[1024] = {0};
fds[servfd].fd = servfd;
fds[servfd].events = POLLIN;int nready = poll(fds, maxfd + 1, -1);
调用poll函数后,操作系统内核同样会遍历这个数组,根据数组中events中所想要监听的状态判断是否就绪,把结果写入revents中返回,用户态中用户看到基于revents中的结果进行相应操作。
int mready = poll(fds, maxfd + 1, -1);
//对于可读状态,同样要写两端逻辑
//第一段逻辑,判断是否是bind端口的文件描述符,如果是,表明有新的连接,需要加入fd数组
if(fds[servfd].revents & POLLIN){int clientfd = accept(servfd, (struct sockaddr*)&clientaddr, &client_len);printf("%d had connected!\n", clientfd);fds[clientfd].fd = clientfd;fds[clientfd].events = POLLIN;if(clientfd > maxfd){maxfd = clientfd;}
}
//第二段逻辑,判断新的连接是否可读,即缓冲区有无内容。
for(int i = servfd + 1; i <= maxfd; i++){if(fds[i].revents & POLLIN){char buffer[1024] = {0};int count = recv(i, buffer, 1024, 0);if(count == 0){printf("%d disconnected!\n", i);close(i);fds[i].fd = -1;fds[i].events = 0;continue;}printf("receive: %s \n", buffer);send(i, buffer, count, 0);}
}
epoll函数
poll的弊端:内核还是会对传入数组进行遍历,判断每个fd是否是所需要的状态,还是会有拷贝和循环的过程。
epoll和select和poll相比,省略了传入新建fd数组并拷贝的环节,而是直接在内核中构造了该数组。
//在内核中构造了一个数组,用来存放监听到的文件描述符,可供用户态直接使用
int epollfd = epoll_create(1);//创建好了之后需要初始化该数组,即把bind端口的fd放入该数组中
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = servfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, servfd, &ev);
之后会调用epoll_wait函数,内核会把已经就绪的文件描述符返回,用户只需要新建一个数组用来接收就可以了
//在用户态建立数组用于接收返回的就绪的文件描述符
struct epoll_event events[1024];
int mready = epoll_wait(epollfd, events, 1024, -1);
//直接对该数组进行遍历
for(int i = 0; i < mready; i++){//同样是两段代码逻辑,一个是判段是否是有新的连接,一个是判断是否有新数据到达if(events[i].data.fd == servfd){int clientfd = accept(servfd, (struct sockaddr*)&clientaddr, &client_len);printf("%d had connected!\n", clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &ev);}else if(events[i].events & EPOLLIN){char buffer[1024] = {0};int count = recv(events[i].data.fd, buffer, 1024, 0);if(count == 0){printf("%d disconnected!\n", events[i].data.fd);close(events[i].data.fd);epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);continue;}printf("receive: %s \n", buffer);send(events[i].data.fd, buffer, count, 0);}
}
课程地址:https://github.com/0voice
总结
io多路复用是面试中常被问到的,之前没有接触也只是笼统的被八股,至于具体内容是什么,原理怎么样并不了解,所以深入问的话是容易露馅的。这次的学习,对io复用有了较深的理解,起码不会像之前那样纯背八股那样停留表面了,起码在操作系统、网络这两个层面有了更进一步的认知。同时不得不感叹,终于有一种把之前学习到的知识串联起来的感觉。之前os是os,计网是计网,考试就是学习对应的知识点应付。这一节通过具体的代码,相当于把这两个进行了连接,有了更立体的认识。如果能有今天这样的认识再回到两个月前参加秋招,别的不提,起码io复用这一块再背问道也会有很大的信心了。还清晰的记得,参加第一场面试被问到io多路复用有了解吗?我竟然回答不是很了解,那么现在我也是直到为啥给我挂了!(哭笑)不过好在什么时候开始都不算晚,知道了哪里不足哪里有欠缺也是一种提升。
果然还是要理论+实践,空背八股会被淘汰,只有代码也很难拿到高薪呀!继续努力吧!