精华内容
下载资源
问答
  • C/S模型(客户/服务器模型
    千次阅读
    2020-07-17 23:46:14

    C/S模型即客户(client)/服务器(server)模型。
    1.特点
    服务器提供服务,客户请求服务。

    2.客户端和服务器之间连接的数量对应关系
    多个客户进程可以同时访问一个服务进程,一个客户进程可以同时访问多个服务器进程提供的服务。

    3.客户端和服务器所在不同网络位置所适用的场合
    运行在同一个机器上的场合,应用于网络应用程序开发测试。
    运行在同一个局域网内的不同机器上的场合,应用于局域网文件共享、局域网打印机。
    运行于广域网不同机器上的场合,应用于最常见的网络应用程序。

    4.客户端和服务器的角色关系
    应用程序仅作为客户运行,如Web服务器。
    应用程序仅作为服务器运行,如局部基本功能的ftp服务器。
    应用程序同时具备客户和服务器的两种角色。

    5.服务器端和客户端的工作过程
    5.1.服务器首先启动监听程序,对指定的端口进行监听,等待接收客户端的连接请求。
    5.2.客户端启动程序,请求连接服务器的指定端口。
    5.3.服务器收到客户端的连接请求后,与客户端建立套接字连接。
    5.4.连接建立成功,客户端与服务器分别打开两个流,其中客户端的输入流连接到服务器的输出流,服务器的输入流连接到客户端的输出流,两边的流连接成功后进行双向通信。
    5.5.当通信完毕后,客户端和服务器两边各自断开连接。

    6.服务器端的编程流程
    6.1创建套接字。
    6.2设置服务器端口和ip地址。
    6.3绑定端口。
    6.4监听。
    6.5接受客户端发送过来的连接请求。
    6.6接收数据。
    6.7处理事件。
    6.8关闭套接字。

    7.客户端的编程流程
    7.1创建套接字。
    7.1设置端口和ip。
    7.3连接服务器。
    7.4发送或接收数据。
    7.5处理事件。
    7.6关闭套接字。

    8.C/S模式特点总结
    结构简单。
    支持分布式、并发环境,有效提高资源的利用率和共享程度。
    服务器集中管理资源,有利于权限控制和系统安全。
    可扩展性较好,客户和服务器均可单独升级。

    更多相关内容
  • 5常用的服务器编程模型

    千次阅读 2016-11-06 22:54:38
    同步阻塞迭代模型是最简单的一IO模型。 其核心代码如下: 12345678bind(srvfd);listen(srvfd);for(;;){ clifd = accept(srvfd,...); //开始接受客户端来的连接 read(clifd,buf,...);

    http://www.cricode.com/3510.html

    1.同步阻塞迭代模型

    同步阻塞迭代模型是最简单的一种IO模型。

    其核心代码如下:

    上面的程序存在如下一些弊端:

    1)如果没有客户端的连接请求,进程会阻塞在accept系统调用处,程序不能执行其他任何操作。(系统调用使得程序从用户态陷入内核态,具体请参考:程序员的自我修养)

    2)在与客户端建立好一条链路后,通过read系统调用从客户端接受数据,而客户端合适发送数据过来是不可控的。如果客户端迟迟不发生数据过来,则程序同样会阻塞在read调用,此时,如果另外的客户端来尝试连接时,都会失败。

    3)同样的道理,write系统调用也会使得程序出现阻塞(例如:客户端接受数据异常缓慢,导致写缓冲区满,数据迟迟发送不出)。

    2.多进程并发模型

    同步阻塞迭代模型有诸多缺点。多进程并发模型在同步阻塞迭代模型的基础上进行了一些改进,以避免是程序阻塞在read系统调用上。

    多进程模型核心代码如下:

    上述程序在accept系统调用时,如果没有客户端来建立连接,择会阻塞在accept处。一旦某个客户端连接建立起来,则立即开启一个新的进程来处理与这个客户的数据交互。避免程序阻塞在read调用,而影响其他客户端的连接。

    3.多线程并发模型

    在多进程并发模型中,每一个客户端连接开启fork一个进程,虽然linux中引入了写实拷贝机制,大大降低了fork一个子进程的消耗,但若客户端连接较大,则系统依然将不堪负重。通过多线程(或线程池)并发模型,可以在一定程度上改善这一问题。

    在服务端的线程模型实现方式一般有三种:

    (1)按需生成(来一个连接生成一个线程)

    (2)线程池(预先生成很多线程)

    (3)Leader follower(LF)

    为简单起见,以第一种为例,其核心代码如下:

    服务端分为主线程和工作线程,主线程负责accept()连接,而工作线程负责处理业务逻辑和流的读取等。因此,即使在工作线程阻塞的情况下,也只是阻塞在线程范围内,对继续接受新的客户端连接不会有影响。

    第二种实现方式,通过线程池的引入可以避免频繁的创建、销毁线程,能在很大程序上提升性能。但不管如何实现,多线程模型先天具有如下缺点:

    1)稳定性相对较差。一个线程的崩溃会导致整个程序崩溃。

    2)临界资源的访问控制,在加大程序复杂性的同时,锁机制的引入会是严重降低程序的性能。性能上可能会出现“辛辛苦苦好几年,一夜回到解放前”的情况。

    4.IO多路复用模型之select/poll

    多进程模型和多线程(线程池)模型每个进程/线程只能处理一路IO,在服务器并发数较高的情况下,过多的进程/线程会使得服务器性能下降。而通过多路IO复用,能使得一个进程同时处理多路IO,提升服务器吞吐量。

    在Linux支持epoll模型之前,都使用select/poll模型来实现IO多路复用。

    以select为例,其核心代码如下:

    select IO多路复用同样存在一些缺点,罗列如下:

    1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)
    2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
    3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
    4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

    相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

    拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

    5.IO多路复用模型之epoll

    epoll IO多路复用:一个看起来很美好的解决方案。 由于文章:高并发网络编程之epoll详解中对epoll相关实现已经有详细解决,这里就直接摘录过来。

    由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。

    设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

    在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

    epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:

    1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

    2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

    3)调用epoll_wait收集发生的事件的连接

    如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

     

    下面来看看Linux内核具体的epoll机制实现思路。

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    epoll数据结构示意图

    从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

    OK,讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。

    第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

    第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

    第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

    最后,附上一个epoll编程实例。(此代码作者为sparkliang)

    展开全文
  • 5种服务器网络编程模型

    千次阅读 多人点赞 2017-10-08 20:40:06
    同步阻塞迭代模型是最简单的一IO模型。 其核心代码如下: [cpp] view plain copy bind(srvfd);  listen(srvfd);  for(;;){   clifd = accept(srvfd,...); //开始接受...

    1.同步阻塞迭代模型

    同步阻塞迭代模型是最简单的一种IO模型。

    其核心代码如下:

    [cpp]  view plain  copy
    1. bind(srvfd);  
    2. listen(srvfd);  
    3. for(;;){  
    4.     clifd = accept(srvfd,...); //开始接受客户端来的连接  
    5.     read(clifd,buf,...);       //从客户端读取数据  
    6.     dosomthingonbuf(buf);    
    7.     write(clifd,buf)          //发送数据到客户端  
    8. }  

    上面的程序存在如下一些弊端:accept,read,write都可能阻塞

    1)如果没有客户端的连接请求,进程会阻塞在accept系统调用处,程序不能执行其他任何操作。(系统调用使得程序从用户态陷入内核态)

    2)在与客户端建立好一条链路后,通过read系统调用从客户端接受数据,而客户端合适发送数据过来是不可控的。如果客户端迟迟不发生数据过来,则程序同样会阻塞在read调用,此时,如果另外的客户端来尝试连接时,都会失败。

    3)同样的道理,write系统调用也会使得程序出现阻塞(例如:客户端接受数据异常缓慢,导致写缓冲区满,数据迟迟发送不出)。

    2.多进程并发模型

    同步阻塞迭代模型有诸多缺点。多进程并发模型在同步阻塞迭代模型的基础上进行了一些改进,以避免是程序阻塞在read系统调用上。

    多进程模型核心代码如下:

    [cpp]  view plain  copy
    1. bind(srvfd);  
    2. listen(srvfd);  
    3. for(;;){  
    4.     clifd=accept(srvfd,...);//开始接受客户端来的连接  
    5.     ret=fork();  
    6.     switch(ret)  
    7.     {  
    8.       case-1:  
    9.         do_err_handler();  
    10.         break;  
    11.       case0  :  // 子进程  
    12.         client_handler(clifd);  
    13.         break;  
    14.       default:  // 父进程  
    15.         close(clifd);  
    16.         continue;  
    17.     }  
    18. }  
    19. //======================================================  
    20. voidclient_handler(clifd){  
    21.     read(clifd,buf,...);      //从客户端读取数据  
    22.     dosomthingonbuf(buf);    
    23.     write(clifd,buf)          //发送数据到客户端  
    24. }  

    上述程序在accept系统调用时,如果没有客户端来建立连接,择会阻塞在accept处。一旦某个客户端连接建立起来,则立即开启一个新的进程来处理与这个客户的数据交互。避免程序阻塞在read调用,而影响其他客户端的连接。

    3.多线程并发模型

    在多进程并发模型中,每一个客户端连接开启fork一个进程,虽然linux中引入了写实拷贝机制,大大降低了fork一个子进程的消耗,但若客户端连接较大,则系统依然将不堪负重。通过多线程(或线程池)并发模型,可以在一定程度上改善这一问题。

    在服务端的线程模型实现方式一般有三种:

    (1)按需生成(来一个连接生成一个线程)

    (2)线程池(预先生成很多线程)

    (3)Leader follower(LF)

    为简单起见,以第一种为例,其核心代码如下:

    [cpp]  view plain  copy
    1. void *thread_callback( void *args ) //线程回调函数  
    2. {  
    3.         int clifd = *(int *)args ;  
    4.         client_handler(clifd);  
    5. }  
    6. //===============================================================  
    7. void client_handler(clifd){  
    8.     read(clifd,buf,...);       //从客户端读取数据  
    9.     dosomthingonbuf(buf);    
    10.     write(clifd,buf)          //发送数据到客户端  
    11. }  
    12. //===============================================================  
    13. bind(srvfd);  
    14. listen(srvfd);  
    15. for(;;){  
    16.     clifd = accept();  
    17.     pthread_create(...,thread_callback,&clifd);  
    18. }  

    服务端分为主线程和工作线程,主线程负责accept()连接,而工作线程负责处理业务逻辑和流的读取等。因此,即使在工作线程阻塞的情况下,也只是阻塞在线程范围内,对继续接受新的客户端连接不会有影响。

    第二种实现方式,通过线程池的引入可以避免频繁的创建、销毁线程,能在很大程序上提升性能。但不管如何实现,多线程模型先天具有如下缺点:

    1)稳定性相对较差。一个线程的崩溃会导致整个程序崩溃。

    2)临界资源的访问控制,在加大程序复杂性的同时,锁机制的引入会是严重降低程序的性能。性能上可能会出现“辛辛苦苦好几年,一夜回到解放前”的情况。

    4.IO多路复用模型之select/poll

    多进程模型和多线程(线程池)模型每个进程/线程只能处理一路IO,在服务器并发数较高的情况下,过多的进程/线程会使得服务器性能下降。而通过多路IO复用,能使得一个进程同时处理多路IO,提升服务器吞吐量。

    在Linux支持epoll模型之前,都使用select/poll模型来实现IO多路复用。

    以select为例,其核心代码如下:

    [cpp]  view plain  copy
    1. bind(listenfd);  
    2. listen(listenfd);  
    3. FD_ZERO(&allset);  
    4. FD_SET(listenfd,&allset);  
    5. for(;;){  
    6.     select(...);  
    7.     if(FD_ISSET(listenfd,&rset)){    /*有新的客户端连接到来*/  
    8.         clifd=accept();  
    9.         cliarray[]=clifd;      /*保存新的连接套接字*/  
    10.         FD_SET(clifd,&allset);  /*将新的描述符加入监听数组中*/  
    11.     }  
    12.     for(;;){    /*这个for循环用来检查所有已经连接的客户端是否由数据可读写*/  
    13.         fd=cliarray[i];  
    14.         if(FD_ISSET(fd,&rset))  
    15.             dosomething();  
    16.     }  
    17. }  

    示例代码:

    /*************************************************************************
      > Description:使用select函数实现I/O复用服务器端
     ************************************************************************/
    
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<arpa/inet.h>
    #include<sys/socket.h>
    #include<sys/time.h>
    #include<sys/select.h>
    
    void error_handling(char *message);
    
    #define BUFF_SIZE 32
    
    int main(int argc, char *argv[])
    {
    	int server_sock;
    	int client_sock;
    
    	struct sockaddr_in server_addr;
    	struct sockaddr_in client_addr;
    	socklen_t client_addr_size;
    
    	char buff[BUFF_SIZE];
    	fd_set reads, reads_init;
    	struct timeval timeout, timeout_init;
    
    	int str_len, i, fd_max, fd_num;
    
    	if(argc!=2){ //命令行中启动服务程序仅限一个参数:端口号
    		printf("Usage : %s <port>\n", argv[0]);
    		exit(1);
    	}
    	
    	//调用socket函数创建套接字
    	server_sock = socket(PF_INET, SOCK_STREAM, 0);
    	if(-1 == server_sock){
    		error_handling("socket() error.");
    	}
    
    	memset(&server_addr, 0, sizeof(server_addr));
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    	server_addr.sin_port = htons(atoi(argv[1]));
    	
    	//调用bind函数分配IP地址和端口号
    	if( -1 == bind( server_sock, (struct sockaddr*)&server_addr, 
    				sizeof(server_addr)) ){
    		error_handling("bind() error");
    	}
    
    	//监听端口的连接请求,连接请求等待队列size为5
    	if( -1 == listen(server_sock, 5) ){
    		error_handling("listen() error");
    	}
    
    	//register fd_set var
    	FD_ZERO(&reads_init);
    	FD_SET(server_sock, &reads_init);//monitor socket: server_sock
    	FD_SET(0, &reads_init);// stdin also works
    	fd_max = server_sock;
    	//
    	timeout_init.tv_sec = 5;
    	timeout_init.tv_usec= 0;
    
    	while(1){
    		//调用select之后,除发生变化的文件描述符对应的bit,其他所有位置0,所以需用保存初值,通过复制使用
    		reads = reads_init;
    		//调用select之后,timeval成员值被置为超时前剩余的时间,因此使用时也需要每次用初值重新初始化
    		timeout = timeout_init;
    		fd_num = select(fd_max+1, &reads, NULL, NULL, &timeout);
    		if(fd_num < 0){
    			fputs("Error select()!", stderr);
    			break;
    		}else if(fd_num == 0){
    			puts("Time-out!");
    			continue;
    		}
    		for(i=0; i<=fd_max; i++){
    			if(FD_ISSET(i, &reads)){
    				if(i == server_sock){//connection request!
    					//接受连接请求
    					client_addr_size = sizeof(client_addr);
    					client_sock = accept( server_sock, (struct sockaddr*)&client_addr, &client_addr_size );
    					//accept函数自动创建数据I/0 socket
    					if(-1 == client_sock){
    						error_handling("accept() error");
    						//健壮性不佳,程序崩溃退出
    					} else{
    						//注册与客户端连接的套接字文件描述符
    						FD_SET(client_sock, &reads_init);
    						if(fd_max < client_sock) fd_max = client_sock;
    						printf("Connected client : %d\n", client_sock);
    					}
    				}else{//read message!
    					str_len = read(i, buff, BUFF_SIZE);
    					if(str_len){//echo to client
    						buff[str_len] = 0;
    						printf("Message from client %d: %s", i, buff);
    						write(i, buff, str_len);
    					}else{ //close connection
    						FD_CLR(i, &reads_init);
    						close(i);
    						printf("Disconnected client %d!\n", i);
    					}
    				}//end of i==|!=server_sock
    			}//end of if(FD_ISSET)
    		}//end of while
    
    	}//end of for
    
    	//断开连接,关闭套接字
    	close(server_sock);
    
    	return 0;
    }
    
    void error_handling(char *message)
    {
    	fputs(message, stderr);
    	fputc('\n', stderr);
    	exit(EXIT_FAILURE);
    }
    


    select IO多路复用同样存在一些缺点,罗列如下:

    1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)
    2. 内核 / 用户空间内存拷贝问题select需要复制大量的句柄数据结构,产生巨大的开销;
    3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件
    4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

    相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

    拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

    5.IO多路复用模型之epoll

    epoll IO多路复用:一个看起来很美好的解决方案。 由于文章:高并发网络编程之epoll详解中对epoll相关实现已经有详细解决,这里就直接摘录过来。

    由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。

    设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

    在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

    epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树,实际为红黑树+双端链表)。把原先的select/poll调用分成了3个部分:

    1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

    2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

    3)调用epoll_wait收集发生的事件的连接

    如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

     

    下面来看看Linux内核具体的epoll机制实现思路。

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

    [cpp]  view plain  copy
    1. struct eventpoll{  
    2.     ....  
    3.     /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/  
    4.     struct rb_root  rbr;  
    5.     /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/  
    6.     struct list_head rdlist;  
    7.     ....  
    8. };  

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

    [cpp]  view plain  copy
    1. structepitem{  
    2.     structrb_node  rbn;//红黑树节点  
    3.     structlist_head    rdllink;//双向链表节点  
    4.     structepoll_filefd  ffd;  //事件句柄信息  
    5.     structeventpoll *ep;    //指向其所属的eventpoll对象  
    6.     structepoll_eventevent;//期待发生的事件类型  
    7. }  

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    epoll数据结构示意图

    从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

    OK,讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。

    第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

    第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

    第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

    /*************************************************************************	
      > Description:基于epoll的回声服务器端
     ************************************************************************/
    
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<arpa/inet.h>
    #include<sys/socket.h>
    #include<sys/epoll.h>
    
    void error_handling(char *message);
    
    #define BUFF_SIZE 100
    #define EPOLL_SIZE 30
    
    int main(int argc, char *argv[])
    {
    	int sock_server;
    	int sock_client;
    
    	struct sockaddr_in addr_server;
    	struct sockaddr_in addr_client;
    	socklen_t size_addr_client;
    
    	char buff[BUFF_SIZE];
    	int str_len, i;
    
    	int epfd, count_event;
    	struct epoll_event *ep_events;
    	struct epoll_event event;
    	
    
    	if(argc!=2){ //命令行中启动服务程序仅限一个参数:端口号
    		printf("Usage : %s <port>\n", argv[0]);
    		exit(1);
    	}
    	
    	//调用socket函数创建套接字
    	sock_server = socket(PF_INET, SOCK_STREAM, 0);
    	if(-1 == sock_server){
    		error_handling("socket() error.");
    	}
    
    	memset(&addr_server, 0, sizeof(addr_server));
    	addr_server.sin_family = AF_INET;
    	addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
    	addr_server.sin_port = htons(atoi(argv[1]));
    	
    	//调用bind函数分配IP地址和端口号
    	if( -1 == bind( sock_server, (struct sockaddr*)&addr_server, 
    				sizeof(addr_server)) ){
    		error_handling("bind() error");
    	}
    
    	//监听端口的连接请求,连接请求等待队列size为5
    	if( -1 == listen(sock_server, 5) ){
    		error_handling("listen() error");
    	}
    
    	//epoll
    	epfd = epoll_create(EPOLL_SIZE);
    	//epfd = epoll_create(0); //epoll_wait() Error
    	ep_events = (struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    
    	event.events = EPOLLIN;//监视需用读取数据事件
    	event.data.fd=sock_server;
    	epoll_ctl(epfd, EPOLL_CTL_ADD, sock_server, &event);
    	//
    	while(1){
    		count_event = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    		if(count_event == -1){
    			puts("epoll_wait() Error");
    			break;
    		}
    
    		for(i=0; i<count_event; i++){
    			if(ep_events[i].data.fd == sock_server){
    				//接受连接请求
    				size_addr_client = sizeof(addr_client);
    				sock_client = accept( sock_server, (struct sockaddr*)&addr_client, &size_addr_client);
    				event.events = EPOLLIN;
    				event.data.fd = sock_client;
    				epoll_ctl(epfd, EPOLL_CTL_ADD, sock_client, &event);
    				printf("Connected client : %d\n", sock_client);
    			}else{
    				str_len = read(ep_events[i].data.fd, buff, BUFF_SIZE);
    				if(str_len){//echo to client
    					buff[str_len] = 0;
    					printf("Message from client %d: %s", i, buff);
    					write(ep_events[i].data.fd, buff, str_len);//echo!
    				}else{ //close connection
    					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
    					close(ep_events[i].data.fd);
    					printf("Disconnected client %d!\n", ep_events[i].data.fd);
    				}
    			}//end of if()
    		}//end of while
    	}//end of for
    
    	//断开连接,关闭套接字
    	close(sock_server);
    	close(epfd);//
    
    	return 0;
    }
    
    void error_handling(char *message)
    {
    	fputs(message, stderr);
    	fputc('\n', stderr);
    	exit(EXIT_FAILURE);
    }
    


    展开全文
  • 两种常用的编程模型

    千次阅读 2017-11-08 22:33:02
    这篇并发模型教程将会较深入地介绍目前(2015年,本文撰写时间)比较流行的几并发模型。并发模型与分布式系统之间的相似性本文所描述的并发模型类似于分布式系统中使用的很多体系结构。在并发系统中线程之间可以...

          虽然下面涉及java,但是抽象出来的编程模型是不语言,思想才是最重要的,感谢原作者的无私分享。文章很好,我就直接转载过来了

          

    原文链接 作者: Jakob Jenkov 译者: 林威建 [weakielin@gmail.com]

    并发系统可以采用多种并发编程模型来实现。并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业。不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同。这篇并发模型教程将会较深入地介绍目前(2015年,本文撰写时间)比较流行的几种并发模型。

    并发模型与分布式系统之间的相似性

    本文所描述的并发模型类似于分布式系统中使用的很多体系结构。在并发系统中线程之间可以相互通信。在分布式系统中进程之间也可以相互通信(进程有可能在不同的机器中)。线程和进程之间具有很多相似的特性。这也就是为什么很多并发模型通常类似于各种分布式系统架构。

    当然,分布式系统在处理网络失效、远程主机或进程宕掉等方面也面临着额外的挑战。但是运行在巨型服务器上的并发系统也可能遇到类似的问题,比如一块CPU失效、一块网卡失效或一个磁盘损坏等情况。虽然出现失效的概率可能很低,但是在理论上仍然有可能发生。

    由于并发模型类似于分布式系统架构,因此它们通常可以互相借鉴思想。例如,为工作者们(线程)分配作业的模型一般与分布式系统中的负载均衡系统比较相似。同样,它们在日志记录、失效转移、幂等性等错误处理技术上也具有相似性。
    【注:幂等性,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同】

    并行工作者

    第一种并发模型就是我所说的并行工作者模型。传入的作业会被分配到不同的工作者上。下图展示了并行工作者模型:
    并行工作者模型
    在并行工作者模型中,委派者(Delegator)将传入的作业分配给不同的工作者。每个工作者完成整个任务。工作者们并行运作在不同的线程上,甚至可能在不同的CPU上。

    如果在某个汽车厂里实现了并行工作者模型,每台车都会由一个工人来生产。工人们将拿到汽车的生产规格,并且从头到尾负责所有工作。

    在Java应用系统中,并行工作者模型是最常见的并发模型(即使正在转变)。java.util.concurrent包中的许多并发实用工具都是设计用于这个模型的。你也可以在Java企业级(J2EE)应用服务器的设计中看到这个模型的踪迹。

    并行工作者模型的优点

    并行工作者模式的优点是,它很容易理解。你只需添加更多的工作者来提高系统的并行度。

    例如,如果你正在做一个网络爬虫,可以试试使用不同数量的工作者抓取到一定数量的页面,然后看看多少数量的工作者消耗的时间最短(意味着性能最高)。由于网络爬虫是一个IO密集型工作,最终结果很有可能是你电脑中的每个CPU或核心分配了几个线程。每个CPU若只分配一个线程可能有点少,因为在等待数据下载的过程中CPU将会空闲大量时间。

    并行工作者模型的缺点

    并行工作者模型虽然看起来简单,却隐藏着一些缺点。接下来的章节中我会分析一些最明显的弱点。

    共享状态可能会很复杂

    在实际应用中,并行工作者模型可能比前面所描述的情况要复杂得多。共享的工作者经常需要访问一些共享数据,无论是内存中的或者共享的数据库中的。下图展示了并行工作者模型是如何变得复杂的:
    并行工作者模型的复杂情况
    有些共享状态是在像作业队列这样的通信机制下。但也有一些共享状态是业务数据,数据缓存,数据库连接池等。

    一旦共享状态潜入到并行工作者模型中,将会使情况变得复杂起来。线程需要以某种方式存取共享数据,以确保某个线程的修改能够对其他线程可见(数据修改需要同步到主存中,不仅仅将数据保存在执行这个线程的CPU的缓存中)。线程需要避免竟态死锁以及很多其他共享状态的并发性问题。

    此外,在等待访问共享数据结构时,线程之间的互相等待将会丢失部分并行性。许多并发数据结构是阻塞的,意味着在任何一个时间只有一个或者很少的线程能够访问。这样会导致在这些共享数据结构上出现竞争状态。在执行需要访问共享数据结构部分的代码时,高竞争基本上会导致执行时出现一定程度的串行化。

    现在的非阻塞并发算法也许可以降低竞争并提升性能,但是非阻塞算法的实现比较困难。

    可持久化的数据结构是另一种选择。在修改的时候,可持久化的数据结构总是保护它的前一个版本不受影响。因此,如果多个线程指向同一个可持久化的数据结构,并且其中一个线程进行了修改,进行修改的线程会获得一个指向新结构的引用。所有其他线程保持对旧结构的引用,旧结构没有被修改并且因此保证一致性。Scala编程包含几个持久化数据结构。
    【注:这里的可持久化数据结构不是指持久化存储,而是一种数据结构,比如Java中的String类,以及CopyOnWriteArrayList类,具体可参考

    虽然可持久化的数据结构在解决共享数据结构的并发修改时显得很优雅,但是可持久化的数据结构的表现往往不尽人意。

    比如说,一个可持久化的链表需要在头部插入一个新的节点,并且返回指向这个新加入的节点的一个引用(这个节点指向了链表的剩余部分)。所有其他现场仍然保留了这个链表之前的第一个节点,对于这些线程来说链表仍然是为改变的。它们无法看到新加入的元素。

    这种可持久化的列表采用链表来实现。不幸的是链表在现代硬件上表现的不太好。链表中得每个元素都是一个独立的对象,这些对象可以遍布在整个计算机内存中。现代CPU能够更快的进行顺序访问,所以你可以在现代的硬件上用数组实现的列表,以获得更高的性能。数组可以顺序的保存数据。CPU缓存能够一次加载数组的一大块进行缓存,一旦加载完成CPU就可以直接访问缓存中的数据。这对于元素散落在RAM中的链表来说,不太可能做得到。

    无状态的工作者

    共享状态能够被系统中得其他线程修改。所以工作者在每次需要的时候必须重读状态,以确保每次都能访问到最新的副本,不管共享状态是保存在内存中的还是在外部数据库中。工作者无法在内部保存这个状态(但是每次需要的时候可以重读)称为无状态的。

    每次都重读需要的数据,将会导致速度变慢,特别是状态保存在外部数据库中的时候。

    任务顺序是不确定的

    并行工作者模式的另一个缺点是,作业执行顺序是不确定的。无法保证哪个作业最先或者最后被执行。作业A可能在作业B之前就被分配工作者了,但是作业B反而有可能在作业A之前执行。

    并行工作者模式的这种非确定性的特性,使得很难在任何特定的时间点推断系统的状态。这也使得它也更难(如果不是不可能的话)保证一个作业在其他作业之前被执行。

    流水线模式

    第二种并发模型我们称之为流水线并发模型。我之所以选用这个名字,只是为了配合“并行工作者”的隐喻。其他开发者可能会根据平台或社区选择其他称呼(比如说反应器系统,或事件驱动系统)。下图表示一个流水线并发模型:
    流水线并发模型

    类似于工厂中生产线上的工人们那样组织工作者。每个工作者只负责作业中的部分工作。当完成了自己的这部分工作时工作者会将作业转发给下一个工作者。每个工作者在自己的线程中运行,并且不会和其他工作者共享状态。有时也被成为无共享并行模型。

    通常使用非阻塞的IO来设计使用流水线并发模型的系统。非阻塞IO意味着,一旦某个工作者开始一个IO操作的时候(比如读取文件或从网络连接中读取数据),这个工作者不会一直等待IO操作的结束。IO操作速度很慢,所以等待IO操作结束很浪费CPU时间。此时CPU可以做一些其他事情。当IO操作完成的时候,IO操作的结果(比如读出的数据或者数据写完的状态)被传递给下一个工作者。

    有了非阻塞IO,就可以使用IO操作确定工作者之间的边界。工作者会尽可能多运行直到遇到并启动一个IO操作。然后交出作业的控制权。当IO操作完成的时候,在流水线上的下一个工作者继续进行操作,直到它也遇到并启动一个IO操作。
    No-blocking-IO

    在实际应用中,作业有可能不会沿着单一流水线进行。由于大多数系统可以执行多个作业,作业从一个工作者流向另一个工作者取决于作业需要做的工作。在实际中可能会有多个不同的虚拟流水线同时运行。这是现实当中作业在流水线系统中可能的移动情况:
    multi-assembly-line

    作业甚至也有可能被转发到超过一个工作者上并发处理。比如说,作业有可能被同时转发到作业执行器和作业日志器。下图说明了三条流水线是如何通过将作业转发给同一个工作者(中间流水线的最后一个工作者)来完成作业:
    complex-condition

    流水线有时候比这个情况更加复杂。

    反应器,事件驱动系统

    采用流水线并发模型的系统有时候也称为反应器系统或事件驱动系统。系统内的工作者对系统内出现的事件做出反应,这些事件也有可能来自于外部世界或者发自其他工作者。事件可以是传入的HTTP请求,也可以是某个文件成功加载到内存中等。在写这篇文章的时候,已经有很多有趣的反应器/事件驱动平台可以使用了,并且不久的将来会有更多。比较流行的似乎是这几个:

    我个人觉得Vert.x是相当有趣的(特别是对于我这样使用Java/JVM的人来说)

    Actors 和 Channels

    Actors 和 channels 是两种比较类似的流水线(或反应器/事件驱动)模型。

    在Actor模型中每个工作者被称为actor。Actor之间可以直接异步地发送和处理消息。Actor可以被用来实现一个或多个像前文描述的那样的作业处理流水线。下图给出了Actor模型:
    actor-model

    而在Channel模型中,工作者之间不直接进行通信。相反,它们在不同的通道中发布自己的消息(事件)。其他工作者们可以在这些通道上监听消息,发送者无需知道谁在监听。下图给出了Channel模型:
    channel-model

    在写这篇文章的时候,channel模型对于我来说似乎更加灵活。一个工作者无需知道谁在后面的流水线上处理作业。只需知道作业(或消息等)需要转发给哪个通道。通道上的监听者可以随意订阅或者取消订阅,并不会影响向这个通道发送消息的工作者。这使得工作者之间具有松散的耦合。

    流水线模型的优点

    相比并行工作者模型,流水线并发模型具有几个优点,在接下来的章节中我会介绍几个最大的优点。

    无需共享的状态

    工作者之间无需共享状态,意味着实现的时候无需考虑所有因并发访问共享对象而产生的并发性问题。这使得在实现工作者的时候变得非常容易。在实现工作者的时候就好像是单个线程在处理工作-基本上是一个单线程的实现。

    有状态的工作者

    当工作者知道了没有其他线程可以修改它们的数据,工作者可以变成有状态的。对于有状态,我是指,它们可以在内存中保存它们需要操作的数据,只需在最后将更改写回到外部存储系统。因此,有状态的工作者通常比无状态的工作者具有更高的性能。

    较好的硬件整合(Hardware Conformity)

    单线程代码在整合底层硬件的时候往往具有更好的优势。首先,当能确定代码只在单线程模式下执行的时候,通常能够创建更优化的数据结构和算法。

    其次,像前文描述的那样,单线程有状态的工作者能够在内存中缓存数据。在内存中缓存数据的同时,也意味着数据很有可能也缓存在执行这个线程的CPU的缓存中。这使得访问缓存的数据变得更快。

    我说的硬件整合是指,以某种方式编写的代码,使得能够自然地受益于底层硬件的工作原理。有些开发者称之为mechanical sympathy。我更倾向于硬件整合这个术语,因为计算机只有很少的机械部件,并且能够隐喻“更好的匹配(match better)”,相比“同情(sympathy)”这个词在上下文中的意思,我觉得“conform”这个词表达的非常好。当然了,这里有点吹毛求疵了,用自己喜欢的术语就行。

    合理的作业顺序

    基于流水线并发模型实现的并发系统,在某种程度上是有可能保证作业的顺序的。作业的有序性使得它更容易地推出系统在某个特定时间点的状态。更进一步,你可以将所有到达的作业写入到日志中去。一旦这个系统的某一部分挂掉了,该日志就可以用来重头开始重建系统当时的状态。按照特定的顺序将作业写入日志,并按这个顺序作为有保障的作业顺序。下图展示了一种可能的设计:
    job-ordering

    实现一个有保障的作业顺序是不容易的,但往往是可行的。如果可以,它将大大简化一些任务,例如备份、数据恢复、数据复制等,这些都可以通过日志文件来完成。

    流水线模型的缺点

    流水线并发模型最大的缺点是作业的执行往往分布到多个工作者上,并因此分布到项目中的多个类上。这样导致在追踪某个作业到底被什么代码执行时变得困难。

    同样,这也加大了代码编写的难度。有时会将工作者的代码写成回调处理的形式。若在代码中嵌入过多的回调处理,往往会出现所谓的回调地狱(callback hell)现象。所谓回调地狱,就是意味着在追踪代码在回调过程中到底做了什么,以及确保每个回调只访问它需要的数据的时候,变得非常困难

    使用并行工作者模型可以简化这个问题。你可以打开工作者的代码,从头到尾优美的阅读被执行的代码。当然并行工作者模式的代码也可能同样分布在不同的类中,但往往也能够很容易的从代码中分析执行的顺序。

    函数式并行(Functional Parallelism)

    第三种并发模型是函数式并行模型,这是也最近(2015)讨论的比较多的一种模型。函数式并行的基本思想是采用函数调用实现程序。函数可以看作是”代理人(agents)“或者”actor“,函数之间可以像流水线模型(AKA 反应器或者事件驱动系统)那样互相发送消息。某个函数调用另一个函数,这个过程类似于消息发送。

    函数都是通过拷贝来传递参数的,所以除了接收函数外没有实体可以操作数据。这对于避免共享数据的竞态来说是很有必要的。同样也使得函数的执行类似于原子操作。每个函数调用的执行独立于任何其他函数的调用。

    一旦每个函数调用都可以独立的执行,它们就可以分散在不同的CPU上执行了。这也就意味着能够在多处理器上并行的执行使用函数式实现的算法。

    Java7中的java.util.concurrent包里包含的ForkAndJoinPool能够帮助我们实现类似于函数式并行的一些东西。而Java8中并行streams能够用来帮助我们并行的迭代大型集合。记住有些开发者对ForkAndJoinPool进行了批判(你可以在我的ForkAndJoinPool教程里面看到批评的链接)。

    函数式并行里面最难的是确定需要并行的那个函数调用。跨CPU协调函数调用需要一定的开销。某个函数完成的工作单元需要达到某个大小以弥补这个开销。如果函数调用作用非常小,将它并行化可能比单线程、单CPU执行还慢。

    我个人认为(可能不太正确),你可以使用反应器或者事件驱动模型实现一个算法,像函数式并行那样的方法实现工作的分解。使用事件驱动模型可以更精确的控制如何实现并行化(我的观点)。

    此外,将任务拆分给多个CPU时协调造成的开销,仅仅在该任务是程序当前执行的唯一任务时才有意义。但是,如果当前系统正在执行多个其他的任务时(比如web服务器,数据库服务器或者很多其他类似的系统),将单个任务进行并行化是没有意义的。不管怎样计算机中的其他CPU们都在忙于处理其他任务,没有理由用一个慢的、函数式并行的任务去扰乱它们。使用流水线(反应器)并发模型可能会更好一点,因为它开销更小(在单线程模式下顺序执行)同时能更好的与底层硬件整合。

    使用那种并发模型最好?

    所以,用哪种并发模型更好呢?

    通常情况下,这个答案取决于你的系统打算做什么。如果你的作业本身就是并行的、独立的并且没有必要共享状态,你可能会使用并行工作者模型去实现你的系统。虽然许多作业都不是自然并行和独立的。对于这种类型的系统,我相信使用流水线并发模型能够更好的发挥它的优势,而且比并行工作者模型更有优势。

    你甚至不用亲自编写所有流水线模型的基础结构。像Vert.x这种现代化的平台已经为你实现了很多。我也会去为探索如何设计我的下一个项目,使它运行在像Vert.x这样的优秀平台上。我感觉Java EE已经没有任何优势了。

    原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 并发编程模型

    展开全文
  • Linux 网络编程——并发服务器的三实现模型

    万次阅读 多人点赞 2015-05-12 17:40:08
    循环服务器与并发服务器模型在网络程序里面,一般来说都是许多客户对应一个服务器(多对一),为了处理客户的请求,对服务端的程序就提出了特殊的要求。目前最常用的服务器模型有:·循环服务器:服务器在同一时刻...
  • 服务器模型——C/S模型和P2P模型

    千次阅读 2017-09-03 21:26:08
    呦呦切克闹,煎饼果子来一套本篇学习两种服务器模型~~ TCP/IP协议在设计和实现上并没有客户端和服务器的概念,在通信过程中所有机器都是对等的。但由于资源都被数据提供者所垄断,所以几乎所有的网络应用程序都...
  • 简单介绍了常见的三种服务器模型,并给出阻塞式服务器的示例代码
  • 两种高效的服务器设计模型:Reactor和Proactor模型

    万次阅读 多人点赞 2015-05-30 18:25:14
    在文章《unix网络编程》(12)五I/O模型中提到了五I/O模型,其中前四:阻塞模型、非阻塞模型、信号驱动模型、I/O复用模型都是同步模型;还有一是异步模型。 Reactor模型  Reactor模式是处理并发I/O比较...
  • 典型的服务器网络编程模型归纳(select poll epoll)

    千次阅读 多人点赞 2016-12-17 10:52:17
    同步阻塞迭代模型是最简单的一IO模型。其核心代码如下:bind(srvfd); listen(srvfd); for(;;) { clifd = accept(srvfd,...); //开始接受客户端来的连接 read(clifd,buf,...); //从客户端读取数据 ...
  • workerman是一个高性能的PHP socket 服务器框架,workerman基于PHP多进程以及libevent事件轮询库,PHP开发者只要实现一个接口,便可以开发出自己的网络应用,例如Rpc服务、聊天室服务器、手机游戏服务器等。...
  • 详细介绍JSP技术的两种架构模型

    千次阅读 2015-12-28 22:59:34
    本文分别介绍这两种模型,并比较它们的优缺点。 一、JSP和JavaBean模型  其工作原理是:当浏览器发出请求时,JSP接收请求并访问JavaBean.若需要访问数据库或后台服务器,则通过JavaBean连接数据库或后台服务器,...
  • 上一篇文章:【Linux】Linux网络编程(含常见服务器模型,上篇)。   高级嵌套字函数 前面介绍的一些函数(read、write等)都是网络程序里最基本的函数,也是最原始的通信函数。下面介绍一下几个网络编程的高级...