目录
一:服务器收到多个客户端信息 方案设计
1. 一个客户端 对应 服务器上的一个进程:accept函数之后通过fork开子进程
缺陷:进程间不能通信,一定需要借助IPC技术
2. 一个客户端 对应 服务器上的一个线程:accept函数之后pthread_create创建子线程
缺陷:因为线程可以做到数据共享,数据不安全,解决方案是互斥量\信号量
主要问题:操作系统承载线程是有上限,上限会根据不同的硬件的配置有所差别,但一定不能到达
百万级别。
3.因此需要学习IO复用,本节做出主要介绍
二:阻塞和非阻塞
以快递物流为例:
1.阻塞:空出大脑可以安心睡觉。(不占用CPU宝贵的时间片)
2.非阻塞:浪费时间,浪费电话费,占用快递员时间(占用CPU,系统资源)
3.为什么需要前后置服务器
一个简单的改进方案是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
举个例子:
4.epoll的作用--多路复用型IO
三:IO模型
•阻塞I/O•非阻塞I/O•I/O复用(select和poll)•信号驱动I/O•异步I/O
1.阻塞I/O模型
•最流行的I/O模型是阻塞I/O模型,缺省时,所有的套接口都是阻塞的
2.非阻塞I/O模型
•当我们把一个套接口设置为非阻塞方式时,即通知内核:当请求的I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误
应用程序连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,一般只在专门提供某种功能的系统中才会用到
四:常用的多路IO复用模型有三种
1.select函数
select函数作用
这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程
【仅仅知道有IO事件发生,却并不知道是哪几种流,只能做无差别轮询所有的流,找到能读出数据或者写入数据的流,对它们进行操作。select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长,select底层用的是有限制长度的数组】
2.Poll模型
•Poll函数和select类似,但它是用文件描述符而不是条件的类型来组织信息的.•也就是说,一个文件描述符的可能事件都存储在struct pollfd中.与之相反,select用事件的类型来组织信息,而且读,写和错误情况都有独立的描述符掩码.poll函数是POSIX:XSI扩展的一部分,它起源于UNIX System V【poll的本质和select没区别(轮询的方式没有改变,从头到尾遍历,数据的存储结构修改了,数组->链表,虽然存储问题解决,但是遍历的问题还是没有解决),select好比数组,而poll是对数组的升级变成容器,看似升级实则无用,它将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,但是它没有最大的连接数限制,原因是因为它是基于链表来存储的】
3.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代码学习
主要函数
服务器完整代码:
#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;
}
客户端完整代码:
#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;
}
结果:一个服务器与多个客户端 可以实现通信
ps -aux查看一下
服务器就一个进程 对应客户端三个进程,符合基本业务流程