java实现两台电脑间TCP协议文件传输

  记录下之前所做的客户端向服务端发送文件的小项目,总结下学习到的一些方法与思路。

注:本文参考自《黑马程序员》视频。

  首先明确需求,在同一局域网下的机器人A想给喜欢了很久的机器人B发送情书,但是机器人B事先并不知道小A的心思,那么作为月老(红娘)该如何帮助他们呢?

  然后建立模型并拆分需求。这里两台主机使用网线直连,在物理层上确保建立了连接,接下来便是利用相应的协议将信息从电脑A传给电脑B。在这一步上,可以将此过程抽象为网络+I/O(Input、Output)的过程。如果能在一台电脑上实现文件之间的传输,再加上相互的网络协议,羞涩的A不就可以将情书发送给B了吗?因此要先解决在一台电脑上传输信息的问题。为了在网络上传输,使用必要的协议是必要的,TCP/IP协议簇就是为了解决计算机间通信而生,而这里主要用到UDP和TCP两种协议。当小A可以向小B发送情书后,又出现了众多的追求者,那么小B如何去处理这么多的并发任务呢?这时便要用到多线程的技术。

  因此接下来将分别介绍此过程中所用到了I/O流(最基础)、网络编程(最重要)、多线程知识(较重要)和其中一些小技巧。

一、I/O流

  I/O流用来处理设备之间的数据传输,Java对数据的传输通过流的方式。

  流按操作数据分为两种:字节流与字符流。如果数据是文本类型,那么需要使用字符流;如果是其他类型,那么使用字节流。简单来说,字符流=字节流+编码表。

  流按流向分为:输入流(将硬盘中的数据读入内存),输出流(将内存中的数据写入硬盘)。

  简单来说,想要将某文件传到目的地,需要将此文件关联输入流,然后将输入流中的信息写入到输出流中。将目的关联输出流,就可以将信息传输到目的地了。

  Java提供了大量的流对象可供使用,其中有两大基类,字节流的两个顶层父InputStream与OutputStream;字符流的两个顶层父类Reader与Writer。这些体系的子类都以父类名作为后缀,而子类名的前缀就是该对象的功能。

流对象技巧

  下提供4个明确的要点,只要明确以下几点就能比较清晰的确认使用哪几个流对象。

  1, 明确源和目的(汇)

  • 源     :InputStream       Reader
  • 目的  :OutputStream    Writer

  2, 明确数据是否是纯文本数据

  • 源   :是纯文本  :Reader

           非纯文本  :InputStream

  • 目的:是纯文本  :Writer

           非纯文本  :OutputStream

  到这里就可以明确需求中具体要用哪个体系。

  3, 明确具体的设备。

  • 源设备:

        硬盘:File

        键盘:System.in

        内存:数组

        网络:Socket流

  • 目的设备:

        硬盘:File

        控制台:System.out

        内存:数组

        网络:Socket流

  4,是否需要其他额外功能。

a) 是否需要高效(缓冲区)?

是,就加上buffer。

