Java NIO文件传输

上次写了个OIO的的Sokcet编程,现在把最近学习的NIO补上

客户端:Client

import java.io.Closeable;
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;

/**
 * 使用SocketChannel实现TCP协议的文件传输
 * NIO中的SocketChannel和OIO中的Socket对应
 * NIO中的ServerSocketChannel和OIO中的ServerSocket对应
 */
public class Client{
    public static void main(String[] args) {
        //先启动服务端,再启动客户端
        Client client = new Client();

        //发送文件
         String filepath = System.getProperty("user.dir")+"\\resource\\client\\";
         client.upload(filepath,"client1.jpg");

    }

    private final Charset charset = Charset.forName("UTF-8");//java默认编码为Unicode,有的操作系统不支持,统一编码格式为UTF-8
    private final String serverAddress = "localhost";
    private final int serverPort = 9111;

    /**
     * 传递到服务端的应该有文件路径和文件名
     * @param filepath
     * @param filename
     */
    public void upload(String filepath,String filename)  {
        try{
            File sendfile = new File(filepath+filename);
            FileChannel fileChannel = new FileInputStream(sendfile).getChannel();//获取该文件的输入流的通道

            SocketChannel socketChannel = SocketChannel.open();//打开Socket通道
            socketChannel.connect(new InetSocketAddress(serverAddress,serverPort));//绑定服务器的链接地址和端口号
            socketChannel.configureBlocking(false);//设置为非阻塞式

            //由于是非阻塞式连接,所以socketChannel.connect()方法不论是否真正的连接成功,都会立即返回,
            while(!socketChannel.finishConnect()){
                //socket没有真正连接前,不断的自旋、等待,或者做一些其他的事情
                System.out.println("等待连接中,做其他事....");
            }
            System.out.println("成功连接到服务器...");
            //将存储在服务器的文件名编码为UTF-8格式的二进制字节序列
            ByteBuffer fileNamebuffer = charset.encode(filename);
            socketChannel.write(fileNamebuffer);//将文件名传过去

            System.out.println("开始传输文件");
            ByteBuffer filebuffer = ByteBuffer.allocate(1024);//开启缓冲内存区域,用于存储文件内容
            int len = 0;
            while((len = fileChannel.read(filebuffer)) != -1){//将文件数据从fileChannel读取并存储到filebuffer缓冲区中
                filebuffer.flip();//将内存缓冲区翻转为读模式
                socketChannel.write(filebuffer);//读取本次缓冲区所有文件并写入通道
                filebuffer.clear();//清空缓冲区
            }

            //单向关闭,表示客户端数据写完了,如果需要服务端响应数据,就要用这种方式
//            socketChannel.shutdownOutput();

            //读完文件通道关闭
            fileChannel.close();
            socketChannel.close();
            System.out.println("传输完成...");
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

服务端Server,对每个客户端的请求视为一个单独的对象,所以加了一个静态内部类作为当前处理的客户端对象

服务端涉及了Selector选择器,通过监听不同的通道来实现IO的多路复用。这样既能在非阻塞式的监听客户端请求,还可以只使用一个线程来处理多个客户端请求,比起以前一个线程对应一个客户端而言,节约了线程上下文切换的开销,效率要高很多。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 使用SocketChannel实现TCP协议的文件传输
 * NIO中的SocketChannel和OIO中的Socket对应
 * NIO中的ServerSocketChannel和OIO中的ServerSocket对应
 */
public class Server{
    public static void main(String[] args) {
        //先启动服务端
        Server server = new Server();
        server.startServer();
    }

    private final Charset charset = Charset.forName("UTF-8");
    private final int serverPort = 9111;
    //将客户端传递的数据封装为一个对象
    static class FileData{
        String clientAddress;
        String filename; //客户端上传的文件名称
        FileChannel fileOutChannel;//输出的文件通道
    }

    //使用Map保存每个客户端传输,当OP_READ通道可读时,根据channel找到对应的对象
    Map<SelectableChannel, FileData> map = new ConcurrentHashMap<>();

    /**
     * 启动服务器
     */
    public void startServer(){
        try{
            // 1、获取Selector选择器
            Selector selector = Selector.open();

            // 2、创建一个通道,用于获取客户端的请求连接
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            ServerSocket serverSocket = serverChannel.socket();

            // 3.设置为非阻塞
            serverChannel.configureBlocking(false);

            // 4、绑定连接
            // InetSocketAddress只传入端口号,则自动绑定当前本机IP
            serverSocket.bind(new InetSocketAddress(serverPort));//即服务端开放99端口

            // 5、将该通道注册到选择器上,并注册的IO事件为:“接收新连接”
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务端已开启,监听新连接。。。");

            // 6、遍历选择器,轮询感兴趣的I/O就绪事件(选择键集合)
            while (selector.select() > 0) {
                // 7、获取选择键集合
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    // 8、获取单个的选择键,并处理
                    SelectionKey key = it.next();

                    // 9、判断key是具体的什么事件,是否为新连接事件
                    if (key.isAcceptable()) {
                        // 10、若接受的事件是“新连接”事件,就获取客户端新连接
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = server.accept();
                        if (socketChannel == null) continue;
                        // 11、客户端新连接,切换为非阻塞模式
                        socketChannel.configureBlocking(false);

                        // 12、将获取到的客户端socket通道再次注册到选择器,并注册为可读事件
                        // 这样下次遍历选择器时,就进入可读事件
                        socketChannel.register(selector, SelectionKey.OP_READ);

                        // 业务处理 - 每次客户端上传一个文件就创建一个对象存到map中
                        // 一个客户端对象对应一个socket通道
                        FileData fileData = new FileData();
                        fileData.clientAddress = socketChannel.getRemoteAddress().toString();
                        map.put(socketChannel, fileData);
                        System.out.println("与客户端"+fileData.clientAddress+ "连接成功...");
                    } else if (key.isReadable()) {
                        receiveFile(key);
                    }
                    // NIO的特点只会累加,已选择的键的集合不会删除
                    // 如果不删除,下一次又会被select函数选中
                    it.remove();
                }
            }

        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 接收文件
     */
    private void receiveFile(SelectionKey key){
        ByteBuffer buffer = ByteBuffer.allocate(1024);//开启内存缓冲区域
        FileData fileData = map.get(key.channel());

        SocketChannel socketChannel = (SocketChannel) key.channel();
        String directory = System.getProperty("user.dir")+"\\resource\\server\\";//服务端收到文件的存储路径
        long start = System.currentTimeMillis();
        try {
            int len = 0;
            while ((len = socketChannel.read(buffer)) != -1) {//将客户端写入通道的数据读取并存储到buffer中
                buffer.flip();//将缓冲区翻转为读模式

                //客户端发送过来的,首先是文件名
                if (null == fileData.filename) {
                    // 文件名 decode解码为UTF-8格式,并赋值给client对象的filename属性
                    fileData.filename = (System.currentTimeMillis()+"_"+charset.decode(buffer).toString()).substring(5);

                    //先检查存储的目录是否存在
                    File dir = new File(directory);
                    if(!dir.exists()) dir.mkdir();

                    //再检查文件是否存在,不存在就创建文件,然后通过FikeChanel写入数据
                    File file = new File(directory + fileData.filename);
                    if(!file.exists()) file.createNewFile();

                    //将设定要存放的文件路径+文件名创建一个输出流通道
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    fileData.fileOutChannel = fileChannel;//赋值给client对象
                }
                //客户端发送过来的,最后是文件内容
                else{
                    // 通过已经创建的文件输出流通道向文件中写入数据
                    fileData.fileOutChannel.write(buffer);
                }
                buffer.clear();//清除本次缓存区内容
            }
            fileData.fileOutChannel.close();
            key.cancel();
            System.out.println("上传完毕,费时:"+Long.valueOf(System.currentTimeMillis()-start)+"毫秒");
            System.out.println("文件在服务端的存储路径:" + directory + fileData.filename);
            System.out.println("");
        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        }
    }
}

最后留个小问题,希望后面自己能来解决,或者有大佬看到了能够指点一二

上面的代码我在windows本地环境测试是完全没问题的,但在linux的远程云服务器上测试就有个小问题,就是客户端的ByteBuffer缓冲区大小会影响服务端接收数据的完整性。

我测试文件是一个约84kb的图片文件

当客户端的ByteBuffer设为1024时,linux云服务器就只能收到64kb的数据

当客户端的ByteBuffer设为10240时,linux云服务器就只能收到70kb的数据

当客户端的ByteBuffer设为102400时,linux云服务器才能收到84kb的完整数据

我寻思缓冲区太小就多循环读取几次不就行了吗?为什么会造成数据得丢失。

如果有大佬能帮忙指教一下,不胜感激,谢谢!!!

猜你喜欢

转载自blog.csdn.net/c_o_d_e_/article/details/113092095