- 使用NIO API代替阻塞式的网络通信可以开发高性能的网络服务器,原因在于它可以让服务器使用一个或者几个有限几个线程来同时处理链接到服务器的所有客户端。
- 先介绍几个必要的类和方法
- Selector:是Selectable对象的多路复用器,所有希望采用非阻塞式进行通信的的Channel都应该注册到Selector
- 所有的SelectionKey集合,代表了注册该Selector上的Channel
- Selector.select():监控所有注册的Channel
- SelectablChannel:表示支持非阻塞IO操作的Channel对象
- 工作原理:服务器上所有的Channel都向Selector注册,所以该Selector可以监控所有Socked(包括服务器)的IO状态,当其中任意一个或者多个Channel上具有可用的IO操作时,该Selector上的select()就会返回大于0的整数,该数值表示当前有多少具有可用的IO操作,所以通过Selector,使得服务器端只需要不断地调用Selector地select()方法,就可以知道当前有多少Channel有需要处理的IO操作。
- 一下为代码示例,具体的讲解穿插在代码中
//这是服务器端 import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.nio.charset.Charset; public class NServer { private Selector selector = null; // 这一句是创建了一个可以检测当前所有Channel状态的Selector static final int PORT = 30000; // 定义链接的端口号 private Charset charset = Charset.forName("UTF-8"); // 定义编码解码使用的字符集 public void init() throws IOException{ selector = Selector.open(); // 打开Selector ServerSocketChannel server = ServerSocketChannel.open(); // 用open()方法来打开一个未绑定的ServerSockedChannel实例 InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT); // InetSocketAddress类实现IP地址(IP地址+端口号) server.bind(isa); // 将ServerSocketChannel绑定到指定IP地址 server.configureBlocking(false); // 设置是否以阻塞方式工作(否) server.register(selector, SelectionKey.OP_ACCEPT); // 将server注册到指定的Selector对象,并且指定它支持的操作 // 这个while的作用是依次处理selector上每个已选择的SelectionKey,也就是处理需要处理IO操作 while (selector.select() > 0){ for(SelectionKey sk : selector.selectedKeys()){ selector.selectedKeys().remove(sk); // 在这个for遍历的循环中,删除当前遍历到的SelectionKey,也就是说操作一个就删除一个。 // 这个if是指如果sk对应的Channel包含对应的客户端链接请求 if(sk.isAcceptable()){ SocketChannel sc = server.accept(); // 调用accept()方法接受链接,产生服务器端的SockedChannel sc.configureBlocking(false); // 采用非阻塞方式 sc.register(selector,SelectionKey.OP_READ); // 将该SockedChannel也注册到selector上 sk.interestOps(SelectionKey.OP_ACCEPT); // sk为遍历selectedKeys中正在遍历到的那一个,把他对应的Channel设置为准备接受其他请求 } // 这个判断是判断sk对应的Channel是否有数据需要读取 if(sk.isReadable()){ SocketChannel sc = (SocketChannel)sk.channel(); // 一旦判断有数据需要被读取,获取该selectedKey对应的Channel,这个Channel中有需要读取的数据 ByteBuffer buff = ByteBuffer.allocate(1024); // 创建ByteBuffer,接下来的操作将把Channel中的数据读取到ByteBuffer中 String content = ""; // 从try将开始读取数据 try{ while(sc.read(buff) > 0){ // sc是获取到的那个Channel buff.flip(); // 这是“封印未使用”的ByteBuffer content += charset.decode(buff); // 将buff中的数据解码后加到content中 } System.out.println("读取的数据" + content); sk.interestOps(SelectionKey.OP_READ); // 将sk对应的Channel设置成准备下一次读取 } // catch里的操作是如果sk对应的Channel出现了问题,就取消sk在Selector中的注册 catch (IOException e){ sk.cancel(); if(sk.channel() != null){ sk.channel().close(); } } // 在上面已经对content进行了拼接,之后在这里对它的长度进行判定,如果长度大于零,则消息不为空 // 就将content向每个客户端“广播” if(content.length() > 0){ // 这个for和if的作用是遍历每个客户端 for(SelectionKey key : selector.keys()){ Channel targetChannel = key.channel(); if(targetChannel instanceof SocketChannel){ // 这两步是将消息写入到客户端对应的Channel中 SocketChannel dest = (SocketChannel)targetChannel; dest.write(charset.encode(content)); } } } } } } } public static void main(String args[]) throws Exception{ new NServer().init(); } }
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Scanner; public class NClient { private Selector selector = null; // 创建用于检测SockedChannel的Selector对象 static final int PORT = 30000; // 定义端口号 private Charset charset = Charset.forName("UTF-8"); // 定义编码和解码的字符集 private SocketChannel sc = null; // 客户端的SockedChannel public void init() throws IOException{ selector = Selector.open(); // 打开Selector InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT); // InetSocketAddress类实现IP套接字地址(IP地址+端口号) sc = SocketChannel.open(isa); // 调用open方法创建链接到指定主机,指定端口的SockedChannel sc.configureBlocking(false); // 设置为非阻塞方式 sc.register(selector, SelectionKey.OP_READ); // 将SocketChannel实例注册到Selector,并且指定为读取操作的操作集位 new NClientThread().start(); // 创建键盘输入流 Scanner scanner = new Scanner(System.in); // 这个循环是从键盘读取并且将其编码后写入到SockedChannel while(scanner.hasNextLine()){ String line = scanner.nextLine(); sc.write(charset.encode(line)); } } //这个线程是读取服务器数据的线程 private class NClientThread extends Thread{ public void run(){ try{ // 这个循环是遍历有可用IO操作的Channel对应的SelectionKey,包括自己 while(selector.select() > 0){ for(SelectionKey sk : selector.selectedKeys()){ if(sk.isReadable()){ selector.selectedKeys().remove(sk); // 一边操作一边删除 SocketChannel sc = (SocketChannel)sk.channel(); // 获取有需要操作的IO操作对应的Channel ByteBuffer buff = ByteBuffer.allocate(1024); // 创建ByteBuffer。接下来将把Channel中的数据写入到ByteBuffer中 String content = ""; while(sc.read(buff) > 0){ sc.read(buff); buff.flip(); content += charset.decode(buff); } System.out.println("聊天信息"+ content); sk.interestOps(SelectionKey.OP_READ); } } } }catch (IOException e){ e.printStackTrace(); } } } public static void main(String args[]) throws IOException{ new NClient().init(); } }
这是我看李刚编著的《疯狂Java讲义》后总结出来的。
java网络编程(4)使用NIO实现非阻塞式的Soctet通信
最新推荐文章于 2023-07-05 16:14:09 发布