目录
一:服务器搭建设计
一个服务器可以连接多个客户端,3种设计方案
1. 一个客户端 对应 服务器上的一个进程:accept函数之后通过fork开子进程
缺陷:进程间不能通信,一定需要借助IPC技术
2. 一个客户端 对应 服务器上的一个线程:accept函数之后pthread_create创建子线程
缺陷:因为线程可以做到数据共享,数据不安全,解决方案是互斥量\信号量
主要问题:操作系统承载线程是有上限,上限会根据不同的硬件的配置有所差别,但一定不能到达百万级别
3. IO多路复用技术,在本节中重点介绍(使用epoll IO多路复用技术,就可以使得无进程、无线程的创建,依旧支持一个服务器可以连接上多个客户端)
二:阻塞 & 非阻塞
什么是阻塞?
下面以快递物流为例,理解阻塞
有电话通知才会去领取快递,在没有通知的情况下,可以自己做自己的事情
阻塞:空出大脑可以安心睡觉(不占用CPU宝贵的时间片)
每隔一会就催促一次,无论对于收件员还是快递员都是大量浪费时间的
非阻塞:浪费时间,浪费电话费,占用快递员时间(占用CPU,系统资源)
为什么需要前后置服务器设计?
一个简单的改进方案是在服务器端使用多线程(或多进程)
多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接
下面还是以快递物流为例,了解多线程/多进程
缺点:多个快递员同时向你电话,可能因为忙机,无法及时领取其他没有接到电话的物件(多个客户端需要排队等待数据处理返回)
IO多路复用技术 epoll(本节重点)
同样是快递物流为例,
epoll就可以类比是一个快递站,在快递到了的时候会发送短信通知你,让你及时取件(不会出现漏接电话的情况)
三:IO模型
阻塞I/O
非阻塞I/O
I/O复用(select和poll epoll)
信号驱动I/O
异步I/O
阻塞I/O模型
最流行的I/O模型是阻塞I/O模型,缺省时,所有的套接口都是阻塞的(数据就绪后才会处理返回)
非阻塞I/O模型
当我们把一个套接口设置为非阻塞方式时,即通知内核:当请求的I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误(无论数据是否准备好都要返回,直到数据就绪后处理返回的那次才是有效的办事)
非阻塞IO模型,应用程序连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,一般只在专门提供某种功能的系统中才会用到
四:IO多路复用技术
select函数
select函数作用:
这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程
(仅仅知道有IO事件发生,却并不知道是哪几种流,只能做无差别轮询所有的流,找到能读出数据或者写入数据的流,对它们进行操作;select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长,select底层用的是[有限制长度]的数组)(类似这种从头遍历也就是O(n)复杂度,举个例子,比如现在有10000个客户端,但是第9999个客户端中才有数据需要处理返回,就需要从1遍历到9999,效率非常低下; 更离谱的是,如果第9999个客户端数据处理返回后,恰好这个时间第9998个客户端有数据需要处理返回,那么又要重头开始对这10000个数据再遍历一次,由此可见,select非常落后!)
Poll
Poll函数和select类似,但它是用文件描述符而不是条件的类型来组织信息的
也就是说,一个文件描述符的可能事件都存储在struct pollfd中;与之相反,select用事件的类型来组织信息,而且读,写和错误情况都有独立的描述符掩码;poll函数是POSIX:XSI扩展的一部分,它起源于UNIX System V
poll的本质和select没区别(轮询的方式没有改变,从头到尾遍历,只是数据的存储结构修改了,数组---》链表,虽然存储问题解决,但是遍历的问题还是没有解决,依旧是O(n)复杂度),select好比数组,而poll是对数组的升级变成容器,看似升级实则无用,它将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,但是它[没有最大的连接数限制],原因是因为它是基于链表来存储的 (数组有限制长度,(容器)链表没有,但是select、poll二者都是遍历O(n)复杂度,效率都是非常低下的!)
epoll
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
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);
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的IO事件通知给程序员/主进程,epoll实际上是事件驱动(每个事件关联上fd),此时我们对这些流的操作就是有意义的,复杂度降低了变成了O(1)
上图可以加深对epoll的理解,打个比方,
上图中的事件队列 可以类比成我们平时坐动车时的动车站候车厅,
而上图中的就绪列表 可以类比成动车在到达时的广播提醒我们,我们就会立马去检票口排队,通知我们坐上动车(上图中的进程),
不过进程在处理完后,会将fd返回给事件队列(在客户端没有下线情况下,可多次为客户端服务)
五:epoll 服务器搭建设计
查看epoll_create函数使用说明
查看epoll_ctl函数使用说明
查看epoll_wait函数使用说明
核心源码:
服务器完整代码(使用epoll IO多路复用技术 无需进程、线程创建)
#include<iostream>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<stdio.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
using namespace std;
int main()
{
struct epoll_event epollevent;
//事件结构体数组 编写代码中用作判断使用
struct epoll_event epolleventArray[5];
int epollfd = 0;
int epollwaitfd = 0;
char buf[50] = { 0 };
struct sockaddr_in addr;
int len = 0;
int acceptfd = 0;
//初始化网络 识别当前计算机是否可以联网
//第一个参数:采用IPV4 IP地址 第二个参数:网络分配TCP
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd == -1)
{
perror("socket error");
}
else
{
cout << "socketfd = " << socketfd << endl;
//确定用IPV4地址
addr.sin_family = AF_INET;
//服务器开放自己的IP地址给客户端连接使用 INADDR_ANY生成默认的可以联网的IP地址
addr.sin_addr.s_addr = INADDR_ANY;
//绑定服务器端口号0-65535 10000以下系统默认使用
addr.sin_port = htons(10086);
len = sizeof(addr);
int opt_val = 1;
//解决 address already is use 报错
//端口复用 设置,一定在bind函数前
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt_val,sizeof(opt_val));
//bind 绑定ip地址 绑定端口号
if (bind(socketfd, (struct sockaddr*)&addr, len) == -1)
{
perror("bind error");
}
if (listen(socketfd, 10) == -1)
{
perror("listen error");
}
cout << "网络搭建成功" << endl;
cout << "epoll创建" << endl;
//事件结构体初始化
bzero(&epollevent, sizeof(epollevent));
//绑定当前准备好的socketfd(服务器可使用的网络通道文件描述符)上线使用/acceptfd发数据使用
epollevent.data.fd = socketfd;
//绑定有可能触发的事件 当前是socketfd 如果有事件发生一定就是 客户端连接
epollevent.events = EPOLLIN;
//创建epoll
epollfd = epoll_create(5);
//epoll事件队列添加socketfd 它感兴趣的事件是epollevent
epoll_ctl(epollfd, EPOLL_CTL_ADD, socketfd,&epollevent);
while (1)
{
cout << "epoll wait........." << endl;
//阻塞式函数 等待事件发生
epollwaitfd = epoll_wait(epollfd, epolleventArray, 5, -1);
if (epollwaitfd < 0)
{
perror("epoll_wait error");
}
for (int i = 0; i < epollwaitfd; i++)
{
//判断是否有客户端上线
if (epolleventArray[i].data.fd == socketfd)
{
cout << "服务器有客户端连接........." << endl;
//服务器等待客户端连接 阻塞式函数 acceptfd在服务器代表已经连接成功的客户端
acceptfd = accept(socketfd, NULL, NULL);
cout << "有客户端成功连接 acceptfd = " << acceptfd << endl;
epollevent.data.fd = acceptfd;
epollevent.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, acceptfd, &epollevent);
}
else if(epolleventArray[i].events & EPOLLIN)
{
//有客户端发来数据
cout << "有事件发生 但不是socketfd 是客户端" << acceptfd << endl;
bzero(buf, sizeof(buf));
int res = read(epolleventArray[i].data.fd, buf, sizeof(buf));
if (res > 0)
{
cout << "服务器收到客户端发来的数据.....buf = " << buf << endl;
}
else if(res <= 0)
{
cout << "客户端掉线............." << acceptfd << endl;
//从epoll中删除该fd
epollevent.data.fd = epolleventArray[i].data.fd;
epollevent.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_DEL, epolleventArray[i].data.fd, &epollevent);
//关闭这个fd所对应的网络通道
close(epolleventArray[i].data.fd);
}
}
}
}
}
return 0;
}
问题1
服务器 端口复用问题解决
(使用setsockopt 并且端口复用设置必须在bind执行前)
int opt_val = 1;
//解决 address already is use 报错
//端口复用 设置,一定在bind函数前
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,
(const void*)&opt_val,sizeof(opt_val));
//bind 绑定ip地址 绑定端口号
if (bind(socketfd, (struct sockaddr*)&addr, len) == -1)
{
perror("bind error");
}
问题2
谁上线:socketfd(服务器可使用的网络通道文件描述符)
谁发送数据:acceptfd(客户端文件描述符)
epoll是可以支持存储多个acceptfd的,不过epollfd会占用一个位置
问题3
客户端 先于服务器下线 问题解决
if (res > 0)
{
cout << "服务器收到客户端发来的数据.....buf = " << buf << endl;
}
else if(res <= 0)
{
cout << "客户端掉线............." << acceptfd << endl;
//从epoll中删除该fd
epollevent.data.fd = epolleventArray[i].data.fd;
epollevent.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_DEL, epolleventArray[i].data.fd, &epollevent);
//关闭这个fd所对应的网络通道
close(epolleventArray[i].data.fd);
}
核心源码:
客户端完整代码(配合服务器测试)
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include<stdio.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
using namespace std;
int main()
{
char buf[50] = { 0 };
struct sockaddr_in addr;
int len = 0;
//初始化网络 识别当前计算机是否可以联网
//第一个参数:采用IPV4 IP地址 第二个参数:网络分配TCP
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
cout << "客户端 socketfd = " << socketfd << endl;
if (socketfd == -1)
{
perror("socket error");
}
else
{
//确定用IPV4地址
addr.sin_family = AF_INET;
//客户端主动寻找服务器IP地址 127.0.0.1本机回环地址 192.168.75.128
addr.sin_addr.s_addr = inet_addr("192.168.75.128");
//绑定服务器端口号0-65535 10000以下系统默认使用
addr.sin_port = htons(10086);
len = sizeof(addr);
//主动去连接服务器 IP和端口
if (connect(socketfd, (struct sockaddr*)&addr, len) == -1)
{
perror("connect error");
}
else
{
cout << "客户端连接服务器成功" << endl;
}
while (1)
{
cin >> buf;
int res = write(socketfd, buf, sizeof(buf));
cout << "客户端发送 res = " << res << endl;
bzero(buf, sizeof(buf));
}
}
return 0;
}
结果:一个服务器与多个客户端连接 可以实现socket通信 数据共享
ps -aux 查看一下 进程
由上图不难看出,服务器就一个进程 对应客户端三个进程(服务器与客户端是一对多的关系)
也就说明了使用epoll无需创建进程