网络编程-3

UNIX/Linux下主要的四种IO模型的特点

(1)阻塞式IO   :最简单、最常用;效率低

  • 阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
  • 缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。
  • 学习的读写函数在调用过程中会发生阻塞相关函数如下:
    • 读操作中的read、recv、recvfrom

              读阻塞--》需要读缓冲区中有数据可读,读阻塞解除

  • 写操作中的write、send

   写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。

注意:sendto没有写阻塞

    1)无sendto函数的原因:

sendto不是阻塞函数,本身udp通信不是面向链接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。

     2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。

    其他操作:accept、connect

例如1:读阻塞

  • 以read函数为例:
    • 进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发生阻塞。
    • 它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。
    • 经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据。
    • 如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。

(2)非阻塞式IO :可以处理多路IO;需要轮询,浪费CPU资源

  • 当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
  • 当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
  • 应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
  • 这种模式使用中不普遍。
  •  例如:

      Recv函数最后一个参数写为0,为阻塞,写为MSG_DONTWAIT:表示非阻塞。

        非阻塞,循环检测,是否有数据发过来,轮询消耗CPU资源。

(3)IO多路复用 :服务器可以响应多个客户端发来的数据。

(4)信号驱动IO :异步通知模式,需要底层驱动的支持

 I/O多路复用

 

  • 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
  • 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
  • 若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;
  • 比较好的方法是使用I/O多路复用。其基本思想是:
    • 先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
    • 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

其基本流程是:

         1. 先构造一张有关文件描述符的表(集合、数组);

         2. 将你关心的文件描述符加入到这个表中;

         3. 然后调用一个函数。 select / poll

         4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候该函数才返回(阻塞)。

         5. 判断是哪一个或哪些文件描述符产生了事件(IO操作);

         6. 做对应的逻辑处理;

select 
    #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);
 功能:select用于监测是哪个或哪些文件描述符产生事件;
	   参数:nfds:    监测的最大文件描述个数
(这里是个数,使用的时候注意,与文件中最后一次打开的文件
描述符所对应的值的关系是什么?)
			readfds:  读事件集合; //读(用的多)
			writefds: 写事件集合;  //NULL表示不关心
		    exceptfds:异常事件集合;  
			timeout:	超时检测 1
				如果不做超时检测:传 NULL 
					select返回值:  <0 出错
									>0 表示有事件产生;
				如果设置了超时检测时间:&tv
					select返回值:
								<0 出错
								>0 表示有事件产生;	
								==0 表示超时时间已到;

 void FD_CLR(int fd, fd_set *set);  //将set集合中的fd清除掉
 int  FD_ISSET(int fd, fd_set *set); //判断fd是否存在于set集合中
 void FD_SET(int fd, fd_set *set); //将fd加入到集合中
 void FD_ZERO(fd_set *set);   //清空集合

poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
	参数:
	struct pollfd *fds
	关心的文件描述符数组struct pollfd fds[N];
		nfds:个数
		timeout: 超时检测
		毫秒级的:如果填1000,1秒
		 如果-1,阻塞
struct pollfd {
		int   fd;         // 关心的文件描述符;
		short events;     // 关心的事件,读
		short revents;    // 如果产生事件,则会自动填充该成员的值(当文件描述符产生事件之后,这个函数会自动的把读事件或者写事件填充到这个结构体revents的成员里面.如果产生读事件,就给revents一个读事件,如果写事件,一样就给revents一个写事件。)

 

IO多路复用 

   a). select

  •         一个进程最多只能监听1024个文件描述符 (千级别)
  •  select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源); select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);

   b). poll

  • 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组的大小为1,如果想监听100个,那么这个结构体数组的大小就为100,由程序员自己来决定)
  •  poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
  •  poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可   

c. epoll

  • 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)
  • 异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
  • epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.

服务器模型 //(能帮助我们明白为什么要用并发服务器)

  //在网络程序里面,通常都是一个服务器处理多个客户机。

  //为了处理多个客户端的请求,服务器端的程序又不同的处理方式。

 目前最常见的有:

1》循环服务器:

            特点-》循环服务器在同一时刻只能相应一个客户端请求(tcp)

                  依次只能处理一个客户端,等这个客户端所有请求都完成后(客户端关闭),

                  才能处理下一个客户端   

                  缺点: 循环服务器所处理的客户端,不能做耗时动作

tcp服务器(循环)标准流程:       

                  socket(...);

                  bind(...);

                  listen(...);

                  while(1)

                  {   

                           accept(...);

                           while(1)

                           {         

                                    recv(...);

                                    process(...);

                                    send(...);

                           }    

                           close(...);

                  }

udp服务器标准流程:

             socket(...);

                  bind(...);

                  while(1)

                           {         

                                    recvfrom(...);

                                    process(...);

                                    sendto(...);

                           }    

                  close(...);

并发服务器:共五种方式实现

           特点-》同一时刻可以响应多个客户端请求。

                  可以同时处理多个客户端的请求,创建子进程/子线程来处理跟客户端的请求,

    或者利用I/O多路复用实现并发。
1.多进程实现并发

多进程实现并发流程:
		void handler(int sigo)
		{
			while (waitpid(-1, NULL, WNOHANG) > 0);	 
//一个SIGCHLD可能对应多个僵尸进程,循环收尸
		}
		int sockfd = socket(...); 
		bind(...); 
		listen(...);
		signal(SIGCHLD, handler); 
 //注册SIGCHLD信号,当产生SIGCHLD信号时调用handler函数
		while(1) { 
			int connfd = accept(...); 
			if (fork() == 0) { 
				close(sockfd);
				while(1) 
				{ 
					recv(...); 
					process(...); 
					send(...); 
				}
				close(connfd);           
				exit(...); 
			} 
			close(connfd); 
		}

2.多线程实现并发

 

3.I/O多路复用: select、poll、epoll 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值