IO复用的相关知识
计算机如何接受网络数据
网卡接受到网络数据,写入到计算机内存的某个地址
socket网络编程
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。客户端和服务端就通过socket来连接,交互。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
socket工作流程
先看服务器端
//创建socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(fd, ...)
//监听
listen(fd, ...)
//接受客户端连接,一直阻塞进程,有连接事件才返回。
int clientfd = accept(fd, ...)
//接收客户端数据
while(true){
if (read(clientfd,acbuf,20) > 0)
{
printf("receive: %s\n",acbuf);
}
}
Linux的socket标识符
socket包含:输入缓冲区,输出缓冲区,等待队列(它指向所有需要等待该 Socket 事件的进程)
在linux万物皆文件的思想中,客户端连接tcp服务的时候,会创建一个"socket文件",socket文件标识符clientfd就可以操作这个"socket文件"。clientfd->read(),就可以读取这个socket的输入缓冲区的数据(就是客户端的请求数据),clientfd->write就可以把输出缓冲区写入数据(操作系统会把这些数据发送到客户端)。
上面这个服务会一直阻塞在aceept函数中,一直到有客户端连接事件发生,触发中断,然后系统会把这个socket等待队列中的进程加入到就绪队列中(就是唤醒进程)。这里一个进程只能处理一个clientfd,一个进程处理一个客户端。
服务端两种socket,监听socket和连接socket
在服务端,首先用函数socket创建一个socket,经过bind,listen后就是监听socket,负责处理客户端的连接事件。也就是clientfd = accept(fd, ...)。fd就是监听socket,只有一个,没次客户端连接事件发生,就会返回一个clientfd。
clientfd就是连接socket,每一个客户端连接,都有一个clientfd。主要处理接受数据事件,客户端断开连接事件,服务端还可以通过clientfd发送数据给对应的客户端。
上面的select模式中,fds[]包含了监听socket和连接socket。epoll模式中,监视的所有socket也包括这两种socket.
数据流程和步骤
-
计算机收到了对端传送的数据(步骤 ①)
-
数据经由网卡传送到内存(步骤 ②)
-
然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行中断程序(步骤 ③)
此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入工作队列中。
以上是内核接收数据全过程,这里我们可能会思考两个问题:
-
操作系统如何知道网络数据对应于哪个 Socket?
-
如何同时监视多个 Socket 的数据?
第一个问题:因为一个 Socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的 Socket。
当然,为了提高处理速度,操作系统会维护端口号到 Socket 的索引结构,以快速读取。
第二个问题是多路复用的重中之重,也正是本文后半部分的重点。
select模式
Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。
当其中一个sock接受到数据后,会触发中断程序
select函数的作用:
1.将进程从sock1,sock2,sock3的等待队列中移除,并且把进程唤醒。
2.进程遍历所有socket得到发生事件的socket
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int fds[] = 存放需要监听的socket,包括fd和所有的clientfd。
while(1){
int n = select(..., fds, ...) //阻塞进程,等有事件发生,引发中断唤醒进程。
for(int i=0; i < fds.count; i++){ //遍历所有可能的socket
if(FD_ISSET(fds[i], ...)){//只有发生事件的socket才进入
if(i==s){
//处理监听socket的事件,也就是连接事件
clientfd = accept(s,...);
FD_SET(clientfd,&fds);//把新加入的clientfd加入到fds中
} else{
//处理clientfd的事件,也就是客户端发送数据,或者客户端结束连接
}
}
}
fds在select前,保存了所有监听的socket fds在select后,只保存发生事件的socket
select模式服务端执行流程
优点:
我们一个进程就可以监视和处理多个socket。
缺点:
每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。
Epoll 的设计思路
先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据:
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int eventpoll = epoll_create(...); //创建eventpoll对象,并且把当前进程放入到eventpoll的等待队列
epoll_ctl(eventpoll , ...); //将所有需要监听的socket添加到eventpoll中
while(1){
int n = epoll_wait(...)//阻塞进程,一直到socket发生事件才唤醒
for(接收到数据的socket){
//处理 同上
}
Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。
rdlist就保存了接收到数据的clientfd。所以epoll_wait只需要返回rdlist里面的clientfd就行。
所以说,epoll方式其实就是多了一个 eventpoll对象,通过epoll_ctl可以把需要监听的clientfd加入到 eventpoll 对象中,同时 eventpoll 对象中还有一个队列rdlist,只要哪个clientfd接收到数据,就会加入到rdlist中。
eventpoll对象认识
rdlist: 双向链表,维护着有接收到数据的clientfd
rbr:红黑树,保存监视的 保存监视的clientfd, 通过epoll_ctl添加
epoll模式服务端执行里流程
select服务端代码demo
#include < sys/types.h>
#include < sys/socket.h>
#include < netinet/in.h> //sockaddr_in
#include < stdio.h>
#include < string.h>
#include < signal.h>
#include < sys/select.h>
#include < unistd.h>
#include < sys/time.h>
//TCP
int main()
{
int fd;
int clientfd;
int ret;
pid_t pid;
int i;
int maxfd; //当前最大套接字
int nEvent;
fd_set set = {0}; //监听集合
fd_set oldset = {0}; //存放所有要监听的文件描述符
struct timeval time = {0};
int reuse = 0;
char acbuf[20] = "";
char client_addr[100] = "";
struct sockaddr_in addr = {0}; //自己的地址
struct sockaddr_in clientAddr = {0}; //连上的客户端的地址
int addrLen = sizeof(struct sockaddr_in);
signal(SIGCHLD,SIG_IGN);
//1.socket()
fd = socket(PF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
//会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
//由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
perror("setsockopet error\n");
return -1;
}
//2.bind()
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = inet_addr("192.168.159.5");
ret = bind(fd,(struct sockaddr *)&addr,addrLen);
if(ret == -1)
{
perror("bind");
return -1;
}
//3.listen()
ret = listen(fd,10);
if(ret == -1)
{
perror("listen");
return -1;
}
//创建监听集合
FD_ZERO(&oldset);
FD_SET(fd,&oldset);
//maxfdp1:当前等待的最大套接字。比如:当前fd的值=3,则最大的套接字就是3
//所以每当有客户端连接进来,就比较一下文件描述符
maxfd = fd;
//select
//select之前,set放的是所有要监听的文件描述符;{3,4,5}
//select之后,set只剩下有发生事件的文件描述符。{3}
while(1)
{
set = oldset;
printf("before accept.\n");
time.tv_sec = 5;
nEvent = select(maxfd + 1,&set,NULL,NULL,&time);//返回文件描述符的个数(即事件的个数)
printf("after accept.%d\n",nEvent);
if(nEvent == -1)
{
perror("select");
return -1;
}
else if(nEvent == 0) //超时
{
printf("time out");
return 1;
}
else
{
//有事件发生,遍历每一个fd,既要处理监听套接口,又要处理连接套接口
//判断是否是客户端产生的事件
for(i = 0 ; i <= maxfd ; i++)
{
if(FD_ISSET(i,&set))//select后,set只剩下发生事件的文件描述符,
{
if(i == fd)//对于fd,只能是连接事件
{
clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
FD_SET(clientfd,&oldset);//有新的连接,把新的clientfd加入到oldset中
printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
if(clientfd > maxfd)
{
maxfd = clientfd;
}
}
else //对于clientfd,只能是发送数据或者客户端退出
{
memset(acbuf,0,20);
if(read(i,acbuf,20) == 0) //客户端退出
{
close(i);
//还要从集合里删除
FD_CLR(i,&oldset);
}
else
printf("receive: %s\n",acbuf);
}
}
}
}
}
return 0;
}
epoll模式服务端demo
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //sockaddr_in
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/epoll.h>
//epoll
//epoll_wait() epoll_creat() epoll_ctl()
//TCP
int main()
{
int fd;
int clientfd;
int ret;
pid_t pid;
int i;
int epfd;
int nEvent;
struct epoll_event event = {0};
struct epoll_event rtl_events[20] = {0}; //事件结果集
int reuse = 0;
char acbuf[20] = "";
char client_addr[100] = "";
struct sockaddr_in addr = {0}; //自己的地址
struct sockaddr_in clientAddr = {0}; //连上的客户端的地址
int addrLen = sizeof(struct sockaddr_in);
signal(SIGCHLD,SIG_IGN);
//1.socket()
fd = socket(PF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket");
return -1;
}
//会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
//由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
perror("setsockopet error\n");
return -1;
}
//2.bind()
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = inet_addr("192.168.159.5");
ret = bind(fd,(struct sockaddr *)&addr,addrLen);
if(ret == -1)
{
perror("bind");
return -1;
}
//3.listen()
ret = listen(fd,10);
if(ret == -1)
{
perror("listen");
return -1;
}
epfd = epoll_create(1000); //同时监听的文件描述符
event.data.fd = fd;
event.events = EPOLLIN; //读
epoll_ctl(epfd,EPOLL_CTL_ADD,fd, &event);
while(1)
{
// nEvent = epoll_wait(epfd,rtl_events,20,-1); //-1:阻塞 0:非阻塞
nEvent = epoll_wait(epfd,rtl_events,20,5000);
if(nEvent == -1)
{
perror("epoll_wait");
return -1;
}
else if(nEvent == 0)
{
printf("time out.");
}
else
{
//有事件发生,立即处理
for(i = 0; i < nEvent;i++)
{
//如果是 服务器fd
if( rtl_events[i].data.fd == fd )
{
clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
//添加
event.data.fd = clientfd;
event.events = EPOLLIN; //读
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&event);
printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
}
else
{
//否则 客户端fd
memset(acbuf,0,20);
ret = read(rtl_events[i].data.fd,acbuf,20);
printf("%d\n",ret);
if( ret == 0) //客户端退出
{
close(rtl_events[i].data.fd);
//从集合里删除
epoll_ctl(epfd,EPOLL_CTL_DEL,rtl_events[i].data.fd,NULL);
}
else
printf("receive: %s\n",acbuf);
}
}
}
}
return 0;
}
参考:https://blog.csdn.net/armlinuxww/article/details/92803381
https://blog.51cto.com/13097817/2054397