没使用 ioio多路复用技术 能有多个客户端连接么

Java IO多路复用技术简介
package com.winwill.
* @author qifuguang
* @date 15-2-4 下午2:07
public class TimeServerMain {
public static void main(String[] args) throws Exception {
// 启动时间服务器
new Thread(new SelectorTimeServer()).start();
package com.winwill.
* @author qifuguang
* @date 15-2-4 下午2:09
public class TimeClientMain {
public static void main(String[] args) throws Exception {
// 创建100个客户端连接到服务器
for (int i = 0; i < 100; i++) {
new Thread(new SelectorTimeClient(i + 1)).start();
package com.winwill.
import java.net.InetSocketA
import java.nio.ByteB
import java.nio.channels.SelectionK
import java.nio.channels.S
import java.nio.channels.ServerSocketC
import java.nio.channels.SocketC
import java.text.SimpleDateF
import java.util.D
import java.util.I
* @author qifuguang
* @date 15-2-4 下午1:21
public class SelectorTimeServer implements Runnable {
private static final String TIME_ORDER = "Query Time";
private ServerSocketChannel serverC
private volatile boolean stop =
* 创建Selector, 创建ServerSocketChannel,并设置为非阻塞模式, 注册到selector.
* @throws Exception
public SelectorTimeServer() throws Exception {
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
* 轮询监听selector.
public void run() {
System.out.println("时间服务器启动!");
while (!stop) {
selector.select(1000);
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
handleKey(key);
if (selector != null) {
selector.close();
} catch (Exception e) {
e.printStackTrace();
* 处理每一种selector感兴趣的事件.
* @param key 轮询监听得到的SelectionKey.
private void handleKey(SelectionKey key) {
if (key.isValid()) {
// 如果连接成功
if (key.isAcceptable()) {
// 监听到有新客户端连接
SocketChannel accept = ((ServerSocketChannel) key.channel()).accept(); // 建立与客户端的连接
accept.configureBlocking(false);
// 设置该连接为非阻塞模式
accept.register(selector, SelectionKey.OP_READ); // 将该连接注册到selector
System.out.println("发现有新客户端连接...");
if (key.isReadable()) {
// 监听到有客户端发送请求
SocketChannel channel = (SocketChannel) key.channel();
// 读取客户端发来的请求
ByteBuffer buff = ByteBuffer.allocate(1024);
int size = channel.read(buff);
if (size > 0) {
byte[] b = new byte[size];
buff.flip();
buff.get(b);
String order = new String(b, "UTF-8");
System.out.println("收到客户端命令:" + order);
String content = "";
if (order.equalsIgnoreCase(TIME_ORDER)) {
content = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
content = "命令错误";
// 根据客户端发来的请求做出相应的动作,并将处理结果返回给客户端
doWrite(channel, content);
} else if (size < 0) {
channel.close();
key.cancel();
} catch (Exception e) {
e.printStackTrace();
* 向指定的SocketChannel发送指定的消息。
* @param sc
需要向哪一个SocketChannel发送消息
* @param content 需要发送的消息
* @throws Exception
private void doWrite(SocketChannel sc, String content) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(content.getBytes("UTF-8"));
buffer.flip();
sc.write(buffer);
if (!buffer.hasRemaining()) {
System.out.println("下发消息给客户端:" + content);
package com.winwill.
import java.net.InetSocketA
import java.nio.ByteB
import java.nio.channels.SelectionK
import java.nio.channels.S
import java.nio.channels.SocketC
import java.util.I
* @author qifuguang
* @date 15-2-4 下午1:21
public class SelectorTimeClient implements Runnable {
private static final String TIME_ORDER = "Query Time";
private SocketC
private volatile boolean stop =
* 创建Selector, SocketChannel.
* @param index 客户端编号.
* @throws Exception
public SelectorTimeClient(Integer index) throws Exception {
selector = Selector.open();
channel = SocketChannel.open();
channel.configureBlocking(false);
this.index =
* 轮询监听selector刚兴趣的事件.
public void run() {
System.out.println("第" + index + "个客户端启动!");
// 先尝试异步连接服务器, 如果连接成功,则只需要把channel注册到selector的READ事件,
// 读取服务器返回的结果. 如果不成功(客户端已经向服务器发送了sync包,但是服务器没有返回ack包, 物理链路还没建立成功)
// 则把该channel注册到selector的CONNECT事件, 等待服务器返回的ack包.
if (channel.connect(new InetSocketAddress(8080))) {
channel.register(selector, SelectionKey.OP_READ);
doWrite(channel, TIME_ORDER);
channel.register(selector, SelectionKey.OP_CONNECT);
while (!stop) {
selector.select(1000);
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
SocketChannel sc = (SocketChannel) key.channel();
iterator.remove();
if (key.isValid()) {
if (key.isReadable()) {
// 监听到可读事件, 读取服务器返回的处理结果.
ByteBuffer buff = ByteBuffer.allocate(1024);
int size = sc.read(buff);
if (size > 0) {
byte[] b = new byte[size];
buff.flip();
buff.get(b);
System.out.println("第" + index + "个客户端获取服务器返回时间:" + new String(b));
} else if (size < 0) {
sc.close();
key.cancel();
if (key.isConnectable()) {
//监听到服务器返回了ack包, 准备完成连接的建立
if (sc.finishConnect()) {
// 调用此方法完成物理链路连接的建立
sc.register(selector, SelectionKey.OP_READ); // 建立连接之后注册监听READ事件
doWrite(sc, TIME_ORDER);
System.exit(1);
//否则,程序退出
if (selector != null) {
selector.close();
} catch (Exception e) {
e.printStackTrace();
* 向指定的channel发送指定的消息.
* @param channel 向哪一个channel发送消息
* @param content 需要发送的消息
* @throws Exception
private void doWrite(SocketChannel channel, String content) throws Exception {
ByteBuffer buff = ByteBuffer.allocate(1024);
buff.put(content.getBytes("UTF-8"));
buff.flip();
channel.write(buff);
if (!buff.hasRemaining()) {
System.out.println("第" + index + "个客户端成功发送请求到服务器:" + content);一、概念说明
同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的环境给出的答案是不同的。所以先限定一下本文的环境。本文讨论的背景是Linux环境下的network IO
在进行解释之前,首先要说明几个概念:- 用户空间和内核空间- 进程切换- 进程的阻塞- 文件描述符- 缓存 I/O
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xCxFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0xxBFFFFFFF),供各个进程使用,称为用户空间。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:1. 保存处理机上下文,包括程序计数器和其他寄存器。2. 更新PCB信息。3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。4. 选择另一个进程执行,并更新其PCB。5. 更新内存管理的数据结构。6. 恢复处理机上下文。
注:总而言之就是很耗资源,具体的可以参考这篇文章:
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
二、I/O模式
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:1. 等待数据准备 (Waiting for the data to be ready)2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。- 阻塞 I/O(blocking IO)- 非阻塞 I/O(nonblocking IO)- I/O 多路复用( IO multiplexing)- 信号驱动 I/O( signal driven IO)- 异步 I/O(asynchronous IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model
阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
所以,blocking IO的特点就是在IO执行的两个阶段都被block了,大大消耗了程序执行的时间
非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有,wait for data阶段进程没有等待,但是copydata从内核拷贝到用户进程时,程序是阻塞状态
I/O 多路复用( IO multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block
异步 I/O(asynchronous IO)
linux下的asynchronous IO其实用得很少。先看一下它的流程
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:- A synchronous I/O operation causes the requesting process to be blocked until that I/O- An asynchronous I/O operation does not cause the requesting p
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据
I/O多路复用之select poll epoll 详解&/lixiaoliuer/p/6735821.html
阅读(...) 评论()UNIX环境高级编程(22)
今天突然想到我什么情况下用IO复用什么情况下用多线程呢?于是上网搜浏了下,以下为答案:
多线程模型适用于处理短连接,且连接的打开关闭非常频繁的情形,但不适合处理长连接。多线程模型默认情况下,(在Linux)每个线程会开8M的栈空间,再TCP长连接的情况下,2000/分钟的请求,几乎可以假定有上万甚至十几万的并发连接,假定有10000个连接,开这么多个线程需要G的内存空间!即使调整每个线程的栈空间,也很难满足更多的需求。甚至攻击者可以利用这一点发动DDoS,只要一个连接连上服务器什么也不做,就能吃掉服务器几M的内存,这不同于多进程模型,线程间内存无法共享,因为所有线程处在同一个地址空间中。内存是多线程模型的软肋。
在UNIX平台下多进程模型擅长处理并发长连接,但却不适用于连接频繁产生和关闭的情形。Windows平台忽略此项。 同样的连接需要的内存数量并不比多线程模型少,但是得益于操作系统虚拟内存的Copy on Write机制,fork产生的进程和父进程共享了很大一部分物理内存。但是多进程模型在执行效率上太低,接受一个连接需要几百个时钟周期,产生一个进程 可能消耗几万个CPU时钟周期,两者的开销不成比例。而且由于每个进程的地址空间是独立的,如果需要进行进程间通信的话,只能使用IPC进行进程间通
信,而不能直接对内存进行访问。在CPU能力不足的情况下同样容易遭受DDos,攻击者只需要连上服务器,然后立刻关闭连接,服务端则需要打开一个进程再关闭。
同时需要保持很多的长连接,而且连接的开关很频繁,最高效的模型是非阻塞、异步IO模型。而且不要用select/poll,这两个API的有着O(N)的时间复杂度。在Linux用epoll,BSD用kqueue,Windows用IOCP,或者用libevent封装的统一接口(对于不同平台libevent实现时采用各个平台特有的API),这些平台特有的API时间复杂度为O(1)。 然而在非阻塞,异步I/O模型下的编程是非常痛苦的。由于I/O操作不再阻塞,报文的解析需要小心翼翼,并且需要亲自管理维护每个链接的状态。并且为了充分利用CPU,还应结合线程池,避免在轮询线程中处理业务逻辑。但这种模型的效率是极高的。以知名的http服务器nginx为例,可以轻松应付上千万的空连接&#43;少量活动链接,每个连接连接仅需要几K的内核缓冲区,想要应付更多的空连接,只需简单的增加内存(数据来源为淘宝一位工程师的一次技术讲座,并未实测)。这使得DDoS攻击者的成本大大增加,这种模型攻击者只能将服务器的带宽全部占用,才能达到目的,而两方的投入是不成比例的。
对于第一点有另一种观点:
采用event loop &#43; thread pool模式,linux下用epoll
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:41334次
积分:1016
积分:1016
排名:千里之外
原创:88篇
转载:10篇
(1)(6)(13)(78)【图文】IO复用编程基础_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
IO复用编程基础
大小:466.00KB
登录百度文库,专享文档复制特权,财富值每天免费拿!
你可能喜欢推荐这篇日记的豆列
&&&&&&&&&&&&

我要回帖

更多关于 linux io复用 的文章

 

随机推荐