如何解决服务器多并发问题?
1、每个进程处理一个客户端
2、每个线程处理一个客户端
3、io复用模型
首先我们想要了解select和epoll的区别,首先应该了解下什么是io复用模型概念。
io复用模型概念?
只需要一个进程就够了。之所以能够同时处理多个客户端的请求,原因是可以查询哪个客户端准备好了,对于准备好的客户端(例如客户端已经发了信息过来,本服务器用read读取数据的时候不会阻塞;另外,客户端已经关闭了连接,那么本服务read的时候,返回0,也不会阻塞),则和它进行通信,而未准备好的,就暂时先不理会。
select函数:
select可以实现这个轮询功能。
select函数原型:
int select(intnfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, structtimeval *timeout);
nfds:最大的那个fd整数加1。
fd_set:fd的集合。三个集合,分别是读、写、异常的集合,当置为空时表示用不到。
可以将关心的fd放到对应的集合里去,然后交给内核去轮询。
fd集合的操作:
void FD_CLR(int fd,fd_set *set);//将集合里对应的fd清掉
vid FD_SET(int fd,fd_set *set);//将fd加到集合里
int FD_IS SET(int fd,fd_set *set);//是否准备好了,好了就返回非零值
void FD_ZERO(fd_set*set);//集合全部清空
timeout:代表超时时间,是struct timeval类型的结构体,参数有:long tv_sec(秒) 和long tv_usec(微秒),为空表示永不超时。
select代码实现:
//server.c
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <mqueue.h> //消息队列
#include <signal.h>
#include <semaphore.h>
#include <sys/socket.h> //套接字接口
#include <arpa/inet.h> //网络地址的转换
#include <time.h>
#include <pthread.h>
int main(void)
{
int listenfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in hostaddr;
hostaddr.sin_family=AF_INET;
hostaddr.sin_addr.s_addr=INADDR_ANY;
hostaddr.sin_port=htons(3017);
if(bind(listenfd,(struct sockaddr *)&hostaddr,sizeof(hostaddr))<0)
{
perror("bind");
}
if(listen(listenfd,10)<0)
{
perror("listen");
}
int fd1=accept(listenfd,NULL,NULL);
int fd2=accept(listenfd,NULL,NULL);
//创建一个fd集合
fd_set rdfdset;
char buffer[1024];
while(1)
{
//记得要清空这个集合
FD_ZERO(&rdfdset);
//将感兴趣的fd加进去
FD_SET(fd1,&rdfdset);
FD_SET(fd2,&rdfdset);
//算出来哪个fd最大
int maxfd=fd1>fd2?fd1:fd2;
//将轮询工作委托给内核,让它找出哪个fd准备好了.
select(maxfd+1,&rdfdset,NULL,NULL,NULL);
//如果fd1准备好了
if(FD_ISSET(fd1,&rdfdset))
{
bzero(buffer,sizeof(buffer));
read(fd1,buffer,sizeof(buffer));
write(fd2,buffer,strlen(buffer));
}
//如果fd2准备好了
if(FD_ISSET(fd2,&rdfdset))
{
bzero(buffer,sizeof(buffer));
read(fd2,buffer,sizeof(buffer));
write(fd1,buffer,strlen(buffer));
}
}
}
//client.c
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <mqueue.h> //消息队列
#include <signal.h>
#include <semaphore.h>
#include <sys/socket.h> //套接字接口
#include <arpa/inet.h> //网络地址的转换
#include <time.h>
#include <pthread.h>
#include "header.h"
int sockfd;
//从键盘读数据
void *readfromkeyboard(void *arg)
{
while(1)
{
/*4, 读取信息*/
char buffer[1024]={0};
printf("\n>>");
//从键盘读取
fgets(buffer,sizeof(buffer),stdin);
//发给服务器
if(write(sockfd,buffer,sizeof(buffer))<0)
{
perror("write");
}
}
}
//从服务器读数据
void *readfromserver(void *arg)
{
char buffer[1024]={0};
while(1)
{
bzero(buffer,sizeof(buffer));
//从服务读回来
if(read(sockfd,buffer,sizeof(buffer))<0)
{
perror("read");
}
//将信息输出到屏幕上
write(STDOUT_FILENO,buffer,strlen(buffer));
}
}
//argv[1]是ip地址,argv[2]是端口
int main(int argc, char *argv[])
{
if(argc<3)
{
printf("usage:filename ipaddress port");
return -1;
}
/*1, 创建套接字*/
sockfd=socket(AF_INET,SOCK_STREAM,0);
/*2, 设置网络地址*/
struct sockaddr_in sockin;
sockin.sin_family=AF_INET;
inet_pton(AF_INET,argv[1],&sockin.sin_addr);
sockin.sin_port=htons(atoi(argv[2]));
/*3, 连接服务器
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
*/
if(connect(sockfd,(struct sockaddr *)&sockin, sizeof(sockin))<0)
{
perror("connet");
exit(EXIT_FAILURE);
}
pthread_t thread1,thread2;
pthread_create(&thread1,NULL,readfromkeyboard,NULL);
pthread_create(&thread2,NULL,readfromserver,NULL);
while(1)
{
}
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
close(sockfd);
}
epoll函数:
现今用的多的是epoll。
和select相比优点在于:
select是对加进去的所有fd进行轮询,返回之后也要对整个fd进行一次轮询,才能找到准备好的fd。
epoll采用事件触发的方式,当某个fd准备好后会触发事件,这样减少了内核的轮询。同时,epoll返回的是那些准备好的fd,避免程序员进行全部的轮询
另外,select的fd数上限一般是1024.但是epoll没有上限。
epoll函数原型:
1、int epoll_create(int size);
创建一个epoll接口,返回一个epoll描述符,后面要用到。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用来从epoll接口中添加fd或删除、修改fd。
(1)epfd是接口描述符,
(2)op是添加、修改、删除三者,fd是要操作的对象,event是监视的事件。结构如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
3、int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
(1)events是输出型的参数,返回的是准备好的描述符,
(2)maxevents是事件数量的上限
(3)timeout是超时,以毫秒为单位,-1代表用不超时,0代表不等待立即返回。
epoll的水平触发和边沿触发?
水平触发:epoll的事件默认情况下是lt水平触发。例如,只要客户端的数据仍然未读完,那么事件就会一直发生。告诉服务器,请将数据读出来。
边沿触发:加上了events那里加了EPOLLET这个选项后,变成边沿触发。也就是数据可读,则只触发一次事件,服务器必须一直读,直到把数据读完。
epoll代码实现:
//server.c
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <mqueue.h> //消息队列
#include <signal.h>
#include <semaphore.h>
#include <sys/socket.h> //套接字接口
#include <arpa/inet.h> //网络地址的转换
#include <time.h>
#include <pthread.h>
#include "header.h"
#include "sys/epoll.h"
int main(void)
{
int listenfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in hostaddr;
hostaddr.sin_family=AF_INET;
hostaddr.sin_addr.s_addr=INADDR_ANY;
hostaddr.sin_port=htons(3017);
if(bind(listenfd,(struct sockaddr *)&hostaddr,sizeof(hostaddr))<0)
{
perror("bind");
}
if(listen(listenfd,10)<0)
{
perror("listen");
}
int fd_array[4];
int i;
for(i=0;i<4;i++)
fd_array[i]=accept(listenfd,NULL,NULL);
int epollfd=epoll_create(10000);
//创建事件结构体,用来输入到函数中的
struct epoll_event ev;
//创建事件结构体数组,用来从函数中带出来数据的
struct epoll_event ev_array[10000];
for(i=0;i<4;i++)
{
//可读事件
ev.events=EPOLLIN|EPOLLET;
//事件对应的fd
ev.data.fd=fd_array[i];
//将fd加入epoll中
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd_array[i],&ev);
}
char buffer[1024];
while(1)
{
//查询有多少个fd准备好了
int nr=epoll_wait(epollfd,ev_array,1000,-1);
for(i=0;i<nr;i++)
{
int fd=ev_array[i].data.fd;
bzero(buffer,sizeof(buffer));
read(fd,buffer,sizeof(buffer));
write(fd,buffer,strlen(buffer));
}
}
}
//client.c
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <mqueue.h> //消息队列
#include <signal.h>
#include <semaphore.h>
#include <sys/socket.h> //套接字接口
#include <arpa/inet.h> //网络地址的转换
#include <time.h>
#include <pthread.h>
#include "header.h"
int sockfd;
//从键盘读数据
void *readfromkeyboard(void *arg)
{
while(1)
{
/*4, 读取信息*/
char buffer[1024]={0};
printf("\n>>");
//从键盘读取
fgets(buffer,sizeof(buffer),stdin);
//发给服务器
if(write(sockfd,buffer,sizeof(buffer))<0)
{
perror("write");
}
}
}
//从服务器读数据
void *readfromserver(void *arg)
{
char buffer[1024]={0};
while(1)
{
bzero(buffer,sizeof(buffer));
//从服务读回来
if(read(sockfd,buffer,sizeof(buffer))<0)
{
perror("read");
}
//将信息输出到屏幕上
write(STDOUT_FILENO,buffer,strlen(buffer));
}
}
//argv[1]是ip地址,argv[2]是端口
int main(int argc, char *argv[])
{
if(argc<3)
{
printf("usage:filename ipaddress port");
return -1;
}
/*1, 创建套接字*/
sockfd=socket(AF_INET,SOCK_STREAM,0);
/*2, 设置网络地址*/
struct sockaddr_in sockin;
sockin.sin_family=AF_INET;
inet_pton(AF_INET,argv[1],&sockin.sin_addr);
sockin.sin_port=htons(atoi(argv[2]));
/*3, 连接服务器
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
*/
if(connect(sockfd,(struct sockaddr *)&sockin, sizeof(sockin))<0)
{
perror("connet");
exit(EXIT_FAILURE);
}
pthread_t thread1,thread2;
pthread_create(&thread1,NULL,readfromkeyboard,NULL);
pthread_create(&thread2,NULL,readfromserver,NULL);
while(1)
{
}
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
close(sockfd);
}