一個簡單的文件分享工具
GitHub 地址:https://github.com/SmythAsen/ShareFilesSystem
在過去的一個星期,我在做一個基于TCP/IP傳輸層協(xié)議的文件分享工具狰闪,這個工具的主要功能就是類似于將要分享文件的分享端作為Ftp文件服務(wù)器,而下載文件方則為客戶端迹鹅。而在此過程中织阳,服務(wù)端需要提供給客戶端ip地址和端口號站削。
說明:編寫此程序目的在于學(xué)習(xí)TCP/IP傳輸協(xié)議相關(guān)知識些侍,http協(xié)議及ftp協(xié)議的底層實(shí)現(xiàn)原理攘蔽。并未打算應(yīng)用此程序,所以沒有說明界面設(shè)計呐粘,主要注重功能的實(shí)現(xiàn)满俗,同時寫本文也是為了記錄和總結(jié)這學(xué)習(xí)的過程,更是為了鍛煉自己的寫作能力作岖,歡迎大家指出我的不足與錯誤唆垃。也歡迎大家來github提出你們的想法。
一痘儡、當(dāng)前進(jìn)度
由于比較忙辕万,項(xiàng)目的整體進(jìn)度比較慢,一個星期下來只實(shí)現(xiàn)了客戶端的基本功能沉删,所以這一次記錄也主要記錄客戶端的工作以及原理渐尿。
二、工具主要設(shè)計思想
- 服務(wù)端:
1.服務(wù)端需要共享一個文件夾矾瑰,并將文件列表發(fā)送給每一個連接上來的客戶端砖茸。
2.需要接受到客戶端發(fā)送過來的下載文件的指令
3.根據(jù)文件下載指令向客戶端傳輸指定的文件以及文件的大小。 - 客戶端:
1.客戶端需要輸入服務(wù)端ip以及端口號來與其進(jìn)行連接殴穴。
2.接受客戶端發(fā)送過來的文件列表
3.將輸入的文件下載指令發(fā)送給服務(wù)端
4.接收服務(wù)端傳送過來的文件并保存到到指定目錄
三凉夯、客戶端實(shí)現(xiàn)原理
- 首先客戶端需要連接上服務(wù)器,非常簡單采幌。
//連接到服務(wù)器
public void connect() throws UnknownHostException, IOException{
socket = new Socket(ip, port);
isconnect = true;
}
注意劲够,這里的ip和端口都是來自客戶端界面的。 同時我們需要將錯誤拋給調(diào)用他的界面休傍,一點(diǎn)出現(xiàn)任何問題讓界面進(jìn)行處理征绎。
- 其次客戶端需要接受來自服務(wù)端的文件列表
//獲取從服務(wù)器傳輸過來的文件名
@SuppressWarnings("unchecked")
public String getFilesName() {
String filesName = "";
if(isconnect){
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(socket.getInputStream());
//從服務(wù)器取得文件列表
files = (TreeMap<Integer, String>) ois.readObject();
Set<Integer> ids = files.keySet();
for (Object id : ids) {
String s = id + ":" +files.get(id)+"\n";
filesName += s;
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return filesName;
}
return "未接收到內(nèi)容";
}
這里需要注意的是服務(wù)端我是用TreeMap將文件名以及對應(yīng)的指令傳輸過來的。所以需要使用ObjectInputStream來接收文件列表并將接收類型強(qiáng)轉(zhuǎn)為TreeMap類型磨取。最后將接收的文件id和文件名拼成一個字符串返回個調(diào)用此方法的客戶端界面炒瘸。
- 接下來淤堵,就是將界面輸入的需要下載的文件指令發(fā)送給服務(wù)端了。
/**
* 檢驗(yàn)客戶端界面發(fā)過來的指令是否正確顷扩,
如果正確則向服務(wù)器發(fā)送下載指令并向客戶端返回tru拐邪,
如果不正確則向客服的界面返回false
* @param id
* @param dir
* @return
*/
public boolean sendComm(int id, String dir) {
//判斷界面?zhèn)鬟^來的指令是否存在
boolean isCommExist = false;
for (int i : files.keySet()) {
if(i == id){
isCommExist = true;
}
}
if(isCommExist){
PrintStream ps;
try {
//發(fā)送下載指令
ps = new PrintStream(socket.getOutputStream());
ps.println(String.valueOf(id));
//設(shè)置下載文件
this.dir = dir;
this.filename = files.get(id);
} catch (IOException e) {
return false;
}
return true;
}
return false;
}
在這里我們首先要檢驗(yàn)界面要我們發(fā)送的指令是否在,如果不存在則要求重新輸入隘截,如果存在則向服務(wù)端發(fā)送該指令扎阶,并向界面給的路徑設(shè)置為我們即將保存文件的路徑。
- 最后就是下載文件了
//下載文件
private void download(String dir,String filename) throws IOException {
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(dir,filename)));
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//取得所下載文件大小
fileSize = ois.readLong();
System.out.println("cc:文件下載的大小為"+fileSize);
byte[] b = new byte[1024];
int len = 0;
while((len = bis.read(b)) != -1){
bos.write(b, 0, len);
currentFileSize += len;
}
bos.close();
}
這里婶芭,我們需要分別準(zhǔn)備服務(wù)端的輸入流和客戶端的輸出流东臀,在接收文件的同時將其保存到指定的路徑下。當(dāng)然為了在客戶端試試顯示下載的情況犀农,這里也需要先獲得服務(wù)端傳輸過來的文件大小并將其返回給界面惰赋,并將當(dāng)前下載進(jìn)度返回給界面。
四呵哨、客戶端判斷邏輯
- 連接判斷邏輯
// 檢驗(yàn)輸入信息
if (!tf_ip.getText().trim().equals("") && !tf_port.getText().trim().equals("")) { // 保證輸入框有內(nèi)容
if (tf_port.getText().trim().matches("^\\d*$")) {// 判斷端口輸入框內(nèi)容是否為數(shù)字
ip = tf_ip.getText().trim(); // 將輸入的ip地址傳輸?shù)教幚沓绦? port = Integer.parseInt(tf_port.getText().trim());// 將輸入的接口傳入到處理程序
try {
// 連接服務(wù)器
cc = new ClientCore(ip, port);
cc.connect();
isconnected = true;
} catch (UnknownHostException e) {
isconnected = false;
} catch (IOException e) {
isconnected = false;
}
}
if (isconnected) {
label_isconnect.setText("連接成功赁濒!");
label_isconnect.setForeground(Color.GREEN);
// 連接成功后將服務(wù)器傳輸過來的內(nèi)容展示在客戶端上面
filesName = cc.getFilesName();// 獲取文件列表
filelist.setText("服務(wù)器文件:\n" + filesName);// 展示文件列表
} else {
connectErr(label_isconnect, "IP地址或端口號錯誤!");
}
} else {
connectErr(label_isconnect, "請輸入IP地址或端口號孟害!");
}
1.我們要判斷輸入框時候有內(nèi)容拒炎,如果沒有這提示輸入ip或端口。
2.我們還要判斷端口號里的內(nèi)容是否為數(shù)字挨务,如果為數(shù)字則將其強(qiáng)轉(zhuǎn)型為int類型并嘗試連接服務(wù)端击你。期間出現(xiàn)任何問題則判斷為連接不成功。
3.根據(jù)標(biāo)志isconnected來判斷是否連接成功谎柄,并提示相應(yīng)的操作丁侄。
- 下載判斷邏輯
if (isconnected) {
// 檢驗(yàn)選擇的指令是否存在,路徑是否正確
int comm = 0;
String dir = null;
// 首先檢驗(yàn)時候有輸入id和路徑
if (!"".equals(tf_id.getText().trim()) && !"".equals(tf_savedir.getText().trim())) {
// 將驗(yàn)證工作交給類ClientCore處理
if (tf_id.getText().trim().matches("^\\d*$")) { // 判斷文件id是否為數(shù)字
comm = Integer.parseInt(tf_id.getText().trim());
dir = tf_savedir.getText().trim();
// 如果通過CilentCore的驗(yàn)證朝巫,則下載該文件,下載文件邏輯交給ClientCore
if (cc.sendComm(comm, dir)) { // 判斷指令是否正確
try {
cc.startDownload();
} catch (IOException e) {
// TODO 處理下載錯誤邏輯...
e.printStackTrace();
}
// 監(jiān)控下載進(jìn)度绒障,此處應(yīng)該判斷,當(dāng)文件下載遇到錯誤是如何處理//TODO
btn_download.setEnabled(false);
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
task = new DownloadTask(ClientFrame.this, cc, btn_download, pr, isdownload);
task.addPropertyChangeListener(this);
task.execute();
}
} else {
downloadErr("請輸入正確指令");
}
} else {
downloadErr("請輸入下載指令或下載路徑");
}
} else {
downloadErr("請先連接服務(wù)器");
}
}
1.在下載之前捍歪,我們首先要檢查一下是否已經(jīng)連接上服務(wù)端了户辱,如果沒有連接則提示先連接服務(wù)端再下載。
2.我們同樣也需要判斷輸入框內(nèi)是否有內(nèi)容糙臼,如果沒有則提示相應(yīng)的信息庐镐。如果有,這里我們同樣要判斷指令為數(shù)字才能將其發(fā)送給處理核心的類变逃。如果一切都沒有問題則開始下載必逆。
在開始下載的同時需要開啟一個線程來監(jiān)控下載的進(jìn)度。下面是監(jiān)控下載進(jìn)度的代碼。
downloadTask.java
package com.asen.client;
import java.awt.Color;
import java.awt.Toolkit;
import java.text.SimpleDateFormat;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;
/**
* 為客戶端下載提供的委托事件名眉,用于更新進(jìn)度條
* @author Asen
*/
public class DownloadTask extends SwingWorker<Void, Void> {
private JFrame frame;
private JProgressBar pr;
private JLabel isdownload;
private JButton btn_download;
private ClientCore cc; // 客服端邏輯處理類
public DownloadTask(JFrame frame, ClientCore cc, JButton btn_download, JProgressBar pr, JLabel isdownload) {
this.frame = frame;
this.btn_download = btn_download;
this.pr = pr;
this.isdownload = isdownload;
this.cc = cc;
}
/*
* 主要任務(wù)粟矿,當(dāng)現(xiàn)場啟動后,會在后臺運(yùn)行
*/
@Override
public Void doInBackground() {
int progress = 0;
// // 初始化進(jìn)度條
setProgress(0);
//讓程序監(jiān)控進(jìn)度的程序休眠0.5ms损拢,保證拿到文件的總大小
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//獲得進(jìn)度條的最大值
long total = cc.getFileSize();
pr.setMinimum(0);
pr.setMaximum((int) total);
String tip = "";
while (progress < total) {
progress = cc.getCurrentFileSize();
pr.setValue(progress);
tip = "正在下載:"+String.valueOf(progress/(1024*1024))+"MB/"+String.valueOf(total/(1024*1024))+"MB";
pr.setString(tip);
isdownload.setText("正在下載..");
isdownload.setForeground(Color.ORANGE);
System.out.println("dt:當(dāng)前下載大小------>" + progress);
}
return null;
}
@Override
public void done() {
SimpleDateFormat sdf = new SimpleDateFormat("yyy-MM-dd HH:mm:dd");
System.out.println(sdf.format(System.currentTimeMillis())+":下載完成陌粹!");
isdownload.setText("下載完成");
pr.setString("下載完成,文件大小:"+cc.getFileSize()/(1024*1024)+"MB");
isdownload.setForeground(Color.GREEN);
Toolkit.getDefaultToolkit().beep(); //下載完成后發(fā)出提示音
btn_download.setEnabled(true);
frame.setCursor(null); // 關(guān)閉鼠標(biāo)等待狀態(tài)
}
}
以上基本就是該項(xiàng)目的所有核心代碼了,當(dāng)然這里界面的代碼我就沒有放進(jìn)來了福压,因?yàn)橛悬c(diǎn)多掏秩,而且還沒多大用處,如果需要詳細(xì)了解該項(xiàng)目的話荆姆,請點(diǎn)擊最頂端的github地址查看項(xiàng)目源碼蒙幻,我會將后期的完善以及升級也提交上去。如果想了解我更多可以點(diǎn)擊下方原文鏈接關(guān)注下面的公眾號哦胆筒。
原文鏈接