单进程、单线程编程:这样的服务器程序同一时刻只能处理一个客户端连接,可以和多个客户端串行交互 int main() { int listenfd = socket(); int res = bind(); res = listen(); while(1) { int c = accept(); while(1) { recv()/send(); } } }
使用多进程(多线程)给一个客户端连接分配一个单独的进程(线程),使得服务器能够同时处理多个客户端请求
多进程编程思想: 父进程:监听端口,接收客户端连接, 获得连接后创建子进程 子进程:与接收的客户端连接完成通讯过程 注意:父子进程之间共享文件描述符,父进程不需要将接收链接的文件描述符传递给子进程 父进程接收链接创建子进程后,需要将连接的文件描述符关闭 如果父进程不关闭文件描述符,则后续创建的子进程会将所有的文件描述符都继承下来, 如果父进程不关闭文件描述符,则后续链接的文件描述符将不断增大,链接的客户端的数量就会受到一个进程最多打开的
int CreateSocket(); // 创建监听socket,并且绑定地址信息启动监听 int GetClientLink(int listenfd); // 获取一个客户端连接 int DealClientLink(int c, struct sockaddr_in cli); //处理客户端连接 int main() { signal(SIGCHLD, SIG_IGN); //处理僵死进程 int listenfd = CreateSocket(); while(1) { int c = GetClientLink(listenfd); pid_t pid = fork(); assert(pid != -1); if(pid == 0) { DealClientLink(c, cli); exit(0); // 处理完客户端连接后子进程结束 } else { close(c); } } }
多线程编程思想: 主线程:监听端口,接收客户端连接,并且获得连接后创建函数线程 函数线程:与链接的客户端完成交互式通讯 注意: 创建函数线程必须将获得的文件描述符以值传递的方式传递给函数线程 主线程不能关闭文件描述符,只能函数线程与客户端交互完成后关闭 int CreateSocket(); //创建监听socket,并且绑定地址信息启动监听 int GetClientLink(int listenfd); //获取一个客户端连接 int DealClientLink(void *arg); //线程函数, 处理客户端连接 int main() { int listenfd = CreateSocket(); while(1) { int c = GetClientLink(listenfd); pthread_t id; pthread_create(&id, NULL, DealClientLink, (void*)c); } }
多进程编程和多线程编程的区别: 创建多进程会消耗大量的系统资源,多线程创建线程资源消耗相对较小 多进程创建的子进程在很短的时间内结束,系统的负担会加重 线程之间数据共享更容易,线程结束释放资源比较少
多进程或者多线程存在的问题: 1、服务器启动后,如果有客户端连接,则为连接创建进程或线程 2、如果客户端与服务器交互不频繁,则创建的进程或线程经常阻塞在recv操作 3、创建的进程或线程只为这一个客户端服务,服务完成后,进程或线程则会销毁 4、客户端连接上后,服务器先创建进程或线程,这样对于客户端的响应变慢 5、如果服务器创建太多的进程或线程,服务器端系统在进程或线程切换方面花费的时间变多,处理数据的时间随之变少
池:初始时,申请比刚开始要使用的资源大很多的资源空间,接下来直接从这个空间中获取资源,这个空间就是池。
池的优势: 1、一个服务器程序需要的进程或线程数量是有限、固定的 2、服务器不需要频繁的创建或者销毁线程,只在启动时创建,结束时销毁 3、客户端连接后,直接触发条件,唤醒一个阻塞的进程或线程,响应速度快捷 4、创建的进程或线程串行的为不同客户端提供服务,更加有效的利用进程、线程资源
池未解决的问题: 1、同一时刻,只能处理有限个客户端连接 2、分配给客户端一个进程或线程后,可能由于交互不频繁,而阻塞在recv操作处
进程池&线程池的原理 在服务器程序启动时,创建固定个数的进程或线程,创建的进程或线程阻塞在某一条件上,当父进程或主线程触发条件后,唤醒一个阻塞在条件上进程或线程,开始执行,执行完成后,接着阻塞在条件上,当服务器程序结束时,再释放池中的进程或线程
进程池:在程序启动时,创建多个进程,将子进程维护在进程池,进程池中的进程必须阻塞在获取到客户端文件描述符之前,主进程负责接收客户连接,并将获取到的客户连接文件描述符传递给进程池中的子进程,必须借助于进程间通讯,不能仅仅传递值,传递的是文件描述符所指向的结构
进程间传递文件描述符: Unix本地socket int socketpair(int domain, int type, int protocol, int fd[2]); domain :AF_UNIX sendmsg函数:size_t sendmsg(int sockfd, struct msghdr* msg, int flags); recvmsg函数:size_t recvmsg(int sockfd, struct msghdr *msg, int flags);
msghdr结构: struct msghdr { void *msg_name; //socket地址 socklen_t msg_namelen; //socket地址长度 struct iovec *msg_iov; //分散的内存块 int msg_iovlen; //分散的内存款的长度 void *msg_control; //指向辅助数据的起始位置 socklen_t msg_controllen; //辅助数据的大小 int msg_flags; //复制函数中的flag参数,在调试过程中更新 } struct iovec { void *iov_base; //内存起始地址 size_t iov_len; //这块内存的长度 }
线程池:在服务器允许初始时,创建n个线程,将这n个线程用池管理起来,当有用户链接时,从池中选取一个线程为其服务,客户端关闭链接后,服务器将线程放回池中。主线程需要将文件描述符传递给函数线程,函数线程启动起来必须阻塞在获取文件描述符之前,信号量来控制主线程向函数线程通知获取文件描述符事件,主线程在数组中插入数据,以及函数线程获取数组中的数据都必须是互斥的,保持原子操作。
线程池实现: 主线程执行,先创建3条线程,主线程等待客户连接,三条函数线程因为信号量的P操作阻塞运行,主线程接收到客户连接后,通过信号量的V操作通知一个函数线程和客户端通讯,主线程怎么将连接的文件描述符传递给函数线程,全局数组作为等待函数线程处理的文件描述符的等待队列 1、线程池中的线程阻塞在某一条件上 -- 信号量的P操作 2、主线程给函数线程传递接收的客户端连接 -- 全局空间 3、主线程唤醒一个线程池中阻塞的线程 -- 信号量的V操作
|