基于NIO 实现简易“聊天室”

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();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小达人Fighting

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值