服务器的基本模型,不知道这么扯对不对,其实就是linux下服务器和客户端的通信模式。也就是面对客户端如狼似渴的访问需求,服务器该如何快速的响应。
我总结下来有这么几种:
- 单进程提供服务
- 多进程提供服务
- 多进程池服务(prefork)
- io复用提供服务(select,poll)
- epoll(其实也是一种IO复用)
- 多线程提供服务
- 多线程池提供服务
- 信号驱动提供服务
一一按照自己的想法写出来,还想和大家后续一起探讨下非阻塞io,异步io,共享内存,进程间通信等服务器常用技术。废话不说,直接开始。
单进程提供服务
这种模式只存在于我们的学习中,一个客户端请求由服务器响应后,这个客户完全占有了服务器,这回如何再来一个新客户,他必须等待服务器伺候完现有的这个客户。伺候不完,服务器是不会为新客户提供服务的,这个就是完全占有。
服务器和客户端的行为:
server : bind -> listen -> accept one request -> do request, send response -> close accept fd -> accept next request ....
client : connect -> send request -> wait response -> recieve response -> close connect
服务器的行为,一般是创建一个socket,然后把相关的端口,ip使用bind捆绑到socket上,通过listen监听该端口,当有客户请求到达时,处理请求,给出回应。处理完一个客户请求后,关闭该请求,在处理下一个。
客户端的行为,通过connect连接到server上去,发送请求,等待回应,收到答复,关闭连接。
这种方式的弊端,显而易见,在一个客户请求未处理完毕时,另一个客户必须等待,直到被accept。在web服务这种高并发请求中,这种服务器模型显然不行。
有一个参数这里我一直觉得很诡异,就是listen的第二个参数 backlog ,按照说明 这个参数是在建立三次握手中的连接数和完成网络连接但尚未被accept的连接数的和的最大值,但貌似在各个内核中实现又有所差异。在自己的本中实现了下,貌似超过了还会connect ok 。
贴上我实验用的server代码,为了节约代码量,所有的错误处理均被忽略
/*
* auther : [email protected]
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_CONNECTION 2
int main(int argc, char** argv){
int fd;
time_t ticks;
int port = 99999;
fd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int size = sizeof(struct sockaddr);
bind(fd,(struct sockaddr*)&addr,size);
listen(fd,MAX_CONNECTION);
struct sockaddr_in client_addr;
while (1){
memset(&client_addr, 0, sizeof (client_addr));
char buf[1024];
memset(&buf,0,sizeof(buf));
int client_fd = accept(fd, (struct sockaddr *) &client_addr, &size);
ticks = time (0);
snprintf(buf, sizeof(buf), "%s", ctime(&ticks));
write(client_fd, buf, sizeof(buf));
sleep(3600);
close(client_fd);
}
close(fd);
return 0;
}
可以用 telnet 127.0.0.1 99999 测试,可以看到但一个客户端使用telnet请求时,下一个必须等待。
多进程提供服务
既然一个进程提供服务已经应付不过来,不如多生几个儿子来处理请求,作为老子只管对儿子进行监控就好。多进程应该就是这么个道理。每个请求都fork一个子进程来处理。
这个模式相对于第一种的优点就是 可以对多个请求进行处理,响应及时。
缺点就是每次请求都要生成一个新进程,处理完毕,还要销毁。成本有些高,在并发请求较高的时候,会把cpu耗尽。毕竟进程这个东西还是稍微有些重的东西。
修改程序很简单 ,在 accept后面插入代码即可,插入的代码为
int client_fd = accept(fd, (struct sockaddr *) &client_addr, &size);
pid = fork();
if(pid > 0){
close(client_fd);
continue;
}
close(fd);
ticks = time (0);
还缺少一步,就是防止子进程成为僵尸进程,要对信号SIGCHLD进行处理,使其在接到该信号后调用waitpid函数 回收子进程。
多进程池的服务
每次请求都生成新进程其实必要性并不大,大部分并发服务器处理的每秒并发量一般最多就在几百左右,因此一般几个或者十几个进程循环提供服务就可以hold住,为了减少每次请求建立新进程的成本,我们的前辈又发明了多进程池(prefork)的模式,预先生成若干进程来处理请求。
见过两种多进程池的实现,一种是父进程只管listen,子进程对每个请求accept。另一种是父进程负责accept,然后把accept后得到的confd句柄传递给子进程。
这里我先说下第一种实现,关于第二种的实现我会在select模式中来说,原因就是第二种模式的实现配合select的效果更佳。
第一种的测试代码为:(这里没有添加错误的处理程序)
/*
* auther : [email protected]
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_CONNECTION 2
int main(int argc, char** argv){
int fd = -1;
time_t ticks;
pid_t pid;
pid_t pids[10];
int port = 99999;
fd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int size = sizeof(struct sockaddr);
bind(fd,(struct sockaddr*)&addr,size);
listen(fd,MAX_CONNECTION);
int i;
for(i = 0; i< 10;i++){
pid = fork();
if(pid > 0){
continue;
}
pids[i] = pid;
struct sockaddr_in client_addr;
while (1){
memset(&client_addr, 0, sizeof (client_addr));
char buf[1024];
memset(&buf,0,sizeof(buf));
printf("wait\n");
int client_fd = accept(fd, (struct sockaddr *) &client_addr, &size);
close(fd);
ticks = time (0);
snprintf(buf, sizeof(buf), "%s", ctime(&ticks));
write(client_fd, buf, sizeof(buf));
sleep(3600);
close(client_fd);
}
}
close(fd);
for(i = 0; i< 10;i++){
int status;
if(pids[i] < 0){
continue;
}
waitpid(pids[i],&status,0);
}
return 0;
}
这里在原来有一个比较纠结的地方,就是accept,原来的linux版本会有惊群现象,也就是当一个请求到来时,多个子进程同时在accept阻塞中被唤醒,导致资源消耗过大,这个比较纠结的问题在现在的较新的linux内核中已经解决,另外一个纠结的问题时select的冲突,这个咱们在select中再续。先说到这里把。