记录下之前所做的客户端向服务端发送文件的小项目, 总结下学习到的一些方法与思路.
注: 本文参考自《黑马程序员》视频.
首先明确需求, 在同一局域网下的机器人 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 处理.
- public class IODemo {
- /**
- * 需求: 将指定文件从 D 盘目录 d:\1 下移动到 d:\2 下
- * @param args
- * @throws IOException
- */
- public static void main(String[] args) throws IOException {
- //1, 明确源和目的, 建立输入流和输出流
- // 注意路径需要使用 \\, 将 \ 转义
- FileInputStream fis = new FileInputStream("d:\\1\\1.png");// 源为 d 盘 1 目录下文件 1.PNG
- FileOutputStream fos = new FileOutputStream("d:\\2\\2.png");// 目的为 d 盘 2 目录下文件 2.PNG
- //2, 使用自定义缓冲区将输入流和输出流关联起来
- byte[] buf = new byte[1024];// 定义 1024byte 的缓冲区
- int len = 0;// 输入流读到缓冲区中的长度
- //3, 将数据从输入流读入缓冲区
- // 循环读入, 当读到文件最后, 会得到值 - 1
- while((len=fis.read(buf))!=-1){
- fos.write(buf,0,len);// 将读到长度部分写入输出流
- }
- //4, 关流, 需要关闭底层资源
- fis.close();
- fos.close();
- }
- }
这样小 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 进行通讯.
客户端的输出对应服务端的输入, 服务端的输出对应客户端的输入.
下面将之前的功能复杂化, 变成将客户端硬盘上的文件发送至服务端.
客户端与服务端的演示
客户端
- // 客户端发数据到服务端
- /*
- * TCP 传输, 客户端建立的过程
- * 1, 创建 TCP 客户端 Socket 服务, 使用的是 Socket 对象.
- * 建议该对象一创建就明确目的地. 要连接的主机.
- * 2, 如果连接建立成功, 说明数据传输通道已建立.
- * 该通道就是 Socket 流, 是底层建立好的. 既然是流, 说明这里既有输入, 又有输出.
- * 3, 使用输出流, 将数据写出.
- * 4, 关闭资源.
- */
- // 建立客户端 Socket
- Socket s = new Socket(InetAddress.getLocalHost(), 9003);
- // 获得输出流
- OutputStream out = s.getOutputStream();
- // 获得输入流
- FileInputStream fis = new FileInputStream("d:\\1\\1.png");
- // 发送文件信息
- byte[] buf = new byte[1024];
- int len = 0;
- while ((len = fis.read(buf)) != -1) {
- // 写入到 Socket 输出流
- out.write(buf, 0, len);
- }
- s.shutdownOutput();
- // 关流
- out.close();
- fis.close();
- s.close();
注意: 在建立客户端 Socket 服务的时候, 需要指定服务端的 IP 地址和端口号, 此处在实现在一台电脑上演示, 因此服务端的地址是本机的 IP 地址.
服务端
- // 建立服务端
- ServerSocket ss = new ServerSocket(9003);// 需要指定端口, 客户端与服务端相同, 一般在 1000-65535 之间
- // 服务端一般一直开启来接收客户端的信息.
- while (true) {
- // 获取客户端 Socket
- Socket s = ss.accept();
- // 获取输入流与输出流
- InputStream in = s.getInputStream();// 输入流
- FileOutputStream fos = new FileOutputStream("d:\\3\\3.png");
- // 创建缓冲区关联输入流与输出流
- byte[] buf = new byte[1024];
- int len = 0;
- // 数据的写入
- while ((len = in.read(buf)) != -1) {
- fos.write(buf, 0, len);
- }
- // 关流
- fos.close();
- s.close();
- }
因为此时还没有用到 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 也更有优势, 此处不再赘述.
完整代码
客户端
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.NET.InetAddress;
- import java.NET.Socket;
- import java.NET.UnknownHostException;
- public class Client {
- public static void main(String[] args) throws UnknownHostException, IOException {
- /*
- * 客户端先向服务端发送一个文件名, 服务端接收到后给客户端一个反馈, 然后客户端开始发送文件
- */
- // 建立客户端 Socket
- Socket s = new Socket(InetAddress.getLocalHost(), 9001);// 修改为服务器 IP 地址
- // 获得输出流
- OutputStream out = s.getOutputStream();
- // 关联发送文件
- File file = new File("D:\\1.png");
- String name = file.getName();// 获取文件完整名称
- String[] fileName = name.split("\\.");// 将文件名按照. 来分割, 因为. 是正则表达式中的特殊字符, 因此需要转义
- String fileLast = fileName[fileName.length-1];// 后缀名
- // 写入信息到输出流
- out.write(name.getBytes());
- // 读取服务端的反馈信息
- InputStream in = s.getInputStream();
- byte[] names = new byte[50];
- int len = in.read(names);
- String nameIn = new String(names, 0, len);
- if(!fileLast.equals(nameIn)){
- // 结束输出, 并结束当前线程
- s.close();
- System.exit(1);
- }
- // 如果正确, 则发送文件信息
- // 读取文件信息
- FileInputStream fr = new FileInputStream(file);
- // 发送文件信息
- byte[] buf = new byte[1024];
- while((len=fr.read(buf))!=-1){
- // 写入到 Socket 输出流
- out.write(buf,0,len);
- }
- // 关流
- out.close();
- fr.close();
- s.close();
- }
- }
服务端
任务类
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.NET.Socket;
- public class Task implements Runnable {
- private Socket s;
- public Task(Socket s){
- this.s = s;
- }
- @Override
- public void run() {
- String ip = s.getInetAddress().getHostAddress();
- try{
- // 获取客户端输入流
- InputStream in = s.getInputStream();
- // 读取信息
- byte[] names = new byte[100];
- int len = in.read(names);
- String fileName = new String(names, 0, len);
- String[] fileNames = fileName.split("\\.");
- String fileLast = fileNames[fileNames.length-1];
- // 然后将后缀名发给客户端
- OutputStream out = s.getOutputStream();
- out.write(fileLast.getBytes());
- // 新建文件
- File dir = new File("d:\\server\\"+ip);
- if(!dir.exists())
- dir.mkdirs();
- File file = new File(dir,fileNames[0]+"."+fileLast);
- FileOutputStream fos = new FileOutputStream(file);
- // 将 Socket 输入流中的信息读入到文件
- byte[] bufIn = new byte[1024];
- while((len = in.read(bufIn))!=-1){
- // 写入文件
- fos.write(bufIn, 0, len);
- }
- fos.close();
- s.close();
- }catch(Exception e){
- e.printStackTrace();
- }
- }
- }
服务器类
- 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