DataPulse-智能文件同步系统-2
一、 客户端登录自动将服务器文件同步到客户端功能
最初的同步方法:
客户端在登录时,获取当前客户端本地的文件,将当前客户端本地文件夹下所有的文件名发送给服务器,服务器接收到文件名后与自己本地的用户文件夹下的文件名进行比较,将服务器特有的文件及文件内容发送给客户端,客户端接收文件名,创建文件并将文件内容写入。
效果为:用户登录时,将下载客户端没有而服务器中有的文件至客户端
每个客户端登录时会扫描自己的本地文件夹,本地文件夹中存储当前客户端登录过的所有用户的文件,目前本地文件夹为固定,后续会根据不同客户端进行指定
User对象中的fileMap为当前客户端当前用户在本地文件夹中的所有文件
问题:
后续上传操作和删除操作需要实现多客户端同步,通过新建线程间断执行synCilent()方法,对于上传操作来说,可以实现A客户端上传文件同步至B客户端,但是删除操作无法实现A客户端删除文件B客户端也删除文件,因此需要修改同步文件的逻辑。使文件的同步对上传和删除操作都生效。
LoginListener_1
增加synClient()方法与服务器文件同步(第一版)
//自动将服务器存储用户A的文件同步至客户端
public void synClient(User user, String ip, int port) {
System.out.println("调用同步服务器文件至当前客户端方法");
//获取当前用户本地所有的文件
HashSet<File> file = user.getFileMap();
ArrayList<String> localFNames = new ArrayList<String>();
Iterator<File> it = file.iterator();
while (it.hasNext()) {
String fName = it.next().getName();
localFNames.add(fName);
}
//登录时发送用户名和所有文件名给服务器
try {
Socket socket = new Socket(ip, port);
InputStream inputStream = socket.getInputStream();
OutputStream os = socket.getOutputStream();
//发送指令
os.write("SYNCHR".getBytes());
os.write(user.getUname().getBytes().length);
os.write(user.getUname().getBytes());
System.out.println("同步文件中用户名为:" + user.getUname());
System.out.println("客户端本地文件数为:"+localFNames.size());
os.write(localFNames.size());
if(localFNames.size() > 0){
for (int i = 0; i < localFNames.size(); i++) {
os.write(localFNames.get(i).getBytes().length);
os.write(localFNames.get(i).getBytes());
}
}
//接收服务器发送的特有文件信息
//读取文件名长度
int len = inputStream.read();
if(len != -1){
byte[] fNameArr = new byte[len];
inputStream.read(fNameArr);
String fName = new String(fNameArr);
System.out.println("客户端接收到的文件名为:"+fName);
File f = new File("userdataB\\"+fName);
if(!f.exists()){
f.createNewFile();
file.add(f);
}
FileOutputStream fos = new FileOutputStream(f);
byte[] bytes = new byte[2048];
int cnt ;
while((cnt = inputStream.read(bytes))!= -1){
fos.write(bytes,0,cnt);
}
fos.close();
}
os.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
第二版同步文件逻辑:
客户端请求获取服务器中登录用户的所有文件名
遍历客户端用户文件,将客户端特有文件删除(同步文件对删除操作生效)
遍历服务器文件,建立连接请求下载服务器特有文件
LoginListener_2
synClient()方法第二版
//自动将服务器存储用户user的文件同步至客户端
public void synClient(User user, String ip, int port) {
System.out.println("调用同步服务器文件至当前客户端方法");
//获取当前用户本地所有的文件
HashSet<File> files = user.getFileMap();
ArrayList<String> localFNames = new ArrayList<String>();
Iterator<File> it = files.iterator();
while (it.hasNext()) {
String fName = it.next().getName();
localFNames.add(fName);
}
try {
Socket socket = new Socket(ip, port);
InputStream is= socket.getInputStream();
OutputStream os = socket.getOutputStream();
//发送获取服务器文件列表指令
os.write("FILIST".getBytes());
//发送用户名
os.write(user.getUname().getBytes().length);
os.write(user.getUname().getBytes());
//获取文件个数
int b1 = is.read();
int b2 = is.read();
int b3 = is.read();
int b4 = is.read();
int fileNum = b1 << 24 | b2 << 16 | b3 << 8 | b4 ;
System.out.println("客户端接收到到服务器用户的文件数为:"+fileNum);
List<String> serverFiles = new ArrayList<>();
while(fileNum != 0 ){
//读取文件名
int len = is.read();
byte[] bytes = new byte[len];
is.read(bytes);
serverFiles.add(new String(bytes));
fileNum --;
}
//遍历本地客户端文件
for (int i = 0; i < localFNames.size(); i++) {
String local = localFNames.get(i);
if(!serverFiles.contains(local)){
//客户端删除服务端没有的文件
File file = new File("userdataB\\"+local);
if(file.exists()){
file.delete();
files.remove(file);
localFNames.remove(local);
}
}
}
os.close();
is.close();
socket.close();
System.out.println(serverFiles);
System.out.println(localFNames);
for(String s : serverFiles) {
if(!localFNames.contains(s)){
System.out.println(s);
File f = new File("userdataB\\"+s);
if(!f.exists()){
f.createNewFile();
files.add(f);
}
Socket so = new Socket(ip,port);
OutputStream out = so.getOutputStream();
//请求下载文件名为s的服务器文件
out.write("DOWNLO".getBytes());
out.write(user.getUname().getBytes().length);
out.write(user.getUname().getBytes());
out.write(s.getBytes().length);
out.write(s.getBytes());
FileOutputStream fos = new FileOutputStream(f);
InputStream in = so.getInputStream();
byte[] content = new byte[2048];
int count ;
while((count = in.read(content)) != -1){
fos.write(content,0,count);
}
fos.close();
in.close();
out.close();
so.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
登录成功后调用synClient方法
登录成功后才初始化主页即展示用户信息和文件列表页面,因此此时不需要更新展示文件信息的表格
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals("登录")) {
String uname = uName.getText();
String passw = psw.getText();
String res = service.login(uname, passw);
showMessageDialog(uname + res);
if (res.equals("登录成功")) {
User user = service.getUser(uname);
synClient(user, "127.0.0.1", 12345);
//跳转另一个页面
ui.showMainUI(user);
}
}
}
二、多客户端同步服务器文件
场景描述:
同一个用户,在A客户端登录了系统,B客户端也登录了系统,在A客户端上传了文件,服务器上同步上传文件,同时B客户端本地也需要同步A客户端上传的文件。在B客户端上传了文件,同时A客户端本地需要同步B客户端上传的文件。在客户端A,B对文件做的删除操作同时也需要对客户端B,A生效。
方式一:
由于之前的代码都是在进行某一个具体操作时,才创建socket与服务器进行连接,操作结束后连接就关闭了,所以不存在服务器同时与两个客户端都保持连接的情况,因此尝试三个操作共用一个socket,用完之后不关闭,服务器记录用户的不同客户端与服务端连接的socket,将上传的文件发送给非本次连接的其他客户端
方式二:
客户端发送请求,不断检查服务端文件与客户端本地文件是否一致,不一致则同步文件
方式一问题:
所有操作共用一个socket,很容易出现java.net.SocketException: Socket is closed错误,且不好排查
在使用socket中的输入输出流之后将流关掉,后续再调用getInputStream(),getOutputStream()方法会报socket关闭
除上述情况还有其他情况会Socket关闭,如客户端socket.close()、网络问题、服务器连接关闭等
若客户端使用流后不关闭流,可能会导致文件无法删除和java.net.SocketException: Software caused connection abort: socket write error
具体原因不明
由于上述问题无法解决,因此决定采用方式二
方式二实现:
在用户登录成功后,新建线程,每过一段时间轮询与服务器进行同步,这样在A上传文件成功后,B通过同步线程也可以与服务器进行同步,只是可能会有一点时间差。
LoginListener
增加startFileSyncListener方法,在登录成功后调用,启动线程每5s同步一次服务器文件,并更新文件table面板
public void startFileSyncListener(User user){
System.out.println("进入startFileSyncListener方法");
new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("线程run方法执行啦!");
synClient(user,"127.0.0.1",12345);
UI.updateTable(user.getFileMap());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals("登录")) {
String uname = uName.getText();
String passw = psw.getText();
String res = service.login(uname, passw);
showMessageDialog(uname + res);
if (res.equals("登录成功")) {
User user = service.getUser(uname);
synClient(user, "127.0.0.1", 12345);
//跳转另一个页面
ui.showMainUI(user);
//登录成功后启动线程 每5s同步一次服务器文件
startFileSyncListener(user);
}
}
}
三、服务器
处理两个请求,一个请求获取用户的所有文件,一个请求下载文件,在handleCilent方法中增加两个分支
if(cmd.equals("DOWNLO")){
System.out.println("开始处理DOWNLO指令");
out = socket.getOutputStream();
int len = in.read();
byte [] bytes = new byte[len];
in.read(bytes);
String fileName = new String(bytes);
System.out.println("客户端请求下载的文件为:"+fileName);
File f = new File("serverFile\\"+uName+"\\"+fileName);
FileInputStream fis = new FileInputStream(f);
if(f.exists()){
byte[] content = new byte[2048];
int count ;
while((count = fis.read(content)) != -1){
out.write(content,0,count);
}
}
out.close();
fis.close();
}else if(cmd.equals("FILIST")){
//获取服务该用户所有的文件名列表
HashMap<String ,File > fileMap = userFile.get(uName);
Set<String> keySet = fileMap.keySet();
Iterator<String> it = keySet.iterator();
System.out.println("用户"+uName+"服务器中的文件数为:"+fileMap.size());
out = socket.getOutputStream();
//发送文件个数
int fileNum = fileMap.size();
int b1 = fileNum >> 24 ;
int b2 = fileNum >> 16 ;
int b3 = fileNum >> 8;
int b4 = fileNum >> 0;
out.write(b1);
out.write(b2);
out.write(b3);
out.write(b4);
while(it.hasNext()){
String fileName = it.next();
out.write(fileName.getBytes().length);
out.write(fileName.getBytes());
}
}
}
问题记录:
服务器文件结构为:根目录serverFile+用户名+fileName
处理下载文件请求时,一时忘记了服务器的文件存储结构,客户端没发送用户名,导致服务器这边不能正常执行,现象表现为客户端遍历服务器文件名时,明明服务器特有文件大于1,但是每次操作执行到第一个文件就没有响应了。
总结
第二版主要实现多客户端同步服务器文件功能
注意点:
客户端每次请求都需要发送用户名,不发送用户名流程会卡住。
服务器使用输入输出流时需要先获取流,不获取只声明会报空指针。