实验内容:
- 复习Java I/O和Socket编程相关概念和方法;
- 基于Java Socket TCP和UDP实现一个简易的网络文件服务程序,包含服务器端FileServer和客户端FileClient;
- 服务器端启动时需传递root目录参数,并校验该目录是否有效;
- 服务器启动后,开启TCP:2021端口,UDP:2020端口,其中,TCP连接负责与用户交互,UDP负责传送文件;
- 客户端启动后,连接指定服务器的TCP 2021端口,成功后,服务器端回复信息:“客户端IP地址:客户端端口号>连接成功”;
- 连接成功后,用户可通过客户端命令行执行以下命令:
[1] ls 服务器返回当前目录文件列表(<file/dir> name size)
[2] cd 进入指定目录(需判断目录是否存在,并给出提示)
[3] get 通过UDP下载指定文件,保存到客户端当前目录下
[4] bye 断开连接,客户端运行完毕 - 服务器端支持多用户并发访问,不用考虑文件过大或UDP传输不可靠的问题。
实现:
一:
建立FileServerThreadPool类
构造函数中,基于JDK中的Executors自动建立线程池, 并绑定服务器TCP端口至2021, UDP端口至2020.
public class FileServerThreadPool {
ServerSocket serverSocket;
DatagramSocket dataSocket;
private final int PORT_TCP = 2021; // 端口
private final int PORT_UDP = 2020;
ExecutorService executorService; // 线程池
final int POOL_SIZE = 4; // 单个处理器线程池工作线程数目
public FileServerThreadPool() throws IOException {
serverSocket = new ServerSocket(PORT_TCP); // 创建服务器端TCP套接字
dataSocket = new DatagramSocket(PORT_UDP);//创建服务器端UDP套接字
// 创建线程池
// Runtime的availableProcessors()方法返回当前系统可用处理器的数目
// 由JVM根据系统的情况来决定线程的数量
executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors() * POOL_SIZE);
System.out.println("服务器启动。");
}
public static void main(String[] args) throws IOException {
new FileServerThreadPool().servic(); // 启动服务
}
/**
* service implements
*/
public void servic() throws SocketException {
Socket socket_TCP = null;
while (true) {
try {
socket_TCP = serverSocket.accept(); // 等待用户连接
executorService.execute(new Handlers(socket_TCP,dataSocket)); // 把执行交给线程池来维护
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
二:
Handlers实现.
Handlers每次被创建提供一个与单个客户通信的线程.
Handlers被FileServerThreadPool类的线程池执行调用.
Handlers主要实现了以下功能:
1.通过建立TCP输入输出流, 对用户命令进行读取和解析
public void initStream() throws IOException { // 初始化输入输出流对象方法
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
bw = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()));
pw = new PrintWriter(bw, true);
}
2.run方法提供服务器运行时, 反馈连接状态, 对输入流进行字符处理, 并进行命令解析与命令执行.处理命令时调用了CmdResolution类中的.resolute方法辅助switch语句处理命令.
public void run() {
try {
System.out.println(socket.getInetAddress() + ":" +
+ socket.getPort() + " > 连接成功"); //客户端信息
initStream(); // 初始化输入输出流对象
String info = null;
boolean out = false;
while (null != (info = br.readLine()) && !out) {
System.out.println(info);
//处理客户端通过TCP输入流传入的命令,进行处理
switch (CmdResolution.resolute(info)){
case 0:{
pw.println("无法解析的命令");
pw.println("...");
break;
}
case 1:{
ls();
pw.println("...");
break;
}
case 2:{
path = rootPath;
pw.println(path + " > OK");
pw.println("...");
break;
}
case 3:{
String cmd = info.substring(3);
changePath(cmd);
pw.println("...");
break;
}
case 4:{
String name = info.substring(4);
getProcess(name);
pw.println("发送完成!");
pw.println("...");
break;
}
case 5:{
out = true;
break;
}
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (null != socket) {
try {
socket.close();
socket_UDP.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.ls函数实现了返回当前路径的所有文件名称, 类型, 大小
public void ls() throws InterruptedException {
File dir = new File(this.path);
//遍历当前路径对应的文件目录, 保存文件名, 文件大小以及文件类型
if(dir.list() == null){
System.out.println("当前目录为空");
}
else{
String[] child = dir.list();
String[] lists = new String[child.length];
for(int i = 0; i < child.length; ++i){
File temp = new File(this.path+'/'+child[i]);
if(temp.isDirectory()){
lists[i] = "<dir> "+child[i];
}
else{
lists[i] = "<file> "+child[i]+" "+temp.length();
}
}
//格式化输出文件信息<file/dir> 文件名 文件大小(文件夹不显示文件大小)
for(int i = 0; i < lists.length; ++i){
StringTokenizer token = new StringTokenizer(lists[i]," ");
if(token.countTokens() == 2){
pw.printf("%-10s",token.nextToken());
pw.printf(token.nextToken()+"\n");
}
else if(token.countTokens() == 3){
pw.printf("%-10s",token.nextToken());
pw.printf("%-30s",token.nextToken());
pw.printf(token.nextToken()+"\n");
}
}
}
}
4.getProces函数用于处理get命令, 对客户端需要get的文件进行存在性检查.
如果存在, 调用fileSender函数进行发送.
public void fileSender(File file) throws IOException {
DatagramPacket dp = new DatagramPacket(new byte[8], 8);
socket_UDP.receive(dp);//接收客户端发送的确认接收报文
String msg = new String(dp.getData(), 0, dp.getLength());
//在服务器端输出当前在向哪台主机通过哪个端口进行传输
System.out
.println(dp.getAddress() + ":" + dp.getPort() + ">" + msg);
String len = String.valueOf(file.length());
dp.setData(("0/"+String.valueOf(file.length())).getBytes());
socket_UDP.send(dp);
//创建 UDP发送类, 指定接收地址, 接收端口, 接收缓冲流大小, 进行文件传输
UDPFileSender client = new UDPFileSender(dp.getAddress(),dp.getPort(),1024,socket_UDP);
client.sendFile(file);//开始文件传输
}
5.fileSender函数通过接收2020端口传入的UDP报文获取客户端的端口, 进行双向确认之后, 实例化UDPFileSender类进行文件传输
public void fileSender(File file) throws IOException {
DatagramPacket dp = new DatagramPacket(new byte[8], 8);
socket_UDP.receive(dp);//接收客户端发送的确认接收报文
String msg = new String(dp.getData(), 0, dp.getLength());
//在服务器端输出当前在向哪台主机通过哪个端口进行传输
System.out
.println(dp.getAddress() + ":" + dp.getPort() + ">" + msg);
String len = String.valueOf(file.length());
dp.setData(("0/"+String.valueOf(file.length())).getBytes());
socket_UDP.send(dp);
//创建 UDP发送类, 指定接收地址, 接收端口, 接收缓冲流大小, 进行文件传输
UDPFileSender client = new UDPFileSender(dp.getAddress(),dp.getPort(),1024,socket_UDP);
client.sendFile(file);//开始文件传输
}
三:
UDPFileSender类
将文件的内容顺序传入缓冲区中, 打包成UDP包发送
public void sendFile(File file) throws IOException {
// 读取系统文件
byte[] fileBuf = new byte[(int)file.length()];
byte[] readBuf = new byte[2048];
int readLen,staPos = 0;
FileInputStream inputStream =new FileInputStream(file);
while ((readLen = inputStream.read(readBuf))!=-1){
System.arraycopy(readBuf,0,fileBuf,staPos,readLen);
staPos += readLen;
}
// 发送文件名长度值和文件长度值
System.arraycopy(Utils.intToBytes(file.getName().getBytes().length),0,this.fileInfoBuf,0,4);
System.arraycopy(Utils.longToBytes(file.length()),0,this.fileInfoBuf,4,8);
socket.send(fileNameLenPacket);
socket.send(fileLenPacket);
// 发送文件名
DatagramPacket fileNamPacket = new DatagramPacket(file.getName().getBytes(),file.getName().getBytes().length,address,port);
socket.send(fileNamPacket);
// 将文件缓冲流循环读入发送包缓冲流中, 进行分包发送
int readIndex = 0;
while (readIndex != fileBuf.length){
if(readIndex + this.packetSize < fileBuf.length){
System.arraycopy(fileBuf,readIndex,packetBuf,0,this.packetSize);
readIndex += this.packetSize;
}else{
int rsize = fileBuf.length - readIndex;
System.arraycopy(fileBuf,readIndex,packetBuf,0,rsize);
readIndex += rsize;
}
socket.send(filePacket);
System.out.println(String.format("已发送:%.4f",(double)readIndex / (double) file.length()));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}