b) 是否需要转换?

  • 源:InputStreamReader 字节流->字符流
  • 目的:OutputStreamWriter 字符流->字节流

  在这里源为硬盘,目的也为硬盘,数据类型为情书,可能是文字的情书,也可能是小A唱的歌《情书》,因此使用字节流比较好。因此分析下来源是文件File+字节流InputStream->FileInputStream,目的是文件File+字节流OutputStream->FileOutputStream,  接下来便是数据如何从输入流到输出流的问题。

  两个流之间没有直接关系,需要使用缓冲区来作为中转,为了将读入流与缓冲区关联,首先自定义一个缓冲区数组byte[1024]。为了将读入流与缓冲区关联,使用fis.read(buf);为了将写出流与缓冲区关联,使用fos.write(buf,0,len)。为了将流中的文件写出到输出源中,要使用fos.flush或者fos.close。flush可以多次刷新,而close只能使用一次。

  代码如下,其中读写中会遇到的异常为了程序的清晰阅读,直接抛出,建议实际使用时利用try,catch处理。

 1 public class IODemo {
 2     /**
 3      * 需求:将指定文件从D盘目录d:\1下移动到d:\2下
 4      * @param args
 5      * @throws IOException 
 6      */
 7     public static void main(String[] args) throws IOException {
 8         //1,明确源和目的,建立输入流和输出流
 9         //注意路径需要使用\\,将\转义
10         FileInputStream fis = new FileInputStream("d:\\1\\1.png");//源为d盘1目录下文件1.png
11         FileOutputStream fos = new FileOutputStream("d:\\2\\2.png");//目的为d盘2目录下文件2.png
12         //2,使用自定义缓冲区将输入流和输出流关联起来
13         byte[] buf = new byte[1024];//定义1024byte的缓冲区
14         int len = 0;//输入流读到缓冲区中的长度
15         //3,将数据从输入流读入缓冲区
16         //循环读入,当读到文件最后,会得到值-1
17         while((len=fis.read(buf))!=-1){
18             fos.write(buf,0,len);//将读到长度部分写入输出流
19         }
20         //4,关流,需要关闭底层资源
21         fis.close();
22         fos.close();
23     }
24 }

   这样小A就可以自己给自己发送情书啦,接下来怎么利用网络给小A和小B前线搭桥呢?

二、网络编程

  在I/O技术中,网络的源设备都是Socket流,因此网络可以简单理解为将I/O中的设备换成了Socket。

  首先要明确的是传输协议使用UDP还是TCP。这里直接使用TCP传输。

TCP

  TCP是传输控制协议,具体的特点有以下几点:

  • 建立连接,形成传输数据的通道
  • 在连接中进行大数据量传输
  • 通过三次握手完成连接,是可靠协议
  • 必须建立连接,效率会稍低

Socket套接字

  不管使用UDP还是TCP,都需要使用Socket套接字,Socket就是为网络服务提供的一种机制。通信的两端都有Socket,网络通信其实就是Socket间的通信,数据在两个Socket间通过I/O传输

TCP传输

  TCP传输的两端分别为客户端与服务端,java中对应的对象为Socket与ServerSocket。需要分别建立客户端与服务端,在建立连接后通过Socket中的IO流进行数据的传输,然后关闭Socket。

  同样,客户端与服务器端是两个独立的应用程序。

  Socket类实现客户端套接字,ServerSocket类实现服务器套接字。

  客户端向服务端发送信息建立通道,通道建立后服务器端向客户端发送信息。

  客户端一般初始化时要指定对方的IP地址和端口,IP地址可以是IP对象,也可以是IP对象字符串表现形式。

  建立通道后,信息传输通过Socket流,为底层建立好的,又有输入和输出,想要获取输入或输出流对象,找Socket来获取。为字节流。getInputStream()和getOutputStream()方法来获取输入流和输出流。

  服务端获取到客户端Socket对象,通过其对象与Cilent进行通讯。

  客户端的输出对应服务端的输入,服务端的输出对应客户端的输入。

  下面将之前的功能复杂化,变成将客户端硬盘上的文件发送至服务端。

客户端与服务端的演示

客户端
 1 //客户端发数据到服务端
 2         /*
 3          * TCP传输,客户端建立的过程
 4          * 1,创建TCP客户端Socket服务,使用的是Socket对象。
 5          *         建议该对象一创建就明确目的地。要连接的主机。
 6          * 2,如果连接建立成功,说明数据传输通道已建立。
 7          *         该通道就是Socket流,是底层建立好的。既然是流,说明这里既有输入,又有输出。
 8          * 3,使用输出流,将数据写出。
 9          * 4,关闭资源。
10          */
11         // 建立客户端Socket
12         Socket s = new Socket(InetAddress.getLocalHost(), 9003);
13         // 获得输出流
14         OutputStream out = s.getOutputStream();
15         // 获得输入流
16         FileInputStream fis = new FileInputStream("d:\\1\\1.png");
17         // 发送文件信息
18         byte[] buf = new byte[1024];
19         int len = 0;
20         while ((len = fis.read(buf)) != -1) {
21             // 写入到Socket输出流
22             out.write(buf, 0, len);
23         }
24         s.shutdownOutput();
25         // 关流
26         out.close();
27         fis.close();
28         s.close();

   注意:在建立客户端Socket服务的时候,需要指定服务端的IP地址和端口号,此处在实现在一台电脑上演示,因此服务端的地址是本机的IP地址。

服务端
 1         // 建立服务端
 2         ServerSocket ss = new ServerSocket(9003);// 需要指定端口,客户端与服务端相同,一般在1000-65535之间
 3         //服务端一般一直开启来接收客户端的信息。
 4         while (true) {
 5             // 获取客户端Socket
 6             Socket s = ss.accept();
 7             // 获取输入流与输出流
 8             InputStream in = s.getInputStream();// 输入流
 9             FileOutputStream fos = new FileOutputStream("d:\\3\\3.png");
10             // 创建缓冲区关联输入流与输出流
11             byte[] buf = new byte[1024];
12             int len = 0;
13             // 数据的写入
14             while ((len = in.read(buf)) != -1) {
15                 fos.write(buf, 0, len);
16             }
17             // 关流
18             fos.close();
19             s.close();
20         }

  因为此时还没有用到File类,因此与流关联的文件夹必须被提前创建,否则没办法成功写入。所以建议后续使用File对象来完成文件与流的关联。

三、传输任意类型后缀的文件

  因为只有一次通信的过程,因此服务端事先不知道客户端所传输文件的类型,因此可以让服务端与客户端进行简单的交互,这里只考虑成功传输的情况。

  具体实现过程为:一、客户端向服务端发送文件完整名称;二、服务端接收到完整名称,提取文件后缀名发送给客户端;三、客户端接收到服务端发送的后缀名进行校验,不同则关闭客户端Socket流,结束客户端进程;四、如果正确,则发送文件信息。五、服务端根据接收到的文件名称和客户端ip地址建立相应的文件夹(如果不存在,则创立文件夹),将客户端Socket输入流信息写入文件,关闭客户端流。这样因为多了一次传输文件后缀名的过程,因此可以传输任意类型的文件,便于之后的拓展,如可以加入图形界面,选择任意想要传输的文件。

  这样基础功能已经大部分完成,但是此时一次只能连接一个客户端,这样如果机器人小B有若干追求者,也只能乖乖等小A将文件传输完毕,为了解决可以同时接收多个客户端的信息,需要用到多线程的技术。

四、多线程

  多线程的实现有两种方法,一种是继承Thread类,另一种是实现Runnable接口然后作为线程任务传递给Thread对象,这里选择第二种实现Runnable接口。需要覆写此接口的run()方法,在之前的基础之上改动,将获取到的客户端Socket对象传入线程任务的run()方法,线程任务类需要持有Socket的引用,利用构造函数对此引用进行初始化。将读取输入流至关闭客户端流的操作封装至run()方法。需要注意的是,此过程中代码会抛出异常,而实现接口类不能throw异常,只能进行try,catch处理(接口中无此异常声明,因此不能抛出)。在服务器类中,新建Thread对象,将线程任务类对象传入,调用Thread类的start()方法开启线程。

五、总结

  以上便基本实现了此任务的核心功能,即通过TCP协议,实现了多台客户端与主机间任意类型文件的传输,其中最核心的知识点在于I/O流,即需要弄清输入流与输出流,利用缓冲区进行二者的关联;在此基础上,加入了网络技术编程,将输入输出流更改为Socket套接字;为了增加拓展性,引入文件对象,实现客户端与服务端的交互;为了实现多台电脑与主机的文件传输,引入了多线程。程序中为了尽量简化与抽象最核心的内容,一些代码与逻辑难免有纰漏,希望大家多多指正与交流。当然此过程完全可以由UDP协议完成,在某些场景下UDP也更有优势,此处不再赘述。

完整代码

客户端

 1 import java.io.File;
 2 import java.io.FileInputStream;
 3 import java.io.IOException;
 4 import java.io.InputStream;
 5 import java.io.OutputStream;
 6 import java.net.InetAddress;
 7 import java.net.Socket;
 8 import java.net.UnknownHostException;
 9 
10 public class Client {
11     public static void main(String[] args) throws UnknownHostException, IOException {
12         /*
13          * 客户端先向服务端发送一个文件名,服务端接收到后给客户端一个反馈,然后客户端开始发送文件
14          */
15         //建立客户端Socket
16         Socket s = new Socket(InetAddress.getLocalHost(), 9001);//修改为服务器IP地址
17         //获得输出流
18         OutputStream out = s.getOutputStream();
19         //关联发送文件
20         File file = new File("D:\\1.png");
21         String name = file.getName();//获取文件完整名称
22         String[] fileName = name.split("\\.");//将文件名按照.来分割,因为.是正则表达式中的特殊字符,因此需要转义
23         String fileLast = fileName[fileName.length-1];//后缀名
24         //写入信息到输出流
25         out.write(name.getBytes());
26         //读取服务端的反馈信息
27         InputStream in = s.getInputStream();
28         byte[] names = new byte[50];
29         int len = in.read(names);
30         String nameIn = new String(names, 0, len);
31         if(!fileLast.equals(nameIn)){
32             //结束输出,并结束当前线程
33             s.close();
34             System.exit(1);
35         }
36         //如果正确,则发送文件信息
37         //读取文件信息
38         FileInputStream fr = new FileInputStream(file);
39         //发送文件信息
40         byte[] buf = new byte[1024];
41         while((len=fr.read(buf))!=-1){
42             //写入到Socket输出流
43             out.write(buf,0,len);
44         }
45         //关流
46         out.close();
47         fr.close();
48         s.close();
49     }
50 }

服务端

任务类
 1 import java.io.File;
 2 import java.io.FileOutputStream;
 3 import java.io.InputStream;
 4 import java.io.OutputStream;
 5 import java.net.Socket;
 6 
 7 public class Task implements Runnable {
 8     private Socket s;
 9     public Task(Socket s){
10         this.s = s;
11     }
12     @Override
13     public void run() {
14         String ip = s.getInetAddress().getHostAddress();
15         try{
16             //获取客户端输入流
17             InputStream in = s.getInputStream();
18             //读取信息
19             byte[] names = new byte[100];
20             int len = in.read(names);
21             String fileName = new String(names, 0, len);
22             String[] fileNames = fileName.split("\\.");
23             String fileLast = fileNames[fileNames.length-1];
24             //然后将后缀名发给客户端
25             OutputStream out = s.getOutputStream();
26             out.write(fileLast.getBytes());
27             //新建文件
28             File dir = new File("d:\\server\\"+ip);
29             if(!dir.exists())
30                 dir.mkdirs();
31             File file = new File(dir,fileNames[0]+"."+fileLast);
32             FileOutputStream fos = new FileOutputStream(file);
33             //将Socket输入流中的信息读入到文件
34             byte[] bufIn = new byte[1024];
35             while((len = in.read(bufIn))!=-1){
36                 //写入文件
37                 fos.write(bufIn, 0, len);
38             }
39             fos.close();
40             s.close();
41         }catch(Exception e){
42             e.printStackTrace();
43         }
44     }
45 }
服务器类
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws IOException {
        /*
         * 服务端先接收客户端传过来的信息,然后向客户端发送接收成功,新建文件,接收客户端信息
         */
        //建立服务端
        ServerSocket ss = new ServerSocket(9001);//客户端端口需要与服务端一致
        while(true){
            //获取客户端Socket
            Socket s = ss.accept();
            new Thread(new Task(s)).start();
        }
    }
}

  以上内容就到这里,如有错误和不清晰的地方,请大家指正!

 

转载于:https://www.cnblogs.com/hughjava/p/10594721.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值