Netty入门学习(一)——传统IO与NIO比较
传统IO与NIO比较
传统的socket IO
传统的socket IO中,需要为每个连接创建一个线程,当并发的连接数量非常巨大时,线程所占用的栈内存和CPU线程切换的开销将非常巨大。并且在单线程的时候只能有一个客户端,虽然使用线程池可以有多个客户端连接,但是性能消耗非常大。当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。
上图为OIO的工作模式,我们将系统看成一个餐厅,serverScoket监听的端口看成是大门,socket看成为客人,服务员则为线程。现在餐厅开业了,一个socket请求过来后,OIO监听到请求进来就派一个线程去服务这个请求,这个时候这个线程会全程都为客人服务,直到这个客人完成所有的操作离开后线程才会继续在门口等待新的请求进来。现在假设一个线程正在为一个请求服务,这时候又有一个客人进入了餐厅,但是由于这个时候餐厅只有一个服务员(单线程模式),所以这个客人只能在门口等待,服务员结束服务后开始用餐。假设现在餐厅老板聘请了多个服务员(多线程模式),但是每一个线程还是服务于一个顾客,并且等待客人用餐结束。也就是说多少个客人来同时用餐,就要起多少个线程。我们算一个服务员的工资为4000,这时候有10位客人来了。那我们就需要10位服务员,老板就要支付4000*10的工资。这样的开销很大(cpu的开销)。
public static void main(String[] args) throws Exception { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //创建socket服务,监听10101端口 ServerSocket server=new ServerSocket(10101); System.out.println("服务器启动!"); while(true){ //获取一个套接字(阻塞) final Socket socket = server.accept(); System.out.println("来个一个新客户端!"); newCachedThreadPool.execute(new Runnable() { @Override public void run() { //业务处理 handler(socket); } }); } } /** * 读取数据 * @param socket * @throws Exception */ public static void handler(Socket socket){ try { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while(true){ //读取数据(阻塞) int read = inputStream.read(bytes); if(read != -1){ System.out.println(new String(bytes, 0, read)); }else{ break; } } } catch (Exception e) { e.printStackTrace(); }finally{ try { System.out.println("socket关闭"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
NIO
非堵塞IO上图为NIO的工作模式,我们还是将系统看成餐厅,监听的端口为大门,客户端连接或请求为客人,服务员为线程+selector。假设餐厅开业,这个时候我们餐厅有一个前台的招待生,还有在里面为顾客服务的服务员,这个时候一个顾客进来后,我们就会判断这个请求是连接的请求还是,其他的事件。这个时候前台的服务生会接待这个顾客,并且带该顾客去做到位置上,通知餐厅里面的服务员有新的顾客来,需要为他服务。这是餐厅里面的服务员是可以服务多个的。顾客进来后我们只需要带他做到位置上,并且把菜单给顾客浏览,这个时候另外一边的顾客可能有新的需求,服务员可以过去给另一个顾客服务。这样一个餐厅可以多客人营业,并且服务员个人减少。比起OIO餐厅这样的开销是较小的(线程数少,资源开销小)。
// 通道管理器 private Selector selector; /** * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 * * @param port * 绑定的端口号 * @throws IOException */ public void initServer(int port) throws IOException { // 获得一个ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 设置通道为非阻塞 serverChannel.configureBlocking(false); // 将该通道对应的ServerSocket绑定到port端口 serverChannel.socket().bind(new InetSocketAddress(port)); // 获得一个通道管理器 this.selector = Selector.open(); // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * * @throws IOException */ public void listen() throws IOException { System.out.println("服务端启动成功!"); // 轮询访问selector while (true) { // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞 selector.select(); // 获得selector中选中的项的迭代器,选中的项为注册的事件 Iterator<?> ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 删除已选的key,以防重复处理 ite.remove(); handler(key); } } } /** * 处理请求 * * @param key * @throws IOException */ public void handler(SelectionKey key) throws IOException { // 客户端请求连接事件 if (key.isAcceptable()) { handlerAccept(key); // 获得了可读的事件 } else if (key.isReadable()) { handelerRead(key); } } /** * 处理连接请求 * * @param key * @throws IOException */ public void handlerAccept(SelectionKey key) throws IOException { ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获得和客户端连接的通道 SocketChannel channel = server.accept(); // 设置成非阻塞 channel.configureBlocking(false); // 在这里可以给客户端发送信息哦 System.out.println("新的客户端连接"); // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); } /** * 处理读的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) throws IOException { // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 创建读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); int read = channel.read(buffer); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服务端收到信息:" + msg); //回写数据 ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes()); channel.write(outBuffer);// 将消息回送给客户端 }else{ System.out.println("客户端关闭"); key.cancel(); } } /** * 启动服务端测试 * * @throws IOException */ public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8000); server.listen(); }
为什么普通IO会堵塞,但是NIO 不会堵塞?
阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。
NIO的服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。