#include<sys/socket.h>
#include<arpa/inet.h> //大小端转换
#include<netdb.h> //DNS
一、Socket套接字
为了开发网络应用,系统提供一套API函数接口,用于网络应用开发,这些接口称为套接字函数
struct sockaddr_int{
sin_family=AF_INET;
sin_port=8080;
sin_addr.s_addr=127.0.0.1;
}addr;
int sockfd=socket(AF_INET,SOCK_STREAM|SOCK_DRAM,0); //socket创建
//成功返回sockfd,失败返回-1
bind(int sockfd,struct sockaddr* addr/*使用旧的网络信息结构体,向前兼容*/,socklen_t addrlen);
//成功返回0,失败返回-1。对socket设置自定义信息,保持信息不变
listen(sockfd,backlog/*等待连接队列大小,默认128*/);//监听连接过程以及对应的链接事件(TCPServer)
//成功返回0,失败返回-1
htons(); //本机到网络16位,小端转大端端口号
htonl(); //小端转大端p
ntohs();
ntohl();
inet_ntop(); //大端序转字符串
inet_pton();
inet_addr();
connect(int sockfd,struct sockaddr* destaddr,sockelen_t addrlen); //请求连接函数(发起握手请求)
//成功返回0,失败返回-1,如果网络异常可能引发阻塞
int clientsock = accept(int serversocket,struct sockaddr* clientaddr,socklen_t* addrlen); //阻塞等待并建立连接函数(完成三次握手),连接成功后立即返回
//成功返回sock,失败返回-1,如果网络异常可能引发阻塞
send(int sockfd,char* msg,int len,MSG_NOSIGNAL/*写忽略信号*/); //向目标发送网络信息
recv(int sockfd,char* buffer,int size,MSG_DONTWAIT/*非阻塞读*/); //读取接收网络信息
TCP连接方式:keep-alive长链接,close短链接
//TcpServer.h
#include<mysock.h>
#define SHUTDOWN 1
/*支持 tcp连接 及连接反馈的模型*/
int main(){
//close 循环持续连接
int server_sock,client_sock;
struct sockaddr_in addrClient;
socklen_t addrlen;
server_sock=net_initializer();
printf("Test tcp server version 1.0\n");
char client_ip[16];
while(SHUTDOWN){
addrlen=sizeof(addrClient);
client_sock=ACCEPT(server_sock,(struct sockaddr*)&addrClient,&addrlen);
bzero(client_ip,16);
inet_ntop(AF_INET,&addrClient.sin_addr.s_addr,client_ip,16);//大端序转字符串
printf("client port %d,client ip %s\n",ntohs(addrClient.sin_port),client_ip);
first_response(client_sock,client_ip);
business(client_sock);//读取请求,处理请求,反馈响应
close(client_sock);
}
close(server_sock);
printf("server tis done\n");
return 0;
}
//TcpClient.h
#include<mysock.h>
int main(){
//创建套接字
int server_sock=SOCKET(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//服务器套接字信息
struct sockaddr_in addrServer;
bzero(&addrServer,sizeof(addrServer));
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(8080);
addrServer.sin_addr.s_addr=inet_addr("82.157.31.74");
CONNECT(server_sock,(struct sockaddr*)&addrServer,sizeof(addrServer));
int nRecvNum=0;
int nSendNum=0;
char recvBuf[1024]="";
char sendBuf[1024]="";
nRecvNum=RECV(server_sock,recvBuf,sizeof(recvBuf),0);
printf("server:%s\n",recvBuf);
fgets(sendBuf,sizeof(sendBuf),stdin);
nSendNum=SEND(server_sock,sendBuf,sizeof(sendBuf),0);
nRecvNum=RECV(server_sock,recvBuf,sizeof(recvBuf),0);
printf("server:%s\n",recvBuf);
close(server_sock);
return 0;
}
套接字函数的包裹,网络功能的包裹:在系统函数的基础上,拓展函数的功能,在函数的基础上包裹一层功能更丰富的函数
//myscok.h
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<errno.h>
#include<ctype.h>
#include<time.h>
int SOCKET(int domain,int type,int protocol);
int BIND(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
int LISTEN(int sockfd,int backlog);
int CONNECT(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
int ACCEPT(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
ssize_t RECV(int sockfd,void* buf,size_t len,int flags);
ssize_t SEND(int sockfd,const void* buf,size_t len,int flags);
int net_initializer();
void first_response(int sock,char* cip);
//业务处理模块
void business(int client_sock);
//mysock.c
#include<mysock.h>
int SOCKET(int domain,int type,int protocol){
int sock;
if((sock=socket(domain,type,protocol))==-1){
perror("socket create failed");
return -1;
}
return sock;
}
int BIND(int sockfd,const struct sockaddr* addr,socklen_t addrlen){
if((bind(sockfd,addr,addrlen))==-1){
perror("bind call failed");
return -1;
}
return 0;
}
int LISTEN(int sockfd,int backlog){
if((listen(sockfd,backlog))==-1){
perror("listen call failed");
return -1;
}
return 0;
}
int CONNECT(int sockfd,const struct sockaddr* addr,socklen_t addrlen){
if((connect(sockfd,addr,addrlen))==-1){
perror("connect call failed");
return -1;
}
return 0;
}
int ACCEPT(int sockfd,struct sockaddr* addr,socklen_t* addrlen){
int sock;
if((sock=accept(sockfd,addr,addrlen))==-1){
perror("accept call failed");
return -1;
}
return sock;
}
ssize_t RECV(int sockfd,void* buf,size_t len,int flags){
ssize_t size;
if((size=recv(sockfd,buf,len,flags))==-1){
if(errno==EAGAIN){
printf("recv nonblock return\n");
}
else{
perror("recv call failed");
}
return -1;
}
return size;
}
ssize_t SEND(int sockfd,const void* buf,size_t len,int flags){
ssize_t size;
if((size=send(sockfd,buf,len,flags))==-1){
perror("send call failed");
return -1;
}
return size;
}
int net_initializer(){
//套接字信息
struct sockaddr_in addrServer;
bzero(&addrServer,sizeof(addrServer));
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(8080);
addrServer.sin_addr.s_addr=htonl(INADDR_ANY);
//创建套接字
int server_sock;
server_sock=SOCKET(AF_INET,SOCK_STREAM,0);
BIND(server_sock,(struct sockaddr*)&addrServer,sizeof(addrServer));
LISTEN(server_sock,128);
return server_sock;
}
void first_response(int sock,char* cip){
char response[1024];
bzero(response,sizeof(response));
sprintf(response,"hi, %s wellcome test TCP server.\n",cip);
SEND(sock,response,strlen(response),0);
}
void business(int client_sock){
//读取一次客户端请求,处理后,立刻断开
ssize_t recv_size;
char recv_buffer[1024];
bzero(recv_buffer,sizeof(recv_buffer));
recv_size=RECV(client_sock,recv_buffer,sizeof(recv_buffer),0);
if((strcmp(recv_buffer,"time\n")==0) || (strcmp(recv_buffer,"time")==0)){
//响应系统时间
time_t tp;
char tbuf[1024];
bzero(tbuf,sizeof(tbuf));
ctime_r(&tp,tbuf);
SEND(client_sock,tbuf,strlen(tbuf),0);
printf("server,response time success.\n");
close(client_sock);
}
else{
//数据处理,大小写转换 toupper()
int cnt=0;
while(cnt<recv_size){
recv_buffer[cnt]=toupper(recv_buffer[cnt]);
cnt++;
}
SEND(client_sock,recv_buffer,recv_size,0);
printf("server,response data sucess.\n");
close(client_sock);
}
}
二、业务
简易业务:例子
客户端向服务端发送time关键字,服务器接收后,向客户端返回系统时间
简单数据处理,客户端向服务端发送字符串,服务端完成大小写转换,并回复
客户端向服务端发送手机号码,服务端向手机发送短信,并附带4位验证码,后续的验证流程忽略
三、服务器基础
CS(客户端服务器模型)BS(浏览器服务器模型)
客户端的主要职责:缓存用户本地数据、面向用户并与服务器进行交互
服务器的基本职责:持久化用户数据,数据中转、高并发(大连接、并发处理(大任务量处理))、安全性、测试
3.1 客户端数据包的传递过程
3.2 服务器的网络穿透
为多端建立一条比较稳定的通信场景
网络信息管理、支持数据中转
3.3 服务器操作系统
3.4 服务器集群(大型网络设备)
3.5 将服务器部署到线上(公网)
个人服务器的搭建:
1、IP 与 MAC 地址绑定,设置绑定固定的IP,MAC地址与ARP地址绑定,避免ARP攻击
2、DNZ主机配置,内网设置DNZ主机(可以对外),设置资源共享,支持外网访问,内网中的其他设备,可以通过地址访问DNZ主机中的资源
3、虚拟服务器,开发设备待定(固定)端口号,设置外部对内访问的端口
4、DDNS 服务,将域名和路由器 WAN + IP进行绑定,互联网其他设备可以通过域名直接定位访问,内网其他设备
3.6 客户端更有效的连接
一个域名上可以绑定多个ip
3.7 负载均衡
1、轮询机制:如果处理任务的复杂程度不同,分配不均匀
2、计算处理机的负载,按负载分发,将新任务分发给负载最低的
3、绑定分发:用户与设备绑定
3.8 服务器的几个版本
3.8.1 单进程阻塞服务器(1对1):轮询排队处理
服务器主要功能:连接(Accept),请求处理与响应(RECV,SEND)
阻塞等待连接时,无法读写用户数据;阻塞读取数据时,无法建立新连接
3.8.2 单进程非阻塞服务器(1对多):连接与处理交替执行
非阻塞连接、非阻塞读写
//将sock设为非阻塞,即可以非阻塞accept
int flag=fcntl(server_sock,F_GETFL);
flag|=O_NONBLOCK;
fcntl(server_sock,F_SETFL,flag);
accpet(server_sock,addr,addrlen);
recv(sock,buffer,sizeof(buffer),MSG_DONTWAIT);
单进程非阻塞模型,可以使用,但是无法彻底解决问题,如果业务较为复杂,依然无法建立新连接。只能满足简易任务和连接数量少的模型
//mysock.c
#include<mysock.h>
int business(int flags){
ssize_t recv_size;
char recv_buffer[1024];
//循环尝试读取客户端请求
for(int i=0;i<1024;i++){
if(client_sock_array[i]!=-1){
int client_sock=client_sock_array[i];
bzero(recv_buffer,sizeof(recv_buffer));
recv_size=RECV(client_sock,recv_buffer,sizeof(recv_buffer),flags);
if(recv_size>0){
printf("client:%s\n",recv_buffer);
if((strcmp(recv_buffer,"time\n")==0) || (strcmp(recv_buffer,"time")==0)){
//响应系统时间
time_t tp;
char tbuf[1024];
bzero(tbuf,sizeof(tbuf));
ctime_r(&tp,tbuf);
SEND(client_sock,tbuf,strlen(tbuf),0);
printf("server,response time success.\n");
}
else{
//数据处理,大小写转换 toupper()
int cnt=0;
while(cnt<recv_size){
recv_buffer[cnt]=toupper(recv_buffer[cnt]);
cnt++;
}
SEND(client_sock,recv_buffer,recv_size,0);
printf("server,response data sucess.\n");
}
}
else if(recv_size==0){//sock断开
printf("client exit, close sock %d\n",client_sock);
close(client_sock);
//从数组中删除
client_sock_array[i]=-1;
}
else if(recv_size==-1){
}
}
}
}
//Tcp_NONLOCK_Server.c
#include<mysock.h>
#define SHUTDOWN 1
int main(){
int server_sock,client_sock;
struct sockaddr_in addrClient;
for(int i=0;i<1024;i++){
client_sock_array[i]=-1;
}
socklen_t addrlen;
server_sock=net_initializer();
//将server_sock改为非阻塞,实现accept非阻塞
int flag=fcntl(server_sock,F_GETFL);
flag|=O_NONBLOCK;
fcntl(server_sock,F_SETFL,flag);
printf("Test tcp server version 1.0\n");
char client_ip[16];
while(SHUTDOWN){
addrlen=sizeof(addrClient);
client_sock=ACCEPT(server_sock,(struct sockaddr*)&addrClient,&addrlen);
if(client_sock>0){
bzero(client_ip,16);
inet_ntop(AF_INET,&addrClient.sin_addr.s_addr,client_ip,16);//大端序转字符串
printf("client port %d,client ip %s\n",ntohs(addrClient.sin_port),client_ip);
first_response(client_sock,client_ip);
for(int i=0;i<1024;i++){
if(client_sock_array[i]==-1){
client_sock_array[i]=client_sock;
break;
}
}
}
//recv非阻塞读
business(MSG_DONTWAIT);//读取请求 处理请求 反馈响应
}
close(server_sock);
printf("server tis done\n");
return 0;
}
3.8.3 并发服务器(多进程)
#include <mysock.h>
int main(){
pid_t pid;
int server_sock,client_sock;
server_sock=net_initializer();
printf("process server version 1.0 acceiting.\n");
struct sockaddr_in client_addr;
socklen_t addrlen;
char client_ip[16];
while(1){
addrlen=sizeof(client_addr);
client_sock=ACCEPT(server_sock,(struct sockaddr*)&client_addr,&addrlen);
pid=fork();
if(pid>0){
//连接成功
bzero(client_ip,16);
inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,client_ip,16);
first_response(client_sock,client_ip);
}
else if(pid==0){
//子进程完成客户端处理
ssize_t recv_size;
char recv_buffer[1024];
bzero(recv_buffer,sizeof(recv_buffer));
while((recv_size=RECV(client_sock,recv_buffer,sizeof(recv_buffer),0))>=0){
if(recv_size>0){
printf("client:%s\n",recv_buffer);
if((strcmp(recv_buffer,"time\n")==0) || (strcmp(recv_buffer,"time")==0)){
//响应系统时间
time_t tp;
char tbuf[1024];
bzero(tbuf,sizeof(tbuf));
ctime_r(&tp,tbuf);
SEND(client_sock,tbuf,strlen(tbuf),0);
printf("server,response time success.\n");
}
else{
//数据处理,大小写转换 toupper()
int cnt=0;
while(cnt<recv_size){
recv_buffer[cnt]=toupper(recv_buffer[cnt]);
cnt++;
}
SEND(client_sock,recv_buffer,recv_size,0);
printf("server,response data sucess.\n");
}
}
else if(recv_size==0){//sock断开
printf("client exit, close sock %d\n",client_sock);
close(client_sock);
exit(0);
}
}
}
else{
perror("fork failed");
exit(0);
}
}
}
解决僵尸态
注意到,wait()是阻塞的,解决方法——进程分层
#include <mysock.h>
//回收的捕捉函数
void sig_wait(int n){
//循环非阻塞回收
pid_t zpid;
while(((zpid=waitpid(-1,NULL,WNOHANG)))>0){
printf("wait thread 0x%x sucess,zpid %d\n",(unsigned int)pthread_self(),zpid);
}
}
//线程函数
void* thread_wait(void* arg){
//设置分离态
pthread_detach(pthread_self());
//捕捉设定
struct sigaction act,oact;
act.sa_handler=sig_wait;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,&oact);
printf("wait thread set sigaction sucess\n");
//解除屏蔽
sigprocmask(SIG_SETMASK,&act.sa_mask,NULL);
while(1)
sleep(1);//等待信号并完成回收
}
void p_business(int client_sock){
ssize_t recv_size;
char recv_buffer[1024];
bzero(recv_buffer,sizeof(recv_buffer));
while((recv_size=RECV(client_sock,recv_buffer,sizeof(recv_buffer),0))>=0){
if(recv_size>0){
printf("client:%s\n",recv_buffer);
if((strcmp(recv_buffer,"time\n")==0) || (strcmp(recv_buffer,"time")==0)){
//响应系统时间
time_t tp;
char tbuf[1024];
bzero(tbuf,sizeof(tbuf));
ctime_r(&tp,tbuf);
SEND(client_sock,tbuf,strlen(tbuf),0);
printf("server,response time success.\n");
}
else{
//数据处理,大小写转换 toupper()
int cnt=0;
while(cnt<recv_size){
recv_buffer[cnt]=toupper(recv_buffer[cnt]);
cnt++;
}
SEND(client_sock,recv_buffer,recv_size,0);
printf("server,response data sucess.\n");
}
}
else if(recv_size==0){//sock断开
printf("client exit, close sock %d\n",client_sock);
close(client_sock);
exit(0);
}
}
}
int main(){
pid_t pid;
int server_sock,client_sock;
server_sock=net_initializer();
printf("process server version 1.0 acceiting.\n");
struct sockaddr_in client_addr;
socklen_t addrlen;
char client_ip[16];
//设置屏蔽
sigset_t set,oset;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_SETMASK,&set,&oset);
//创建回收线程
pthread_t tid;
pthread_create(&tid,NULL,thread_wait,NULL);
while(1){
addrlen=sizeof(client_addr);
client_sock=ACCEPT(server_sock,(struct sockaddr*)&client_addr,&addrlen);
pid=fork();
if(pid>0){
//连接成功
bzero(client_ip,16);
inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,client_ip,16);
first_response(client_sock,client_ip);
}
else if(pid==0){
//子进程完成客户端处理
p_business(client_sock);
}
else{
perror("fork failed");
exit(0);
}
}
}
3.8.4 并发服务器(多线程)
四、多路IO复用技术/多路IO转接
各个监听模型的主要区别:监听集合和函数不同
4.1 socket监听
问题:是否应该在连接(accept)或读取(recv)事件触发之前,执行相关函数,是否应该阻塞等待连接或数据,这种阻塞开销是否有意义和价值?
一个sock上有很多事件:
SOCK_RD读事件:socket缓冲区有数据待读、待处理,触发读事件
SOCK_WR写事件:socket缓冲中存在待写数据,触发写事件,用户通过send发送
SOCK_ER异常事件:socket出现异常,触发异常事件
上述处理方式开销大,效率低
socket流程:
1、socket监听某种事件
2、等待事件就绪
3、事件就绪
4、处理就绪
5、处理完毕
socket应该先监听,当socket就绪后再进行处理,accept和recv都是处理流程
前置为socket监听,后置为处理
4.2 select 经典IO复用模型
轮询模型
监听集合fd_set对应文件描述符表,每一位对应一个socket。位码0表示取消监听,1表示启动监听
select()阻塞轮询监听集合中所有的socket
#include <sys/select.h>
fd_set set;
select(maxfd+1/*当前监听的socket数*/,
&set/*读事件*/,
&set/*写事件*/,
&set/*异常事件*/,
NULL/*timeout,空表示阻塞工作*/);
//返回就绪的数量
select()传入监听集合,传出就绪集合,就绪的保留为1,未就绪的归0
以监听为主导,交替执行。大多数时为睡眠态,没有大量的非阻塞返回,实现单进程的1对多
select模型最大能监听1020个(1024 - server_sock - stidin - stdout - stderr = 1020)
FD_ZERO(&set); //初始化集合为0
FD_SET(int sock,&set); //对监听集合中某个sock对应位设置1,启动监听
FD_CLR(int sock,&set); //对监听集合中某个sock对应位设置0,取消监听
FD_ISSET(int sock,&oset); //返回值0或1,查看sock在oset中的位码
select函数支持定时阻塞,可以在最后一个参数timeout设置定时时长,到时立即返回
timeout==NULL 阻塞监听
timeout.s=0 timeout.us=0 非阻塞监听
timeout.s=1 timeout.us=1000 定时阻塞监听
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <ctype.h>
#include <string.h>
int main(){
int server_sock,client_sock;
int client_sock_array[1020];
for(int i=0;i<1020;i++)
client_sock_array[i]=-1;
struct sockaddr_in addrServer,addrClient;
bzero(&addrServer,sizeof(addrServer));
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(8080);
addrServer.sin_addr.s_addr=htonl(INADDR_ANY);
server_sock=socket(AF_INET,SOCK_STREAM,0);
bind(server_sock,(struct sockaddr*)&addrServer,sizeof(addrServer));
listen(server_sock,128);
socklen_t addrlen;
char cip[16];
int maxfd=server_sock;//记录最大sock
fd_listen_set set,ready_set;
FD_ZERO(&listen_set);
FD_SET(server_sock,&listen_set);//设置首个监听
char buffer[1024];
int recv_size;
printf("select running.\n");
int readynum;
while(1){
ready_set =listen_set;
if((readynum=select(maxfd+1,&ready_set,NULL,NULL,NULL))==-1){
perror("select failed");
exit(0);
}
//readynum=3;
printf("readynum:%d\n",readynum);
while(readynum){//循环处理多个就绪
//辨别就绪
if(FD_ISSET(server_sock,&ready_set)){
//server_sock 就绪处理流程
addrlen=sizeof(addrClient);
client_sock=accept(server_sock,(struct sockaddr*)&addrClient,&addrlen);
if(client_sock>0){
//插入数组
for(int i=0;i<1020;i++)
if(client_sock_array[i]==-1){
client_sock_array[i]=client_sock;
break;
}
//设置监听
FD_SET(client_sock,&listen_set);
//重置最大maxfd
if(maxfd<client_sock)
maxfd=client_sock;
//返回首次响应
bzero(cip,16);
inet_ntop(AF_INET,&addrClient.sin_addr.s_addr,cip,16);
bzero(buffer,sizeof(buffer));
sprintf(buffer,"hi %s connection sucess..\n",cip);
send(client_sock,buffer,strlen(buffer),0);
//当对应的sock事件处理完毕,server_sock(连接成功)client_sock(读取成功),应该将就绪集合中套接字这一位的位码置为0,因为如果ready_num > 1,不做该处理服务器会一直认为是客户端发送了TCP链接请求,从而导致错误处理
FD_CLR(server_sock,&ready_set);
}
}
else{
//查找就绪的client_sock并处理
for(int i=0;i<1020;i++)
if(client_sock_array[i]!=-1)
if(FD_ISSET(client_sock_array[i],&ready_set)){
//就绪处理流程,小写转大写
recv_size=recv(client_sock_array[i],buffer,sizeof(buffer),0);
if(recv_size>0){
int flag=0;
while(recv_size>flag){
buffer[flag]=toupper(buffer[flag]);
flag++;
}
send(client_sock_array[i],buffer,recv_size,0);
}
else if(recv_size==0){//断开流程
//关闭连接
close(client_sock_array[i]);
//取消监听
FD_CLR(client_sock_array[i],&listen_set);
//数组删除
client_sock_array[i]=-1;
}
FD_CLR(client_sock_array[i],&ready_set); //消去client_sock
break;
}
}
readynum--;
}
}
close(server_sock);
printf("server shutdown\n");
return 0;
}
select模型的优点:
1、兼容性较强,各个平台语言都有对select的支持,方便移植
2、方便实现,代码较轻量级
3、支持微妙级别的定时阻塞,如果监听模型对时间精度有要求,select支持
select模型的缺点:
1、监听数量少(1024不可修改),可以采用并发模型通过多个进程增加总监听数量(开销大)
2、select只返回就绪的数量,用户需要自行查找就绪的socket
3、select传入监听集合,就绪后改为就绪集合,导致监听集合不可重用,需要进行传入传出分离
4、select设置sock事件监听,是以集合为单位,批处理,无法对不同的sock设置不同的监听事件,可监听的事件种类较少
5、select监听采用轮询模式,随着监听数量怎大,导致IO处理性能线性下降
为了避免轮询问题持续占用cpu资源,锁死1024
cpu的大量占用影响服务器服务、数据库访问、网络数据吞吐量,磁盘数据的访问等等IO操作
6、随着select多轮使用,会产生大量的拷贝开销与挂载开销,这些开销都是没有意义的
每轮select都会将用户层监听集合拷贝到内核层的监听集合(会出现无意义的重复拷贝),然后将sock挂载到IO设备监听序列(也会出现无意义的重复挂载),轮询监听后返回就绪集合
4.3 poll监听模型
轮询模型
poll模型支持用户自定义的结构体数组作为监听集合
#include <sys/poll.h>
struct pollfd {
fd=sock; //设置对应的sock表示监听,-1表示取消监听
events=POLLIN|POLLOUT|POLLERR; //设置监听事件
revents=POLLIN; //传出就绪事件,当对应的sock就绪,系统设置
}node;
struct pollfd listen_list[10000]; //poll模型的监听集合
poll(struct pollfd* listen_list/*监听数组地址*/,
10000/*最大监听数*/,
-1/*timeout:-1阻塞,0非阻塞,>0定时阻塞*/); //监听函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#include <sys/poll.h>
int main(){
int server_sock,client_sock;
struct sockaddr_in addrServer,addrClient;
bzero(&addrServer,sizeof(addrServer));
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(8080);
addrServer.sin_addr.s_addr=htonl(INADDR_ANY);
server_sock=socket(AF_INET,SOCK_STREAM,0);
bind(server_sock,(struct sockaddr*)&addrServer,sizeof(addrServer));
listen(server_sock,128);
socklen_t addrlen;
char cip[16];
struct pollfd client_sock_array[1024];//监听数组
for(int i=0;i<1024;i++)//初始化监听集合
client_sock_array[i].fd=-1;
//设置首个监听
client_sock_array[0].fd=server_sock;
client_sock_array[0].events=POLLIN;
char buffer[1024];
int recv_size;
printf("poll running.\n");
int readynum;
while(1){
if((readynum=poll(client_sock_array,1024,-1))==-1){
perror("poll failed");
exit(0);
}
readynum=3;
while(readynum){//循环处理多个就绪
//辨别就绪
if(client_sock_array[0].revents==POLLIN){
//server_sock 就绪处理流程
addrlen=sizeof(addrClient);
client_sock=accept(server_sock,(struct sockaddr*)&addrClient,&addrlen);
if(client_sock>0){
//插入数组
for(int i=1;i<1024;i++)
if(client_sock_array[i].fd==-1){
//设置监听
client_sock_array[i].fd=client_sock;
client_sock_array[i].events=POLLIN;
break;
}
//返回首次响应
bzero(cip,16);
inet_ntop(AF_INET,&addrClient.sin_addr.s_addr,cip,16);
bzero(buffer,sizeof(buffer));
sprintf(buffer,"hi %s connection sucess..\n",cip);
send(client_sock,buffer,strlen(buffer),0);
client_sock_array[0].revents=0;
//消0,防止ready_num > 1时会多次误判断就绪套接字为服务器套接字
}
}
else{
//查找就绪的client_sock并处理
for(int i=1;i<1024;i++)
if(client_sock_array[i].fd!=-1)
if(client_sock_array[i].revents==POLLIN){
//就绪处理流程,小写转大写
recv_size=recv(client_sock_array[i].fd,buffer,sizeof(buffer),0);
if(recv_size>0){
int flag=0;
while(recv_size>flag){
buffer[flag]=toupper(buffer[flag]);
flag++;
}
send(client_sock_array[i].fd,buffer,recv_size,0);
}
else if(recv_size==0){//断开流程
//关闭连接
close(client_sock_array[i].fd);
//数组删除
client_sock_array[i].fd=-1;
}
client_sock_array[i].revents=0; //消0
break;
}
}
readynum--;
}
}
close(server_sock);
printf("server shutdown.\n");
return 0;
}
poll模型的优点:
1、events传入监听,revents传出就绪,无需用户分离
2、可以监听的sock事件比较丰富,可以对不同的sock设置不同的监听事件,比较灵活
poll模型的缺点:
1、受到轮询限制,poll监听数量有限
2、poll只返回就绪的数量,用户需要自行查找就绪的socket
3、poll监听采用轮询模式,随着监听数量怎大,导致IO处理性能线性下降
cpu的大量占用影响服务器服务、数据库访问、网络数据吞吐量,磁盘数据的访问等等IO操作
4、随着poll多轮使用,会产生大量的拷贝开销与挂载开销,这些开销都是没有意义的
5、兼容性比较差,linux操作系统都没有完全兼容
6、poll不支持微妙级别定时,支持毫秒
4.4 Epoll模型
异步回调模型
Epoll采用红黑树作为监听集合,也称为监听树
可以支持系统中最大数量的sock监听,且没有额外开销
struct epoll_event{
data.fd=sock; //设置监听sock
events=EPOLLIN|EPOLLOUT|EPOLLERR; //设置监听事件
ep_poll_callback(); //回调函数
}node;
//epoll监听到就绪,返回就绪数量,并且传出就绪的节点,方便用户处理
int epfd=epoll_create(max/*监听的最大值*/); //创建监听树
//返回指向监听树的描述符,后续访问修改树都通过epfd
epoll_ctl(epfd/*监听树描述符*/,
EPOLL_CTL_ADD|EPOLL_CTL_DEL|EPOLL_CTL|MOD/*添加|删除|修改*/,
int index_sock/*目标套接字*/,
struct epoll_event* node/*修改的节点*/);
//对监听树进行增删改,此函数是线程安全的,自带互斥锁,多线程使用没用问题
int readynum=epoll_wait(epfd/*监听树描述符*/,
struct epoll_event* ready_array/*就绪数组*/,
max/*就绪最大数*/,
-1/*工作模式timeout:-1阻塞,0非阻塞,>0定时阻塞*/);
//监听函数,就绪数组需要用户自行定义
修改树节点只允许修改监听的事件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#include <sys/epoll.h>
#define EPOLL_MAX 10000
int main(){
int server_sock,client_sock;
struct sockaddr_in addrServer,addrClient;
bzero(&addrServer,sizeof(addrServer));
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(8080);
addrServer.sin_addr.s_addr=htonl(INADDR_ANY);
server_sock=socket(AF_INET,SOCK_STREAM,0);
bind(server_sock,(struct sockaddr*)&addrServer,sizeof(addrServer));
listen(server_sock,128);
socklen_t addrlen;
char cip[16];
int epfd;//监听树描述符
struct epoll_event node;
struct epoll_event ready_array[EPOLL_MAX];
//创建监听树
epfd=epoll_create(EPOLL_MAX);
node.data.fd=server_sock;
node.events=EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,server_sock,&node);//设置监听
char buffer[1024];
int recv_size;
printf("epoll running.\n");
int readynum;
while(1){
if((readynum=epoll_wait(epfd,ready_array,EPOLL_MAX,-1))==-1){
perror("epoll failed");
exit(0);
}
int i=0;
while(readynum){//循环处理多个就绪
//辨别就绪
if(ready_array[i].data.fd==server_sock){
//server_sock 就绪处理流程
addrlen=sizeof(addrClient);
client_sock=accept(server_sock,(struct sockaddr*)&addrClient,&addrlen);
if(client_sock>0){
node.data.fd=client_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,client_sock,&node);
//返回首次响应
bzero(cip,16);
inet_ntop(AF_INET,&addrClient.sin_addr.s_addr,cip,16);
bzero(buffer,sizeof(buffer));
sprintf(buffer,"hi %s connection sucess..\n",cip);
send(client_sock,buffer,strlen(buffer),0);
}
}
else{
//就绪处理流程,小写转大写
recv_size=recv(ready_array[i].data.fd,buffer,sizeof(buffer),0);
if(recv_size>0){
int flag=0;
while(recv_size>flag){
buffer[flag]=toupper(buffer[flag]);
flag++;
}
send(ready_array[i].data.fd,buffer,recv_size,0);
}
else if(recv_size==0){//断开流程
//关闭连接
close(ready_array[i].data.fd);
//删除节点
epoll_ctl(epfd,EPOLL_CTL_DEL,ready_array[i].data.fd,NULL);//删除监听
}
}
i++;
readynum--;
}
}
close(server_sock);
printf("server shutdown.\n");
return 0;
}
关于轮询: 当一个sock就绪时,网卡得到信息后没有把信息压入IO设备监听序列。而是通过回调函数
等待队列:控制挂起和唤醒
每一个node都绑定自己的就绪项,就绪项可以注册回调函数
Epoll的监听树在内核层创建,用户拷贝以节点为单位,避免了重复的拷贝开销与内核开销,保证每个node只拷贝、绑定一次
流程:当网卡接收到数据,socket缓冲区信息变更,触发回调函数,激活就绪项。就绪项将node加入就绪链表,唤醒等待队列,检查就绪链表是否为空,将就绪链表内容拷贝到就绪数组
一个进程有一个文件描述符表,默认情况下一个进程可以使用1024个文件描述符(包括stdin stdout stderr)
ulimit -a //查看系统限制
电脑能使用的文件描述符最大值
cat /proc/sys/fs/file-max //查看系统可用描述符最大数
工作模式:
水平触发模式 EPOLLT
边缘触发模式 EPOLLET
EPOLLONESHOT
IO复用技术只有监听能力的差别,与处理无关
Epoll监听能力强
处理能力是一样的,处理能力要看进程池和线程池
4.5 epoll+thread_pool版本服务器
① 协议的设计、协议封装、协议传输、协议的解析
② 服务器socket监听
③ 请求的并发处理
生产者与消费者关系(Producer,Customer)
生产者负责监听sock处理任务的分发
多进程多线程模型相比线程池的缺点:
1、进程线程随客户端持续(声明周期),如果客户端频繁的连接断开,导致大量的进程线程频繁创建销毁,无意义的开销过大
2、如果每个客户端绑定一个进程或线程,并发连接数量却取决于处理单位数量,太少了
3、处理单元(线程)无法重用
4、没有管理措施,例如根据业务调整处理单位的数量,缺乏线程管理
5、当任务抵达,才进行处理单元的创建,影响任务处理效率
线程池的处理原则(线程池的重用性):
1、预创建原则,线程池中保留备用线程,当任务抵达,直接处理
2、线程的重用性高、不允许线程与某一个特定任务绑定
3、线程管理机制,记录线程数量与状态,根据业务动态的扩容与缩减线程
4、线程池中采用生产者消费者模型,进行任务传递与管理
管理者manager
1、扩容条件?
1.任务数量>=闲线程数量
2. 随时保留一部分活性线程备用,忙线程占存活线程的70%
3. 扩容不允许超出最大线程阈值
1) citds应该如何利用?
2、缩减条件?
1. 闲线程是忙线程的倍数,启动缩减
2. 缩减后不允许小于阈值
1)如何缩减?
线程数量?
IO密集型 和 CPU密集型
大约为1/8 * 可执行的最大线程(381)
//epoll_thread_server.h
#include<mysock.h>
#include<sys/epoll.h>
//红黑树
int epfd;
//任务类型
typedef struct{
void* (*business)(void*);//任务地址
void* arg;//任务参数
}task_t;
//线程池类型
typedef struct{
int thread_shutdown;//线程池开关
int thread_max;//线程最大值
int thread_min;//线程最小值
int thread_alive;//有效数量
int thread_busy;//繁忙线程
int thread_exitcode;//缩减码
task_t* list;//任务队列
int list_front;//队列头索引
int list_rear;//队列尾索引
int list_max;//队列最大值
int list_cur;//当前任务数
pthread_mutex_t lock;//互斥锁
pthread_cond_t not_full;//生产者条件变量
pthread_cond_t not_empty;//消费者条件变量
pthread_t* ctids;//存储消费者tid
pthread_t mid;//存储管理者id
}pool_t;
void* data_business(void*);//数据处理业务(client_sock就绪)
void* accept_business(void*);//连接业务(server_sock就绪)
pool_t* thread_pool_create(int tmax,int tmin,int list_max);//线程池创建初始化,对线程进行预创建(消费者管理者)
int producer_add_task(pool_t*,task_t);//生产者调用此模块,添加一次任务
int epoll_initializer(int sock);//创建初始化epoll模型,并且设置第一次监听server_scok
int epoll_listen(int sock,pool_t*);//监听模块,负责监听sock是否就绪并按规则添加任务
void* customer_thread(void*);//消费者线程工作,等待于任务队列,持续获取任务并执行
void* manager_thread(void*);//管理者线程工作,等待于阈值,根据条件判断扩容缩减 管理线程数量
int if_thread_alive(pthread_t tid);//线程失效返回0,存活返回1
//main.c
#include<epoll_thread_server.h>
int main(){
//网络初始化
int sock;
sock=net_initializer();
epoll_initializer(sock);
pool_t* pt;
pt=thread_pool_create(100,10,1000);
//启动监听
epoll_listen(sock,pt);
return 0;
}
//epoll_initializer.c
#include <epoll_thread_server.h>
int epoll_initializer(int server_sock){
//epoll创建初始化
if((epfd=epoll_create(100000))==-1){
perror("epoll_inintializer create epoll failed");
return -1;
}
//初始化监听
struct epoll_event node;
node.data.fd=server_sock;
node.events=EPOLLIN|EPOLLONESHOT/*|EPOLLET*/;//边缘
if((epoll_ctl(epfd,EPOLL_CTL_ADD,server_sock,&node))==-1){
perror("epoll_initializer listen server_sock failed");
return -1;
}
printf("epoll_init success\n");
return 0;
}
//thread_pool_create.c
#include <epoll_thread_server.h>
pool_t* thread_pool_create(int tmax,int tmin,int list_max){
pool_t* pool=NULL;
if((pool=(pool_t*)malloc(sizeof(pool_t)))==NULL){
perror("thread_poll_create pool malloc failed");
return NULL;
}
pool->thread_shutdown=1;
pool->thread_max=tmax;
pool->thread_min=tmin;
pool->thread_alive=0;
pool->thread_busy=0;
pool->thread_exitcode=0;
if((pool->list=(task_t*)malloc(sizeof(task_t)*list_max))==NULL){
perror("thread_poll_create list malloc failed");
return NULL;
}
pool->list_front=0;
pool->list_rear=0;
pool->list_cur=0;
pool->list_max=list_max;
if(pthread_mutex_init(&pool->lock,NULL)!=0 || pthread_cond_init(&pool->not_empty,NULL)!=0 || pthread_cond_init(&pool->not_full,NULL)!=0){
printf("thread_pool_create init lock or cond error\n");
return NULL;
}
if((pool->ctids=(pthread_t*)malloc(sizeof(pthread_t)*tmax))==NULL){
perror("thread_pool_create init ctids failed");
return NULL;
}
//初始化ctids
bzero(pool->ctids,sizeof(pthread_t)*tmax);
//预创建线程
int err;
for(int i=0;i<tmin;i++){
if((err=pthread_create(&pool->ctids[i],NULL,customer_thread,(void*)pool))>0){
printf("thread_pool_create,create_customer failed,%s\n",strerror(err));
return NULL;
}
//pthread_mutex_lock(&pool->lock);
++pool->thread_alive;
//pthread_mutex_unlock(&pool->lock);
}
//创建管理者
if((err=pthread_create(&pool->mid,NULL,manager_thread,(void*)pool))>0){
printf("thread_pool_create,create manager failed,%s\n",strerror(err));
return NULL;
}
return pool;
}
//customer_thread.c
#include <epoll_thread_server.h>
void* customer_thread(void* arg){
pool_t* p=(pool_t*)arg;
task_t temp;
pthread_detach(pthread_self());
printf("customer 0x%x waiting jobs\n",(unsigned int)pthread_self());
while(p->thread_shutdown){
//消费线程,持续获取任务
pthread_mutex_lock(&p->lock);
while(p->list_cur==0){
pthread_cond_wait(&p->not_empty,&p->lock);
if(!p->thread_shutdown){
pthread_mutex_unlock(&p->lock);
printf("customer 0x%x,shutdown its 0,closeint\n",(unsigned int)pthread_self());
pthread_exit(NULL);
}
//缩减
if(p->thread_exitcode){
--p->thread_alive;
--p->thread_exitcode;
pthread_mutex_unlock(&p->lock);
printf("customer 0x%x exit\n",(unsigned int)pthread_self());
pthread_exit(NULL);
}
}
temp.business=p->list[p->list_rear].business;
temp.arg=p->list[p->list_rear].arg;
--(p->list_cur);
p->list_rear=(p->list_rear+1)%p->list_max;
++(p->thread_busy);
pthread_mutex_unlock(&p->lock);
pthread_cond_signal(&p->not_full);
//执行任务
temp.business(temp.arg);
pthread_mutex_lock(&p->lock);
--(p->thread_busy);
pthread_mutex_unlock(&p->lock);
}
printf("customer 0x%x,shutdown its 0,closeint\n",(unsigned int)pthread_self());
return NULL;
}
//manager_thread.c
#include <epoll_thread_server.h>
void* manager_thread(void* arg){
pool_t* p=(pool_t*)arg;
//持续扫描阈值,完成扩容缩减
int alive,busy,cur;
int flag;//遍历下标 ctids
int add;//计数器(创建线程)
int err;
pthread_detach(pthread_self());
printf("manager thread 0x%x Running\n",(unsigned int)pthread_self());
while(p->thread_shutdown){
//取值
pthread_mutex_lock(&p->lock);
alive=p->thread_alive;
busy=p->thread_busy;
cur=p->list_cur;
pthread_mutex_unlock(&p->lock);
//判断扩容
printf("server status:alive %d busy %d idel %d cur %d B/A %.2f%% A/M %.2f%%\n",alive,busy,alive-busy,cur,(double)busy/alive*100,(double)alive/p->thread_max*100);
if((alive-busy<=cur || (double)busy/alive*100>=70) && alive+p->thread_min<=p->thread_max){
for(flag=0,add=0;flag<p->thread_max && add<p->thread_min;flag++){
if(p->ctids[flag]==0 || !if_thread_alive(p->ctids[flag])){
if((err=pthread_create(&p->ctids[flag],NULL,customer_thread,(void*)p))>0){
printf("manager_thread,create cusotomer failed:%s\n",strerror(err));
}
++add;
pthread_mutex_lock(&p->lock);
++(p->thread_alive);
pthread_mutex_unlock(&p->lock);
}
}
}
//缩减
if(busy*2<=alive-busy && alive-p->thread_min>=p->thread_min){
p->thread_exitcode=p->thread_min;
for(int i=0;i<p->thread_min;i++){
pthread_cond_signal(&p->not_empty);
}
}
sleep(1);
}
printf("shutdown close,manager exiting..\n");
pthread_exit(NULL);
}
//epoll_listen.c
#include <epoll_thread_server.h>
int epoll_listen(int server_sock,pool_t* p){
//监听接口,生产者进行持续的sock事件监听并完成任务分支
int readynum;
int flag;
struct epoll_event readylist[100000];
task_t temp_bs;
//循环监听
printf("epoll_listen\n");
while(p->thread_shutdown){
//边缘触发,忽略为处理 避免epoll_wait异常返回
//水平模式+EPOLLONESHOT or 边缘模式
//水平模式异常添加任务的问题,将就绪sock投递后,消费者为及时处理导致生产者多次调用epoll_wait,函数立即返回,多次添加任务(异常)
if((readynum=epoll_wait(epfd,readylist,100000,-1))==-1){
perror("epoll_listen,epoll_wait call failed");
return -1;
}
//循环判断处理就绪
flag=0;
while(readynum){
if(readylist[flag].data.fd==server_sock){
//向任务容器中添加一次连接业务
temp_bs.business=accept_business;
temp_bs.arg=(void*)&server_sock;
producer_add_task(p,temp_bs);
printf("add accept_business %p sucess\n",accept_business);
}
else{
//添加数据处理业务
temp_bs.business=data_business;
temp_bs.arg=(void*)&readylist[flag].data.fd;
producer_add_task(p,temp_bs);
printf("add data_business %p sucess\n",data_business);
}
--readynum;
++flag;
}
}
printf("epoll_listen exit\n");
return 0;
}
//accept_business.c
#include<epoll_thread_server.h>
void* accept_business(void* arg){
int server_sock,client_sock;
struct sockaddr_in client_addr;
socklen_t addrlen;
server_sock=*(int*)arg;
//处理就绪sock
addrlen=sizeof(client_addr);
client_sock=ACCEPT(server_sock,(struct sockaddr*)&client_addr,&addrlen);
//设置监听
struct epoll_event node;
node.data.fd=client_sock;
node.events=EPOLLIN|EPOLLONESHOT/*|EPOLLET*/;
epoll_ctl(epfd,EPOLL_CTL_ADD,client_sock,&node);
//首次响应
char cip[16];
bzero(cip,16);
inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,cip,16);
first_response(client_sock,cip);
//epolloneshot
struct epoll_event nodes;
nodes.data.fd=server_sock;
nodes.events=EPOLLIN|EPOLLONESHOT;
epoll_ctl(epfd,EPOLL_CTL_MOD,server_sock,&nodes);
return NULL;
}
//data_business.c
#include <epoll_thread_server.h>
void* data_business(void* arg){
int client_sock=*(int*)arg;
char buffer[1024];
bzero(buffer,sizeof(buffer));
int len;
time_t tp;
char time_buf[1024];
bzero(time_buf,sizeof(time_buf));
//读取请求数据,非阻塞读
while((len=RECV(client_sock,buffer,sizeof(buffer),MSG_DONTWAIT))>0){
//判断客户端请求
if(strcmp(buffer,"time\n")==0){
tp=time(NULL);
ctime_r(&tp,time_buf);
SEND(client_sock,time_buf,strlen(time_buf),0);
}
else{
SEND(client_sock,"PLease try again\n",17,0);
}
//epolloneshot
struct epoll_event node;
node.data.fd=client_sock;
node.events=EPOLLIN|EPOLLONESHOT;
epoll_ctl(epfd,EPOLL_CTL_MOD,client_sock,&node);
}
//协议解析器
return NULL;
}
4.6 EPOLL工作模式
4.6.1 水平触发模式(EPOLLLT)
步骤:
1. 设置监听
2. 监听等待
3. 触发就绪
4. 处理就绪
5. 处理完毕
使用者默认使用水平触发模式监听sock,如果sock事件就绪,必须处理就绪数据,否则不允许进行下一轮监听(负责人模式)
优点:可以督促使用者尽快完整的处理sock数据
缺点:开销大
epoll_wait阻塞表示阻塞监听,如果立即返回,返回值为未处理完毕的sock就绪数量。
4.6.2 边缘触发模式(EPOLLET)
采用边缘模式监听sock,当sock就绪epoll触发一次就绪通知,但是无论用户是否处理就绪都与epoll无关,epoll边缘模式随时可以进行下一轮监听,即使上一轮的没有处理
优点:开销小
缺点:epoll无法保证及时处理就绪,导致丢失
可以针对不同的sock设置不同的监听模式
4.7 epoll安全(EPOLLONSHOT)
避免多个线程处理相同的sock,采用epollonshot方案
EPOLL_CTL_MOD
onshot?
自定义实现keep-alive心跳?(时间轮[较复杂])
五、 服务器性能指标
5.1 TPS QPS
TPS:服务器每秒钟处理事务的能力
1. 客户端发送请求,开始计时
2. 服务器处理
3. 客户端接收响应,计时完毕
QPS:每秒查询效率
服务器的数据库查询访问能力
一个T中包含三个Q?
一个事务中要查三次
多线程处理数据库,服务器性能应当用QPS评价
5.2 吞吐量
大多数时间服务器处理,IO处理(数据库访问以及磁盘或sock处理)较多
一个请求对CPU的消耗比较高,外部接口执行以及IO关联操作都会受影响——吞吐能力低
有一个较高能力的吞吐,都要减少业务处理时的cpu消耗
5.3 负载测试和压力测试
负载测试(load test):性能测试,在数据超出标准数量时,观察服务器负担。
目的:在平均处理量外加入处理量,查看服务器承受性,如何在硬件不变的前提下,优化、提高处理能力
压力测试(strest test):服务器极限测试,在系统资源比较低的情况下,持续执行程序,目的是找到系统宕机原因,失效定位
目的:压力测试持续进行,直到服务器宕机,查看宕机原因以及最大承压数据,进行硬件扩展
5.4 计算并发数
QPS每秒事务数量=事务数量/秒
平均响应时间:服务器采集样本,发送请求,处理请求,反馈响应的时长
并发数量:QPS*平均响应时间
5.5 服务器处理能力
使用服务器测试工具,测试服务器处理能力,返回并发数量