Java随笔记 - BIO,Socket实例,实现多笔数据的传输

实现多笔数据的传输
  • 在之前的博客(Java随笔记 - BIO Socket 编程实例)中,给出了两个简单的Socket编程实例。一个是实现了字节流传输,其中由于服务端并不清楚客户端的数据传输在什么时候结束,所以在客户端代码的第23行可以看到,通过调用socket.shutdownOutput( )告知服务端,客户端的数据已经传输完毕了,相应的在服务端的代码第24行可以看到,服务端通过调用socket.isInputShutdown( )来检测客户端是否完成其数据的传输。
  • 虽然上述方法可以实现数据传输完毕后的标识效果,但是一旦客户端调用了socket.shutdownOutput( )方法后,就不能再使用当前的连接向服务端发送其他数据,如果想要发送新的数据,那只能发起新的连接,而发起连接的代价,可想而知。所以,我们肯定尽可能的在一个连接上进行多笔数据的传输。但是问题也随之而来,传输多个字符串或者多个文件时,我们需要制定一个传输协议,以标识每笔数据的边界。常用的数据边界标识方法有两种:一种是在每笔数据的最后添加一个结束标识;另一种则是在发送每笔数据之前,先发送这笔数据的长度。
  • 在Java随笔记 - BIO Socket 编程实例中,另一个编程实例实现了字符流传输,它就是采用了上述中的第一种方法,使用了换行符来作为每笔字符串数据的结束符。但是这一策略在字节流传输中并不适用,因为类似的结束符十分容易与文件中的数据发生冲突,最后导致数据传输错误。所以在字节流传输中,应采用上述中的第二种方法。
  • 下面的代码将会使用这一策略,在连接建立后,客户端向服务端发送多个文件。客户端在发送每一个文件之前,需要计算当前文件的字节数(使用int类型变量进行存储),然后转换成4个字节发送给服务端,以此让服务端得知接下来将传输的文件大小。服务端在连接建立后,首先从输入流中读取4个字节,并将其转换成int类型,就能知道接下来客户端将要传输的文件大小,然后通过循环读取以及累计,将客户端传输过来的数据写入到服务端的文件中,在累计达到文件大小后,则完成此次数据传输,向客户端返回一个"ok"字符串。后续服务端则继续尝试从输入流中读取4个字节的数据,开始新的数据的传输,以此类推。而客户端则在完成所有文件的传输后,调用socket.shutdownOutput( )告知服务端其数据传输完毕,服务端则会通过socket.isInputShutdown( )来决定是否结束数据读取的循环。具体的代码实现如下:
// 客户端代码 Client.java
public class Client {

    public static final Logger logger = LoggerFactory.getLogger(Client.class);

    public static byte[] intToBytearray(int num) {
        return new byte[] {(byte)((num >> 24) & 0xFF), (byte)((num >> 16) & 0xFF),
                (byte)((num >> 8) & 0xFF), (byte)(num & 0xFF)};
    }

    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        BufferedInputStream bufferedInputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        byte[] buffer = new byte[16];
        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1" ,8080);
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
            for (int i = 1; i < 4; i++) {
                String fileName = "tmpClient" + i + ".txt";
                bufferedInputStream = new BufferedInputStream(new FileInputStream(new File(fileName)));
                int t = bufferedInputStream.available();
                logger.info("send file {}, size = {}", fileName, t);
                bufferedOutputStream.write(intToBytearray(t));
                int r = 0;
                while ((r = bufferedInputStream.read(buffer)) > 0) {
                    t -= r;
                    logger.info("read {} bytes from file, {} bytes remain.", r, t);
                    bufferedOutputStream.write(buffer, 0, r);
                    bufferedOutputStream.flush();
                }
                String s = bufferedReader.readLine();
                if ("ok".equalsIgnoreCase(s)) {
                    logger.info("transport file {} ok", fileName);
                } else {
                    logger.error("transport file {} fail", fileName);
                }
            }
            socket.shutdownOutput();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            StreamUtil.close(bufferedReader);
            StreamUtil.close(bufferedOutputStream);
            StreamUtil.close(bufferedInputStream);
            StreamUtil.close(socket);
        }
    }

}
// 服务端代码 Processor.java 实现了Runnable接口
//  Server每接收到一个客户端的请求,就会实例化一个Processor对象,开启一个线程进行处理

public class Processor implements Runnable {

    public static final Logger logger = LoggerFactory.getLogger(Processor.class);

    private Socket socket;

    public Processor(Socket socket) {
        this.socket = socket;
    }

    public static int byteArrayToInt(byte[] arr) {
        return (arr[0] & 0xFF) << 24 | (arr[1] & 0xFF) << 16 |
                (arr[2] & 0xFF) << 8 | (arr[3] & 0xFF);
    }

    @Override
    public void run() {
        PrintWriter printWriter = null;
        BufferedInputStream bufferedInputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        byte[] buffer = new byte[16];
        int cnt = 0;
        byte[] header = new byte[4];
        try {
            bufferedInputStream = new BufferedInputStream(socket.getInputStream());
            printWriter = new PrintWriter(socket.getOutputStream(), true);
            while (!socket.isInputShutdown() && bufferedInputStream.read(header) == 4) {
                int t = byteArrayToInt(header);
                if (t < 1) {
                    printWriter.println("error");
                    break;
                }
                logger.info("incoming file size = {}", t);
                String fileName = "tmpServer" + cnt + ".txt";
                bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(new File(fileName)));
                int r = 0;
                while (t > 0 && (r = bufferedInputStream.read(buffer)) > 0) {
                    t -= r;
                    logger.info("t bytes remain, {}", t);
                    bufferedOutputStream.write(buffer, 0, r);
                    bufferedOutputStream.flush();
                }
                logger.info("received file {}", fileName);
                printWriter.println("ok");
                cnt++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            StreamUtil.close(printWriter);
            StreamUtil.close(bufferedOutputStream);
            StreamUtil.close(bufferedInputStream);
            StreamUtil.close(socket);
        }
    }
}
// 服务端代码 Server.java
// 主要就是负责监听来自客户端的连接请求

public class Server implements Runnable {

    public static final Logger logger = LoggerFactory.getLogger(Server.class);

    private final ExecutorService threadPool = Executors.newCachedThreadPool();
    private ServerSocket serverSocket = null;

    public void start() throws IOException {
        serverSocket = new ServerSocket(8080);
        threadPool.execute(this);
    }

    @Override
    public void run() {
        Socket socket = null;
        try {
            while ((socket = serverSocket.accept()) != null) {
                logger.info("client {} connected.", socket.getRemoteSocketAddress());
                threadPool.execute(new Processor(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void close() {
        if (serverSocket != null && !serverSocket.isClosed()) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                logger.error("error on close.", e);
            }
        }
        threadPool.shutdown();
    }

    public static void main(String[] args) {
        Server server = new Server();
        BufferedReader keyboardReader = null;
        try {
            server.start();

            System.out.println("type 'exit' to end.");

            keyboardReader = new BufferedReader(new InputStreamReader(System.in));
            String cmd = null;
            while ((cmd = keyboardReader.readLine()) != null) {
                if ("exit".equalsIgnoreCase(cmd)) {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            StreamUtil.close(keyboardReader);
            server.close();
        }
    }

}
// 工具类
	
public class StreamUtil {
	
    public static final Logger logger = LoggerFactory.getLogger(StreamUtil.class);
	
    public static void close(Closeable stream) {
        if (stream == null) {
            return ;
        }
        try {
            stream.close();
        } catch (Exception e) {
            logger.error("errors on close {}", stream.getClass().getName(), e);
        }
    }
	
}