[toc]

1. 测试项目

测试项目来自于”基于Socket编程接口实现网络通信”的所有要求

要求: 一、基于Socket编程接口实现网络通信

  1. 基本要求(70分):单Client端 C 向Server S 发送了一个文件名 test.txt,Server检查当前目录(可自行定义)下是否有这个文件。如果有这个文件,Server将这个文件发送给Client端 C;如果不存在这个文件,Server S发送文件不存在的错误信息到Client端。
  2. 附加要求(10分):Client端在成功接收到文件后需要自动向Server发送一个确认(自行定义)。
  3. 附加要求(10分):Server设置超时重传机制。即Server在发送完文件后的3秒内没有收到Client端发送过来的确认,或者确认的内容与规定不符,重新发送文件(一次即可)。
  4. 附加要求(10分):用多线程实现多Client端,即Server可以同时为多个Client端的请求提供服务。

同时本人附加了

  • 客户端接收文件持久化到本地磁盘并改名防止覆盖同名文件

2. 测试用例

本次测试使用黑盒测试方法,遍历实现的功能路径.设计实现的功能路径图可表示如下: 可以看到主要有四个测试用例,主要是在并发环境下考察各个设计功能是否正常.考虑到本机难以实现客户端不回写或者回写错误的情况,这一用例通过更改源代码实现.表示如下:

  1. 并发1000个客户端—有”test.txt”文件—,人为改变服务端接收错误,模拟客户端发送错误声明,如下图所示:

  2. 并发1000个客户端—有”test.txt”文件

  3. 并发1000个客户端—有”test.txt”文件—人为将客户端接收文件的状态改为false

4. 并发1000个客户端—无”test.txt”文件

2.1. 并发测试说明

想要做到多个客户端真正并发访问Server来测试,需要多个Client同时运行,因为是个人单台机器能力有限,使用一个主java类通过多个线程来并发启动Client来替代.

package com.socket.netcom;
 
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * 多线程来并发启动100个Client进行测试,
 */
public class ParallelTest {
    private static ExecutorService pool;
    public static void main(String[] args) {
        ClientThreadFactory clientThread = new ClientThreadFactory("Client");
//        Runnable一个没有参数和返回值的异步方法
        Runnable r = () ->{
            Client client = new Client();
            try {
                client.clientModel();
            } catch (IOException e) {
                e.printStackTrace();
            }
        };
        pool = new ThreadPoolExecutor(0,1000,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
        for(int i=0;i<1000;i++){
            pool.execute(()->{
                Thread t= clientThread.newThread(r);
                t.start();
            });
        }
//        使用指定1000大小的60s存活的线程池,1000个线程并发,最后只需要476个即可
        System.out.println(pool);
 
        pool.shutdown();
 
    }
 
 
    public static class ClientThreadFactory implements ThreadFactory {
        private final String namePrefix;
        private final AtomicInteger nextId = new AtomicInteger(1);
        ClientThreadFactory(String whatFeature){
            namePrefix = whatFeature;
        }
        @Override
        public Thread newThread(Runnable task){
            String name = namePrefix + nextId.getAndIncrement();
            Thread thread = new Thread(null,task,name,0,false);
            System.out.println(thread.getName());
            return thread;
        }
    }
 
}
 

3. 预期测试结果(分析说明)

对于前面的四个测试用例,预期应实现图中路径表示的各个功能: 实现原理请见附录部分.

4. 实际测试结果(说明原因)

4.1. 1

4.2. 2

4.3. 3

4.4. 4

5. 总结

  • Client端和Server对本地硬盘读写,使用自己创建的字节流对象(本地流)
  • Client端和Server之间进行读写,必须使用Socket中提供的字节流对象(网络流)

6. 附录

