那一天,人们终于回想起了被BUG支配的恐惧
Toggle navigation
Home
AboutMe
Links
Archives
Tags
服务器三种并发方式
2017-05-28 13:37:15
661
0
0
weibo-007
# 服务器三种并发方式 ## 迭代服务器 在学习TCP/IP协议中,如果不考虑并发,我们所写出来的服务器往往叫做迭代服务器。**迭代服务器会依次处理客户端的连接,只要当前连接的任务没有完成,服务器的进程就会一直被占用,直到任务完成后,服务器关闭这个socket,释放连接** 看一个迭代服务器的例子,服务器的功能是接收客户端的输入,服务器接受之后原样返回客户端,即回射服务器。迭代服务器C源码 ``` #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> #define MAXLEN 1024 int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); struct sockaddr_in server_sockaddr; server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(1024); server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr)); listen(server_sockfd, 20); for( ; ; ){ int clnt_sock = accept(server_sockfd, NULL, NULL); printf("add client\n"); char buf[1024] = {'\0'}; if(read(clnt_sock, buf, 1024) != 0){ printf("read end \n"); fputs(buf, stdout); write(clnt_sock, buf, 1024); } close(clnt_sock); } } ``` 服务器代码执行流程: 1. 在服务器端设置监听套接字:server_sockfd,监听端口为1024 2. 进入死循环调用accept阻塞,直到有客户端请求时候,accept返回客户端套接字:clnt_sock 3. 然后对通过read读取客户端套接字的输入,**如果没有接收到客户端没有输入,一直阻塞,程序不会往下执行** 4. 如果客户端套接字由输入读取了,read阻塞被释放,继续执行连接下一个客户端 可以看到,这样的服务器回阻塞在read函数中,也就是说它只能处理一个客户端,当处理完这个客户端的时候,继续接收并处理下一个客户端。这种情况下用到的IO模型是阻塞IO模型,也是最低效的一种IO模型。 ![image](http://note.youdao.com/yws/api/personal/file/WEB670791c0e109171ed81cd1d03145acec?method=download&shareKey=6b613b332d08522514a6423287525e5b) ## 为什么设计服务器要并发 通过上面的例子可以看出,迭代服务器的问题:服务进程是阻塞的,也就是必须逐一处理客户端请求,效率低下。在生产环境中,我们不可能使用迭代服务器,因为这种模式的服务器特别浪费资源,大部分时间资源花费在IO等待上。所以我们必须设计并发服务器,目前有三种方式可以选择: 1. 基于进程并发编程 2. 基于IO多路复用并发编程 3. 基于线程的并发编程 ## 基于进程并发编程 构造并发程序最简单的方法就是用进程。使用大家都很熟悉的函数如fork,exec和waitpid。所以,使用多进程实现并发方式可以是:**在父进程中接受客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务**。值得注意的是,基于进程的并发编程并使用的依然是阻塞IO模型,只是这种阻塞出现在子进程中。基于进程并发服务器流程 1. 服务器阻塞于accept调用且来自客户的连接请求到达时的客户端与服务器的状态 ![image](http://note.youdao.com/yws/api/personal/file/WEBb7219fa222a86827ce4b75df6bf606ca?method=download&shareKey=48c49fdea5bb2fbb25cb9996d675efd9) 2. 从accept返回后,连接已经在内核中注册,并且新的套接口connfd被创建。这是一个已建起连接的套接口,可以进行数据的读写。 ![image](http://note.youdao.com/yws/api/personal/file/WEB7c4d9e28990fdf6f96ad73199102ae2f?method=download&shareKey=141a216d06a156f66fbdf1b278119aca) 3.并发服务器在调用fork之后,listenfd和connfd这两个描述字在父进程以及子进程之间共享(实际为其中一份为copy) ![image](http://note.youdao.com/yws/api/personal/file/WEB1a9fc48f2c0d5528315d92d31e3ac6ff?method=download&shareKey=c863897204153764bdda8746ebf4cc84) 4. 接下来是由父进程关闭已连接套接口(connfd),由子进程关闭监听套接口(listenfd)。然后由子进程负责为客户端提供服务 ![image](http://note.youdao.com/yws/api/personal/file/WEBeb7883cfe2218b5d0c01daf78458ab50?method=download&shareKey=cc9f575193b603a6e7ef89cf8906c1bc) 这里我们还是使用上面那个回射服务器例子,改成基于进程的并发服务器源码: ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <sys/socket.h> #define MAXLEN 1024 void doit(int); int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); pid_t pid; struct sockaddr_in server_sockaddr; server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(1024); server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr)); listen(server_sockfd, 20); for( ; ; ){ int clnt_sock = accept(server_sockfd, NULL, NULL); printf("add client \n"); //创建子进程 if( (pid = fork()) == 0 ){ close(server_sockfd);/*子进程不需要监听,关闭*/ doit(clnt_sock);/*针对已连接的客户端套接字进行读写*/ close(clnt_sock);/*处理完毕,关闭客户端连接*/ exit(0);/*自觉退出*/ } close(clnt_sock); } } //子进程调用的处理客户端函数 void doit(int clnt_sock){ char buf[1024] = {'\0'}; if(read(clnt_sock, buf, 1024) != 0){ printf("read end \n"); fputs(buf, stdout); write(clnt_sock, buf, 1024); } } ``` 服务器代码执行流程: 1. 在服务器端设置监听套接字:server_sockfd,监听端口为1024 2. 进入死循环调用accept阻塞,直到有客户端请求时候,accept返回客户端套接字:clnt_sock 3. 通过fork函数创建一个子进程,在子进程中独立处理客户端连接,这是read函数阻塞在子进程中 4. 又回到死循环接受其他客户端连接,针对主进程死循环而言,没有阻塞 这种多进程实现并发有一个很明显的优势:共享文件表,但是不共享用户地址空间,子进程拥有自己独立的地址空间,不容易出错。同样,它也有一个很明显的劣势:各子进程之间共享信息状态变得更加困难,往往需要花费很大的代价。所以,目前服务器也很少使用这种模式,更多的是使用下面介绍的IO复用模型实现的并发服务器 ## 基于IO多路复用的并发编程 使用IO多路复用技术实现并发服务器的基本思路:使用select函数要求内核挂起进程,只有在一个或者多个IO事件发生后,才将控制返回给应用程序。IO多路复用阻塞于select函数,入下图是IO复用模型示意图 ![image](http://note.youdao.com/yws/api/personal/file/WEBb71b651f36bafcfb9d6ae8d78ef330a7?method=download&shareKey=ebc1d1ca6943b29e6628539acf8be811) select函数基本用法:select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。我们调用select可以告诉内核我们感兴趣的描述符(读/写)去监听,也可以指定等待的时间,当然最后还有异常处理,它的函数定义: ``` /** param maxfdp1:指定待测试的描述符个数,它的值是待测试的最大描述符加1 param readset: 指定测试读描述符 param writeset:指定测试写描述符 param exceptset:指定异常条件的描述符 param timeout最后一个参数是关于时间的结构体 */ int select(int maxfdp1, fd_set *readset, fd_set *writeset,fd_set *exceptset, const struct timeval *timeout); //返回:若有就绪描述符就返回其数目,超时返回0,出错返回-1 ``` select函数处理的fd_set的集合,也叫作描述符集合。可以通过下面四个宏指令来操作描述符集合 ``` void FD_CLR(int fd, fd_set *set);//关闭描述符fd int FD_ISSET(int fd, fd_set *set);//判断该描述符是否就绪 void FD_SET(int fd, fd_set *set);//打开某个描述符 void FD_ZERO(fd_set *set);//初始化 ``` 所以,最后编写出基于select函数实现的IO复用模型并发服务器源码: ``` #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/time.h> #include <stdio.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <signal.h> #define MAXLEN 1024 #define SERV_PORT 1024 // 1024~49151未被使用的端口 #define LISTENQ 1024 //定义客户端连接池结构体 typedef struct { int maxfd; //最大的sockfd fd_set read_set;//连接描述符 fd_set ready_set;//可读描述符 int nready;//满足读条件描述符格式 int maxi;//最大描述符 int clientfd[FD_SETSIZE];//存储客户端描述符 } pool; void check_clients(pool *p); void add_client(int connfd, pool *p); void init_pool(int listenfd, pool *p); int main(){ struct sockaddr_in serveraddr; //服务器套接字结构 struct sockaddr_in cliaddr;//客户端套接字结构 int listenfd;//监听套接字描述符 int connfd; //连接套接字字描述符 static pool pool; //初始化 memset(&serveraddr,0x00,sizeof(serveraddr)); memset(&cliaddr,0x00,sizeof(cliaddr)); serveraddr.sin_family=AF_INET; serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); serveraddr.sin_port=htons(SERV_PORT); if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0){ printf("socket error\n"); return -1; } if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))<0){ printf("bind error\n"); return -1; } if(listen(listenfd,LISTENQ)<0){ printf("listen error\n"); return -1; } init_pool(listenfd, &pool); while(1){ pool.ready_set = pool.read_set; pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL); if(FD_ISSET(listenfd, &pool.ready_set)){ printf("add client\n"); connfd = accept(listenfd,NULL,NULL); //调用accept add_client(connfd, &pool); } check_clients(&pool); } } void init_pool(int listenfd, pool *p){ int i; p->maxi = -1; //客户端连接池初始化全部标志位为-1 for(i=0; i < FD_SETSIZE; i++){ p->clientfd[i] = -1; } p->maxfd = listenfd; FD_ZERO(&p->read_set); FD_SET(listenfd, &p->read_set);//添加监听套接字到select } void add_client(int connfd, pool *p){ int i; //p->nready --; for(i=0; i < FD_SETSIZE; i++){ if(p->clientfd[i] < 0){ p->clientfd[i] = connfd; FD_SET(connfd, &p->read_set); if(connfd > p->maxfd){ p->maxfd = connfd; } if(i > p->maxi){ p->maxi = i; } printf("maxi is %d \n", p->maxi); break; } } if(i == FD_SETSIZE){ //连接数超过请求 printf("too many clients \n"); } } void check_clients(pool *p){ int i, connfd, n; char buf[MAXLEN]; //for(i=0; (i < p->maxi) && (p->nready > 0); i++){ for(i=0; i <= p->maxi; i++){ printf("beging check clients\n"); connfd = p->clientfd[i]; if((connfd > -1) && (FD_ISSET(connfd, &p->ready_set))){ printf("client ready read\n"); //p->nready --; if((n=read(connfd,buf,MAXLEN))==0){ close(connfd); FD_CLR(connfd,&p->read_set); p->clientfd[i]=-1; }else{ write(connfd,buf,n); } } } } ``` 这个基于IO多路复用模型并发服务器执行流程: 1. 通过init_pool函数初始化Socket连接池,将监听套接字都放在pool结构体中的并注册到select监听中 2. 开始进入死循环,通过select监测到一个新的客户端连接到达的时候,接收客户端请求,并通过add_client行数将客户端套接字加入到pool结构体中clientfd属性 3. 调用check_clients通过select检测客户端套接字可读的情况 ## 基于线程的并发编程 线程是运行在进程上下文中的逻辑流,线程由内核自动调度,每个线程都有它自己的线程上下文,所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。 每个进程开始生命周期时都是单一线程,这个线程称为主线程。在某一时刻,主线程创建一个对等线程,从这个时刻开始,两个线程就并发地执行。列举一个简单的线程例子 ``` #include<stdio.h> #include<pthread.h> #include<stdio.h> void * thr_fn(void * arg); int main(){ pthread_t ntid; pthread_create(&ntid, NULL, thr_fn, NULL); pthread_join(ntid, NULL); } void * thr_fn(void * arg){ printf("Hello"); } ``` 创建线程函数函数 ``` typedef void *(func)(void *); int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg); ``` 第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。一旦线程被创建了之后就可以在每个线程中并行运行函数了。 线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源,这里我们可以使用分离线程,让系统帮我们回收资源,分离线程函数 ``` int pthread_detach(pthread_t tid); ``` 所以,我们采用最简单的方式可以实现我们的多线程并发服务器,和多进程实现非常像,只是在关键地方改成了多线程实现。 ``` #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <sys/socket.h> #include <pthread.h> void *doit(void *sockfd); int main() { int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP); pid_t pid; if(server_sockfd == -1){ printf("socket error"); return -1; } struct sockaddr_in server_sockaddr;/*声明一个变量,类型为协议地址类型*/ server_sockaddr.sin_family = AF_INET;/*使用IPv4协议*/ server_sockaddr.sin_port = htons(8887);/*监听8887端口*/ server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*绑定本机IP,使用宏定义绑定*/ if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ printf("bind error"); return -1; } if(listen(server_sockfd, 20) == -1){ printf("listen error"); return -1; } pthread_t tid; for( ; ; ){ struct sockaddr_in clnt_addr;/*只是声明,并没有赋值*/ socklen_t clnt_addr_size = sizeof(clnt_addr); int *clnt_sock = malloc(sizeof(int)); *clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size); pthread_create(&tid, NULL, doit, clnt_sock); } } void *doit(void *sockfd){ int connfd = *((int *)sockfd); pthread_detach(pthread_self()); free(sockfd); char str[] = "Hello World"; sleep(3);//3秒之后再向客户端发送数据 write(connfd, str, sizeof(str)); close(connfd); } ```
Pre:
从CGI到FastCGI到PHP-FPM
Next:
C语言编写服务端入门
0
likes
661
Weibo
Wechat
Tencent Weibo
QQ Zone
RenRen
Submit
Sign in
to leave a comment.
No Leanote account?
Sign up now.
0
comments
More...
Table of content
No Leanote account? Sign up now.