咱们先来看一段传统IO的代码java
public class OioServer { public static void main(String[] args) throws IOException { //这里能够直接写成ServerSocket server = new ServerSocket(10101); ServerSocket server = new ServerSocket(); server.bind(new InetSocketAddress(10101)); System.out.println("服务器启动"); while (true) { //此处会阻塞 Socket socket = server.accept(); System.out.println("来了一个新客户端"); handler(socket); } } 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 (IOException e) { e.printStackTrace(); }finally { try { System.out.println("socket关闭"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用telnet链接web
admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.shell
咱们会看到OioServer的运行状况缓存
服务器启动
来了一个新客户端服务器
可是当咱们又使用一个telnet链接进来的时候,OioServer的运行状况没变,说明一个服务端只能接收一个客户端点链接,缘由在于Socket socket = server.accept();发生了堵塞,如今咱们将其改写成多线程websocket
public class OioServerThread { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(10101); ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); System.out.println("服务器启动"); while (true) { Socket socket = server.accept(); System.out.println("来了一个新客户端"); service.execute(() -> handler(socket)); } } 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 (IOException e) { e.printStackTrace(); }finally { try { System.out.println("socket关闭"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
运行可知,当咱们启动了多个telnet进行链接的时候,它是能够一块儿链接进来的多线程
服务器启动
来了一个新客户端
来了一个新客户端并发
可是这里有一个问题,咱们线程池的可用线程是有限的,不可能无限提供线程来接收大量客户端的链接,早晚它会无响应被堵塞的。socket
咱们如今来看一下NIO,NIO实际上是使用传统IO的特性建立一个channel(通道),经过该通道来注册事件SelectionKey测试
SelectionKey有四种事件
这里 注意,下面两种,SelectionKey.OP_READ ,SelectionKey.OP_WRITE ,
1.当向通道中注册SelectionKey.OP_READ事件后,若是客户端有向缓存中write数据,下次轮询时,则会 isReadable()=true;
2.当向通道中注册SelectionKey.OP_WRITE事件后,这时你会发现当前轮询线程中isWritable()一直为ture,若是不设置为其余事件
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(10101); server.listen(); } }
NIO与传统IO最大的不一样
使用telnet测试,NIO是确定支持多个客户端同时操做的,但很重要的一点是NIO是单线程的,传统IO和NIO的逻辑以下
传统IO
NIO
至于NIO如何多线程,能够参考NIO如何多线程操做 ,这其实也是Netty的原理。
分别用两个telnet链接
admindeMacBook-Pro:IOServer admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
dsfds
好的
admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
22222
好的
服务端显示以下
服务端启动成功!
新的客户端链接
服务端收到信息:dsfds
新的客户端链接
服务端收到信息:22222
当咱们退出其中一个的时候
admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
22222
好的^]
telnet> quit
Connection closed.
服务端显示以下
服务端启动成功!
新的客户端链接
服务端收到信息:dsfds
新的客户端链接
服务端收到信息:22222
客户端关闭
若是咱们使用telnet链接进去之后,直接关闭shell,则服务端会抛出异常
服务端启动成功!
新的客户端链接
服务端收到信息:
Exception in thread "main" java.io.IOException: Connection reset by peer
at sun.nio.ch.FileDispatcherImpl.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:197)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at com.guanjian.websocket.io.NIOServer.handelerRead(NIOServer.java:111)
at com.guanjian.websocket.io.NIOServer.handler(NIOServer.java:77)
at com.guanjian.websocket.io.NIOServer.listen(NIOServer.java:59)
at com.guanjian.websocket.io.NIOServer.main(NIOServer.java:134)
说明在读取Buffer缓冲区的时候,抛出了异常,因此咱们应该在读取的时候捕获异常,而不是抛出异常
/** * 处理读的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) { // 服务器可读取消息:获得事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); try { 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(); } } catch (IOException e) { e.printStackTrace(); } }
咱们如今来证实NIO是单线程的,将以上代码修改一下
/** * 处理读的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) { // 服务器可读取消息:获得事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); try { int read = channel.read(buffer); Thread.sleep(60000); 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(); } } catch (Exception e) { e.printStackTrace(); } }
咱们让他发送消息的时候睡一分钟。启动服务端,链接第一个telnet进来,并发几个字符
此时咱们连进第二个telnet,会发现服务端没反应,须要等到一分钟以后,第一个telnet才会收到"好的",而服务端才会显示"新的客户端链接"。
说明服务端在处理发送字符的时候被阻塞,NIO为单线程。