1.迭代服务器模型
1.1 迭代服务器是处理多个请求时一种最简单直接的思路,即使用while循环,它不具有并发能力,即必须一个一个的处理客户的请求。
1.2 程序示例。
#include "def.h"
int listenfd_init(); //返回一个处于监听状态的套接字描述符
void do_service(int peerfd); // 处理客户端的请求
int main(int argc, const char *argv[])
{
if(signal(SIGPIPE, SIG_IGN) == SIG_ERR)
ERR_EXIT("signal");
int listenfd = listenfd_init(); // 生成一个套接字并使其处于监听状态
struct sockaddr_in peeraddr;
int len = sizeof(peeraddr);
int peerfd;
while(1){
if((peerfd = accept(listenfd, (struct sockaddr*)&peeraddr, &len)) == -1) //接受一个TCP连接请求
ERR_EXIT("accpet");
do_service(peerfd); // 处理请求
close(peerfd);
}
close(listenfd);
return 0;
}
int listenfd_init(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(9999);
if(listenfd == -1)
ERR_EXIT("socket");
if(bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1)
ERR_EXIT("bind");
if(listen(listenfd, 10) == -1)
ERR_EXIT("listen");
return listenfd;
}
void do_service(int peerfd){
char recvbuf[1024] = {0};
int ret;
while(1){
ret = readline(peerfd, recvbuf, 1024);
if(ret <= 0)
break;
printf("recv data : %s", recvbuf);
writen(peerfd, recvbuf, strlen(recvbuf));
}
}
2.多进程服务器模型
2.1 使用多进程编写并发服务器的一般步骤:
a)while(1)循环,每次accept一个连接都fork一个进程;
b)在子进程中要close(listenfd),最后要exit(这里子进程一定要退出,否则会继续执行);
c)父进程要关闭accept返回的peerfd。
2.2 一些要点:
a)父进程要关闭peerfd,这主要是因为close根据引用计数关闭fd,如果父进程不这样做,那么所有通过accept创建的fd都不会被真正释放,这将造成资源耗尽;
b)父进程不可以直接采用waitpid来回收子进程,这样会使得server变为一个迭代服务器,而不具备并发的能力,,必须采用信号这种异步的处理手段;
c)使用信号处理必须注意,使用while而不是if,尽可能多处理僵尸进程,这是为了防止信号的额阻塞和丢失问题,waitpid要使用WNOHANG悬选项。
d)子进程要关闭listenfd;
e)子进程执行do_service之后务必exit(EXIT_SUCCESS)。
2.3 程序示例。
#include "def.h"
/*
* 服务器端使用 多进程 模型
*
*/
int listenfd_init(); //返回一个处于监听状态的套接字描述符
void do_service(int peerfd); // 处理客户端的请求
void handler(int signum); //处理sigchld信号 回收子进程资源
int main(int argc, const char *argv[])
{
if(signal(SIGPIPE, SIG_IGN) == SIG_ERR)
ERR_EXIT("signal");
if(signal(SIGCHLD, handler) == SIG_ERR)
ERR_EXIT("signal");
int listenfd = listenfd_init(); // 生成一个套接字并使其处于监听状态
//多进程模型
while(1){
struct sockaddr_in peeraddr;
memset(&peeraddr, 0,sizeof(peeraddr));
int len = sizeof(peeraddr);
int peerfd;
// 接受一个TCP连接请求
if((peerfd = accept(listenfd, (struct sockaddr*)&peeraddr, &len)) == -1)
ERR_EXIT("accpet");
pid_t pid;
if((pid = fork()) < 0)
ERR_EXIT("fork");
else if(pid == 0){
close(listenfd); //子进程要关闭listenfd
do_service(peerfd); // 处理请求
exit(EXIT_SUCCESS);
}
close(peerfd); //这里必须关闭peerfd,否则导致资源耗尽
}
close(listenfd);
return 0;
}
int listenfd_init(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(9999);
if(listenfd == -1)
ERR_EXIT("socket");
if(bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1)
ERR_EXIT("bind");
if(listen(listenfd, 10) == -1)
ERR_EXIT("listen");
return listenfd;
}
void do_service(int peerfd){
char recvbuf[1024] = {0};
int ret;
while(1){
ret = readline(peerfd, recvbuf, 1024);
if(ret <= 0){
close(peerfd);
exit(EXIT_SUCCESS);
}
printf("recv data : %s", recvbuf);
writen(peerfd, recvbuf, strlen(recvbuf));
}
close(peerfd);
}
void handler(int signum){
while(waitpid(-1, 0, WNOHANG) > 0) ; //while尽可能多回收子进程
}
3.多线程服务器模型
3.1 使用多线程编写并发服务器的一般步骤: while(1)循环内,每次accpet一个连接,都创建一个线程。
3.2 一些要点:
a)往线程中传fd,最好使用动态分配内存(有些机器int和void*不兼容),在线程中务必释放内存,防止内存泄露;
b)线程务必使用detach函数,将自己设置为分离状态,自动回收资源。
3.3 程序示例。
#include "def.h"
/*
* 服务器端使用 多线程 模型
*
*/
int listenfd_init(); //返回一个处于监听状态的套接字描述符
void do_service(int peerfd); // 处理客户端的请求
void handler(int signum); //处理sigchld信号 回收子进程资源
void *thread_func(void *arg); //线程处理函数
int main(int argc, const char *argv[])
{
if(signal(SIGPIPE, SIG_IGN) == SIG_ERR)
ERR_EXIT("signal");
if(signal(SIGCHLD, handler) == SIG_ERR)
ERR_EXIT("signal");
int listenfd = listenfd_init(); // 生成一个套接字并使其处于监听状态
//多线程模型
while(1){
struct sockaddr_in peeraddr;
memset(&peeraddr, 0,sizeof(peeraddr));
int len = sizeof(peeraddr);
int peerfd;
if((peerfd = accept(listenfd, (struct sockaddr*)&peeraddr, &len)) == -1)
ERR_EXIT("accpet");
int *pfd = (int *)malloc(sizeof(int));
*pfd = peerfd;
pthread_t tid;
pthread_create(&tid, NULL, thread_func, pfd);
}
close(listenfd);
return 0;
}
int listenfd_init(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(9999);
if(listenfd == -1)
ERR_EXIT("socket");
if(bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1)
ERR_EXIT("bind");
if(listen(listenfd, 10) == -1)
ERR_EXIT("listen");
return listenfd;
}
void *thread_func(void *arg){ //线程处理函数
pthread_detach(pthread_self()); //将当前线程设置为分离状态 自己主动回收资源
int *pfd = (int *)arg;
int peerfd = *pfd;
free(pfd);
do_service(peerfd);
}
void do_service(int peerfd){
char recvbuf[1024] = {0};
int ret;
while(1){
ret = readline(peerfd, recvbuf, 1024);
if(ret <= 0){
close(peerfd);
exit(EXIT_SUCCESS);
}
printf("recv data : %s", recvbuf);
writen(peerfd, recvbuf, strlen(recvbuf));
}
close(peerfd);
}
void handler(int signum){
while(waitpid(-1, 0, WNOHANG) > 0) ; //while尽可能多回收子进程
}
4.select服务器模型
4.1 使用 select 编写并发服务器的一般步骤:
a)初始化参数,这里包括readset(描述符集合,用作readyset的备份),readyset用作select函数的传出参数,clients数组(存储所有已连接的fd,这里的数组长度FD_SETSIZE是一个系统定义的宏),maxi(数组的最大下标,便于提高效率),maxfd(要监听的fd的最大值,用作select的第一个参数),nready(用作select的返回值);
b)进入while(1)循环,readyset = readset;
c)执行select函数,并检查其返回值;
d)检查listenfd是否在准备好的集合中,此时这里需要accpet一个连接,将返回的fd加入到clients数组中和readset结合中,并且还要更新maxi和maxfd;
e)遍历clients数组,依次查看每个fd是否在准备好的集合readyset中,这里要注意,当某一个客户关闭连接时,本地需要close这个fd,更新clients数组,将该fd从readset集合中移除。
4.2 程序示例。
void do_select(int listenfd){
//初始化参数
fd_set readset, readyset;//readset 用作备份 存储要监听的所有fd,readyset用作返回
FD_ZERO(&readset);
FD_ZERO(&readyset);
FD_SET(listenfd, &readset);
//定义数组,存储所有已连接的客户fd 初始化为-1
int clients[FD_SETSIZE]; //FD_SETSIZE 是一个系统的宏定义
int i;
for(i = 0; i < FD_SETSIZE; i++){
clients[i] = -1;
}
int maxi = -1; //数组的最大下标 //?
int nready; //select的返回值
int maxfd = listenfd;//监听的最大fd
while(1){
//执行select,检查返回值
readyset = readset;
nready = select(maxfd + 1, &readyset, NULL, NULL, NULL);
if(nready == -1){
if(errno == EINTR)
continue;
ERR_EXIT("select");
}
//检查listenfd 是否在准备好的集合中
if(FD_ISSET(listenfd, &readyset)){
int peerfd = accept(listenfd, NULL, NULL);
if(peerfd == -1)
ERR_EXIT("accept");
//为新的fd在clients数组中找一个空位,并更新maxi
int i;
for(i = 0; i < FD_SETSIZE; i++){
if(clients[i] == -1){
clients[i] = peerfd;
if(i > maxi)
maxi = i;
break;
}
}
if(i == FD_SETSIZE){//找不到一个空闲fd位置给新的fd
fprintf(stderr, "too many clients\n");
close(peerfd);
continue;
}
//将新的fd添加到集合中, 更新maxfd
FD_SET(peerfd, &readset);
if(peerfd > maxfd)
maxfd = peerfd;
if(--nready <= 0)//执行下一次select
continue;
}//if listenfd
//依次检查每个普通fd是否在准备好的集合中
int i;
char recvbuf[1024] = {0};
for(i = 0; i <= maxi; i++){
if(FD_ISSET(clients[i], &readyset)){
int ret = readline(clients[i], recvbuf, 1024);
if(ret == -1)
ERR_EXIT("readline");
else if(ret == 0){ //对端已关闭tcp连接
//从监听集合中移除fd ,并关闭连接
printf("client closed\n");
close(clients[i]);
FD_CLR(clients[i], &readset);
clients[i] = -1;
break;
}
printf("recv data: %s", recvbuf);
writen(clients[i], recvbuf, strlen(recvbuf));
if(--nready <= 0)
break; //不在轮询后面的fd
}
}//for
}//while(1)
}
5.poll服务器模型
5.1 使用poll编写并发服务器的一般步骤:
a)创建struct pollfd events[]数组存放用来监听的fd,并且初始化为-1;
b)将listenfd加入到该数组中;
c)进入while(1)循环,执行poll系统调用,并检查返回值;
d)检查listenfd是否可读,若可读,则需要accept一个连接,从events数组中找一个空闲的位置,将返回的fd加入到数组中;
e)检查其他的fd,这里要注意fd关闭的问题(如果客户关闭了连接,本地需要关闭连接,并将数组元素置为-1)。
5.2 程序示例。
void do_poll(int listenfd){
struct pollfd clients[2048]; //存储所有要监听的fd
//初始化数组
int i;
for(i = 0; i < 2048; i++){
clients[i].fd = -1;
}
clients[0].fd = listenfd;
clients[0].events = POLLIN;
int maxi = 0; // clients数组的最大下标
int nready; // 接收poll 的返回值
//执行 poll 函数
while(1){
nready = poll(clients, maxi+1, -1);
if(nready == -1){
if(errno == EINTR)
continue;
ERR_EXIT("poll");
}
// 1.处理 listenfd
if(clients[0].revents & POLLIN){
int peerfd = accept(listenfd, NULL, NULL);
if(peerfd == -1)
ERR_EXIT("accept");
// 为新的fd 找一个空位置
int i;
for(i = 0; i < 2048; i++){
if(clients[i].fd == -1){
clients[i].fd = peerfd;
clients[i].events = POLLIN;
if(i > maxi) //更新maxi
maxi = i;
break;
}
}
if(i == 2048){
fprintf(stderr, "too many clients\n");
close(peerfd);
continue;
}
if(--nready <= 0)
continue;
}
// 2.依次轮询已连接的fd
int i;
for(i = 0; i <= maxi; i++){
if(clients[i].fd == -1) //当前位置没有client 连接
continue;
char recvbuf[1024] = {0};
if(clients[i].revents & POLLIN){
//处理请求 回显
int ret = readline(clients[i].fd, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("readline");
else if(ret == 0){
close(clients[i].fd);
clients[i].fd = -1;
printf("client closed\n");
continue;
}
printf("recv data: %s", recvbuf);
writen(clients[i].fd, recvbuf, strlen(recvbuf));
if(--nready <= 0)
break;
}
}
}
}
6.epoll服务器模型
6.1 使用epoll编写并发服务器的一般步骤:
a)创建epoll句柄(使用epoll_create函数),把listenfd加入到epollfd中(这里使用epoll_ctl函数);
b)创建一个数组,用于接收epoll_wait的返回结果;
c)进入while(1)循环,执行epoll_wait,并检查返回值;
d)判断数组中的每个fd,如果是listenfd,那么需要accept,如果是普通fd,需要echo服务。
6.2 程序示例。
void do_epoll(int listenfd){
//创建epoll句柄
int epollfd = epoll_create(2048);
if(epollfd == -1)
ERR_EXIT("epoll_create");
//把listenfd加入到epollfd中
struct epoll_event ev;
ev.data.fd = listenfd;
ev.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev) == -1)
ERR_EXIT("epoll_ctl");
//创建数组 用作返回
struct epoll_event events[2048];
int nready;
while(1){
//执行wait 检查返回值
nready = epoll_wait(epollfd, events, 2048, -1);
if(nready == -1){
if(errno == EINTR)
continue;
ERR_EXIT("epoll_wait");
}
//遍历events数组
int i;
for(i = 0; i < nready; i++){
if(events[i].data.fd == listenfd){ //listenfd
int peerfd = accept(listenfd, NULL, NULL);
if(peerfd == -1)
ERR_EXIT("accept");
//新的fd加入到句柄中
struct epoll_event ev;
ev.data.fd = peerfd;
ev.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, peerfd, &ev) == -1)
ERR_EXIT("epoll_ctl");
}
else{//普通fd 回显
int fd = events[i].data.fd;
char recvbuf[1024] = {0};
int ret = readline(fd, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("readline");
else if(ret == 0){ //客户端关闭连接
//移除fd
printf("client closed\n");
struct epoll_event ev;
ev.data.fd = fd;
if(epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &ev) == -1)
ERR_EXIT("epoll_ctl");
//关闭该连接
close(fd);//注意这里要先移除在关闭 否则会出错
continue;
}
printf("recv data: %s", recvbuf);
writen(fd, recvbuf, strlen(recvbuf));
}
}
}
//关闭epoll句柄
close(epollfd);
}
7.总结
7.1 select、poll、epoll之间的区别:
a)select 文件描述符的大小受到限制,而且FD_SETSIZE受内核参数的限制,如果需要更改,需要重新编译内核;
b)poll没有文件描述符大小的限制;
c)select和poll共同的缺点是:内部的数组不同在内核空间和用空间中相互拷贝。而epoll采用共享内存,避免了这一开销;
d)select和poll内部都是采用“轮询”机制,随着fd的增多,select和poll的效率随之下降,而epoll只关心已经准备好的fd,不存在这个缺点。
7.2 write是把数据从用户空间拷贝至内核空间,而read是把数据从内核空间拷贝至用户空间。因此,当用read,write读写文件时,效率很低,有一个sendfile函数直接将数据从在内核之间拷贝,不经过用户空间。这叫做零拷贝技术。