基于JAVA NIO 实现的 Socket 文件传输

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

/**
 * @desc 基于JAVA NIO 实现的 Socket 文件传输--客户端
 **/
public class Client
{
   private static final Logger log = LoggerFactory.getLogger(Client.class);

   public static void main(String[] args)
   {
      SocketChannel sc = null;
      FileChannel fc = null;
      try
      {
         sc = SocketChannel.open();
         // client 设置阻塞模式
         sc.configureBlocking(true);
         sc.connect(new InetSocketAddress("127.0.0.1", 9000));
         if (!sc.finishConnect())
         {
            log.error("Can not connect to server");
            return;
         }
         // 分配缓冲区【从堆内申请内存】
         ByteBuffer buffer = ByteBuffer.allocate(10240);
         int r = 0;
         //从文件读取内容,并通过 Socket 发送
         fc = new FileInputStream(new File("F:\\1.txt")).getChannel();
         try
         {
            // 先发送此文件的大小
            buffer.putLong(fc.size());
            while ((r = fc.read(buffer)) > 0)
            {
               log.debug("Read {} bytes from file", r);
               /**
                * 解释: 缓冲区中的 position 表示“下一次读或者写的位置(数组下标)”
                *                   limit 表示“可读或者可写的最大位置”
                *                   capacity 表示“缓冲区数组的总长度,只读”
                *
                *        缓冲区新建的时候: position = 0 , limit = capacity
                *        在缓冲区写入 n 个字节后, position 向后移动 n 位 ,即 position = n , limit = capacity
                *
                * 设置 limit 为 position 的值 r , 再将 position 设置为 0 【调用 flip() 即可实现 】
                */
               buffer.flip();

               /**
                * 将 buffer 中的内容全部发送出去。【将 buffer 中的数据写入 Channel 中】
                * buffer.hasRemaining()  判断缓冲区中是否还有数据
                */
               while (buffer.hasRemaining() && (r = sc.write(buffer)) > 0)
               {
                  log.debug("Write {} bytes to server", r);
               }
               /**
                * 使用  clear() 之后,  缓冲区中的 position = 0 , limit = capacity
                */
               buffer.clear();
            }
         }
         finally
         {
            StreamUtil.close(fc);
         }

         // 读取服务端返回的响应字符串【将 Channel 中的数据读到 buffer 中】
         while ( (r = sc.read(buffer)) > 0)
         {
            log.debug("Read {} bytes from socket",r);
         }
         buffer.flip();
         // 解码服务端返回的数据
         log.info(Charset.forName("UTF-8").decode(buffer).toString());
      }
      catch (IOException e)
      {
         log.error("Error on send file", e);
      }
      finally
      {
         StreamUtil.close(sc);
      }
      System.out.println("done");
   }
}

-------------------

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**

 * @desc 基于JAVA NIO 实现的 Socket 文件传输--服务端

 **/
public class Server implements Runnable
{
   private static final Logger log = LoggerFactory.getLogger(Server.class);

   /**
    *  多路复用器,用于同时处理多个通道上的多个事件
    */
   private Selector selector = null;

   /**
    *  服务端 Socket 通道
    */
   private ServerSocketChannel ssc = null;

   /**
    * 工作线程对象
    */
   private Thread thread = new Thread(this);

   /**
    * 工作线程退出标识,需要 volatile 关键字来保证可见性
    */
   private volatile boolean live = true;

   public void start() throws IOException
   {
      // 创建多路复用器
      selector = Selector.open();
      // 创建 ServerSocket
      ssc = ServerSocketChannel.open();
      // 绑定 9000 端口,开始监听连接请求
      ssc.socket().bind(new InetSocketAddress(9000));
      // 使用非阻塞式
      ssc.configureBlocking(false);
      // 注册新连接请求事件
      /**
       *  SelectionKey.OP_ACCEPT : 注册 SocketChannel 上的“连接请求事件”
       *  SelectionKey.OP_READ : 注册 SocketChannel 上的“可读事件”
       *  SelectionKey.OP_WRITE : 注册 SocketChannel 上的“可写事件”
       */
      ssc.register(selector, SelectionKey.OP_ACCEPT);
      // 开启线程,循环处理新的事件
      thread.start();
   }

