BIO、NIO实战
BIO
BIO:blocking IO,分别写一个服务端和客户端交互的C/S实例。
服务器端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; /** * Created by atai on 2019/3/19. */ public class BIOServer { private String host; private int port; private static Charset charset = Charset.forName( "UTF-8" ); public static void main(String[] args) { int port = 9010 ; try (ServerSocket ss = new ServerSocket(port)) { while ( true ) { Socket s = ss.accept(); BufferedReader reader = new BufferedReader( new InputStreamReader(s.getInputStream(), charset)); String mess = null ; while ((mess = reader.readLine()) != null ) { System.out.println(mess); } s.close(); } } catch (IOException e) { e.printStackTrace(); } } } |
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import java.nio.charset.Charset; import java.util.Scanner; /** * Created by atai on 2019/3/19. */ public class BIOClient implements Runnable { private String host; private int port; private Charset charset = Charset.forName( "UTF-8" ); public BIOClient(String host, int port) { super (); this .host = host; this .port = port; } @Override public void run() { try (Socket s = new Socket(host, port); OutputStream out = s.getOutputStream();) { Scanner scanner = new Scanner(System.in); System.out.println( "请输入:" ); String mess = scanner.nextLine(); out.write(mess.getBytes(charset)); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { BIOClient client = new BIOClient( "localhost" , 9010 ); client.run(); } } |
启动时,记得先启动服务器代码,才能正常启动客户端代码,不然客户端会报连接异常(不存在可用端口号)。
上面的服务器端代码每次只能同时受理一个客户端请求,其他客户端此时只能等待,为了让服务端支持处理多个客户端请求,可以改造成多线程形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public class BIOServerV2 { private static Charset charset = Charset.forName( "UTF-8" ); public static void main(String[] args) { int port = 9010 ; try (ServerSocket ss = new ServerSocket(port)) { while ( true ) { new Thread( new SocketProcess(ss.accept())).start(); } } catch (IOException e) { e.printStackTrace(); } } static class SocketProcess implements Runnable { Socket s; public SocketProcess(Socket s) { super (); this .s = s; } @Override public void run() { try (BufferedReader reader = new BufferedReader( new InputStreamReader(s.getInputStream(), charset))) { String mess = null ; while ((mess = reader.readLine()) != null ) { System.out.println(mess); } } catch (Exception e) { e.printStackTrace(); } } } } |
1、理解什么是阻塞
2、思考:阻塞对服务端有什么影响?
3、阻塞时,服务端什么也干不了,不能处理其他客户端的连接,如何改进?
4、多线程
5、如果并发请求量很大,比如一万、十万,会有什么问题?
6、32位系统1个线程对象默认最大需要320KB内存,64位系统默认最大需要1M内存,业务对象也需要内存,内存会不足。过多的线程需要OS频繁切换,也会大大影响性能。
7、怎么办?
8、线程池
既然使用线程池可以避免频繁创建、销毁、切换线程,那就写一个使用线程池的服务端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | public class BIOServerV3 { private static Charset charset = Charset.forName( "UTF-8" ); public static void main(String[] args) { int port = 9010 ; int threads = 100 ; ExecutorService tpool = Executors.newFixedThreadPool(threads); try (ServerSocket ss = new ServerSocket(port)) { while ( true ) { Socket s = ss.accept(); // 丢到线程池中执行 tpool.execute( new SocketProcess(s)); } } catch (Exception e) { e.printStackTrace(); } tpool.shutdown(); } static class SocketProcess implements Runnable { Socket s; public SocketProcess(Socket s) { super (); this .s = s; } @Override public void run() { try (BufferedReader reader = new BufferedReader( new InputStreamReader(s.getInputStream(), charset))) { String mess = null ; while ((mess = reader.readLine()) != null ) { System.out.println(mess); } } catch (Exception e) { e.printStackTrace(); } } } } |
9、阻塞对线程池的方式有什么影响?
10、阻塞等待接收客户端的数据时,这段时间占着线程,而池中线程数是有限的,并发量大时,将导致没有线程处理请求,请求的响应时间长,甚至拒绝服务。
11、如果能不阻塞,在没有数据时,就去干点别的事情,有数据了才处理数据那该多好。
这个时候,终于等到NIO闪亮登场。
NIO
NIO:new IO,java1.4开始推出的可非阻塞IO,在java.io包中。特点如下:
1、可解决BIO阻塞的不足;
2、但比BIO学习、使用复杂;
3、可以以阻塞、非阻塞两种方式工作;
4、在非阻塞模式下,可以用少量(甚至一个)线程处理大量的IO连接;
5、Java7推出了NIO.2(又称AIO,即异步IO)
Select选择器:非阻塞模式下,一个选择器可检测多个SelectableChannel,获得为读写等操作准备好的通道,就不需要我们用循环去判断了。
Selector的用法:
1、创建Selector
1 | Selector selector = new Selector.open(); |
2、将要交给Selector检测的SelectableChannel注册进来
1 2 | channel.configureBlocking( false ); // 注意:一定要设为非阻塞模式 SelectionKey key = channel.register(selector, SelectionKey.OP_READ); |
channel.register方法的第二个参数指定要selector帮忙监听的就绪操作:
1 2 3 4 | SelectionKey.OP_CONNECT SelectionKey.OP_ACCEPT SelectionKey.OP_READ SelectionKey.OP_WRITE |
3、通过Selector来选择就绪的Channel,有三个select方法
1 2 3 | int select() // 阻塞直到有就绪的Channel int select( long timeout) // 阻塞最长多久 int selectNow() // 不阻塞这 |
这三个方法返回值:就绪的Channel数量
1 | int n = selector.select(); |
注意:select()方法返回当前的就绪数量。
4、获得就绪的SelectionKey集合(当有就绪的Channel时)
1 | Set<SelectionKey> selectedKey = selector.selectedKeys(); |
5、处理selectedKeys set(详见后面的服务端代码)
Channel通道:数据的来源或去向目标
1、Channel的实现
FileChannel(只能用于BIO)
DatagramChannel
SocketChannel
SocketChannel
ServerSocketChannel
2、各Channel的API方法
open():创建通道
read(Buffer):从通道中读取数据放入到buffer
write(Buffer):将buffer中的数据写给通道
Buffer缓冲区,数据的临时存放区
ByteBuffer、MappedByteBuffer、CharBuffer、DoubleBuffer等
Buffer的基本使用步骤:
1、调用xxxBuffer.allocate(int)创建Buffer
2、调用put方法往Buffer中写数据
3、调用buffer.flip()将buffer转为读模式
4、读取buffer中的数据
5、清理数据buffer.clear(),整理数据buffer.compact()
Buffer的三个重要属性capacity、position、limit
以下是NIO代码的具体实例。
服务器端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | public class NioServer { private static Charset charset = Charset.forName( "UTF-8" ); private static CharsetDecoder decoder = charset.newDecoder(); public static void main(String[] args) throws IOException { // 创建一个selector Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); int port = 9200 ; ssc.bind( new InetSocketAddress(port)); // 2注册到selector // 设置非阻塞 ssc.configureBlocking( false ); // ssc向selector注册,监听连接到来 ssc.register(selector, SelectionKey.OP_ACCEPT); // 连接的计数 int connectionCount = 0 ; // 极少量线程 int threads = 3 ; ExecutorService tpool = Executors.newFixedThreadPool(threads); while ( true ) { // 阻塞等待就绪的事件 int readyChannelCount = selector.select(); if (readyChannelCount == 0 ) { continue ; } Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. ServerSocketChannel ssssc = (ServerSocketChannel) key.channel(); // 接收连接 SocketChannel cc = ssssc.accept(); // 请selectoror帮忙监测数据到了没 cc.configureBlocking( false ); // 向selector注册 cc.register(selector , SelectionKey.OP_READ, ++connectionCount); } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading // 交给线程池去处理数据读 tpool.execute( new SocketProcess(key)); // 取消Selector注册,防止线程池处理不及时,重复选择 key.cancel(); } else if (key.isWritable()) { // a channel is ready for writing } // 处理后,一定要从selectedKey集合中移除 keyIterator.remove(); } } } static class SocketProcess implements Runnable { SelectionKey key; public SocketProcess(SelectionKey key) { super (); this .key = key; } @Override public void run() { try { System.out.println( "连接" + key.attachment() + " 发来了:" + readFromChannel()); key.channel().close(); } catch (Exception e) { e.printStackTrace(); } } private String readFromChannel() throws IOException { SocketChannel sc = (SocketChannel) key.channel(); int bfsize = 1024 ; ByteBuffer rbf = ByteBuffer.allocateDirect(bfsize); // 定义一个更大的buffer ByteBuffer bigBf = null ; // 读的次数 int count = 0 ; while ((sc.read(rbf) != - 1 )) { count++; ByteBuffer temp = ByteBuffer.allocateDirect(bfsize * (count + 1 )); if (bigBf != null ) { // 将buffer由写转为读模式 bigBf.flip(); temp.put(bigBf); } bigBf = temp; // 将这次读到的数据放入大buffer rbf.flip(); bigBf.put(rbf); // 为了下次读,清理Buffer rbf.clear(); } if (bigBf != null ) { bigBf.flip(); try { // 将字节转为字符,返回接收到的字符串 return decoder.decode(bigBf).toString(); } catch (CharacterCodingException e) { e.printStackTrace(); } } return null ; } } } |
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class NioClient { private static Charset charset = Charset.forName( "UTF-8" ); public static void main(String[] args) { try (SocketChannel sc = SocketChannel.open()) { boolean connected = sc.connect( new InetSocketAddress( "localhost" , 9200 )); System.out.println( "connected=" + connected); // 写 Scanner scanner = new Scanner(System.in); System.out.println( "请输入:" ); String mess = scanner.nextLine(); ByteBuffer bf = ByteBuffer.wrap(mess.getBytes(charset)); while (bf.hasRemaining()) { int writedCount = sc.write(bf); } } catch (Exception e) { e.printStackTrace(); } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
2018-03-19 [转]JDK动态代理