NIO(生活篇)
2018-11-26 07:57:08来源:博客园 阅读 ()
今晚是个下雨天,写完今天最后一行代码,小鲁班起身合上电脑,用滚烫的开水为自己泡制了一桶老坛酸菜牛肉面。这大概是苦逼程序猿给接下来继续奋战的自己最好的馈赠。年轻的程序猿更偏爱坐在窗前,在夜晚中静静的享受独特的泡面香味。。。
科班出身的小鲁班虽然写了N多复杂(CRUD)代码,但仍口味清淡,他们往往不加或少加料包,由泡面热腾腾的蒸汽熏蒸自己的脸频,润湿又干又涩的双眼,抚慰受伤的心灵。然后,看着外边依然还是熙熙攘攘的车流和不属于自己的任何一个亮灯的窗口,却思考着如何才能成为ー个名垂青史的程序猿。小鲁班不迷茫。。。
"我们一起学猫叫,一起喵喵喵"~~~小鲁班放在书桌上的大哥大手机突然响了,打破了小鲁班脑子里美好的yy
小鲁班心想:都这么晚了,谁TM还打电话过来,拿起电话一看,哦原来是他表哥鲁班大师
鲁班大师:小老弟。晚上好嘛!
小鲁班:嘤嘤嘤,原来是大表哥呀,能和你通话真让我难以置信呀。
鲁班大师:听说你今天早上请两老去馆子喝早茶去了呀,有钱人,看来混的很不错嘛。
小鲁班:哎,别提了,等了半天才通知有位置(接待阻塞),坐下之后又没人来负责写菜单(点餐阻塞),写完菜单又没有人负责上菜,我去~气死老子
鲁班大师:哈哈哈哈哈,这馆子的老板也太奥特曼(out)了,现在规模大点的饭馆都采用NIO(同步非阻塞IO)模式啦。
小鲁班:额?,NIO是什么鬼,这和饭馆有什么关系呢?
鲁班大师: emmmmm,故事得从一段很长很长的网络编程模式历史开始说起呢~
S1.传统的网络编程模式(单线程下的通信)
在单线程模式下,IO操作没完成的时候,无法返回,造成服务器线程阻塞,其他客户端不能连上服务端。
在只有一个餐厅服务员的情况下,服务员接待了一位客人,客人到餐桌上坐下后,服务员等待客人点餐,此时又有一个客人来吃饭,但是已经没有服务员去接待了,因为这个服务员在等待第一个客人点餐,直到第一个客人点完餐后,服务员把菜单交给厨房,然后才能去接待第二个进来的客人。。。(这样的服务客人早就走了)
那么我们来看看如何改进
S2改良后网络编程模式(多线程)
在S1中我们发现了一些问题,当IO阻塞的时候,服务端无法接受请求,因此S2改用了多线程模式
在多线程模式下,只要有客户端连进来,我们都会为之创建一个线程专门去处理客户端的IO操作。当完成之后,线程就会自动销毁。但是这样会带来一个问题,就是线程的频繁的创建和销毁非常消耗服务器的资源。
饭馆里的老板面对这种情况,只好继续请服务员去写菜单了,来一个客人,就请一个服务员去负责客人的单子,问题是请服务员非常消耗老板的money呀,而且当写完单子后又要计算工资,这个过程非常耗时间。
S3继续改良后的网络编程模式(线程池)
S2我们发现了这样的问题就是线程的创建和销毁非常损耗系统的性能,因此我们想到JDBC中连接池的解决方案,同样的,这里我们可以创建线程池
启动服务后,事先创建100个线程,当有客户端连进来的时候,不需要创建,就给他分配一个线程用于IO读写操作,当客户完成IO操作完成之后就归还线程到线程池中而不是销毁,这样做的好处就是解决了在运行的时候线程创建和销毁对系统资源的损耗。同时也暴露了一些问题,其一在高并发的情况下,线程池中的线程不够用了,此时会造成客户端等待阻塞(当然也可以继续创建线程来解决),其二多线程环境下由于线程抢夺CPU资源的随机性,使得线程一起频繁的进行上下文的切换,消耗的大量的资源,而这些资源本来是用来处理业务用的,现在则用来切换线程,这就大大的降低了系统有效的资源真正利用率。
老板觉得一直请人不划算,干脆就请30个人是在餐厅一个角落待命,当有客人坐下来的时候,就分配一个服务员去点餐。但是当有31客人同时来的时候,假设30个服务员都在等待写单,那么第31个客人假如要点餐的时候就没人为他服务了,同时点完餐时候的,突然客人想加餐,此时每个服务员都想着去抢到这个客户,竞争过程消耗了时间,同时得知道刚刚的账单都点了些什么还要交接任务,就更浪费人力物力。
S4再次改良后的网络编程模式(NIO)
S3我们发现线程池不够用,以及高并发情况下频繁线程抢夺CPU资源的损耗性能的问题。主要原因都是线程太多引起的,因此我们可以通过改变线程的创建时机,不是Socket刚刚连上来的时候创建线程,而是等待需要进行IO操作的时候再去创建线程。
这张图对比上面的题我们发现多个三个陌生的面孔,下面介绍一下他们
Channel表示为一个已经建立好的支持I/O操作的实体(如文件和网络)的连接,在此连接上进行数据的读写操作,使用的是Buffer缓冲区来实现读写。
ServerSocketChannel------->open() 获得实例 ----------->register(selector,accept) 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件
Selector一个专门的选择器来同时对多个Socket通道进行监听(轮询,不算阻塞),当其中的某些Socket通道上有它感兴趣的事件发生时,这些通道就会变为可用状态,当状态是IO状态的时候,就会为他分配一个线程处理业务,当不是IO状态的时候只会为他注册一个连接,不会分配线程,这样的话就保证了,系统中存在的线程都是用来处理业务的而不是用来等待的,这样就能够减少线程,也就减少了线程上下文的切换损耗资源。
Selector----->open()获得实例------>select()监听动作(读还是写还是连接,相当于之前的accept()方法),通过源码发现SelectorProvider.provider().poll()依赖于操作系统创建
Buffer缓冲区,里边有些方法例如clear、flip、rewind都是操作limit和position的值来实现重复读写,这样的话IO就不会阻塞,不会出现客户端在写入的时候,服务端不能写出造成线程的阻塞。
position(初始的位置,读的时候,位置会移动)
limit(当你读取完成了,数据需要进行固定flip(),limit=position)
capacity(数组大小的一个容量)
clear()把position回归到原位
其实这里的Selector相当于一个接待主管,当有一个客人从大门(Channel)进来来吃饭的时候,先带它到位置上,给他安排一个台号,然后一直监听客人的需求,当客人需要点餐的时候,此时接待主管监听到了,就立马给他分配一个服务员去帮你它完成点餐,当客人需要加餐的时候,接待主管分配服务员到指定台号,然后只需要在账单(Buffer)上添加即可!
小鲁班:哇塞,有点晕,但是我还是能看懂的,这些都是概念,表哥有代码么?
鲁班大师:代码嘛,我今天打排位的时候用鲁班被喷没皮肤,咳咳!
小鲁班:皮肤好说好说!
1.OIO服务端代码
public class OioServer { @SuppressWarnings("resource") 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(); } } } }
2.NIO服务端代码
public class NIOServer { // 通道管理器 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(); } }
鲁班大师:好好理解,有什么不懂的就留言,你表哥先撤了,皮肤可别忘了送,不早了晚安~
小鲁班:好滴!
转载请注明出处,谢谢
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- 初识Java 2020-04-07
- Java世界里的BIO,NIO,AIO 2020-04-04
- Netty:初识Netty 2020-04-01
- Java NIO:NIO概述 2020-04-01
- NIO中的ZeroCopy 2020-03-22
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash