记录下以前所作的客户端向服务端发送文件的小项目,总结下学习到的一些方法与思路。java
注:本文参考自《黑马程序员》视频。程序员
首先明确需求,在同一局域网下的机器人A想给喜欢了好久的机器人B发送情书,可是机器人B事先并不知道小A的心思,那么做为月老(红娘)该如何帮助他们呢?正则表达式
而后创建模型并拆分需求。这里两台主机使用网线直连,在物理层上确保创建了链接,接下来即是利用相应的协议将信息从电脑A传给电脑B。在这一步上,能够将此过程抽象为网络+I/O(Input、Output)的过程。若是能在一台电脑上实现文件之间的传输,再加上相互的网络协议,羞涩的A不就能够将情书发送给B了吗?所以要先解决在一台电脑上传输信息的问题。为了在网络上传输,使用必要的协议是必要的,TCP/IP协议簇就是为了解决计算机间通讯而生,而这里主要用到UDP和TCP两种协议。当小A能够向小B发送情书后,又出现了众多的追求者,那么小B如何去处理这么多的并发任务呢?这时便要用到多线程的技术。编程
所以接下来将分别介绍此过程当中所用到了I/O流(最基础)、网络编程(最重要)、多线程知识(较重要)和其中一些小技巧。数组
I/O流用来处理设备之间的数据传输,Java对数据的传输经过流的方式。服务器
流按操做数据分为两种:字节流与字符流。若是数据是文本类型,那么须要使用字符流;若是是其余类型,那么使用字节流。简单来讲,字符流=字节流+编码表。网络
流按流向分为:输入流(将硬盘中的数据读入内存),输出流(将内存中的数据写入硬盘)。多线程
简单来讲,想要将某文件传到目的地,须要将此文件关联输入流,而后将输入流中的信息写入到输出流中。将目的关联输出流,就能够将信息传输到目的地了。并发
Java提供了大量的流对象可供使用,其中有两大基类,字节流的两个顶层父InputStream与OutputStream;字符流的两个顶层父类Reader与Writer。这些体系的子类都以父类名做为后缀,而子类名的前缀就是该对象的功能。ide
下提供4个明确的要点,只要明确如下几点就能比较清晰的确认使用哪几个流对象。
1, 明确源和目的(汇)
2, 明确数据是不是纯文本数据
非纯文本 :InputStream
非纯文本 :OutputStream
到这里就能够明确需求中具体要用哪一个体系。
3, 明确具体的设备。
硬盘:File
键盘:System.in
内存:数组
网络:Socket流
硬盘:File
控制台:System.out
内存:数组
网络:Socket流
4,是否须要其余额外功能。
a) 是否须要高效(缓冲区)?
是,就加上buffer。
b) 是否须要转换?
是
在这里源为硬盘,目的也为硬盘,数据类型为情书,多是文字的情书,也多是小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是传输控制协议,具体的特色有如下几点:
无论使用UDP仍是TCP,都须要使用Socket套接字,Socket就是为网络服务提供的一种机制。通讯的两端都有Socket,网络通讯其实就是Socket间的通讯,数据在两个Socket间经过I/O传输。
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对象来完成文件与流的关联。
由于只有一次通讯的过程,所以服务端事先不知道客户端所传输文件的类型,所以可让服务端与客户端进行简单的交互,这里只考虑成功传输的状况。
具体实现过程为:1、客户端向服务端发送文件完整名称;2、服务端接收到完整名称,提取文件后缀名发送给客户端;3、客户端接收到服务端发送的后缀名进行校验,不一样则关闭客户端Socket流,结束客户端进程;4、若是正确,则发送文件信息。5、服务端根据接收到的文件名称和客户端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(); } } }
以上内容就到这里,若有错误和不清晰的地方,请你们指正!