JAVA基础知识之网络编程——-基于NIO的非阻塞Socket通信
阻塞IO与非阻塞IO
通常情况下的Socket都是阻塞式的, 程序的输入输出都会让当前线程进入阻塞状态, 因此服务器需要为每一个客户端都创建一个线程。
从JAVA1.4开始引入了NIO API, NIO可以实现非阻塞IO, 这样就可以使用一个线程处理所有的客户请求。
基于NIO的非阻塞Socket通信
服务器将用来监听客户端请求的channel注册到selector上,启动一个线程,使用selector的select()获取求情的客户端的channel数量,
当监听到有客户端请求时,就通过SelectionKey返回对应的客户端channel进行通信。
下面是一个非常简单的例子,
服务器端
1 package niochat; 2 3 import java.io.IOException; 4 import java.net.InetSocketAddress; 5 import java.nio.ByteBuffer; 6 import java.nio.channels.Channel; 7 import java.nio.channels.SelectionKey; 8 import java.nio.channels.Selector; 9 import java.nio.channels.ServerSocketChannel; 10 import java.nio.channels.SocketChannel; 11 import java.nio.charset.Charset; 12 13 public class NServer { 14 //用于检查所有channel状态的selector 15 private Selector selector = null; 16 static final int PORT = 3001; 17 private Charset charset = Charset.forName("utf-8"); 18 public void init() throws IOException, InterruptedException { 19 selector = Selector.open(); 20 //通过open方法打开一个未绑定的ServerSocketChannel实例 21 ServerSocketChannel server = ServerSocketChannel.open(); 22 InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT); 23 server.bind(isa); 24 //设置非阻塞 25 server.configureBlocking(false); 26 //注册ServerSocketChannel到selector 27 server.register(selector, SelectionKey.OP_ACCEPT); 28 29 //服务器端需要轮询selector看是否有channel需要通信 30 while ( true ) { 31 //select()将会一直阻塞,直到选择了至少一个channel(进行通信),此时sector就会调用wakeup(),select()方法才能返回 32 int num = selector.select(); 33 Thread.sleep(1000); 34 //select()方法返回后,用selectedKeys()返回对应channel的SelectionKey集合,通过key.channel()方法可以返回对应的channel实例 35 //被选中的key集合selectedKeys表示需要进行IO处理的channel集合,一个key代表一个channel 36 System.out.println("SelectionKey.size = "+selector.selectedKeys().size()); 37 for (SelectionKey sk : selector.selectedKeys()) { 38 //从selector上的已选择的key集合中删除正在处理的key 39 System.out.println("SelectionKey.value = "+sk); 40 selector.selectedKeys().remove(sk); 41 //如果sk对应channel包含客户端连接请求 42 if (sk.isAcceptable()) { 43 //调用accept方法接受请求,产生一个服务器端的SocketChannel 44 //在非阻塞模式下,如果没有连接则直接返回null 45 SocketChannel sc = server.accept(); 46 //设置非阻塞模式 47 sc.configureBlocking(false); 48 //将SocketChannel也注册到selector 49 sc.register(selector, SelectionKey.OP_READ); 50 //再将sk对应的channel设置为请求准备接受其他请求 51 sk.interestOps(SelectionKey.OP_ACCEPT); 52 } 53 54 //如果sk对应的channel有数据需要读取 55 if (sk.isReadable()) { 56 //获取sk对应的channel 57 SocketChannel sc = (SocketChannel)sk.channel(); 58 //channel中的数据必须先写入buffer中,然后才能写入进content中 59 ByteBuffer buff = ByteBuffer.allocate(1024); 60 String content = ""; 61 try { 62 while(sc.read(buff) > 0) { 63 //buffer 复位 64 buff.flip(); 65 content += charset.decode(buff); 66 } 67 System.out.println("读取的数据:"+ content); 68 //sk复位 69 sk.interestOps(SelectionKey.OP_READ); 70 } //遇到channel有异常说明客户端有异常,取消注册此sk 71 catch (IOException ex) { 72 //从已选择key集合中取消sk,下一次select()时此channel将自动被删除 73 ex.printStackTrace(); 74 75 sk.cancel(); 76 if(sk.channel() != null) { 77 sk.channel().close(); 78 } 79 80 } 81 82 if (content.length() > 0) { 83 //广播 84 for(SelectionKey key : selector.keys()) { 85 Channel targetChannel = key.channel(); 86 if (targetChannel instanceof SocketChannel) { 87 SocketChannel dest = (SocketChannel)targetChannel; 88 dest.write(charset.encode(content)); 89 } 90 } 91 } 92 } 93 94 } 95 } 96 } 97 98 public static void main(String[] args) throws IOException, InterruptedException { 99 new NServer().init(); 100 } 101 }
客户端
1 package niochat; 2 3 import java.io.IOException; 4 import java.net.InetSocketAddress; 5 import java.nio.ByteBuffer; 6 import java.nio.channels.Channel; 7 import java.nio.channels.SelectionKey; 8 import java.nio.channels.Selector; 9 import java.nio.channels.SocketChannel; 10 import java.nio.charset.Charset; 11 import java.util.Scanner; 12 13 public class NClient { 14 //定义检测SocketChannel的selector 15 private Selector selector = null; 16 private static final int PORT = 3001; 17 private Charset charset = Charset.forName("utf-8"); 18 //客户端的SocketChannel 19 private SocketChannel sc = null; 20 public void init() throws IOException { 21 selector = Selector.open(); 22 InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT); 23 //调用open静态方法创建连接到指定主机的SocketChannel 24 sc = SocketChannel.open(isa); 25 sc.configureBlocking(false); 26 sc.register(selector, SelectionKey.OP_READ); 27 //创建子线程读取服务器返回的数据 28 new ClientThread().start(); 29 //键盘输入流 30 Scanner scan = new Scanner(System.in); 31 while (scan.hasNextLine()) { 32 String line = scan.nextLine(); 33 //放进SocketChannel 34 sc.write(charset.encode(line)); 35 } 36 } 37 38 private class ClientThread extends Thread { 39 public void run() { 40 try { 41 while (selector.select() > 0) { 42 //被选中的key集合selectedKeys表示需要进行IO处理的channel集合 43 for (SelectionKey sk : selector.selectedKeys()) { 44 //删除正在处理的key 45 selector.selectedKeys().remove(sk); 46 if (sk.isReadable()) { 47 SocketChannel sc = (SocketChannel)sk.channel(); 48 ByteBuffer buff = ByteBuffer.allocate(1024); 49 String content = ""; 50 while (sc.read(buff) > 0) { 51 sc.read(buff); 52 buff.flip(); 53 content += charset.decode(buff); 54 } 55 System.out.println("聊天信息:"+content); 56 sk.interestOps(SelectionKey.OP_READ); 57 } 58 } 59 } 60 } catch (IOException e) { 61 e.printStackTrace(); 62 } 63 } 64 } 65 66 public static void main(String[] args) throws IOException { 67 new NClient().init(); 68 } 69 }
执行结果,启动了一个服务器端,然后启动了两个客户端,