服务器模型
在使用socket进行网络编程时,首先要选择一个合适的服务器模型是很重要的。在网络程序里,通常都是一个服务器服务多个客户机,为了处理多个客户机的请求,服务器端的程序有不同的处理方式。
目前最常用的服务器模型分为两大类,循环服务器模型和并发服务器模型
循环服务器模型
UDP循环服务器模型
UDP循环服务器每次获取一个客户端的请求,处理后将结果返回给客户端。
//UDP循环服务器模型伪代码 main() { listenfd = socket(...);//创建监听套接字 bind(...);//将地址信息和listenfd绑定 while(1) {
recvfrom(...);//从客户端读取
process(...);//处理 sendto(...);//发送回客户端 }
close(listenfd); }
TCP循环服务器模型
同样是每次从等待客户端取出一个,对其进行处理然后将结果返回客户端
//TCP循环服务器模型伪代码 main() { listenfd = socket(...);//创建监听套接字 bind(...);//将地址信息和listenfd绑定 listen(..);//监听 while(1) { accept(...);//接受客户端连接请求 while(1) { read/recv(...);//接受 procecc(...);//处理 write/send(..);//返回 }
close(sockfd); }
close(listenfd); }
并发服务器模型
为了弥补循环服务器一次只能服务于一个客户端的缺陷,人们又设计了并发服务器模型。
多进程并发服务器模型
多进程并发服务器模型。为了避免一个客户端独占服务器,在客户端建立连接时会为每个客户端创建一个子进程。这样一来多个客户端同时响应,就会对操作系统的效率有所影响,但不可否认满足了同时服务多个客户端的需求。具体做法是在监听到客户端连接请求时,首先fork一个子进程服务于客户端,父进程继续监听新的客户端连接。实际可用进程池解决使用时才创建进程的资源开销问题。
//多进程并发服务器模型伪代码 main() { listenfd = socket(...);//创建监听套接字 //装填服务器地址信息 bind(...);//将监听套接字listenfd与地址信息绑定 listen(listenfd, 10);//开始监听,并设置监听数量 while(1) { //有客户端连接请求时,获取到客户端sockfd,没有请求时阻塞 sockfd = accept(listenfd, ..., ...); pid = fork();//创建子进程,服务于客户端 if (pid == 0) { while(1) { close(listenfd);//首先在子进程关闭掉监听套接字,防止子进程对其他客户端请求进行监听 recv(...); //处理; send(...); } close(sockfd);//处理结束后关闭套接字 exit(0);//结束子进程 } close(sockfd); } close(listenfd) }
多线程并发服务器模型
多线程服务器与多进程服务器模型类似。相较于多进程并发服务器,使用多线程技术完成并发服务器对系统开销要小得多。使用多线程并发服务器模型时,要注意对临界资源(能被多个线程访问,但同时只应被一个线程访问)进行保护。实际使用时,可以采用线程池技术避免每次客户端连接请求到来时创建子线程时,不必要的系统开销。
//多线程并发服务器模型伪代码 //服务程序 void *serv_routine(void *arg) { sockfd = (int )arg; while(1) { read(sockfd, buf, sizeof buf); //处理 write(sockfd, buf, ret); } } main() { //初始化线程池 thread_pool_init(); //创建监听套接字 listenfd = socket(...); //填充地址信息 bind(...);//将地址信息与监听套接字绑定 listen(listenfd, 10);//开始监听 while(1) { //接受客户端连接请求,获取器sockfd sockfd = accept(...); //向进程池添加客户端服务程序 thread_pool_addtask(..., serv_routine, (void*)sockfd); } close(sockfd); close(listenfd); //销毁线程池 thread_pool_destroy(...); }
I/O多路服用并发服务器
I/O多路复用可以解决多线程和多进程资源限制的问题。此模型实际上是将UDP循环模型用在了TCP上面。但是它也存在问题,由于它也是一次处理客户端的请求,可能会导致有些客户端等待时间过长。
//I/O多路复用——select模型 int main() { //创建监听套接字描述符 listenfd = socket(AF_INET, SOCK_STREAM, 0); //装填地址 //将监听套接字描述符与装填好的地址绑定 bind(listenfd, (struct sockaddr*)&myaddr, len)); //开始监听 listen(listenfd, 10); fd_set readfds; //设置监听读文件描述符集合 fd_set writefds; //设置监听写文件描述符集合 FD_ZERO(&readfds); //清空这些集合 FD_ZERO(&writefds); FD_SET(listenfd, &readfds); //将listenfd添加到读文件描述符集合中 fd_set temprfds = readfds; //定义这个两个temp集合是为了在每次有可读写文件描述符时,都可以在处理完成后继续监听之前加入的文件描述符 fd_set tempwfds = writefds; int maxfd = listenfd; #define BUFSIZE 100 #define MAXNFD 1024 int nready; char buf[MAXNFD][BUFSIZE] = {0}; while(1) { temprfds = readfds; tempwfds = writefds; //获取可可读或可写的文件描述符,放到集合中, select返回可读、写的文件描述符个数 nready = select(maxfd+1, &temprfds, &tempwfds, NULL, NULL); //有客户端访问时监听套接字描述符可读,可以通过FD_ISSET来判断具体是哪个文件描述符 if(FD_ISSET(listenfd, &temprfds)) { //接收客户端连接请求、并获取其sockfd int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //将获取到的套接字描述符加入到读操作监听集合中 FD_SET(sockfd, &readfds); maxfd = maxfd>sockfd?maxfd:sockfd; if(--nready==0) continue; } int fd = 0; //遍历文件描述符集合,对就绪的文件秒速符进行处理 for(;fd<=maxfd; fd++) { if(fd == listenfd) continue; //读操作就绪的套接字描述符 if(FD_ISSET(fd, &temprfds)) { int ret = read(fd, buf[fd], sizeof buf[0]); if(0 == ret) { close(fd); //处理完成后,将其冲监听集合中移除 FD_CLR(fd, &readfds); if(maxfd==fd) --maxfd; continue; } //以为要把处理后的结果发送回客户端,因此将套接字描述符添加到写操作监听集合中 FD_SET(fd, &writefds); } //写操作就绪的套接字描述符 if(FD_ISSET(fd, &tempwfds)) { int ret = write(fd, buf[fd], sizeof buf[0]); printf("ret %d: %d\n", fd, ret); FD_CLR(fd, &writefds); } } } close(listenfd); }