import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; 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.Iterator; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedDeque; /** * @desc 基于 JAVA NIO 实现聊天室 --客户端 **/ public class Client implements Runnable { private static final Logger log = LoggerFactory.getLogger(Client.class); private static final Charset charset = Charset.forName("UTF-8"); private Selector selector; private SocketChannel socketChannel; private Thread thread = new Thread(this); // 用于读取的 buffer private ByteBuffer buffer = ByteBuffer.allocate(1024); // 待发送的消息列表 private Queue<String> queue = new ConcurrentLinkedDeque<String>(); private volatile boolean live = true; public void start() throws IOException { selector = Selector.open(); socketChannel = SocketChannel.open(); // 使用非阻塞的方式 socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("127.0.0.1",9000)); if(socketChannel.finishConnect()) { // 连接成功后同时注册“通道可读”和“通道可写”事件,客户端只需要关心这两件事情 socketChannel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE); //开启线程,循环处理事件 thread.start(); } } @Override public void run() { try { while(live && !Thread.interrupted()) { // 轮询选取事件,为避免一直阻塞,设置超时时间为 1 秒 if( selector.select(1000) == 0 ) { continue; } //遍历发生的事件,并在处理前或者处理后移除 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while( iterator.hasNext()) { SelectionKey key = iterator.next(); // 处理事件前移除 iterator.remove(); SocketChannel sc = null; int r = 0; String s = null; // 如果通道可读,且当前事件有效,读取服务端发来的字符串 if(key.isValid() && key.isReadable()) { sc = (SocketChannel)key.channel(); StringBuilder sb = new StringBuilder(); buffer.clear(); while( (r = sc.read(buffer)) > 0 ) { log.debug("Received {} bytes from {}",r,sc.getRemoteAddress()); buffer.flip(); //解码 sb.append(charset.decode(buffer)); buffer.clear(); s = sb.toString(); // 如果传入的字符串以“换行符”结尾,就表示至少已经读取到了一条完整的消息 if(s.endsWith("\n")) { break; } } // 收到的消息发生了粘包,包含多条消息(但不会只有半条),需要切分 String[] split = s.split("\n"); for(String sp : split) { if(!StringUtils.isEmpty(sp)) { System.out.println(sp); } } } // 如果队列中有消息要发送,且通道可写,就立即发送 if(key.isValid() && key.isWritable() && !queue.isEmpty()) { s = queue.poll(); sc = (SocketChannel)key.channel(); ByteBuffer buf = ByteBuffer.wrap(s.getBytes("UTF-8")); // “wrap” 后, buf 的 limit 为 0 ,需要修改 buf.limit(s.length()); while( buf.hasRemaining() && (r = socketChannel.write(buf)) > 0 ) { log.debug("Write {} bytes to server",r); } } } } }catch(IOException e) { log.error("Error on socket I/O",e); }finally { StreamUtil.close(socketChannel); StreamUtil.close(selector); } } public void close() { live = false; try { thread.join(); } catch (InterruptedException e) { log.error("Be interrupted on join",e); } StreamUtil.close(socketChannel); StreamUtil.close(selector); } public boolean isLive() { return thread.isAlive(); } public void send(String s) throws IOException { queue.add(s); } public static void main(String[] args) { BufferedReader ir = null; Client client = new Client(); try { // 启动客户端通信线程 client.start(); // 接收用户的输入,并通过客户端通信线程发送出去 String cmd = null; ir = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Say 'bye' to exit"); while( (cmd = ir.readLine()) != null && client.isLive()) { if(!StringUtils.isEmpty(cmd)) { //将“消息”放入队列,等待发送 client.send(cmd.concat("\n")); if( "bye".equalsIgnoreCase(cmd)) { client.close(); break; } } } } catch (Exception e) { log.error("Error on run client",e); }finally { StreamUtil.close(ir); //优雅地关闭 client.close(); } System.out.println("done"); } }
------------------------
import com.sun.org.apache.bcel.internal.generic.Select; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; 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.nio.charset.Charset; import java.util.Iterator; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedDeque; /** * @desc 基于 JAVA NIO 实现聊天室 -- 服务端 **/ public class Server implements Runnable { private static final Logger log = LoggerFactory.getLogger(Server.class); private static final Charset charset = Charset.forName("UTF-8"); private Selector selector = null; private ServerSocketChannel serverSocketChannel = null; private Thread thread = new Thread(this); // 暂存来自客户端的消息,以便广播发送 private Queue<String> queue = new ConcurrentLinkedDeque<String>(); private volatile boolean live = true; public void start() throws IOException { selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9000)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); thread.start(); } @Override public void run() { try { while( live && !Thread.interrupted()) { if(selector.select(1000) == 0) { continue; } // 遍历通道事件前,先将队列中的消息取出并转成 ByteBuffer ByteBuffer outBuf = null; String outMsg = queue.poll(); if(!StringUtils.isEmpty(outMsg)) { outBuf = ByteBuffer.wrap(outMsg.getBytes("UTF-8")); outBuf.limit(outMsg.length()); } // 遍历这些事件(通道) Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while(iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if(key.isValid() && key.isAcceptable()) { this.onAcceptable(key); } if(key.isValid() && key.isReadable()) { this.onReadable(key); } // 如果当前通道可写,并且有消息需要发送,则将消息发送出去 if( key.isValid() && key.isWritable() && outBuf != null) { SocketChannel sc = (SocketChannel)key.channel(); this.write(sc , outBuf); } } } }catch(IOException e) { log.error("Error on socket I/O",e); } } private void onAcceptable(SelectionKey key) { ServerSocketChannel ssc = (ServerSocketChannel)key.channel(); SocketChannel sc = null; try { sc = ssc.accept(); if(sc!= null) { log.info("Client {} connected",sc.getRemoteAddress()); sc.configureBlocking(false); // 为新建立的连接注册“通道可读事件”和“通道可写事件”,并创建一个读取用的缓冲区(避免重复创建) sc.register(selector,SelectionKey.OP_READ | SelectionKey.OP_WRITE,ByteBuffer.allocate(1024)); } } catch (Exception e) { log.error("Error on accept connection",e); StreamUtil.close(sc); } } private void onReadable(SelectionKey key) { SocketChannel sc = (SocketChannel)key.channel(); ByteBuffer buf = (ByteBuffer)key.attachment(); int r = 0; StringBuilder sb = new StringBuilder(); String rs = null; String remote = null; buf.clear(); try { remote = sc.getRemoteAddress().toString(); while( (r = sc.read(buf)) > 0) { log.debug("Received {} bytes from {}",r,remote); buf.flip(); sb.append(charset.decode(buf)); buf.clear(); rs = sb.toString(); // 至少读取一行消息(如果发生粘包,则可能读到多行,但不会只读半条消息) if(rs.endsWith("\n")) { break; } } } catch (IOException e) { log.error("Error on read socket",e); StreamUtil.close(sc); return ; } // 收到消息后,将消息添加到“队列”,并在各通道可写(writable 事件)发送 if( !StringUtils.isEmpty( rs)) { // 由于客户端可能连续地发送多条消息,因此收取时可能发生粘包,需要切分 String[] split = rs.split("\n"); for(String sp : split) { if( !StringUtils.isEmpty(sp)) { log.info("{}:{}",remote,sp); queue.add(String.format("%s: %s \n",remote,sp)); // 如果收到 bye , 则表示需要关闭当前通道 if("bye".equalsIgnoreCase(sp)) { StreamUtil.close(sc); } } } } } private void write(SocketChannel sc,ByteBuffer buf) throws IOException { buf.position(0); int r = 0; try { while( buf.hasRemaining() && ( r = sc.write(buf)) > 0) { log.debug("Write back {} bytes to {}",r , sc.getRemoteAddress()); } }catch(IOException e) { log.error("Error on write socket",e); sc.close(); return; } } private void close() { live =false; try { thread.join(); } catch (InterruptedException e) { log.error("Be interrupted on join",e); } StreamUtil.close(selector); StreamUtil.close(serverSocketChannel); } public static void main(String[] args) { BufferedReader br = null; Server server = new Server(); try { // 启动 Server server.start(); String cmd = null; System.out.println("Enter 'exit' to exit"); br = new BufferedReader(new InputStreamReader(System.in)); while( (cmd = br.readLine()) != null ) { if("exit".equalsIgnoreCase(cmd)) { break; } } } catch (IOException e) { log.error("Error on run server",e); }finally { StreamUtil.close(br); // 优雅地关闭 server.close(); } System.out.println("bye"); } }
------------------
import java.io.Closeable; import java.io.IOException; /** * @desc 关闭I/O流的工具类 **/ public class StreamUtil { public static void close(Closeable p){ if( p == null){ return ; } try { p.close(); } catch (IOException e) { e.printStackTrace(); } } }