OrientShare,Java写的跨平台文件传输软件-服务端
前两天搞的跨平台文件传输软件,仅限局域网内,功能很简单,算是个Demo吧。
主要功能:
客户端自动上线;
服务端自动握手链接;
服务端向客户端进行文件同步操作;
文件改变时自动发送。
最小化到托盘,自动运行。
三种平台(Win Mac Linux)兼容。
说实话,这个跨平台有点作弊的嫌疑,因为本身就是用Java写的,除了做了点判断平台的操作之外,基本没什么技术含量。
最近事情有点多,过两天整理并贴出代码。见谅。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
11月9日22:48更新
注意,这里没有讲怎么用Java的Socket进行编程,这方面的教程已经有人写了,而且肯定比我写的好,所以,这里介绍的是应用Socket、Java中的ClientSocket以及ServerSocket的小例子。🌰
程序分为服务端和客户端两部分。
流程如下:
服务端有三个线程,分别为:
BroadcastListener,广播监听线程
SyncThread,文件同步线程
SyncFolder,目录监视线程
客户端有两个线程,分别是:
BroadcastListener,广播监听线程
SyncThread,文件同步线程
其中界面线程不算在内,因为界面直接是NetBeans的JFormDesigner做的。😄
服务端或者客户端启动后,通过5021端口发送上线消息,同时启动广播监听线程,监听并解析消息。如果服务端捕获到了客户端的上线消息,则进行握手并且发送本地文件。在服务端内由文件同步线程监控同步文件夹内的文件更改。如果发现了文件更改,则对当前所有已连接的客户端发送文件。
本篇先说服务端。
服务端在主函数中调用界面MainFrame的构造函数,启动文件同步线程SyncThread。
这里只给出了主函数,MainFrame是继承的JFrame,在构造函数中增加了点其他的初始化操作,具体是:
增加了个托盘区的图标(Windows和Mac下可用,图标很丑。。。大家将就看吧,也许本来就不会有人来我这荒草院子~)以及响应事件;
初始化了各组件的位置和大小;
启动了主界面更新线程。
注意,这个主要更新的是服务器当前状态,也就是一个字符串,下面各个类里面如果有StatusString,那就是他了~
1 public static void main(String args[]) throws Exception { 2 //NetBeans Generate Code. 3 java.awt.EventQueue.invokeLater(new Runnable() { 4 public void run() { 5 MainFrame mf; 6 mf = new MainFrame(); 7 mf.setVisible(true); 8 } 9 }); 10 sync = new SyncThread(); 11 sync.start(); 12 }
在SyncThread的构造函数中,做了如下几件事情:
创建服务端的UUID;构造记录用户IP与UUID的HashMap;
构造封装好的广播发送类,BroadcastSend sender。
判断当前操作系统的类型,启动SyncFolder线程;
在线程的执行函数中执行如下两个操作:
循环判断服务器当前状态;
每隔1s发送消息;(注意,这里有个BUG。)
发送广播的时候,服务端发送的是最简单的本机的UUID和消息代码,在客户端中,我们发送的广播是比较详细的。
1 package widekuan; 2 3 import java.io.IOException; 4 import java.util.Iterator; 5 import java.util.Map; 6 import java.util.Properties; 7 import java.util.UUID; 8 import java.util.logging.Level; 9 import java.util.logging.Logger; 10 11 /** 12 * 13 * @author sunkuan 14 */ 15 public class SyncThread extends Thread{ 16 UserMap map; 17 BroadcastSend sender; 18 BroadcastListenThread listener; 19 FileShareServer client; 20 String globalUuid; 21 String osName; 22 SyncFolder Folder; 23 /* 24 * int status: 25 * 0:idle. 26 * 1:online. 27 * 2:offline. 28 */ 29 int status; 30 String FileName; 31 public int osFlag; 32 public String StatusString; 33 34 public SyncThread() throws Exception{ 35 map = new UserMap(); 36 globalUuid = UUID.randomUUID().toString();//length:36. 37 sender = new BroadcastSend(map,this); 38 /* 39 * 3:Server is online. 40 */ 41 globalUuid = globalUuid.concat("3"); 42 sender.SendMessage(globalUuid); 43 /* 44 * osFlag: 45 * 0:Mac 46 * 1:Linux 47 * 2:Win 48 */ 49 Properties props=System.getProperties(); 50 osName = props.getProperty("os.name"); 51 System.out.println(osName); 52 if(osName.startsWith("Mac")){ 53 FileName = "DFT/Send/"; 54 }else if(osName.startsWith("Linux")){ 55 FileName = "DFT/Send/"; 56 }else if(osName.startsWith("Win")){ 57 FileName = "DFT\\Send\\"; 58 } 59 Folder = new SyncFolder(FileName,this); 60 Folder.start(); 61 status = 0; 62 this.listener = sender.listener; 63 StatusString = "Initialize."; 64 } 65 66 @Override 67 public void run(){ 68 while(true){ 69 switch(status){ 70 case 0: 71 //idle. 72 StatusString = "Waiting Client online."; 73 break; 74 case 1: 75 //online. 76 StatusString = "Client online"; 77 Iterator iterIPID = map.MapIPID.entrySet().iterator(); 78 79 //The MapIPID has NO next entry,but the folder is changed. 80 while (iterIPID.hasNext()){ 81 Map.Entry entryIPID = (Map.Entry) iterIPID.next(); 82 83 for(int i = 0;i < Folder.fileList.size();i++){ 84 //Creat a new Server. 85 try { 86 StatusString = "Connecting Client @ "+entryIPID. 87 getValue().toString(); 88 client = new FileShareServer(entryIPID.getValue().toString()); 89 StatusString = "Connected Client @ "+entryIPID.getKey().toString(); 90 client.UUID = entryIPID.getKey().toString(); 91 StatusString = "Get respons from client @"+entryIPID.getValue().toString(); 92 client.getRespond(Folder.fileList.get(i).getAbsolutePath()); 93 //Must have a little nap here. 94 //Or the AcceptFlag can NOT work correctly. 95 Thread.sleep(1000); 96 } catch (Exception ex) { 97 Logger.getLogger(SyncThread.class.getName()).log(Level.SEVERE, null, ex); 98 } 99 StatusString = "Waiting for server accept file."; 100 while(!listener.respondFlag); 101 if(!listener.AcceptFlag){ 102 //Wait the client to repond. 103 //If ANY respond recieved,it won't be online here. 104 StatusString = "Client refused file."; 105 client.closeServer(); 106 StatusString = "Client @ "+entryIPID.getValue().toString()+"rejected file "+Folder.fileList.get(i).getName(); 107 // System.out.println("Rejected."); 108 continue; 109 110 }else{ 111 112 StatusString = "Client @ "+entryIPID.getValue().toString()+"accepted file "+Folder.fileList.get(i).getName(); 113 // System.out.println("Client Accept."); 114 try { 115 StatusString = "Sending file to Client @ "+entryIPID.getValue().toString()+"File name:"+Folder.fileList.get(i).getName(); 116 client.Send(Folder.fileList.get(i).getAbsolutePath()); 117 client.closeServer(); 118 } catch (IOException ex) { 119 Logger.getLogger(SyncThread.class.getName()).log(Level.SEVERE, null, ex); 120 } 121 /* 122 * Make the flag flase here. 123 * So the next file we need to check that bitch again. 124 * And that's MAYBE make sense. 125 */ 126 listener.AcceptFlag = false; 127 listener.respondFlag = false; 128 } 129 130 } 131 } 132 Folder.isChanged = false; 133 System.out.println("All Done."); 134 status = 0; 135 break; 136 case 2: 137 //offline. 138 139 break; 140 } 141 try { 142 StatusString = "Broadcasting online message."; 143 Thread.sleep(1000); 144 sender.SendMessage(globalUuid); 145 } catch (Exception ex) { 146 System.out.println(ex.getMessage()); 147 } 148 } 149 } 150 151 }
下面是BroadcastSend类,主要负责发送消息,因为服务端需要发送的消息比较少,因此只对这个类进行了最简单的封装。
1 package widekuan; 2 3 import java.io.IOException; 4 import java.net.DatagramPacket; 5 import java.net.DatagramSocket; 6 import java.net.InetAddress; 7 8 /** 9 * 10 * @author sunkuan 11 */ 12 public class BroadcastSend { 13 public DatagramSocket server;// UDP 14 private InetAddress broadcastAddress;//Broadcast Addr. 15 private int port;// Listen port. 16 BroadcastListenThread listener; 17 /** 18 * 19 * @param UserMap,contains the users' UUID and IP address. 20 */ 21 public BroadcastSend(UserMap usermapArg,SyncThread syncArg){ 22 try { 23 port = 5021; 24 server = new DatagramSocket(port); 25 broadcastAddress = InetAddress.getByName("255.255.255.255"); 26 if(listener == null) 27 { 28 listener = new BroadcastListenThread(server,usermapArg,syncArg); 29 listener.start(); 30 } 31 } catch (Exception ex) { 32 System.err.println("Socket Connection FAIL"); 33 } 34 } 35 public void sendBroadcast(byte[] data, InetAddress address) { 36 try { 37 DatagramPacket dp = new DatagramPacket(data, data.length, address, port); 38 server.send(dp); 39 } catch (IOException ex) { 40 System.err.println("Send Broadcast FAIL."); 41 } 42 } 43 /** 44 * @param Message need to send. 45 * @return void. 46 * @exception Exceptions. 47 */ 48 public void SendMessage(String msgArg) throws Exception{ 49 //String sendMSG = new String("Fuck!"); 50 sendBroadcast(msgArg.getBytes(), broadcastAddress); 51 52 } 53 }
文件同步类,SyncFolder,通过文件夹的修改时间来判断其中文件是否有变化。
1 package widekuan; 2 3 import java.io.File; 4 import java.util.ArrayList; 5 6 /** 7 * 8 * @author sunkuan 9 */ 10 public class SyncFolder extends Thread{ 11 File file; 12 ArrayList<File> fileList; 13 File[] fileArray; 14 String FolderName; 15 long lastmodifiedOld; 16 boolean isChanged; 17 SyncThread syncThread; 18 public SyncFolder(String FolderNameArg,SyncThread syncThreadArg){ 19 FolderName = FolderNameArg; 20 file = new File(FolderName); 21 lastmodifiedOld = file.lastModified(); 22 //ScanFile(FolderName); 23 isChanged = false; 24 syncThread = syncThreadArg; 25 26 if(file.isDirectory()){ 27 fileArray = file.listFiles(); 28 }else{ 29 System.err.println("The file name is not a FOLDER!"); 30 } 31 fileList = new ArrayList<File>(); 32 ScanAllFile(file); 33 lastmodifiedOld = file.lastModified(); 34 } 35 /* 36 * Don't use this. 37 */ 38 private void ScanFile(String PathArg){ 39 File searchFile = new File(PathArg); 40 if(searchFile.isDirectory()){ 41 fileArray = file.listFiles(); 42 } 43 fileList = new ArrayList<>(); 44 for(int i = 0;i < fileArray.length;i++){ 45 if(fileArray[i].getName().startsWith(".")){ 46 continue; 47 } 48 if(fileArray[i].isDirectory()){ 49 ScanFile(fileArray[i].getName()); 50 } 51 fileList.add(fileArray[i]); 52 System.out.println(fileArray[i].getName()); 53 } 54 } 55 private void ScanAllFile(File fileArg){ 56 File[] foo; 57 /* 58 * We must guarantee the arg is a folder. 59 */ 60 foo = fileArg.listFiles(); 61 for(int i = 0;i <foo.length;i++){ 62 if(foo[i].isDirectory()){ 63 ScanAllFile(foo[i]); 64 }else{ 65 if(!foo[i].getName().startsWith(".")){ 66 if(!fileList.contains(foo[i])){ 67 fileList.add(foo[i]); 68 System.out.println(foo[i].getAbsoluteFile()); 69 }else{ 70 continue; 71 } 72 } 73 } 74 } 75 } 76 @Override 77 public void run(){ 78 while(true){ 79 try { 80 Thread.sleep(5000); 81 } catch (InterruptedException ex) { 82 continue; 83 } 84 if(lastmodifiedOld != file.lastModified()){ 85 //File modify detected. 86 ScanAllFile(file); 87 lastmodifiedOld = file.lastModified(); 88 System.err.println("File Changed."); 89 isChanged = true; 90 syncThread.status = 1; 91 }else{ 92 //No motified files. 93 //Do NOTHING here.Modify the boolean in the sync Thread. 94 95 } 96 97 } 98 } 99 }
万事具备,只欠客户端发来的上线消息了~所以我们需要一个监听客户端发来消息的东东,BroadcastListener。
客户端发来的消息都是明码,所以这个代码也就很浅显易懂了。
1 /* 2 * To change this template, choose Tools | Templates 3 * and open the template in the editor. 4 */ 5 package widekuan; 6 7 import java.net.DatagramPacket; 8 import java.net.DatagramSocket; 9 import java.net.InetAddress; 10 import java.util.HashMap; 11 12 13 public class BroadcastListenThread extends Thread{ 14 15 private DatagramSocket server; 16 public UserMap userMap; 17 public HashMap<String,Integer> respondMap; 18 private SyncThread sync; 19 String msg; 20 DatagramPacket dp; 21 boolean DoneFlag; 22 boolean respondFlag; 23 boolean AcceptFlag; 24 InetAddress RespondIP; 25 public BroadcastListenThread( 26 DatagramSocket serverArg, 27 UserMap usermapArg,SyncThread syncArg) throws Exception{ 28 server = serverArg; 29 userMap = usermapArg; 30 sync = syncArg; 31 AcceptFlag = false; 32 DoneFlag = false; 33 respondFlag = false; 34 RespondIP = null; 35 36 } 37 38 @Override 39 public void run() { 40 dp = new DatagramPacket( new byte[1024], 1024); 41 while (true) { 42 try { 43 server.receive(dp); 44 //decode broadcast pakage. 45 msg = new String(dp.getData(), 46 0, dp.getLength(), "UTF-8"); 47 } catch (Exception e) { 48 msg = ""; 49 } 50 if(msg.endsWith("online") && !userMap.MapIPID.containsKey(msg)){ 51 sync.status = 1; 52 userMap.add(msg, dp.getAddress()); 53 System.out.printf("Client count is %d \n", 54 userMap.MapIPID.size()); 55 //continue; 56 }else if(msg.endsWith("online") && userMap.MapIPID.containsKey(msg)){ 57 //Client send a useless online message. 58 //continue; 59 }else if(msg.length()<36||msg.length() == 36){ 60 //No client here. 61 sync.status = 0; 62 continue; 63 }else if(msg.endsWith("offline")){ 64 System.out.println("Client offline."); 65 // userMap.remove(msg,dp.getAddress()); 66 // if(userMap.MapIPID.isEmpty()){ 67 // sync.status = 2; 68 // } 69 70 //Modify the HashMap . 71 //So the server won't send file to it. 72 //The orignal version has a wrong String. 73 userMap.remove(msg.replaceAll("offline", "online"), RespondIP); 74 continue; 75 }else if(msg.endsWith("accept")){ 76 //Client Accept the file. 77 //System.out.println(msg); 78 AcceptFlag = true; 79 RespondIP = dp.getAddress(); 80 respondFlag = true; 81 continue; 82 }else if(msg.endsWith("reject")){ 83 AcceptFlag = false; 84 RespondIP = dp.getAddress(); 85 respondFlag = true; 86 continue; 87 }else if(msg.endsWith("done")){ 88 DoneFlag = true; 89 respondFlag = false; 90 AcceptFlag = false; 91 continue; 92 }else if(msg.endsWith("done")){ 93 DoneFlag = true; 94 respondFlag = false; 95 AcceptFlag = false; 96 continue; 97 } 98 } 99 } 100 101 }
由BroadcastListener的功能可知,这个线程只是根据客户端发来的消息更改了几个“Flag”变量,那么,这几个“Flag”变量是怎么被应用的呢?让我们回头看下SyncThread 的执行函数。我们会发现,在case 1(客户端上线)中,有两个个判断。首先是判断当前有木有客户端连接上,如果木有,则继续等待;如果有,则循环发送文件列表中的所有文件。发送文件的过程中,则又有一个问题。如果客户端不想要这个文件怎么办呢?我们不能强塞给人家啊,所以就需要一个AcceptFlag。服务端首先发送的是文件名以及文件大小(当然这个完全可以用MD5和Hash值替代啊~),然后由客户端判断这个文件是不是我们所要的。如果文件重复的话(光靠文件名和大小是不能判断重复的!这里只是做个演示啊!),则拒绝该文件,这时服务端的AcceptFlag当然是false。如果客户端接受文件的话,AcceptFlag显然就为true了,我们就可以调用FlieShareServer去发送文件了。当然,不管是客户端拒绝还是接受,总得给个回应是吧,这就是respondFlag的用处,首先先判断下客户端是否发送回应给我们。这个标识位是用于扩展程序功能以及防止丢包的。
那么,FileShareServer是怎么发送文件的呢?下面的代码就是答案。
1 package widekuan; 2 3 import java.io.BufferedInputStream; 4 import java.io.BufferedOutputStream; 5 import java.io.DataInputStream; 6 import java.io.DataOutputStream; 7 import java.io.File; 8 import java.io.FileInputStream; 9 import java.io.IOException; 10 import java.net.ServerSocket; 11 import java.net.Socket; 12 13 /** 14 * 15 * @author sunkuan 16 */ 17 public class FileShareServer { 18 private String SERVER_IP; 19 private static final int SERVER_PORT = 5020; 20 21 private ServerSocket server; 22 private Socket client; 23 //private DataInputStream dis; 24 private DataInputStream fis; 25 private DataOutputStream dos; 26 public int osFlag; 27 String UUID; 28 public float progress; 29 public FileShareServer(String IPArg) throws IOException{ 30 SERVER_IP = IPArg.substring(1, IPArg.length()); 31 //System.out.println("try to connect server."); 32 if(server == null){ 33 server = new ServerSocket(SERVER_PORT); 34 } 35 //System.out.println("Connected."); 36 } 37 38 39 public void Send(String FileNameArg) throws IOException{ 40 File carbageFile = new File(FileNameArg); 41 //dis = new DataInputStream(new BufferedInputStream(client.getInputStream())); 42 fis = new DataInputStream(new BufferedInputStream(new FileInputStream(FileNameArg))); 43 //dos = new DataOutputStream(client.getOutputStream()); 44 dos = new DataOutputStream(new BufferedOutputStream(client.getOutputStream())); 45 //Send the File name and length again? 46 // dos.writeUTF(carbageFile.getName()); 47 // dos.flush(); 48 // dos.writeLong((long) carbageFile.length()); 49 // dos.flush(); 50 int bufferSize = 8192; 51 byte[] buffer = new byte[bufferSize]; 52 while (true) { 53 int read = 0; 54 if (fis != null) { 55 read = fis.read(buffer); 56 } 57 if (read == -1) { 58 break; 59 } 60 dos.write(buffer, 0, read); 61 progress += (float) ((bufferSize/carbageFile.length())*100); 62 } 63 dos.flush(); 64 fis.close(); 65 client.close(); 66 server.close(); 67 System.out.println("Done."); 68 } 69 70 /* 71 * Send the filename and length,and wait for the client to response 72 * in the SyncThread. 73 */ 74 public void getRespond(String FileNameArg) throws IOException{ 75 File carbageFile = new File(FileNameArg); 76 client = server.accept(); 77 fis = new DataInputStream(new BufferedInputStream(new FileInputStream(FileNameArg))); 78 dos = new DataOutputStream(new BufferedOutputStream(client.getOutputStream())); 79 dos.writeUTF(carbageFile.getName()); 80 dos.flush(); 81 dos.writeLong((long) carbageFile.length()); 82 dos.flush(); 83 } 84 public void closeServer(){ 85 try { 86 server.close(); 87 } catch (IOException ex) { 88 System.err.println("Server Closed ERROE"); 89 } 90 } 91 }
我们可以看到,在getRespond这个函数中我们只发送了文件名和大小,然后交给客户端去判断。实际上,这个地方改称哈希值要更好些。
对了,上文中的UserMap就是个HashMap,之所以封装一下是为了以后拓展一些功能。
服务端到这里基本就结束了,Quite simple~?
PS.第一次在博客园发文章,写的不好,见谅。之前主页里面还有一些东西,不过空间都已经删了,而且自己的地盘,写起来总是有点肆无忌惮,不提了。
PPS.最近复习的跟shi一样。。。也不是没有努力,只是感觉自己好笨。数学学不好也许只有一辈子当码农了,玩不了什么高级的东西了。
PPPS.客户端的代码近期内贴出来,并且最后会打包发到pudn上。