记录一次用TCP协议传文件的探索
记录一次用TCP协议传文件的探索
总的思路,客户端创建Socket对象和服务端通信,通过Socket对象获取的IO流进行数据传输.基本的代码如下:
客户端(发送端)部分
public class Client {
public static void main(String[] args) throws IOException {
// 要上传的文件
File file = new File("C:\\Users\\Yaoxi\\Pictures\\Saved Pictures\\图像.jpg");
// 创建一个socket对象用来连接服务器
// Socket(套接字)是两台计算机之间进行通信的端点
Socket socket = new Socket("127.0.0.1", 7341);
// 通过socket对象获取输入输出流
// 通过流分别创建字符缓冲输入流(读服务器反馈)和打印流(传数据给服务器)
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
// 发文件数据
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = fis.read(bytes)) != -1) {
ps.write(bytes, 0, len);
}
// 读服务器反馈
String s = br.readLine();
System.out.println(s);
// 资源释放
socket.close();
}
}
服务器(接收端)部分
public class Server {
public static void main(String[] args) throws IOException {
// 创建ServerSocket对象
// ServerSocket会等待请求通过网络进入
ServerSocket ss = new ServerSocket(7341);
// 监听客户端的连接(阻塞方法,如果没有会一直等待)
Socket socket = ss.accept();
// 获取I/O流
PrintStream ps = new PrintStream(socket.getOutputStream());
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
// 设置文件接收位置
File fileName = new File("D:\\server\\a.jpg");
// 收文件
FileOutputStream fos = new FileOutputStream(fileName);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = br.read()) != -1) {
fos.write(bytes, 0, len);
}
// 发反馈给客户端
ps.println("接收完毕");
}
}
这样子将会存在一定的问题
1.客户端未发送结束标记,服务端的read会一直等待读取
2.文件名被固定写死,每次接收都会覆盖
3.服务端一次运行后只能接收一次文件
4.服务端不能同时处理多个文件上传
对于问题1,可以给客户端发文件的时候加个结束标记
shutdownOutput()方法会通过关闭io流作为文件传输结束的标记通知服务端.
客户端(发送端)部分改造后
public class Client {
public static void main(String[] args) throws IOException {
// 要上传的文件
File file = new File("C:\\Users\\Yaoxi\\Pictures\\Saved Pictures\\图像.jpg");
// 创建一个socket对象用来连接服务器
// Socket(套接字)是两台计算机之间进行通信的端点
Socket socket = new Socket("127.0.0.1", 7341);
// 通过socket对象获取输入输出流
// 通过流分别创建字符缓冲输入流(读服务器反馈)和打印流(传数据给服务器)
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
// 发文件数据
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = fis.read(bytes)) != -1) {
ps.write(bytes, 0, len);
}
// 写结束标记
socket.shutdownOutput();
// 读服务器反馈
String s = br.readLine();
System.out.println(s);
// 资源释放
socket.close();
}
}
对于问题2,可以采取加一个随机的文件名去解决.
在客户端发送的时候可以先发一次文件名,然后服务端收到后拼接随机的字符串,作为即将接收的文件名,再接收文件存入本地磁盘中.
这个过程中会遇到另一个问题,接收文件名的时候服务端用的是BufferedReader,有缓冲区的存在导致之后传输文件时,可能会出现数据丢失.因此当文件名接收到后,服务端回写一条信息,通知客户端再发文件,作为操作的分隔.
客户端(发送端)部分改造后
public class Client {
public static void main(String[] args) throws IOException {
// 要上传的文件
File file = new File("C:\\Users\\Yaoxi\\Pictures\\Saved Pictures\\图像.jpg");
// 创建一个socket对象用来连接服务器
// Socket(套接字)是两台计算机之间进行通信的端点
Socket socket = new Socket("127.0.0.1", 7341);
// 通过socket对象获取输入输出流
// 通过流分别创建字符缓冲输入流(读服务器反馈)和打印流(传数据给服务器)
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream ps = new PrintStream(socket.getOutputStream());
// 给服务器传递文件名
ps.println(file.getName());
// 接收服务器回写的分隔信息
String start = br.readLine();
System.out.println(start);
// 再发文件数据
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = fis.read(bytes)) != -1) {
ps.write(bytes, 0, len);
}
// 写结束标记
socket.shutdownOutput();
// 读服务器反馈
String s = br.readLine();
System.out.println(s);
// 资源释放
socket.close();
}
}
服务器(接收端)部分改造后
public class Server {
public static void main(String[] args) throws IOException {
// 创建ServerSocket对象
// ServerSocket会等待请求通过网络进入
ServerSocket ss = new ServerSocket(7341);
// 监听客户端的连接(阻塞方法,如果没有会一直等待)
Socket socket = ss.accept();
// 获取I/O流
PrintStream ps = new PrintStream(socket.getOutputStream());
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
// 读文件名
String fileName = br.readLine();
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(socket.getInetAddress().getHostAddress() + " 发送文件 " + fileName);
// 回写消息给客户端
// 作为两个读取操作的分隔,防止用InputStream读数据时,BufferedReader缓冲区内容丢失
ps.println("开始接收文件");
// 创建流关联到本地硬盘
File dir = new File("D:\\server");
// 防止文件重复使用UUID作为随机字符串
String uuid = UUID.randomUUID().toString().replace("-", "");
// 设置文件接收位置
File dir = new File("D:\\server");
// 收文件并拼接文件名
FileOutputStream fos = new FileOutputStream(dir + "\\" + uuid + "_" + fileName);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = br.read()) != -1) {
fos.write(bytes, 0, len);
}
// 发反馈给客户端
ps.println("接收完毕");
}
}
对于问题3,在服务端的处理方法外加一层循环即可
服务器(接收端)部分改造后
public class Server {
public static void main(String[] args) throws IOException {
// 创建ServerSocket对象
// ServerSocket会等待请求通过网络进入
ServerSocket ss = new ServerSocket(7341);
// 使用死循环保证程序一直能接收请求
while (true) {
// 监听客户端的连接(阻塞方法,如果没有会一直等待)
Socket socket = ss.accept();
// 获取I/O流
PrintStream ps = new PrintStream(socket.getOutputStream());
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
// 读文件名
String fileName = br.readLine();
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(socket.getInetAddress().getHostAddress() + " 发送文件 " + fileName);
// 回写消息给客户端
// 作为两个读取操作的分隔,防止用InputStream读数据时,BufferedReader缓冲区内容丢失
ps.println("开始接收文件");
// 创建流关联到本地硬盘
File dir = new File("D:\\server");
// 防止文件重复使用UUID作为随机字符串
String uuid = UUID.randomUUID().toString().replace("-", "");
// 设置文件接收位置
File dir = new File("D:\\server");
// 收文件并拼接文件名
FileOutputStream fos = new FileOutputStream(dir + "\\" + uuid + "_" + fileName);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = br.read()) != -1) {
fos.write(bytes, 0, len);
}
// 发反馈给客户端
ps.println("接收完毕");
}
}
}
对于问题4,在服务端改用多线程去实现
其中需要注意 accept
方法是个阻塞方法,所以它不能放在线程的run
方法中.否则就会一直开线程,然后每个线程在执行accept的时候阻塞.
服务器(接收端)部分改造后
public class Server {
public static void main(String[] args) throws IOException {
// 创建ServerSocket对象
// ServerSocket会等待请求通过网络进入
ServerSocket ss = new ServerSocket(7341);
// 使用死循环保证程序一直能接收请求
// 可以多次访问
while (true) {
// 监听客户端的连接(阻塞方法)
// (用来阻塞main线程,当有访问的时候才继续运行开线程)
Socket socket = ss.accept();
// 用Runnable开线程
Runnable runnable = new Runnable() {
@Override
public void run() {
FileOutputStream fos = null;
try {
// 获取I/O流
PrintStream ps = new PrintStream(socket.getOutputStream());
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
// 读文件名
String fileName = br.readLine();
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(socket.getInetAddress().getHostAddress() + " 发送文件 " + fileName);
// 回写消息给客户端
// 作为两个读取操作的分隔,防止用InputStream读数据时,BufferedReader缓冲区内容丢失)
ps.println("开始接收文件");
// 创建流关联到本地硬盘
File dir = new File("D:\\server");
// 防止文件重复
String uuid = UUID.randomUUID().toString().replace("-", "");
fos = new FileOutputStream(dir + "\\" + uuid + "_" + fileName);
byte[] bytes = new byte[1024 * 8];
int len;
// 从客户端的流读
while ((len = in.read(bytes)) != -1) {
// 写到本地文件的流
fos.write(bytes, 0, len);
}
// 回写给客户端
ps.println("接收完毕");
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(socket.getInetAddress().getHostAddress() + " 发送文件接收完毕");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
};
// 提交到线程池
new Thread(runnable).start();
}
}
}
现在有了一个线程,不如顺便加个线程池,方便使用.
服务器(接收端)部分改造后
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(7341);
// 创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5,
3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 可以多次访问
while (true) {
// 监听客户端的连接(阻塞方法)
// (用来阻塞main线程,当有访问的时候才继续运行开线程)
Socket socket = ss.accept();
// 用Runnable开线程
Runnable runnable = new Runnable() {
@Override
public void run() {
FileOutputStream fos = null;
try {
// 获取I/O流
PrintStream ps = new PrintStream(socket.getOutputStream());
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
// 读文件名
String fileName = br.readLine();
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(socket.getInetAddress().getHostAddress() + " 发送文件 " + fileName);
// 回写消息给客户端
// (作为两个读取操作的分隔,
// 防止用InputStream读数据时,
// BufferedReader缓冲区内容丢失)
ps.println("开始接收文件");
// 创建流关联到本地硬盘
File dir = new File("D:\\server");
// 防止文件重复
String uuid = UUID.randomUUID().toString().replace("-", "");
fos = new FileOutputStream(dir + "\\" + uuid + "_" + fileName);
byte[] bytes = new byte[1024 * 8];
int len;
// 从客户端的流读
while ((len = in.read(bytes)) != -1) {
// 写到本地文件的流
fos.write(bytes, 0, len);
}
// 回写给客户端
ps.println("接收完毕");
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(socket.getInetAddress().getHostAddress() + " 发送文件接收完毕");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
};
// 提交到线程池
pool.submit(runnable);
}
}
}