实验内容:

  1. 复习Java I/O和Socket编程相关概念和方法;
  2. 基于Java Socket TCP和UDP实现一个简易的网络文件服务程序,包含服务器端FileServer和客户端FileClient;
  3. 服务器端启动时需传递root目录参数,并校验该目录是否有效;
  4. 服务器启动后,开启TCP:2021端口,UDP:2020端口,其中,TCP连接负责与用户交互,UDP负责传送文件;
  5. 客户端启动后,连接指定服务器的TCP 2021端口,成功后,服务器端回复信息:“客户端IP地址:客户端端口号>连接成功”;
  6. 连接成功后,用户可通过客户端命令行执行以下命令:
    [1] ls 服务器返回当前目录文件列表(<file/dir> name size)
    [2] cd 进入指定目录(需判断目录是否存在,并给出提示)
    [3] get 通过UDP下载指定文件,保存到客户端当前目录下
    [4] bye 断开连接,客户端运行完毕
  7. 服务器端支持多用户并发访问,不用考虑文件过大或UDP传输不可靠的问题。

实现:
一:
建立FileServerThreadPool类
构造函数中,基于JDK中的Executors自动建立线程池, 并绑定服务器TCP端口至2021, UDP端口至2020.

 public class FileServerThreadPool {
    ServerSocket serverSocket;
    DatagramSocket dataSocket;
    private final int PORT_TCP = 2021; // 端口
    private final int PORT_UDP = 2020;
    ExecutorService executorService; // 线程池
    final int POOL_SIZE = 4; // 单个处理器线程池工作线程数目

    public FileServerThreadPool() throws IOException {
        serverSocket = new ServerSocket(PORT_TCP); // 创建服务器端TCP套接字
        dataSocket = new DatagramSocket(PORT_UDP);//创建服务器端UDP套接字
        // 创建线程池
        // Runtime的availableProcessors()方法返回当前系统可用处理器的数目
        // 由JVM根据系统的情况来决定线程的数量
        executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
                .availableProcessors() * POOL_SIZE);
        System.out.println("服务器启动。");
    }

    public static void main(String[] args) throws IOException {
        new FileServerThreadPool().servic(); // 启动服务
    }

    /**
     * service implements
     */
    public void servic() throws SocketException {
        Socket socket_TCP = null;
        while (true) {
            try {
                socket_TCP = serverSocket.accept(); // 等待用户连接
                executorService.execute(new Handlers(socket_TCP,dataSocket)); // 把执行交给线程池来维护
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

二:
Handlers实现.
Handlers每次被创建提供一个与单个客户通信的线程.
Handlers被FileServerThreadPool类的线程池执行调用.
Handlers主要实现了以下功能:
1.通过建立TCP输入输出流, 对用户命令进行读取和解析

public void initStream() throws IOException { // 初始化输入输出流对象方法
        br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        bw = new BufferedWriter(
                new OutputStreamWriter(socket.getOutputStream()));
        pw = new PrintWriter(bw, true);
    }

2.run方法提供服务器运行时, 反馈连接状态, 对输入流进行字符处理, 并进行命令解析与命令执行.处理命令时调用了CmdResolution类中的.resolute方法辅助switch语句处理命令.

public void run() {
        try {
            System.out.println(socket.getInetAddress() + ":" +
                    + socket.getPort() + " > 连接成功"); //客户端信息
            initStream(); // 初始化输入输出流对象
            String info = null;
            boolean out = false;
            while (null != (info = br.readLine()) && !out) {
                System.out.println(info);
                //处理客户端通过TCP输入流传入的命令,进行处理
                switch (CmdResolution.resolute(info)){
                    case 0:{
                        pw.println("无法解析的命令");
                        pw.println("...");
                        break;
                    }
                    case 1:{
                        ls();
                        pw.println("...");
                        break;
                    }
                    case 2:{
                        path = rootPath;
                        pw.println(path + " > OK");
                        pw.println("...");
                        break;
                    }
                    case 3:{
                        String cmd = info.substring(3);
                        changePath(cmd);
                        pw.println("...");
                        break;
                    }
                    case 4:{
                        String name = info.substring(4);
                        getProcess(name);
                        pw.println("发送完成!");
                        pw.println("...");
                        break;
                    }
                    case 5:{
                        out = true;
                        break;
                    }
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (null != socket) {
                try {
                    socket.close();
                    socket_UDP.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

3.ls函数实现了返回当前路径的所有文件名称, 类型, 大小

public void ls() throws InterruptedException {

        File dir = new File(this.path);

        //遍历当前路径对应的文件目录, 保存文件名, 文件大小以及文件类型
        if(dir.list() == null){
            System.out.println("当前目录为空");
        }
        else{
            String[] child = dir.list();
            String[] lists = new String[child.length];
            for(int i = 0; i < child.length; ++i){
                File temp = new File(this.path+'/'+child[i]);
                if(temp.isDirectory()){
                    lists[i] = "<dir> "+child[i];
                }
                else{
                    lists[i] = "<file> "+child[i]+" "+temp.length();
                }
            }

            //格式化输出文件信息<file/dir> 文件名 文件大小(文件夹不显示文件大小)
            for(int i = 0; i < lists.length; ++i){
                StringTokenizer token = new StringTokenizer(lists[i]," ");

                if(token.countTokens() == 2){
                    pw.printf("%-10s",token.nextToken());
                    pw.printf(token.nextToken()+"\n");
                }
                else if(token.countTokens() == 3){
                    pw.printf("%-10s",token.nextToken());
                    pw.printf("%-30s",token.nextToken());
                    pw.printf(token.nextToken()+"\n");
                }
            }

        }
    }

4.getProces函数用于处理get命令, 对客户端需要get的文件进行存在性检查.
如果存在, 调用fileSender函数进行发送.

public void fileSender(File file) throws IOException {
        DatagramPacket dp = new DatagramPacket(new byte[8], 8);
        socket_UDP.receive(dp);//接收客户端发送的确认接收报文
        String msg = new String(dp.getData(), 0, dp.getLength());
        //在服务器端输出当前在向哪台主机通过哪个端口进行传输
        System.out
                .println(dp.getAddress() + ":" + dp.getPort() + ">" + msg);
        String len = String.valueOf(file.length());
        dp.setData(("0/"+String.valueOf(file.length())).getBytes());
        socket_UDP.send(dp);

        //创建 UDP发送类, 指定接收地址, 接收端口, 接收缓冲流大小, 进行文件传输
        UDPFileSender client = new UDPFileSender(dp.getAddress(),dp.getPort(),1024,socket_UDP);
        client.sendFile(file);//开始文件传输
    }

5.fileSender函数通过接收2020端口传入的UDP报文获取客户端的端口, 进行双向确认之后, 实例化UDPFileSender类进行文件传输

public void fileSender(File file) throws IOException {
        DatagramPacket dp = new DatagramPacket(new byte[8], 8);
        socket_UDP.receive(dp);//接收客户端发送的确认接收报文
        String msg = new String(dp.getData(), 0, dp.getLength());
        //在服务器端输出当前在向哪台主机通过哪个端口进行传输
        System.out
                .println(dp.getAddress() + ":" + dp.getPort() + ">" + msg);
        String len = String.valueOf(file.length());
        dp.setData(("0/"+String.valueOf(file.length())).getBytes());
        socket_UDP.send(dp);

        //创建 UDP发送类, 指定接收地址, 接收端口, 接收缓冲流大小, 进行文件传输
        UDPFileSender client = new UDPFileSender(dp.getAddress(),dp.getPort(),1024,socket_UDP);
        client.sendFile(file);//开始文件传输
    }

三:
UDPFileSender类
将文件的内容顺序传入缓冲区中, 打包成UDP包发送

public void sendFile(File file) throws IOException {
        // 读取系统文件
        byte[] fileBuf = new byte[(int)file.length()];
        byte[] readBuf = new byte[2048];
        int readLen,staPos = 0;
        FileInputStream inputStream =new FileInputStream(file);
        while ((readLen = inputStream.read(readBuf))!=-1){
            System.arraycopy(readBuf,0,fileBuf,staPos,readLen);
            staPos += readLen;
        }
        // 发送文件名长度值和文件长度值
        System.arraycopy(Utils.intToBytes(file.getName().getBytes().length),0,this.fileInfoBuf,0,4);
        System.arraycopy(Utils.longToBytes(file.length()),0,this.fileInfoBuf,4,8);
        socket.send(fileNameLenPacket);
        socket.send(fileLenPacket);
        // 发送文件名
        DatagramPacket fileNamPacket = new DatagramPacket(file.getName().getBytes(),file.getName().getBytes().length,address,port);
        socket.send(fileNamPacket);

        // 将文件缓冲流循环读入发送包缓冲流中, 进行分包发送
        int readIndex = 0;
        while (readIndex != fileBuf.length){
            if(readIndex + this.packetSize < fileBuf.length){
                System.arraycopy(fileBuf,readIndex,packetBuf,0,this.packetSize);
                readIndex += this.packetSize;
            }else{
                int rsize = fileBuf.length - readIndex;
                System.arraycopy(fileBuf,readIndex,packetBuf,0,rsize);
                readIndex += rsize;
            }
            socket.send(filePacket);
            System.out.println(String.format("已发送:%.4f",(double)readIndex / (double) file.length()));
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

服务器源代码