1.通过NIO实现网络编程
概述和核心API
前面在进行文件IO 时用到的FileChannel 并不支持非阻塞操作,学习NIO 主要就是进行
网络IO,Java NIO 中的网络通道是非阻塞IO 的实现,基于事件驱动,非常适用于服务器需
要维持大量连接,但是数据交换量不大的情况,例如一些即时通信的服务等等....
在Java 中编写Socket 服务器,通常有以下几种模式:
一个客户端连接用一个线程,优点:程序编写简单;缺点:如果连接非常多,分配的线
程也会非常多,服务器可能会因为资源耗尽而崩溃。
把每一个客户端连接交给一个拥有固定数量线程的连接池,优点:程序编写相对简单,
可以处理大量的连接。确定:线程的开销非常大,连接如果非常多,排队现象会比较严
重
使用Java 的NIO,用非阻塞的IO 方式处理。这种模式可以用一个线程,处理大量的客
户端连接。
1. Selector(选择器),能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获
取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也
就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,
就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并
且避免了多线程之间的上下文切换导致的开销。
该类的常用方法如下所示:
public static Selector open(),得到一个选择器对象
public int select(long timeout),监控所有注册的通道,当其中有IO 操作可以进行时,将
对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys(),从内部集合中得到所有的SelectionKey
2. SelectionKey,代表了Selector 和网络通道的注册关系,一共四种:
int OP_ACCEPT:有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ 和int OP_WRITE:代表了读、写操作,值为1 和4
该类的常用方法如下所示:
public abstract Selector selector(),得到与之关联的Selector 对象
public abstract SelectableChannel channel(),得到与之关联的通道
public final Object attachment(),得到与之关联的共享数据
public abstract SelectionKey interestOps(int ops),设置或改变监听事件
public final boolean isAcceptable(),是否可以accept
public final boolean isReadable(),是否可以读
public final boolean isWritable(),是否可以写
3. ServerSocketChannel,用来在服务器端监听新的客户端Socket 连接,常用方法如下所示:
public static ServerSocketChannel open(),得到一个ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
取值false 表示采用非阻塞模式
public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
4. SocketChannel,网络IO 通道,具体负责进行读写操作。NIO 总是把缓冲区的数据写入通
道,或者把通道里的数据读到缓冲区。常用方法如下所示:
public static SocketChannel open(),得到一个SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
取值false 表示采用非阻塞模式
public boolean connect(SocketAddress remote),连接服务器
public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成
连接操作
public int write(ByteBuffer src),往通道里写数据
public int read(ByteBuffer dst),从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置
监听事件,最后一个参数可以设置共享数据
public final void close(),关闭通道
示例代码:
1.服务端代码:
//聊天室服务端 public class ChatServer { private final int POST = 9999; //服务端端口号 private ServerSocketChannel serverSocketChannel; //服务端监听管道对象 private Selector selector; //选择器 public ChatServer() { try { //1.创建监听管道对象 serverSocketChannel = ServerSocketChannel.open(); //2.设置非阻塞 serverSocketChannel.configureBlocking(false); //绑定端口 serverSocketChannel.bind(new InetSocketAddress(POST)); //4.创建选择监控对象 selector = Selector.open(); //5.将监听管道对象注册到选择器中,并监听accept事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); printInfo("----服务端已经就绪-----"); } catch (IOException e) { e.printStackTrace(); } } /** * 6开始干活 * */ public void start() throws Exception { //循环工作 while (true) { //6.1通过selector查看是否有客户端连接 if (selector.select(2000) == 0) { //没有客户端连接服务端 System.out.println("-----server:目前没有客户端链接到服务端-------"); continue; } //6.2有客户端连接到服务端,获取所有的客户端连接SelectionKey Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); //通过key,可以获取到相关的三种事件 if (key.isAcceptable()) { //连接请求事件 //获取客户端的网络通道 SocketChannel channel = serverSocketChannel.accept(); //设置非阻塞 channel.configureBlocking(false); //注册到selector选择器中,并注册为读事件 channel.register(selector,SelectionKey.OP_READ); System.out.println("---服务端监听到客户端:"+channel.getRemoteAddress().toString().substring(1)+"上线了"); } if (key.isValid() && key.isReadable()) { //如果监听到的是读事件 readMsg(key); } if (key.isValid() && key.isWritable()) { //如果监听到是写事件 } //处理完要把当前的SelectionKey移除,防止反复处理 keyIterator.remove(); } } } /** * 读取客户端发送过来的数据,并广播出去 * */ private void readMsg(SelectionKey key) throws Exception { //1.通过key获取管道 SocketChannel channel = (SocketChannel) key.channel(); //2.获取缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //3,将接收到的数据保存到缓冲区中 int write = 0; try { write = channel.read(buffer); } catch (IOException e) { key.cancel(); channel.socket().close(); //e.printStackTrace(); } if (write > 0) { String msg = new String(buffer.array()); printInfo(msg); //广播给所有的客户端,除了发送消息的客户端 broadcast(channel, msg); } } /** * 广播所有的客户端 * excet 消息的发送客户端 * */ private void broadcast(SocketChannel excet, String msg) throws Exception { System.out.println("---服务端开始广播----"); //通过selector获取所有的注册的selectionkey Set<SelectionKey> keys = selector.keys(); for (SelectionKey key: keys) { Channel channel = key.channel(); //获取所有的对应管道,包括客户端管道还有其他的管道类型 if (channel instanceof SocketChannel && channel != excet) { //如果该管道未客户端网络管道且不为发送消息的客户端网络管道,就执行一下的的代码 //将管道强转为网络管道对象,方便调用api SocketChannel socketChannel = (SocketChannel) channel; ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); socketChannel.write(buffer); } } } /** * 控制台输出方法 * @param str */ private void printInfo(String str) { //往控制台打印消息 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println("[" + sdf.format(new Date()) + "] -> " + str); } /*启动服务端*/ public static void main(String[] args) { try { new ChatServer().start(); } catch (Exception e) { e.printStackTrace(); } } }
2.客户端代码:
//聊天室客户端 public class ChatClient { private final String HOST = "127.0.0.1"; //服务端地址 private final int POST = 9999; //socket 端口号 private SocketChannel socketchannel; //网络管道对象 private String userName; //聊天用户名 private InetSocketAddress address; //构造方法 public ChatClient() throws Exception { //1.初始化网络管道对象 socketchannel = SocketChannel.open(); //2.设置非阻塞 socketchannel.configureBlocking(false); //3.连接到指定服务端 address = new InetSocketAddress(HOST, POST); if (!socketchannel.connect(address)) {//如果没有连接上,则继续连接,直到连接成功 while (!socketchannel.finishConnect()) { System.out.println("目前客户端还未连接到服务端。。。。。"); } } //4.得到客户端IP地址和端口信息,作为聊天的用户信息 userName = socketchannel.getLocalAddress().toString().substring(1); System.out.println("-------客户端:"+userName +"已经连接到服务端---------"); } //向服务端发送信息 public void sendMsg(String msg) throws Exception{ //如果客户端发送的信息为bye,则说明客户端离开群聊,关闭当前的客户端网络管道 if (msg.equalsIgnoreCase("bye")) { socketchannel.close(); //离开群聊,结束连接 return; } msg = "客户端:"+ userName + "说:"+ msg; //创建缓冲区对象 ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); try { if (socketchannel.isOpen()) { socketchannel.write(buffer); }else { //如果当前的网络管道已经失效,需要重新新建连接 socketchannel = SocketChannel.open(); //2.设置非阻塞 socketchannel.configureBlocking(false); //3.连接到指定服务端 address = new InetSocketAddress(HOST, POST); if (!socketchannel.connect(address)) {//如果没有连接上,则继续连接,直到连接成功 while (!socketchannel.finishConnect()) { System.out.println("目前客户端还未连接到服务端。。。。。"); } } //4.得到客户端IP地址和端口信息,作为聊天的用户信息 userName = socketchannel.getLocalAddress().toString().substring(1); System.out.println("-------客户端:"+userName +"已经连接到服务端---------"); } } catch (IOException e) { socketchannel.close(); e.printStackTrace(); } } //接收服务端发送过来的信息 public void receiveMsg() throws Exception { //1.获取缓冲区对象, ByteBuffer buffer = ByteBuffer.allocate(1024); //2.从缓冲区读取数据 int read = 0; try { read = socketchannel.read(buffer); } catch (IOException e) { socketchannel.socket().close(); //e.printStackTrace(); } if (read > 0) { //大于零,说明缓冲区有数据 //3.输出数及 System.out.println("-------服务器发送了:"+new String(buffer.array()).trim()); } } }
3.开启客户端代码
public class ChatTest { public static void main(String[] args) throws Exception { //创建客户端对象 ChatClient chatClient = new ChatClient(); //开一个线程,专门接收服务端的广播消息 new Thread() { public void run() { while (true) { try { chatClient.receiveMsg(); Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } } } }.start(); //控制台模拟客户端输入消息 Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { //如果有输入 String msg = scanner.next(); try { chatClient.sendMsg(msg); } catch (Exception e) { e.printStackTrace(); } } } }