网络编程 基于Socket的多文件传输程序实现(一)

[0] 前言-写在开始之前

新人,Java学习中,文章中遗漏错误之处,欢迎斧正
个人博客,完全原创
转载请注明出处。
项目全代码地址:GitHub

最近的学习了IO流和网络编程相关的API.IO流时学会了如何进行本地文件之间的读写操作,而网络编程的Socket类则实现了两台计算机间的数据联通交流.

每次学习了新知识,就会忍不住的进行脑洞和实践.因此本文旨在利用两个知识点进行网络编程和IO流相关的实践操作.

实现一个多文件传输系统大雾.这个系统的出发点源自于小时候对黑客一键拷贝文件的潇洒姿态的憧憬.

[1] 从一次理论成功的文件传输出发,设计类,方法和数据结构

[1.0] 主要步骤

若要实现机算机间的多文件传输系统一次成功的文件流的传输需要经过以下几个主要步骤:

  1. 客户端的目标文件夹读取,将想要传输的文件夹中所有文件都循环读取一边,存储她们
  2. 客户端与服务端建立通信连接
  3. 客户端按照文件地址进行文件读取,将数据流按顺序传输给客户端
  4. 客户端获取客户端传输的数据,将它们写入对应的文件
[1.1] 客户端递归存储文件地址

设计一个方法,传入文件夹地址参数,传出含有该文件夹下所有文件地址参数的数据结构。
根据方法的描述,我们可以设计的是一个File复制相关工具类FilesCopyUtil,并设计从指定文件夹内文件读取存储所有文件的静态(工厂)方法
static List<File> findFile(String path)
在这个方法中递归文件夹,存储所有文件地址。

伪代码:
class FileCopyUtil{

    public static List<String> findFile(String path){
        List<File> files = new ArrayList<File>();
        File file = new File(path);
        if(file.isFile) {
            files.add(file);
        } else{
            List<File> subs = file.listFiles();
            for(File sub : subs)
                findFile(sub);
        }
    }
}
[1.2] 客户端与服务端简历通信

客户端和服务端在实际应用中都是处于两台不同的计算机中进行工作,在本博客中的计算机间网络地址定位和数据数据传输使用的是Java已经进行了高层次封装好的Socket类和ServerSocket类,实际上使用的是TCP/IP协议。

TCP/IP协议(传输控制协议)由网络层的IP协议和传输层的TCP协议组成。IP层负责网络主机的定位,数据传输的路由,由IP地址可以唯一的确定Internet上的一台主机。TCP层负责面向应用的可靠的或非可靠的数据传输机制,这是网络编程的主要对象。

Socket和ServerSocket是java.net包下的两个类。ServerSocket常用与服务器端,Socket常用于客户端类建立连接时使用的。当客户端与服务器端建立网络连接时就会在两头同时创建一个Socket实例,通过对两个实例的操作,实现想要完成的操作。

因此需要创建两个类客户端类Client和服务器端类Server。

[1.21] 客户端Client类

Client类的创建和使用其中最基本的属性为Socket对象。

Socket常用的构造方法

Socket(InetAddress address, int port);//创建一个流套接字并将其连接到指定 IP 地址的指定端口号 
Socket(String host, int port);//创建一个流套接字并将其连接到指定主机上的指定端口号 
Socket(InetAddress address, int port, InetAddress localAddr, int localPort);//创建一个套接字并将其连接到指定远程地址上的指定远程端口 
Socket(String host, int port, InetAddress localAddr, int localPort);//创建一个套接字并将其连接到指定远程主机上的指定远程端口 
Socket(SocketImpl impl);//使用用户指定的 SocketImpl 创建一个未连接 Socket

在上述构造方法中的参数address,host,port分别是服务端计算机的ip地址,主机名和对应的端口号。localPort表示本地主机的端口号,localAddr和bindAddr是本地机器的地址(ServerSocket的主机地址),impl是socket的父类,既可以用来创建serverSocket又可以用来创建Socket。count则表示服务端所能支持的最大连接数。

在本程序中使用最简单的一个构造器Socket(InetAddress address, int port);来对Socket对象进行实例化,并将它实例化所需要的参数存入配置文件Client_config.xml中,这也有助于增加后期程序的移植性。需要注意这里xml文件的读取,我们将它分作两部进行,目的是为了实现同功能代码模块化。

伪代码:
class Client{
    //客户端套接字对象
    private Socket client;
    //服务器端口号_client-config.xml
    private static int PORT;
    //服务器地址
    private static String HOST;
    public Client(){
        //初始化配置信息方法
        init();
        try {
            System.out.println("客户端启动中~");
            client = new Socket(HOST, PORT);
        } catch (Exception e) {
            System.out.println("客户端启动失败~");
        }
    }

    /**
     * 从配置文件读取中客户端需要的属性值,并将它们初始化
     */
    private static void init() {
        //后期该配置从xml文件中读取
        PORT = 8088;
        HOST = "localhost";
    }
}
[1.22] 服务器端Server类

Server类的中的ServerSocket类对象最重要的作用是与客户端建立连接,并生成一个Socket实例,后续的服务器端操作都是通过这个Socket实例进行的。

ServerSocket常用的构造方法

