服务器模型
Author:尼古拉·道格拉斯·叶四
Descrition:时光里的那一缕风
email:1783644194@qq.com
Creat Time:2022年10月15日
SOCKET介绍
要想在客户端和服务器能在网络中通信就必须使用socket 编程,可以满足跨主机之间通信的需求。
socket中文名叫做插口。在客户端服务器进行通信时,各自创建一个socket相当于双方开了一个口子,所有的数据交互都是通过这个口子进行的,创建socket时可以指定网络层使用的是IPv4还是IPv6,传输层使用的是TCP 还是 UDP,在本章服务器模型中我们使用的是基于TCP的socket编程。
socket编程相关API
socket();函数,创建socket套接字。
bind();函数,给socket绑定ip地址和端口。
- 绑定端口目的:当内核收到TCP报文后,通过TCP头里的端口号,来找到我们的应用程序,然后传输数据
- 绑定ip地址的目的:一台机器是可以有多个网卡的,每个网卡都有对应的IP地址,当绑定一个网卡时,内核在收到该网卡的包,才会发给我们
listen();函数,进行监听(监听的是对应的端口号)。
accept();函数,从内核中获取客户端的连接,该函数的参数要指明服务端的ip地址和端口号,然后进行TCP三次握手。
read()函数 write()函数,来进行读写数据。
基于linux一切皆文件的理念,在内核中socket也是以文件的形式存在,也有相应的文件描述符
服务器
本章代码
本章代码主要在于服务器的设计实现和优化上,客户端实现基本功能用于测试即可(连接、断开、收发数据获取系统时间),对套接字进行二次包裹便于后续的使用和操作,实现一个简单的业务。
Socket套接字二次包裹,网络研发工程师通常会对各种函数API进行二次包裹,本章中的socket二次包裹体现在将报错包裹起来,直接便于后续的调用使用。
服务器基本概述:企业级后端服务软件,各种软件客户端无法脱离服务器技术,服务器为各样的功能软件提供后台支持,技术支持,数据支持等等,服务器软件执行周期较长(7*24)小时不间断执行。
服务器种类
**软件服务器(CS)
** 自行设计服务器组织架构 、 协议指定 、协议状态数据处理,千万级别中转交互 设计实现取决于软件功能与用户要求 一般是UDP和TCP自定义包裹UDP和TCP协议。
**web服务器(BS)
**网站服务器、网页服务器 HTTP协议超文本传输协议基于TCP协议实现的。
服务器作用与职责
- 网络穿透职责数据中转职责 info_list 可以保存客户端最新的信息 为独立无关联的客户端中转数据,建立通信渠道,实现通信效果
- 高并发能力:并发能力是一个服务器的基本能力,主要体现在并发数量(并发连接数量多少个用户可以同时连接访问服务器,服务器一对多完成
- 安全性 访问权限校验,数据通信加密,隔离机制(反向代理服务器),防火墙策略和设置。
- 存储 缓存临时数据,持久化用户数据等(磁盘存储,数据存储,mysql,redis,sqlserver)。
- 业务处理 处理客户端请求,完成特定任务,高并发处理。
服务器操作系统:
服务器操作系统时长占有率最高的即linux/unix操作系统,cantos,ubuntu,linuxf服务器系统稳定性,安全性,可用性较高,当然也有windows操作系统
开源服务器软件
- web服务器 无需用户过多干预,设置配置,搭建服务流程结构,实现业务主体,即可投入使用 强调通用性:Apache服务器和Nginx服务器
- 软件服务器 没有特定的服务器强调自定义
- 功能性服务器 处理服务器 文件服务器 数据库服务器 代理服务器(中转)
- 分布式服务器集群 由海量服务器主机设备构成、实现资源互享套接字包裹
SOCKET套接字二次包裹
//linuxserver.c文件
#include "linuxserver.h"
int SOCKET(int domain,int type,int protocol){
int sockfd;
if((sockfd = socket(domain,type,protocol))==-1){
perror("Socket Creadte sockfd Error:");
}
return sockfd;
}
int LISTEN(int sockfd,int backlog){
int reval;
if((reval = listen(sockfd,backlog))==-1){
perror("listen Error:");
}
return reval;
}
int BIND(int sockfd,const struct sockaddr* addr,socklen_t addrlen){
int reval;
if((reval = bind(sockfd,addr,addrlen))==-1){
perror("bind sockfd Error:");
}
return reval;
}
int CONNECT(int sockfd,const struct sockaddr* addr,socklen_t addrlen){
int reval;
if((reval = connect(sockfd,addr,addrlen))==-1){
perror("connect sockfd Error:");
}
return reval;
}
int ACCEPT(int sockfd,struct sockaddr* addr,socklen_t* addrlen){
int clientfd;
if((clientfd = accept(sockfd,addr,addrlen))==-1){
perror("accept sockfd Error:");
}
return clientfd;
}
int SEND(int sockfd,char * buf, int L,0){
int sendLen;
if(sendLen = send(sockfd,buf,L,0) == -1){
perror("send failed:");
}
return sendLen;
}
int RECV(int sockfd,const char * buf, int L,0){
int recvLen;
if(recvLen = recv(sockfd,buf,L,0) == -1){
perror("recv failed:");
}
return recvLen;
}
//linuxserver.h文件
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>c
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<sys/wait.h>
#include<signal.h>
#include<time.h>
#define BUFSIZE 1500
#define BACKLOG 128
#define TIMEOUT 2
#define SERVER_IP "222.171.151.226"
#define SERVER_PORT 8080
#define SHOWDOWN 1
int SOCKET(int domain,int type,int protocol);
int LISTEN(int sockfd,int backlog);
int BIND(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
int CONNECT(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
int SEND(int sockfd,const char * Response,socklen_t len,int N);
int RECV(int sockfd,char * Response,socklen_t len,int N);
int ACCEPT(int sockfd,struct sockaddrc* addr,socklen_t* addrlen);
服务器客户端测试模型
//完成简单的系统时间请求
#include "linuxserver.h"
#include <errno.h>
int main(){
int mysock;
struct sockaddr_in serverAddr;
bzero(&serverAddr,sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET,"192.168.229.128",&serverAddr.sin_addr.s_addr);
mysock = SOCKET(AF_INET,SOCK_STREAM,0);
CONNECT(mysock,(struct sockaddr *)&serverAddr,sizeof(serverAddr));
ssize_t recvLen;
char buffer[1500];
bzero(buffer,sizeof(buffer));
while((recvLen = RECV(mysock,buffer,sizeof(buffer),0))){
if(recvLen == 0){
if(errno == EAGAIN)
continue;
else
perror("recv Call failed");
}else if(recvLen>0){
printf("%s\n",buffer);
break;
}
}
while((fgets(buffer,sizeof(buffer),stdin))!=NULL){
SEND(mysock,buffer,strlen(buffer),0);//发送请求
bzero(buffer,sizeof(buffer));
recvLen = RECV(mysock,buffer,sizeof(buffer),0);
printf("%s\n",buffer);
}
return 0;
}
c
CS服务器模型
CS模型TCP Socket调用流程是最简单、最基本的,它基本只能一对一通信,因为使用的是同步阻塞的方式,当服务端在还没处理完一个客户端的网络IO时,或者读写操作发生阻塞时,其他客户端是无法与服务端进行连接的。只能服务一个客户端,这样很浪费资源。
#include "linuxserver.h"
int main(){
int server_fd;
int client_fd;
struct sockaddr_in serverAddr, clientAddr;
bzero(&serverAddr,sizeof(serverAddr));//初始化服务器socket信息结构体
serverAddr.sin_family = AF_INET;//地址族IPV4
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//初始化监听网卡 any
serverAddr.sin_port = htons(SERVER_PORT);//监听端口
server_fd = SOCKET(AF_INET,SOCK_STREAM,0);//创建TCP套接字
BIND(server_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));//绑定套接字
LISTEN(server_fd,BACKLOG);//设置监听
socklen_t addrLen;
char Response[1024];
char ip[16];
char str_time[1024];
time_t tp;
printf("Linux Server Demo Version 0.1 Runing...\n");
while(1){
addrLen = sizeof(clientAddr);
client_fd = ACCEPT(server_fd,(struct sockaddr *)&clientAddr,&addrLen);//接收连接
bzero(Response,sizeof(Response));//初始化回复
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,16);//大端ip转字符串
printf("Hi (%s) conection successly...\n",ip);//接收到客户端的连接进行打印输出
sprintf(Response,"Hi (%s) Thanks user Linmux Server Demo,Version 0.1",ip);
SEND(client_fd,Response,strlen(Response),0);//发送回复信息
//读取客户端请求 识别time关键字
bzero(Response,sizeof(Response));//清空字符串缓冲区
while(RECV(client_fd,Response,sizeof(Response),0)){
//等待回复
printf("respons = %s\n",Response);//打印回复
if((strcmp(Response,"time\n")) == 0){
tp = time(NULL);
bzero(str_time,sizeof(str_time));//初始化时间字符
ctime_r(&tp,str_time);//填入时间
SEND(client_fd,str_time,strlen(str_time),0);//回复时间种子
}else{
SEND(client_fd,Response,strlen(Response),0);//回复错误请求
}
}
}
return 0;
}
如何服务更多的用户探讨
上文提到,简单的CS模型只能完成一对一的通信,这样太过浪费资源,因此我们需要改进服务器模型。
TCP是由四元组唯一确认的,这个四元组就是:本地IP,本地端口,对端IP,对端端口
服务器作为服务方通常会在本地固定监听一个端口,等待客户端的连接。因此服务器的本地IP和端口是固定的,于是对于服务端TCP连接的四元组只有对端IP和端口是会变化的,所以最大TCP连接数=客户端IP数*客户端端口数。
对于IPv4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大的TCP连接数约为2的48次方。当然,这个理论值很丰满,但是服务器肯定承载不了这么大的连接数
原因如下:
- 文件描述符 Socket实际上是一个文件,也就是会对应一个文件描述符。在Linux下,单进程打开文件描述符数是有限制的,没有进行修改一般是1024,但是我们可以认为修改增大文件描述符最大数量
- 系统内存,每个TCP的连接在内核中都有对应的数据结构,意味着每个连接会占用一定内存
多进程服务器模型
多进程服务器模型:基于最原始的阻塞网络IO,如果服务器要支持多个客户端,其中比较传统的方式,就是使用多进程模型,为每个客户端分配一个进程来处理需求。
父进程在该模型中起一个接待员的作用进行accept连接,一旦接收连接成功返回一个socket并进行fork创建子进程,子进程是业务员,进行业务处理一个客户端对应一个子进程,子进程退出则客户端退出,我们通过返回值来区分是父进程还是子进程,用以划分工作区,在这个过程中,父进程只关心监听socket,子进程只关心已连接的socket。客户端退出则父进程要进行子进程的回收,防止出现**僵尸进程
**,创建回收线程完成回收。
子进程回收问题
:创建回收线程进行回收,同时采用信号技术,当子进程结束时,系统会向父进程发送一个信号,通知父进程该子进程结束,在线程中对该信号进行一个信号捕捉设定,捕捉**SIGHLD信号
,该信号绑定的有一个回收函数**,我们可以在该捕捉函数中进行回收,不要用**SIGCHLD
**的数量决定回收次数,因为经典信号不支持排队,可能会导致漏回收僵尸进程。
线程间共享信号行为问题
:线程之间共享信号行为,因此会影响accept函数的执行,因为捕捉信号会导致accept函数被强制中断退出。绝对不允许主线程处理中断信号,解决方法使用信号屏蔽杜绝主线程调用信号捕捉函数,具体操作方法:主线程屏蔽该信号
,而后创建普通线程,普通线程会集继承该**信号屏蔽字
**,普通线程进行信号捕捉,信号捕捉完毕后解除信号屏蔽字,这是主线程一直是屏蔽状态。
多进程模型优缺点
优点
- 稳定性高,一个进程崩溃并不会影响其他进程
- 操作简单,易于上手
缺点
- 不能满足高数量的客户端连接,因为每产生一个进程都会占用一定的系统资源,而且进程之间的上下文切换会大大影响性能和效率
#include "linuxserver.h"
void sig_wait(int n){
//捕捉函数
pid_t zpid;
//循环回收,将所有可回收的僵尸全部处理
while(zpid = waitpid(-1,NULL,WNOHANG)>0){
printf("wait thread 0x%x , wait sucess , zomble process pid %d\n",(unsigned int)pthread_self(),zpid);
}
}
void * thread_job(void * arg){
//回收线程函数
//设置分离态线程
pthread_detach(pthread_self());
//1、信号捕捉设定
struct sigaction act , oact;
act.sa_handler = sig_wait;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,&oact);
//解除屏蔽
sigprocmask(SIG_SETMASK,&act.sa_mask,NULL);
printf("Wait thread 0x%x waiting.Waitg...\n",(unsigned int)pthread_self());
while(1)
sleep(1);
return 0;
}
int main(){
int server_fd;
int client_fd;
struct sockaddr_in serverAddr, clientAddr;
bzero(&serverAddr,sizeof(serverAddr));//初始化服务器socket信息结构体
serverAddr.sin_family = AF_INET;//地址族IPV4
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//初始化监听网卡 any
serverAddr.sin_port = htons(SERVER_PORT);//监听端口
server_fd = SOCKET(AF_INET,SOCK_STREAM,0);//创建TCP套接字
BIND(server_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));//绑定套接字
LISTEN(server_fd,BACKLOG);//设置监听
socklen_t addrlen;
char Respons[1024];
char ip[16];
char str_time[1024];
time_t tp;
ssize_t recvLen;
pid_t pid;
//主线程设置信号屏蔽
sigset_t set,oset;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_SETMASK,&set,&oset);
ssize_t recelen;
//创建回收线程
pthread_t tid;
pthread_create(&tid,NULL,thread_job,NULL);
printf("Linux server Demo Version 0.2 Running...\n");
while(1){
addrlen = sizeof(clientAddr);
if((client_fd = ACCEPT(server_fd,(struct sockaddr *)&clientAddr,&addrlen))>0){
pid = fork();//创建业务员 子进程处理
}
if(pid > 0 ){
//这是父进程工作区
bzero(ip,sizeof(ip));
bzero(Respons,sizeof(Respons));//清空响应缓冲区
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,16);//大端ip转字符串
printf("client (%s) connect successly...\n",ip);
sprintf(Respons,"Hi ,(%s) Thanks user server Demo , Version 0.2!",ip);
send(client_fd,Respons,strlen(Respons),0);//发送响应给客户端
}else if(pid == 0){
//这里是子进程工作区
bzero(Respons,sizeof(Respons));//清空字符串缓冲区
while(recvLen = recv(client_fd,Respons,sizeof(Respons),0)){
//等待回复
if(recvLen > 0) {
printf("respons = %s\n",Respons);//打印回复
if((strcmp(Respons,"time\n")) == 0){
tp = time(NULL);
bzero(str_time,sizeof(str_time));//初始化时间字符
ctime_r(&tp,str_time);//填入时间
SEND(client_fd,str_time,strlen(str_time),0);//回复时间种子
}else{
SEND(client_fd,"requset failed",strlen("requset failed")+1,0);//回复错误请求
}
bzero(Respons,sizeof(Respons));
}
}
if(recvLen == 0){
printf("有客户端断开了,接下来应该是调用信号捕捉函数\n");
close(client_fd);
exit(0);
}
}
}
return 0;
}
多线程服务器模型
多线程服务端模型与多进程服务器模型基本一致
在多线程模型中,线程是运行在进程中的一个“逻辑流”,单进程中可以运行多个线程,通进程里的线程共享进程部分资源,上下文切换开销较小。
当服务器与客户端TCP完成连接后,通过pthread_create()函数创建线程,然后将已经连接的socket文件描述符传递给线程函数,在线程中和客户端进行通信,从而达到并发处理的目的。
多线程模型优缺点探讨
优点
- 线程间的上下文切换系统开销较小
缺点
- 如果进行频繁创建销毁线程,系统开销也是不小的(可以使用线程池来解决这个问题)
#include "linuxserver.h"
void * Business(int client_fd){
char Respons[1024];
time_t tp;
ssize_t recvLen;
char str_time[1024];
bzero(Respons,sizeof(Respons));//清空字符串缓冲区
while(recvLen = recv(client_fd,Respons,sizeof(Respons),0)){
//等待回复
if(recvLen > 0) {
printf("respons = %s\n",Respons);//打印回复
if((strcmp(Respons,"time\n")) == 0){
tp = time(NULL);
bzero(str_time,sizeof(str_time));//初始化时间字符
ctime_r(&tp,str_time);//填入时间
send(client_fd,str_time,strlen(str_time),0);//回复时间种子
}else{
send(client_fd,"requset failed",strlen("requset failed")+1,0);//回复错误请求
}
bzero(Respons,sizeof(Respons));
}
}
if(recvLen == 0){
printf("回收线程"\n");
close(client_fd);
pthread_exit(NULL);
}
}
void * thread_job(void * arg){
int client_fd ;
client_fd = *(int *)arg;
pthread_detach(pthread_self());//设置分离态线程
Business(client_fd);//调用函数处理客户请求
}
int main(){
int server_fd;
int client_fd;
struct sockaddr_in serverAddr, clientAddr;
bzero(&serverAddr,sizeof(serverAddr));//初始化服务器socket信息结构体
serverAddr.sin_family = AF_INET;//地址族IPV4
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//初始化监听网卡 any
serverAddr.sin_port = htons(SERVER_PORT);//监听端口
server_fd = SOCKET(AF_INET,SOCK_STREAM,0);//创建TCP套接字
BIND(server_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));//绑定套接字
LISTEN(server_fd,BACKLOG);//设置监听
socklen_t addrlen;
char Respons[1024];
char ip[16];
ssize_t recvLen;
pthread_t tid;
printf("Linux server Demo Version 0.2 Running...\n");
while(1){
addrlen = sizeof(clientAddr);
if((client_fd = ACCEPT(server_fd,(struct sockaddr *)&clientAddr,&addrlen))>0){
bzero(ip,sizeof(ip));
bzero(Respons,sizeof(Respons));//清空响应缓冲区
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,16);//大端ip转字符串
sprintf(Respons,"Hi ,(%s) Thanks user server Demo , Version 0.2!",ip);
send(client_fd,Respons,strlen(Respons),0);//发送响应给客户端
pthread_create(&tid,NULL,thread_job,(void *)&client_fd);
}
}
close(server_fd);
return 0;
}
IO复用技术|多路IO转接
IO复用技术可以帮助我们监听多个socket上的网络事件,便于后续处理socket数据可以实现一个单进程的服务器模型(单进程模型是有很多缺陷,尽管如此也是一种经典的IO复用技术)
使用一个进程来维护多个socket,一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的时间时,耗时控制在1毫秒以内,这样一秒内可以处理上千请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想类似于CPU并发处理多个进程。
socket事件
读事件 :对端向socket上发送数据,导致client_fd上读事件触发
写事件 :通过client_fd向对端发送数据,触发写事件
异常事件 :使用socket产生错误或者异常、触发异常事件
IO复用技术对上述事件进行监听,就绪后处理就绪即可
典型的IO复用技术
- select
- poll
- epll
使用IO模型进行轮询监听,释放阻塞函数,轮询监听socket事件,进程可以通过一个系统掉用函数从内核中获取多个事件,在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了时间的连接,然后在用户态中再处理这些连接对应的请求即可。
select模型
- 头文件 #include <sys/select.h>
- fd_set set 监听集合,fd_set的大小为1024个,这意味着select技术最多只能监听1024个socket的事件
- select流程:监听事件->就绪事件->处理就绪事件
select实现多路复用的方式是,将已连接的socket都放在一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核中,让内核来检查是否有网络事件产生,检查方式是通过轮询遍历文件描述符集合的方式,当检查到有事件产生后,将此socket标记为可读或者可写,接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再遍历的方法找到可读或者可写的socket,然后再进行处理,上述过程中发生了两次遍历文件描述符集合,一次再内核态一次在用户态,还会发生两次拷贝文件描述符集合,分别是从用户空间拷贝到内核空间,在内核空间修改后又传回到用户空间。因此在select使用的过程中,多次执行select多轮监听,但是每一轮监听可能都会出现大量无意义的拷贝开销和设备挂在开销。
seletc使用固定长度的bitsmap标识文件描述符集合,支持的文件描述符个数是有限的,在Linux系统中,由内核中的FD_SETSIZE限制,默认最大值为1024,只能监听0-1023的文件描述符。
设置监听集合使用select模块进行轮询监听返回就绪的数量,传出就绪集合,就绪的socket对应位保留为1,其他未就位的socket保留为0,便于后续开发者后续辨别和处理就绪的socket.
在单进程select服务器模型中,sock_fd检测到就绪即进行accept接收连接,client_fd(指向客户端)就绪后进行业务处理模块并反馈响应,衍生一步操作:将client_fd保存到一个数组并且加入到监听数组中,便于后续使用。
recv返回值为0时代表客户端退出,那么后续要将对应的client_fd从监听集合以及数组中删除,最后close掉client_fd。
注意:recv函数要设置为非阻塞模式,单进程下不允许出现阻塞情况,阻塞读取可能会导致其他流程不能继续,一次请求只处理一次,使用FD_ISSET进行辨别就绪,通过遍历fd数组与就绪数组比较,查看位码,如果为1,则代表就绪 *
select 技术相关API
FD_ZERO(fd_set * set);//初始化监听集合,将所有的位设置为0
FD_SET(int sockfd,fd_set *set);//对监听中的某个socket对应的位码设置为1,设置监听
FD_CLR(int sockfd,fs_set * set);//对监听集合中的某一个socket对应的位码设置为0,取消监听
int code = FD_ISSET(int sockfd,fd_set *set);//可以返回监听集合中的某一个socket对应的位码值
int readycode = select(maxfd+1,fd_set *rd_set,fd_set * wr_set,fd_set * err_set,struct timeval_t * timeout);
#include "linuxserver.h"
#include <sys/select.h>
int clientfd_array[1020];
int Business(int client_fd,fd_set * Nset,int i){
char Respons[1024];
time_t tp;
ssize_t recvLen;
char str_time[1024];
bzero(Respons,sizeof(Respons));//清空字符串缓冲区
recvLen = recv(client_fd,Respons,sizeof(Respons),0);
//等待回复
if(recvLen > 0) {//这里不能使用while考虑一下原因
printf("respons = %s\n",Respons);//打印回复
if((strcmp(Respons,"time\n")) == 0){
tp = time(NULL);
bzero(str_time,sizeof(str_time));//初始化时间字符
ctime_r(&tp,str_time);//填入时间
send(client_fd,str_time,strlen(str_time),0);//回复
}else{
send(client_fd,"requset failed",strlen("requset failed")+1,0);//回复错误请求
}
bzero(Respons,sizeof(Respons));
}
if(recvLen == 0){
printf("Client fd %d Exit closed\n",client_fd);
FD_CLR(client_fd,Nset);//删除监听
close(client_fd);
clientfd_array[i] = -1;
}
return 0;
}
int main(){
int server_fd;
int client_fd;
struct sockaddr_in serverAddr, clientAddr;
bzero(&serverAddr,sizeof(serverAddr));//初始化服务器socket信息结构体
serverAddr.sin_family = AF_INET;//地址族IPV4
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//初始化监听网卡 any
serverAddr.sin_port = htons(SERVER_PORT);//监听端口
server_fd = SOCKET(AF_INET,SOCK_STREAM,0);//创建TCP套接字
BIND(server_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));//绑定套接字
LISTEN(server_fd,BACKLOG);//设置监听
int readycode;
socklen_t addrlen;
char Respons[1024];
char ip[16];
//定义监听集合
fd_set Nset,Oset;
FD_ZERO(&Nset);//初始化
FD_SET(server_fd,&Nset);//对设置的server_fd的监听
ssize_t recvLen;
int i = 0;
for(i;i<1020;i++){
clientfd_array[i] = -1;
}
int maxfd;
maxfd = server_fd;
printf("Linux server Demo Version 0.2 Running...\n");
while(1){
Oset = Nset;
readycode = select(maxfd+1,&Oset,NULL,NULL,NULL);//阻塞监听事件
if(readycode == -1){
perror("select call failed");
exit(0);//退出进程
}
while(readycode){
//辨别就绪
if(FD_ISSET(server_fd,&Oset)){
//server_fd就绪 i
addrlen = sizeof(clientAddr);
client_fd = ACCEPT(server_fd,(struct sockaddr *)&clientAddr,&addrlen);
if(client_fd>maxfd){
maxfd = client_fd;
}
for(i = 0; i<1020;i++){
if(clientfd_array[i] == -1){
clientfd_array[i] = client_fd;
break;
}
}
FD_SET(client_fd,&Nset);//设置clientfd的监听
bzero(Respons,sizeof(Respons));
bzero(ip,sizeof(ip));
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,16);//大端ip转字符串
printf("client (%s) connect successly...\n",ip);
sprintf(Respons,"Hi ,(%s) Thanks user server Demo , Version 0.2!",ip);
send(client_fd,Respons,strlen(Respons),0);//发送响应给客户端
}else{
printf ("先应该走到这\n");
//clientfd就绪,查找并处理
for(i = 0;i<1020;i++){
if( clientfd_array[i]!= 1){
if(FD_ISSET(clientfd_array[i],&Oset)){
printf("客户端发 送信息后应该走到这\n");
Business(clientfd_array[i],&Nset,i);//处理业务
break;
}
}
}
}
--readycode;
}
}
close(server_fd);
return 0;
}
select模型优缺点
优点
- 各个平台系统和语言都有对select的支持和实现,兼容性和跨平台能力较强
- 使用简单,易于理解,可应用于一些简易模型
- select支持微妙级别定时阻塞监听,如果IO监听对时间精度要求较高,可以使用select
缺点
- 监听数量限制,因为select使用fd_set做监听集合,最大舰艇数量仅为1024,无法满足高并发需求
- 轮询问题,select采用轮询方式监听socket事件,轮询比较耗时,轮询开销随监听数量增加而增长,轮询开销增大,处理能力呈线性下降
- 在select使用的过程中,多次执行select多轮监听,但是每一轮监听可能都会出现大量无意义的拷贝开销和设备挂在开销
使用问题
- select传入监听集合就绪后,修改监听集合为就绪集合,用户需要自行分离监听集合和就绪集合
- select监听到就绪后,只会返回就绪的数量,不会返回就绪的socket,需要使用者自行查找判断就绪的socket然后处理
- select可以监听的就绪事件较少
- select设置监听不够灵活,只能批次设置监听,无法对不同的socket设置不同的监听
poll模型
poll模型也是经典的IO复用技术,整体的使用思路和工作模式跟select模型没有任何差别,也是三部分构成(监听、就绪、处理就绪)
POLL相关API
poll模型与select不同的是,poll不再使用bitsmap来存储所关注的文件描述符,取而代之的是动态数组,以链表的形式阻止,突破了select的文件描述符个数的限制(当然最大长度回受到系统文件描述符的限制),允许用户自定义长度的结构体数组,将此数组作为监听集合。
struct pollfd listen_arry[4096];//poll的监听集合
struct pollfd node;
- node.fd = sock_fd;//将要监听的sock传到fd中,如果取消监听则传-1
- node.events = POLLIN|POLLOUT|POLLERR;设置监听的事件
- node.revents;//系统传出就绪事件
poll();// 监听函数
- readycode = poll(struct pollfd * fds,int nfds,int timeout);//监听函数
- fds:监听数组首地址
- nfds:监听最大数量,一般是数组长度
- timeout:监听方式 -1(阻塞) 0 (非阻塞) >0(定时阻塞) , 定时阻塞默认以毫秒为单位,如果系统不支持毫秒定时则向上取秒
- readycode:返回监听的就绪数量,失败返回-1
#include "linuxserver.h"
#include <poll.h>
struct pollfd clientfd_array[1020];
int Business(int client_fd,int i){
char Respons[1024];
time_t tp;
ssize_t recvLen;
char str_time[1024];
bzero(Respons,sizeof(Respons));//清空字符串缓冲区
recvLen = recv(client_fd,Respons,sizeof(Respons),0);
//等待回复
if(recvLen > 0) {
printf("respons = %s\n",Respons);//打印回复
if((strcmp(Respons,"time\n")) == 0){
tp = time(NULL);
bzero(str_time,sizeof(str_time));//初始化时间字符
ctime_r(&tp,str_time);//填入时间
send(client_fd,str_time,strlen(str_time),0);//回复
}else{
send(client_fd,"requset failed",strlen("requset failed")+1,0);//回复错误请求
}
bzero(Respons,sizeof(Respons));
}
if(recvLen == 0){
printf("Client fd %d Exit closed\n",client_fd);
close(client_fd);
clientfd_array[i].fd = -1;
}
return 0;
}
int main(){
int server_fd;
int client_fd;
struct sockaddr_in serverAddr, clientAddr;
bzero(&serverAddr,sizeof(serverAddr));//初始化服务器socket信息结构体
serverAddr.sin_family = AF_INET;//地址族IPV4
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//初始化监听网卡 any
serverAddr.sin_port = htons(SERVER_PORT);//监听端口
server_fd = SOCKET(AF_INET,SOCK_STREAM,0);//创建TCP套接字
BIND(server_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));//绑定套接字
LISTEN(server_fd,BACKLOG);//设置监听
int readycode;
socklen_t addrlen;
char Respons[1024];
char ip[16];
ssize_t recvLen;
int i = 0;
for(i;i<1020;i++){
clientfd_array[i].fd = -1;//对监听集合进行初始化
}
clientfd_array[0].fd = server_fd;
clientfd_array[0].events = POLLIN;
printf("Linux server Demo Version 0.2 Running...\n");
while(1){
readycode = poll(clientfd_array,1020,-1);//阻塞监听事件
if(readycode == -1){
perror("select call failed");
exit(0);//退出进程
}
while(readycode){
//辨别就绪
if(clientfd_array[0].revents == POLLIN){
//server_fd就绪
addrlen = sizeof(clientAddr);
client_fd = ACCEPT(server_fd,(struct sockaddr *)&clientAddr,&addrlen);
for(i = 1;i<1020;i++){
if(clientfd_array[i].fd == -1){
clientfd_array[i].fd = client_fd;
clientfd_array[i].events = POLLIN;
break;
}
}
bzero(Respons,sizeof(Respons));
bzero(ip,sizeof(ip));
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,16);//大端ip转字符串
printf("client (%s) connect successly...\n",ip);
sprintf(Respons,"Hi ,(%s) Thanks user server Demo , Version 0.2!",ip);
send(client_fd,Respons,strlen(Respons),0);//发送响应给客户端
}else{
printf ("先应该走到这\n");
//clientfd就绪,查找并处理
for(i = 0;i<1020;i++){
if( clientfd_array[i].fd!= 1){
if(clientfd_array[i].revents == POLLIN){
printf("客户端发 送信息后应该走到这\n");
Business(clientfd_array[i].fd,i);//处理业务
break;
}
}
}
}
--readycode;
}
}
close(server_fd);
return 0;
}
tip:系统默认进程最大文件描述符为1024,但可以人为修改 : cat /proc/sys/fs/file-max(查看系统支持最大数量文件描述符) sudo vi /etc/secrity/limits.conf*
POLL模型的优缺点
优点
- 可监听的事件较多
- 对socket的事件监听设置更为灵活,可以对不同的socket设置不同的监听事件
- poll通过events和reventx完成传入传出的分离,用events设置监听的事件,用revents传出就绪的事件
缺点
- 虽然poll采用自定义长度的数组作为监听集合,但是内部依然是轮询监听,这导致没有根本解决问题,不宜监听过多的socket数量
- 轮询问题,poll采用轮询的方式监听socket事件,轮询比较耗事,轮询开销随监听数量增加而增长
- 在poll使用的过程中,多次执行poll,多轮监听,但是每一轮监听可能都会出现大量无意义的拷贝开销和设备挂载开销
EPOLL模型
EPOLL模型也是经典的IO复用技术,整体的使用思路和工作模式跟select没有任何差别,也是三部分构成(监听(监听集合),就绪(辨别就绪的socket),处理就绪(不同的socket就绪不通的处理过程))
EPOLL采用红黑树作为监听集合
EPOLL模型相关API
int epfd = epoll_create(int max);//参数为树的大小,返回值为监听树的描述符,可以通过此描述符操作监听树
struct epoll_event Node;//监听节点类型
- Node.data.fd = sockfd;//存储监听的socket
- Node.events = EPOLLN|EPOLLOUT|EPOLLERR;//设置监听事件
epoll_ctl(int epfd,int cmd,int sockfd,struct epoll_event * Node);//可以对监听树进行增删改操作
- epfd : 监听树的描述符
- cmd : 操作方式,EPOLL_CTL_ADD(添加节点) ,EPOLL_CTL_MOD(修改) EPOLL_CTL_DEL(修改)
- sockfd : 节点索引
- Node : 即将操作的节点
int readycode = epoll_wait(int epfd,struct epoll_event * ready_arry,int ready,int timeout);
- ready_arry : 就绪队列,epoll监听到就绪不仅返回就绪的数量,还会将所有就绪的socket节点传出到就绪队列中,便于用户处理
- ready : 最大就绪数,一般与最大监听数一致 timeout : -1(阻塞) 0 (非阻塞) >0(定时阻塞)
注意:修改操作只能修改监听的事件,无法修改监听的socket_fd
#include "linuxserver.h"
#include <sys/epoll.h>
#define EPOLL_MAX 10000
int Business(int client_fd,int epfd){
char Respons[1024];
time_t tp;
ssize_t recvLen;
char str_time[1024];
bzero(Respons,sizeof(Respons));//清空字符串缓冲区
recvLen = recv(client_fd,Respons,sizeof(Respons),0);
//等待回复
if(recvLen > 0) {
printf("respons = %s\n",Respons);//打印回复
if((strcmp(Respons,"time\n")) == 0){
tp = time(NULL);
bzero(str_time,sizeof(str_time));//初始化时间字符
ctime_r(&tp,str_time);//填入时间
send(client_fd,str_time,strlen(str_time),0);//回复
}else{
send(client_fd,"requset failed",strlen("requset failed")+1,0);//回复错误请求
}
bzero(Respons,sizeof(Respons));
}
if(recvLen == 0){
printf("Client fd %d Exit closed\n",client_fd);
close(client_fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,client_fd,NULL);//删除监听项
}
return 0;
}
int main(){
int server_fd;
int client_fd;
struct sockaddr_in serverAddr, clientAddr;
bzero(&serverAddr,sizeof(serverAddr));//初始化服务器socket信息结构体
serverAddr.sin_family = AF_INET;//地址族IPV4
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//初始化监听网卡 any
serverAddr.sin_port = htons(SERVER_PORT);//监听端口
server_fd = SOCKET(AF_INET,SOCK_STREAM,0);//创建TCP套接字
BIND(server_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));//绑定套接字
LISTEN(server_fd,BACKLOG);//设置监听
int readycode;
socklen_t addrlen;
char Respons[1024];
char ip[16];
struct epoll_event ready_array[EPOLL_MAX];
int epfd;
int i = 0;
ssize_t recvLen;
struct epoll_event Node;
epfd = epoll_create(EPOLL_MAX);
Node.data.fd = server_fd;
Node.events = EPOLLIN;
//将首个节点添加到树中
epoll_ctl(epfd,EPOLL_CTL_ADD,server_fd,&Node);//创建监听树
printf("Linux server Demo Version 0.2 Running...\n");
while(1){
readycode = epoll_wait(epfd,ready_array,EPOLL_MAX,-1);//阻塞监听事件
if(readycode == -1){
perror("select call failed");
exit(0);//退出进程
}
i= 0;
while(readycode){
//辨别就绪
if(ready_array[i].data.fd == server_fd){//表示server_fd事件就绪
//server_fd就绪 i
addrlen = sizeof(clientAddr);
client_fd = ACCEPT(server_fd,(struct sockaddr *)&clientAddr,&addrlen);
Node.data.fd = client_fd;
Node.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&Node);//将新的socket添加到监听树中
bzero(Respons,sizeof(Respons));
bzero(ip,sizeof(ip));
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,16);//大端ip转字符串
printf("client (%s) connect successly...\n",ip);
sprintf(Respons,"Hi ,(%s) Thanks user server Demo , Version 0.2!",ip);
send(client_fd,Respons,strlen(Respons),0);//发送响应给客户端i
printf("亲 走到这里了吗?\n");
}else{
printf ("先应该走到这\n");
//clientfd就绪,查找并处理
Business(ready_array[i].data.fd,epfd);//处理业务
}
--readycode;
++i;
}
}
close(server_fd);
return 0;
}
EPOLL优缺点
优点
- epoll没有监听数量限制,理论上系统可以使用多少文件描述符epoll就可以监听多少个
- epoll没有轮询问题,不用担心监听数量的增多开销变大,或者处理能力变弱