Linux服务器模型。
Linux服务器模型
分类
- 循环服务器——同一时刻只能处理一个客户端请求
- 并发服务器——同一时刻可以处理多个客户端的请求
UDP服务器通常采用循环服务器模型,TCP服务器通常采用并发服务器模型
TCP循环服务器模型
UDP循环服务器模型
UDP并发服务器模型
客户端请求在一个数据报中完成
客户端请求在多个数据报中完成
TCP并发服务器模型
一个子进程对应一个客户端
- 模型简单
- 每个子进程完全独立
- 可以同时处理多个客户端
- 创建子进程开销较大,适合处理时间长的客户请求,例如FTP文件传输
- 当客户端数量大、客户请求处理时间短时会大大降低效率,如HTTP服务器
延迟创建子进程
- 循环与并发混合的服务器模型
- 处理时间短的客户请求以循环方式完成
- 处理时间长的客户请求以并发方式完成
- 减少创建子进程的开销
- 建立进程表项的开销
- 复制数据段和堆栈段的开销
预创建子进程(数量固定)
实现思路:服务器建立侦听socket,并创建子进程。所有子进程调用accept,无连接时将睡眠。有连接到来时所有子进程被唤醒。某一个子进程接受连接后,其他进程继续睡眠。
- 响应速度快,节省创建子进程时间
- 需要预先估计创建进程的数量
- 预创建子进程数量少时将导致客户端等待
- 预创建子进程数量多时将浪费系统资源
一个预先创建子进程的例子。
//
// Created by prime on 17-6-21.
//
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <cstdio>
#include <signal.h>
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
#define PIDNUMB 3
static void handle_connect(int s_s)
{
int s_c;
struct sockaddr_in client;
socklen_t len= sizeof(client);
while (1)
{
s_c=accept(s_s,(struct sockaddr*)&client,&len);
char buff[BUFFLEN];
int n=0;
memset(buff,0, sizeof(buff));
n=recv(s_c,buff,BUFFLEN,0);
printf("receive %d bytes",n);
memset(buff,0, sizeof(buff));
sprintf(buff,"I receieved!");
send(s_c,buff,strlen(buff),0);
close(s_c);
}
}
void sig_int(int num)
{
exit(1);
}
int main()
{
int s_s;
signal(SIGINT,sig_int);
struct sockaddr_in server;
s_s=socket(AF_INET,SOCK_STREAM,0);
bzero(&server, sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(SERVER_PORT);
server.sin_addr.s_addr=htonl(INADDR_ANY);
bind(s_s,(struct sockaddr *)&server, sizeof(server));
listen(s_s,BACKLOG);
pid_t pid[PIDNUMB];
for (int i=0;i<PIDNUMB;++i)
{
pid[i]=fork();
if(pid[i]==0)
{
handle_connect(s_s);
exit(0);
}
}
while(1);//不让父进程退出,不然所有子进程都被init收养了(其实也没关系,依旧可以运行)。
close(s_s);
return 0;
}
预创建子进程(动态)
实现思路1:
- 服务器建立socket,并创建一定数量子进程
- 服务器父进程维护所有子进程的状态表,父进程和子进程通过管道通信
- 子进程接受连接时给父进程发1,关闭连接时发0。
- 父进程收到1时检查空闲子进程数目是否小于下限,小于下限则创建新的子进程。
- 父进程收到0时检查空闲子进程数目是否大于下限,大于下限则终止一些子进程。
实现思路:
- 服务器建立侦听socket,并创建固定数量子进程
- 服务器父进程维护所有子进程的状态表,父进程和子进程通过UNIX域socket通信
- 父进程在有空闲子进程时接收连接,将已连接socket描述符传递给空闲子进程
- 子进程处理客户请求,结束后通知父进程,父进程修改子进程状态
和上面的区别在于,这里由父进程完成连接,然后把描述符传给子进程。
多路复用IO
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
该函数可以先对需要操作的描述符进行查询,看是否可读、可写或有错误。
各个参数解释如下:
nfds
:一个整型变量,比文件描述符集合中的最大的文件描述符大1。
readfds
:可读的描述符集合。函数监视描述符集合中是否有描述符可读,函数返回时将清除集合中不可读的描述符。
writefds
:同上,只不过是写的描述符集合。
exceptfds
:监视文件集中的任何文件是否发生错误。
timeout
:设置最长等待时间,如果为NULL就工作在阻塞模式。
返回值大于零表示三个描述符集合中至少一个不为空,返回0表示超时,-1表示错误。
服务器实现思路:
- 设置读和写描述符集合,其中读描述符集合包括侦听socket
- 调用select测试socket描述符是否就绪
- 侦听socket就绪则接收新连接
- 其他socket就绪则执行读写操作
要求将所有socket描述符设置为非阻塞方式。
特点:
- 优点
- 只需要一个进程来处理所有客户机请求
- 没有创建和管理进程的开销,系统资源消耗少
- 没有进程间通信
- 缺点
- 服务器必须依次处理所有的请求,编程较复杂
- 服务器循环处理所有就绪客户端,可能会造成延时较长
适合于请求多但请求内容少的服务器。