ServerSocket(int port);//创建绑定到特定端口的服务器套接字
ServerSocket(int port, int backlog);//利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号
ServerSocket(int port, int backlog, InetAddress bindAddr);//使用指定的端口、侦听 backlog 和要绑定到的本地 IP地址创建服务器

在上述的构造方法中的参数port是声明本地计算机端口编号,需要注意的是,端口号的选择必须小心,因为计算机的每一个端口都提供一个特定的服务,值较低的端口都已被其他常用程序占用,比如0~1023端口为系统自用保留端口号。因此在创建端口号时最好使用3000以后的端口号。

在服务器端与客户端连接时有一个重要方法就是Socket accept()方法,该方法的作用在于在服务器段启动后和客户端接入前将程序“阻塞”,直到有一个客户端接入时程序被重新唤醒继续运行。除了监听客户端的接入外,它同时还会返回一个服务器端的Socket实例,该实例就是我们实际操中用于与对应客户端进行数据交流的对象。

伪代码
class Server {
    private ServerSocket server;
    //服务器端口号_server-config.xml
    private static int   PORT;

    /**
     * 启动服务端,前提是对HOST和PORT进行获取(init())
     */
    private Server() {
        //调用init(),初始化配置
        init();
        System.out.println("服务器启动中~");
        try {
            server = new ServerSocket(PORT);
        } catch (IOException e) {
            System.out.println("服hjk务器启动失败~");
        }
    }
    private static void init() {
        // 正式时需要从配置文件server-config.xml中读取
        PORT = 8088;
    }
}
[1.3] 客户端文件读取和数据传输

在这步中需要一组IO流进行配合对文件进行读取和传输,因为文件的类型和编码不同,我们不能使用已经封装好的字符流及缓冲流来进行数据传输,但是又因为我们需要在每一文件进行传输之前向服务端传送待传文件的具体属性(名字与长度),此时推荐使用DataOutputStream来对底层IO进行包装。因为DOS类中有方法void writeUTF()可以将字符串以UTF-8编码的形式向服务端传输,同时还有void writeLong()可以传输文件长度,还有基础的字节数组写出方法int write(byte[] b)。同时因为文件主要数据是以字节数组的形式进行传输的,所有文件读取的类选择采用随机读取类RandonAccessFile类。

Socket类在数据传输方法有两个十分重要的方法:

OutputStream getOutputStream()
该方法用于获得网络连接输入,同时返回一个IutputStream对象实例,读取从服务器端传入的数据
InputStream getInputStream()
该方法用于获取网络连接输出,同时返回一个OutputStream对象实例,用于向服务器端传输数据

伪代码:
private OutputStream writer = client.getOutputStream();
//数据输出流
private DataOutputStream dos = new DataOutputStream(writer);
private InputStream reader = client.getInputStream();
//数据输入流
private DataInputStrea-m dis = new DataInputStream(reader);
List<File> files = FileCopyUtil.fileFind(path) 
for(File file : files) {
    dos.writeUTF(file.getName);
    dos.writeLong(file.length());
    RAF raf = new RAF(file)
    int len = 0;
    byte[] bytes = new byte[1024*10];
    while((len = raf.read(bytes)) > 0)
        dos.write(bytes, 0, len)

    dos.flush();
} 
[1.4] 服务器端的数据接收和文件写入

在进行设计IO流之前需要知道的是,一个服务器端对应的是多个客户端,我们需要将每个客户端与服务器端对应的Socket对象的处理放入线程中,以防多客户端同时进行读写。

在完成一次文件读写的过程中,服务器端首先需要接收两个数据即fileName和fileLength,fileName用于创建文件,fileLength主要用于判断当前文件是否写入完毕。

在服务端计算机内创建好对应的文件后,就可以开始接收客户端传过来的实际文件数据并进行写入了。在这里使用和客户端对应的IO流类DataInputStream来对getInputStream()获取的低级流进行包装。
向文件写出数据的对象使用DOS对象,而不能使用RAF对象,因为RAF对象不支持行刷新方法flush()。

伪代码:
Socket Socket = server.accept();
Thread t = new Thread(new ClientHandler(socket));

class ClientHandler implements Runnable{
    //客户端对象
    private Socket socket;
    //按字节数组读入的流对象
    private InputStream reader;
    //按字节数组写出的流对象
    private OutputStream writer;
    //数据写入文件RAF对象
    private RandomAccessFile raf;
    //读取的字节数组长度
    public ClientHandler(Socket socket) throws IOException {
        this.socket = socket;
    }

    run(){
        DataInputStream dis = new DataInputStream(socket.getInputStream());
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        //每次需要写的文件对象
        File file;
        while(true){
            String name = dis.readUTF();
            long length = dis.readLong();
            file = new File(filePath+name);

            DataOutputStream fileWriter = new DataOutputStream(new FileOutputStream(file));
            int len = 0;
            long index = 0;
            while((len = dis.readd(bytes)) > 0){
                index += len;
                fileWrite.write(bytes, 0, len);
                fileWriter.flush();
                //查看文件传输进度
                System.out.println(file.getName()+"传输进度:"+len+","+index+"/"+length+":"+(index*100/length)+"%");
                if(index >= length)break;
            }
    }   
}

感谢以下文章及作者
循序渐进Socket网络编程(多客户端、信息共享、文件传输) - OpenFire
深入剖析socket网络编程的数据传输底层实现
JAVA的网络编程【转】 - 火之光

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值