   @Override
   public void run()
   {
      try
      {
         // 不断处理 Socket 通道事件,直到 live 为 false ,或者当前线程被中断
         /**
          *  interrupted() : 用于判断当前线程是否被中断,同时也会直接擦除掉线程的 interrupt 标识
          */
         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();
               // 如果此事件已经处理,需要从现有的集合中移除
               /**
                *  remove() 方法: 从迭代器指向的 collection 中移除迭代器返回的最后一个元素
                */
               iterator.remove();
               // 判断是否是一个有效的连接请求事件【SelectionKey.OP_ACCEPT】
               // 注:如果通道被关闭,对应的事件就会失效
               if (key.isValid() && key.isAcceptable())
               {
                  this.onAcceptable(key);
               }
               // 判断是否为一个有效的“通道可读”事件
               if (key.isValid() && key.isReadable())
               {
                  this.onReadable(key);
               }
               // 判断是否为一个有效的“通道可写”事件
               if (key.isValid() && key.isWritable())
               {
                  this.onWritable(key);
               }


            }
         }
      }
      catch (IOException e)
      {
         log.error("Error on socket I/O", e);
      }
   }

   public void onAcceptable(SelectionKey key)
   {
      System.out.println("onAcceptable正在执行");
      // 处理 Acceptable 事件的时候 key.channel() 返回的是 ServerSocketChannel
      ServerSocketChannel channel = (ServerSocketChannel) key.channel();
      SocketChannel sc = null;
      try
      {
         //  此方法会立即返回当前可建立的连接,如果没有可接受的连接将返回 null
         sc = channel.accept();
         if (sc != null)
         {
            InetSocketAddress remoteAddress = (InetSocketAddress) sc.getRemoteAddress();
            log.info("Client {} connected", remoteAddress);
            // 服务端使用“非阻塞的方式”
            sc.configureBlocking(false);
            /**
             *  public final SelectionKey register(Selector sel, int ops, Object att)
             *
             *  源码中使用: attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(SelectionKey.class, Object.class, "attachment");
             *         将    Object att 设置到 attachment 属性中。
             *
             */
            /**
             * 1)在新建立的 Socket 通道上“注册读事件”,因为在没有读完数据前,不会向客户端返回应答。
             * 2)为每个新创建的连接都创建一个缓冲区,并作为附件放到  SelectionKey 里面,以便后面取用
             */
            sc.register(key.selector(), SelectionKey.OP_READ, new TempBuff(remoteAddress.getPort()));

         }
      }
      catch (IOException e)
      {
         log.error("Error on accept connection", e);
         StreamUtil.close(sc);
      }
   }

   public void onReadable(SelectionKey key)
   {
      System.out.println("onReadable正在执行");
      // 处理 Readable 事件的时候 key.channel() 返回的是 SocketChannel
      SocketChannel sc = (SocketChannel)key.channel();
      // 将此前创建并作为附件放入 SelectionKey  的缓冲区取出来,避免每次都创建一个缓冲区
      TempBuff tb= (TempBuff)key.attachment();
      ByteBuffer buffer = tb.buffer;
      try
      {
         InetSocketAddress inetSocketAddress = (InetSocketAddress)sc.getRemoteAddress();
         int r =0;
         /**
          * 调用  clear() 后, position = 0 , limit = capacity
          */
         buffer.clear();
         // 循环从 Socket 通道读取数据,放到 buffer 中
         while( (r = sc.read(buffer)) > 0 )
         {
            // 已经收取的字节数
            tb.received += r;
            log.debug("Received {}/{}/{} from {}",r,tb.received,tb.required,inetSocketAddress);
            // 准备读取 buffer
            buffer.flip();
            //将数据从 buffer 中读出来,写入文件
            r = tb.channel.write(buffer);
            log.debug("Write {}/{}/{} bytes to file",r,tb.received,tb.required);
            buffer.clear();
         }
         // 数据读取完毕后,注册写事件,并将要返回客户端的应答字符串作为附件
         if( tb.required > tb.received )
         {
            sc.register(key.selector(),SelectionKey.OP_READ,tb);
         }
         else
         {
            sc.register(key.selector(),SelectionKey.OP_WRITE,tb);
         }
      }
      catch (IOException e)
      {
         log.error("Error on read socket",e);
         // 关闭文件
         StreamUtil.close(tb.channel);
         // 关闭 Socket
         StreamUtil.close(sc);

      }
   }

   public void onWritable(SelectionKey key)
   {
      System.out.println("onWritable正在执行");
      // 处理 Writable 事件时 key.channel() 返回的是  SocketChannel
       SocketChannel sc = (SocketChannel)key.channel();
       // 取出前面注册 writable 时附加的字符串
      TempBuff tb = (TempBuff)key.attachment();
      try
      {
         // 将字符串转换为 ByteBuffer
         byte[] bytes = "ok".getBytes("UTF-8");
         ByteBuffer buf = ByteBuffer.wrap(bytes);
         // 用上面的 wrap 方法得到的缓冲区,其 limit 为 0 , 需要移动到最后
         buf.limit(bytes.length);
         int r = 0;
         // 将缓冲区中的内容全部发送出去
         while(buf.hasRemaining() && ( r =sc.write(buf))>0)
         {
            log.debug("Write {} bytes to {}",r,sc.getRemoteAddress());
         }
      }catch(Exception e)
      {
         log.error("Error on write socket",e);
      }finally
      {
         StreamUtil.close(tb.channel);
         StreamUtil.close(sc);
      }

   }

   public void close()
   {
      // 让线程 Thread 退出循环
      live = false;
      try
      {
         //让当前线程(主线程)等待线程 thread 退出循环
         thread.join();
      }catch(InterruptedException e)
      {
         log.error("Be interrupted on join",e);
      }finally
      {
         // 关闭  Selector 和 ServerSocketChannel
         StreamUtil.close(selector);
         StreamUtil.close(ssc);
      }
   }

   public static void main(String[] args)
   {
      BufferedReader br = null;
      Server server = new Server();
      try
      {
         // 启动服务器
         server.start();
         // 循环读取键盘输入,当用户输入 exit 时退出程序
         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 start Server",e);
      }finally
      {
         StreamUtil.close(br);
         // 优雅的关闭服务器,退出事件处理循环,并等待内部线程结束
         server.close();
      }
   }
   class TempBuff
   {
      private long required = -1L;
      private long received = 0L;
      private ByteBuffer buffer = ByteBuffer.allocate(10240);
      private FileChannel channel;

      public TempBuff(int port) throws FileNotFoundException
      {
         channel = new FileOutputStream(new File(String.format("F:/%d.zip",port))).getChannel();
      }
   }

}

-------------------------

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();
        }
    }
}

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小达人Fighting

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

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

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

打赏作者

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

抵扣说明:

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

余额充值