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