  • Server端:
/**
 *
 * 要求:
 *      1. 如果请求文件有则返回给客户端,否则返回错误消息
 *      2. 超时3s或错误返回则重传
 *      3. 多线程允许同时为多个客户端服务
 * 实现步骤:
 *      1. 创建一个服务器ServerSocket对象--ss 绑定客户端指定的端口号
 *      2. 使用ss的方法accept()获取到请求的客户端Socket对象,可以循环执行accept()
 *      3. 使用Socket对象中的方法getInputStream(),获取到网络字节输入流InputStream对象
 *      (对每一个socket新开一个线程,并在执行所有操作后释放资源)
 *      4. 把ips网络字节输入流对象转换为字符缓冲输入流来读取网络字节输入流的数据(这里即文件名,不需要做多余字符串操作)
 *      5. 判断有无文件,对应执行7
 *      6. 调用客户端Socket的socket对象的getOutputStream(),获取ops对象
 *      7. 使用ops中的write方法给客户端回写相关信息
 *          7.1 如果是本地文件,需要执行本地IO流API
 *      8. 等待3秒,确认客户端返回的确认消息;如果不符合,重发文件
 *      9. 释放资源(FileInputStream,Socket,ServerSocket)
 * @author MBin_王艺辉istarwyh
 */
public class Server {
    private static ExecutorService pool;
    /**
     *     双方协商好结束符号"EOF"
     */
    public String endMessage = "EOF";
 
    public static void main(String[] args) throws IOException {
//         *      1. 创建一个服务器ServerSocket对象--ss 绑定客户端指定的端口号
        ServerSocket ss = new ServerSocket(8081);
//        使用线程池提高服务器多线程性能,超出1000线程并发则拒绝服务
        pool = new ThreadPoolExecutor(0,1000,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
 
// *      2. 使用ss的方法accept()获取到请求的客户端Socket对象,可以循环执行accept()
        while(true){
            Socket socket = ss.accept();
            pool.execute(
                    ()-> new Thread(() -> new Server().acceptFileServer(socket)).start()
            );
 
        }
//      9. 释放资源(FileInputStream,Socket,ServerSocket) ServerSocket一直执行,就不用关闭了
//        ss.close();
    }
    public void acceptFileServer(Socket socket){
        try{
            String line = this.readLine(socket);
            File targetFile = new File("H:\\桌面\\upload\\"+line);
//                *      5. 判断有无文件,对应执行7
//                *      6. 调用客户端Socket的socket对象的getOutputStream(),获取ops对象
            if(targetFile.exists()){
                this.sentFile(socket,targetFile);
//                *      8. 等待3秒再确认客户端返回的确认消息;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                String goodMessage = "文件接收实验";
//               8. 如果不符合,重发文件一次
                if(!goodMessage.equals(this.readLine(socket))){
                    System.out.println("重发了吗");
                    this.sentFile(socket,targetFile);
                }
            }else{
                OutputStream ops = socket.getOutputStream();
                ops.write("文件不存在".getBytes());
            }
 
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                socket.close();
            }catch (IOException e){
//                *      9. 释放资源(FileInputStream,Socket,ServerSocket)
                e.printStackTrace();
            }
        }
 
    }
    public String readLine(Socket socket) throws IOException{
//           3. 使用Socket对象中的方法getInputStream(),获取到网络字节输入流InputStream对象
        InputStream ips = socket.getInputStream();
//           4. 把ips网络字节输入流对象转换为字符缓冲输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(ips));
//           4. 读取客户端的请求,这里即文件名不需要做多余字符串操作
        String line = br.readLine();
        return line;
    }
    public void sentFile(Socket socket,File targetFile) throws IOException{
        OutputStream ops = socket.getOutputStream();
//                *      7.1 如果是本地文件,需要执行本地IO流API
        FileInputStream fis = new FileInputStream(targetFile);
        byte[] bytes = new byte[1024];
        int len = 0;
        while((len = fis.read(bytes))!=-1){
            byte[] endMessageBytes = endMessage.getBytes();
            int endMessageLen = endMessageBytes.length;
            if(len < 1024-endMessageLen){
                for(byte b : endMessageBytes){
                    bytes[len++]=b;
                }
                ops.write(bytes,0,len+endMessageLen);
                break;
            }else if(len <1024){
//                防止数组越界,必须进行扩容
                byte[] bytesPlus = new byte[1027];
                int index=0;
                for(byte b : bytes){
                    bytesPlus[index++]=b;
                }
                for(byte b : endMessageBytes){
                    bytesPlus[index++]=b;
                }
                ops.write(bytes,0,len+endMessageLen);
            }else{
                ops.write(bytes,0,len);
            }
        }
    }
}
 
