日常中我们最常用的网络通信有UDP和TCP,其中TCP是最为常用的。TCP连接使用Socket来实现,一般最为常用的Socket为 BIO,既阻塞流,模型如下图。
即每当有新的连接连入后都开启一个线程来处理这次的连接,这样当连接量过大时会导致线程数越来越多,而线程是非常耗费资源的。自JDK1.4后引入了NIO我们便可以摆脱这种情况。
NIO的连接模型如下
接下来看一看NIO的使用
首先是服务端代码
1.启动类
public class Server { private static int DEFAULT_PORT = 8090; private static TaskHandle taskHandle; public static void start(){ start(DEFAULT_PORT); } public static synchronized void start(int port){ if(taskHandle!=null) taskHandle.stop(); taskHandle = new TaskHandle(port); new Thread(taskHandle,"Server").start(); } public static void main(String[] args){ start(); } }
2.任务处理类
package nio.socketChennel; 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.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class TaskHandle implements Runnable { private Selector selector; private ServerSocketChannel serverChannel; private volatile boolean started; /** * 构造方法 * * @param port * 指定要监听的端口号 */ public TaskHandle(int port) { try { // 创建选择器 selector = Selector.open(); // 打开监听通道 serverChannel = ServerSocketChannel.open(); // 如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式 serverChannel.configureBlocking(false);// 开启非阻塞模式 /* * 绑定端口 backlog设为1024 backlog参数的含义是等待队列的长度,既当前连接正在处理时, * 最多能有backlog个连接排队等候,当队列满后,再请求的连接会被拒绝掉 */ serverChannel.socket().bind(new InetSocketAddress(port), 1024); // 监听客户端连接请求 serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 标记服务器已开启 started = true; System.out.println("服务器已启动,端口号:" + port); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } public void stop() { started = false; } @Override public void run() { // 循环遍历selector while (started) { try { // 无论是否有读写事件发生,selector每隔1s被唤醒一次 selector.select(1000); // 阻塞,只有当至少一个注册的事件发生的时候才会继续. // selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while (it.hasNext()) { key = it.next(); it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Throwable t) { t.printStackTrace(); } } // selector关闭后会自动释放里面管理的资源 if (selector != null) try { selector.close(); } catch (Exception e) { e.printStackTrace(); } } private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { // 处理新接入的请求消息 if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); // 通过ServerSocketChannel的accept创建SocketChannel实例 // 完成该操作意味着完成TCP三次握手,TCP物理链路正式建立 SocketChannel sc = ssc.accept(); // 设置为非阻塞的 sc.configureBlocking(false); // 注册为读 sc.register(selector, SelectionKey.OP_READ); } // 读消息 if (key.isReadable()) { SocketChannel sc = (SocketChannel) key.channel(); // 创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); // 读取到字节,对字节进行编解码 if (readBytes > 0) { // 将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作 /* * 将缓存字节数组的指针设置为数组的开始序列即数组下标0。这样就可以从buffer开头,对该buffer进行遍历(读取 * )了。 */ buffer.flip(); // 根据缓冲区可读字节数创建字节数组 /* * ByteBuffer.remaining(),此方法返回剩余的可用长度,此长度为实际读取的数据长度,即缓冲区可读字节数长度 * ByteBuffer.get(byte[]),从ByteBuffer中读取byte[] */ byte[] bytes = new byte[buffer.remaining()]; // 将缓冲区可读字节数组复制到新建的数组中 buffer.get(bytes); String expression = new String(bytes, "UTF-8"); System.out.println("服务器收到消息:" + expression); // 处理数据 String result = null; try { // do something ..... result = "OK I get it!"; } catch (Exception e) { result = "计算错误:" + e.getMessage(); } // 发送应答消息 doWrite(sc, result); } // 没有读取到字节 忽略 // else if(readBytes==0); // 链路已经关闭,释放资源 else if (readBytes < 0) { key.cancel(); sc.close(); } } } } // 异步发送应答消息 private void doWrite(SocketChannel channel, String response) throws IOException { // 将消息编码为字节数组 byte[] bytes = response.getBytes(); // 根据数组容量创建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); // 将字节数组复制到缓冲区 writeBuffer.put(bytes); // flip操作 writeBuffer.flip(); // 发送缓冲区的字节数组 channel.write(writeBuffer); // ****此处不含处理“写半包”的代码 } }
客户端代码
1.启动类
public class Client { private static String DEFAULT_HOST = "127.0.0.1"; private static int DEFAULT_PORT = 8090; private static ClientHandle clientHandle; private static int sign = 1; public static void start(){ start(DEFAULT_HOST,DEFAULT_PORT); } public static synchronized void start(String ip,int port){ if(clientHandle!=null) clientHandle.stop(); clientHandle = new ClientHandle(ip,port,sign); sign = sign + 1; new Thread(clientHandle,"Server").start(); } //向服务器发送消息 public static boolean sendMsg(String msg) throws Exception{ if(msg.equals("q")) return false; ClientHandle clientHandle = new ClientHandle(DEFAULT_HOST,DEFAULT_PORT,sign); new Thread(clientHandle,"Server").start(); Thread.sleep(1000); clientHandle.sendMsg(msg); sign = sign + 1; return true; } public static void main(String[] args){ start(); } }
2.连接线程类
package nio.socketChennel; 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.util.Iterator; import java.util.Set; public class ClientHandle implements Runnable { private String host; private int port; private Selector selector; private SocketChannel socketChannel; private volatile boolean started; private int sign; public ClientHandle(String ip,int port, int sign) { this.host = ip; this.port = port; this.sign = sign; try{ //创建选择器 selector = Selector.open(); //打开监听通道 socketChannel = SocketChannel.open(); //如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式 socketChannel.configureBlocking(false);//开启非阻塞模式 started = true; }catch(IOException e){ e.printStackTrace(); System.exit(1); } } public void stop(){ started = false; } @Override public void run() { try{ doConnect(); }catch(IOException e){ e.printStackTrace(); System.exit(1); } //循环遍历selector while(started){ try{ //无论是否有读写事件发生,selector每隔1s被唤醒一次 selector.select(1000); //阻塞,只有当至少一个注册的事件发生的时候才会继续. // selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while(it.hasNext()){ key = it.next(); it.remove(); try{ handleInput(key); }catch(Exception e){ if(key != null){ key.cancel(); if(key.channel() != null){ key.channel().close(); } } } } }catch(Exception e){ e.printStackTrace(); System.exit(1); } } //selector关闭后会自动释放里面管理的资源 if(selector != null) try{ selector.close(); }catch (Exception e) { e.printStackTrace(); } } private void handleInput(SelectionKey key) throws IOException{ if(key.isValid()){ SocketChannel sc = (SocketChannel) key.channel(); if(key.isConnectable()){ if(sc.finishConnect()); else System.exit(1); } //读消息 if(key.isReadable()){ //创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); //读取到字节,对字节进行编解码 if(readBytes>0){ //将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作 buffer.flip(); //根据缓冲区可读字节数创建字节数组 byte[] bytes = new byte[buffer.remaining()]; //将缓冲区可读字节数组复制到新建的数组中 buffer.get(bytes); String result = new String(bytes,"UTF-8"); System.out.println("客户端收到消息:" + result); } //没有读取到字节 忽略 // else if(readBytes==0); //链路已经关闭,释放资源 else if(readBytes<0){ key.cancel(); sc.close(); } } } } //异步发送消息 private void doWrite(SocketChannel channel,String request) throws IOException{ //将消息编码为字节数组 byte[] bytes = request.getBytes(); //根据数组容量创建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //将字节数组复制到缓冲区 writeBuffer.put(bytes); //flip操作 writeBuffer.flip(); //发送缓冲区的字节数组 channel.write(writeBuffer); //****此处不含处理“写半包”的代码 } private void doConnect() throws IOException{ if(socketChannel.connect(new InetSocketAddress(host,port))); else socketChannel.register(selector, SelectionKey.OP_CONNECT); } public void sendMsg(String msg) throws Exception{ socketChannel.register(selector, SelectionKey.OP_READ); doWrite(socketChannel, msg); System.out.println("第" + sign + "个客户端发出信息"); } }
测试代码
import java.util.Scanner; public class Test { //测试主方法 @SuppressWarnings("resource") public static void main(String[] args) throws Exception{ //运行服务器 Server.start(); //避免客户端先于服务器启动前执行代码 Thread.sleep(100); //运行客户端 // Client.start(); Thread.sleep(1000); while(true){ Client.sendMsg("test message"); //Thread.sleep(1000); } } }
经过使用,个人认为。NIO解决了之前BIO在请求连接客户端数量庞大时带来的服务端线程数量过多的问题。使得服务端线程不必与客户端连接数量1:1,而是1:N。但是它也有相应的一些问题无法解决,使用时所有的客户端连接都在一个线程中管理,相应的任务也都在这个线程中类执行。如果是执行比较耗时的操作那么对于客户端的响应会变慢,因为要一个一个连接轮流处理,这样每个连接轮流等待处理的情况我觉得就是另一种意义上的阻塞。但如果为每个连接开线程的话就会又沦落为BIO的情况了。所以网上的其他相关资料也说NIO适合那些客户端链接量大但每个连接操作不太耗时的情景,如聊天室。BIO适合客户端连接数小的情况。