  • Client端:
/**
 *
 * 要求:
 *      向服务器发送请求文件名,接收服务器端返回的文件或消息
 *      成功接收到文件后自动向Server发送一个确认(自行定义)
 * 实现步骤:
 *      1.创建一个客户端Socket对象,绑定服务器IP地址和端口号
 *      2.使用Socket中的方法getOutputStream(),获取网络输出流OutputStream对象
 *      3.使用OutputStream的write(),发送文件名到服务器
 *      4.使用Socket中的方法getInputStream(),获取网络字节输入流对象InputStream
 *      5.使用InputStream中的read()读取服务器回写的数据
 *      6.将服务器端传来的文件写到本地磁盘上
 *      7.根据文件接收状态使用ops中的write方法给客户端回写相关信息
 *      8.释放资源(Socket,FileOutputStream(try()已释放)) 不能提前关闭掉socket
 * @author MBin_王艺辉istarwyh
 */
public class Client {
    private static Client client;
/**
 *     双方协商好结束符号"EOF"
 */
    public  String endMessage = "EOF";
    public  String errorMessage = "文件不存在";
    public static void main(String[] args) throws IOException {
        client = new Client();
        client.clientModel();
    }
    public void clientModel() throws IOException{
        // *      1.创建一个客户端Socket对象,绑定服务器IP地址和端口号
        Socket socket = new Socket("127.0.0.1",8081);
// *      2.使用Socket中的方法getOutputStream(),获取网络输出流OutputStream对象
        OutputStream ops = socket.getOutputStream();
// *      3.使用OutputStream的write(),发送文件名到服务器
        ops.write("test.txt".getBytes());
//        类比http换行,服务器端readLine()读取\n为换行符
        ops.write("\n".getBytes());
 
        int fileState = this.acceptFileClient(socket);
//        7.根据文件接收状态使用ops中的write方法给客户端回写相关信息
        switch (fileState){
            case 1: ops.write("文件接收成功\n".getBytes());
            case 2: {
                ops.write("文件接收失败\n".getBytes());
                this.acceptFileClient(socket);
            }
            case 3: {
                System.out.println(errorMessage);
                return;
            }
            default: System.out.println("未知情况");
        }
 
// *      8.释放资源(Socket,FileOutputStream(try()已释放)) 不能提前关闭掉socket
        socket.close();
    }
 
    public int acceptFileClient(Socket socket) throws IOException{
        // *      4.使用Socket中的方法getInputStream(),获取网络字节输入流对象InputStream
        InputStream ips = socket.getInputStream();
// *      5.使用InputStream中的read()读取服务器回写的数据
        //        提高读取效率,使用数组固定每次从输入流拉取的字节大小
        int messageLen = 2*endMessage.getBytes().length;
        byte[] bytes = new byte[1024+messageLen];
        int len=0;
        //        read()默认返回读到的byte数量,当读不到更多的数据时会返回-1
        if((len = ips.read(bytes))!=-1) {
//            取出bytes中的数据判断
            if (errorMessage.equals(new String(bytes, 0, len))) {
                return 3;
            } else {
                File filePath = new File("H:\\桌面\\download");
                if (!filePath.exists()) {
                    filePath.mkdirs();
                }
                /**
                 * 自定义一个文件的命名规则:防止同名文件被覆盖
                 *      规则:域名+毫秒值+随机数
                 */
                String fileName = "127.0.0.1." + System.currentTimeMillis() + new Random().nextInt(999999) + ".txt";
                try (FileOutputStream fos = new FileOutputStream(filePath + "\\" + fileName)) {
                    do {
//                      6.将服务器端传来的文件写到本地磁盘上
//                      上一步已经读入bytes,故这里采用do-while
//                      去掉最后的"EOF"结束标志
                        fos.write(bytes, 0, len-messageLen);
                        String m = new String(bytes, len-messageLen, messageLen);
                        if (myEquals(m,endMessage)) {
                            return 1;
                        }
                    } while ((len = ips.read(bytes)) != -1);
                }
            }
        }
        return 2;
    }
 
    /**
     * 因为默认utf8编码,从字节流中拉取的"EOF"长度为6字节,必须重写String的默认equals()
     *         System.out.println(s1.length()+"+"+c1.length);
     */
    public boolean myEquals(String s1,String s2){
        char[] c1 = s1.toCharArray();
        char[] c2 = s2.toCharArray();
        for(int i=0;i<c2.length;i++){
            if(c1[i] != c2[i]){
                return false;
            }
        }
        return true;
    }
 
}