Android如何實現茄子快傳
茄子快傳是一款文件傳輸應用,相信你們都很熟悉這款應用,應該不少人用過用來文件的傳輸。它有兩個核心的功能:html
端到端的文件傳輸
Web端的文件傳輸
這兩個核心的功能咱們具體來分析一下!java
端到端的文件傳輸
所謂的端到端的文件傳輸是指應用端發送到應用端(這裏的應用端指Android應用端),這種文件傳輸方式是文件發送端和文件接收端必須安裝應用。android
效果圖
文件發送方
git
文件接收方
github
簡單的文件傳輸的話,咱們能夠用藍牙,wifi直連,ftp這幾種方式來進行文件的傳輸。可是:web
藍牙傳輸的話,速度太慢,並且要配對。相對比較麻煩。
wifi直連差很少跟藍牙同樣,可是速率很快,也要配對。
ftp能夠實現文件的批量傳輸,可是沒有文件的縮略圖。
最初分析這個項目的時候就想着經過自定義協議的Socket的通訊來實現,自定義的協議包括header + body的自定義協議, header部分包括了文件的信息(長度,大小,文件路徑,縮略圖), body部分就是文件。如今實現這一功能。(後序:後面開發《網頁傳》功能的時候,能夠考慮這兩個核心的功能都能用在Android架設微型Http服務器來實現。這是後話了。)chrome
流程圖瀏覽器
編碼實現
兩部設備文件傳輸是須要在一個局域網的條件下的,只有文件發送方鏈接上文件接收方的熱點(搭建了一個局域網),這樣文件發送方和文件接收方就在一個局域網裏面,咱們才能夠進行Socket通訊。這是一個大前提!服務器
初始化條件 – Ap(熱點)和Wifi的管理, 文件的掃描
對Android的Ap(熱點)和Wifi的一些操做都封裝在下面兩個類:網絡
WifiMgr.java
APMgr.java
關於熱點和Wifi的操做都是根據WifiManager來操做的。因此要像操做WifiManeger是必需要一些權限的。必須在AndroidManifest.xml清單文件裏面聲明權限:
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
1
2
3
4
5
文件接收端打開熱點而且配置熱點的代碼:
//1.初始化熱點
WifiMgr.getInstance(getContext()).disableWifi();
if(ApMgr.isApOn(getContext())){
ApMgr.disableAp(getContext());
}
//熱點相關的廣播
mWifiAPBroadcastReceiver = new WifiAPBroadcastReceiver() {
@Override
public void onWifiApEnabled() {
Log.i(TAG, "======>>>onWifiApEnabled !!!");
if(!mIsInitialized){
mUdpServerRuannable = createSendMsgToFileSenderRunnable();
AppContext.MAIN_EXECUTOR.execute(mUdpServerRuannable);
mIsInitialized = true;
tv_desc.setText(getResources().getString(R.string.tip_now_init_is_finish));
tv_desc.postDelayed(new Runnable() {
@Override
public void run() {
tv_desc.setText(getResources().getString(R.string.tip_is_waitting_connect));
}
}, 2*1000);
}
}
};
IntentFilter filter = new IntentFilter(WifiAPBroadcastReceiver.ACTION_WIFI_AP_STATE_CHANGED);
registerReceiver(mWifiAPBroadcastReceiver, filter);
ApMgr.isApOn(getContext()); // check Ap state :boolean
String ssid = TextUtils.isNullOrBlank(android.os.Build.DEVICE) ? Constant.DEFAULT_SSID : android.os.Build.DEVICE;
ApMgr.configApState(getContext(), ssid); // change Ap state :boolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
對於類WifiAPBroadcastReceiver是熱點的一個廣播類,最後一行代碼是配置指定名稱的熱點,這裏是以設備名稱做爲熱點的名稱。
文件發送端發送文件,文件發送端首先要選擇要發送的文件,而後將要選擇的文件存儲起來,這裏我是用了一個HashMap將發送的文件存儲起來,key是文件的路徑,value是FileInfo對象。
如下是掃描手機存儲盤上面的文件列表的代碼:
/**
* 存儲卡獲取 指定後綴名文件
* @param context
* @param extension
* @return
*/
public static List<FileInfo> getSpecificTypeFiles(Context context, String[] extension){
List<FileInfo> fileInfoList = new ArrayList<FileInfo>();
//內存卡文件的Uri
Uri fileUri= MediaStore.Files.getContentUri("external");
//篩選列,這裏只篩選了:文件路徑和含後綴的文件名
String[] projection=new String[]{
MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.TITLE
};
//構造篩選條件語句
String selection="";
for(int i=0;i<extension.length;i++)
{
if(i!=0)
{
selection=selection+" OR ";
}
selection=selection+ MediaStore.Files.FileColumns.DATA+" LIKE '%"+extension[i]+"'";
}
//按時間降序條件
String sortOrder = MediaStore.Files.FileColumns.DATE_MODIFIED;
Cursor cursor = context.getContentResolver().query(fileUri, projection, selection, null, sortOrder);
if(cursor != null){
while (cursor.moveToNext()){
try{
String data = cursor.getString(0);
FileInfo fileInfo = new FileInfo();
fileInfo.setFilePath(data);
long size = 0;
try{
File file = new File(data);
size = file.length();
fileInfo.setSize(size);
}catch(Exception e){
}
fileInfoList.add(fileInfo);
}catch (Exception e){
Log.i("FileUtils", "------>>>" + e.getMessage());
}
}
}
Log.i(TAG, "getSize ===>>> " + fileInfoList.size());
return fileInfoList;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
注意**:這裏掃描的FileInfo對象只是掃描了文件路徑filePath, 還有文件的大小size。
FileInfo的其餘屬性到文件傳輸的時候再二次獲取,獲取FileInfo的其餘屬性都在FileUtils這個工具類裏面了。
文件發送端打開wifi掃描熱點而且鏈接熱點的代碼:
if(!WifiMgr.getInstance(getContext()).isWifiEnable()) {//wifi未打開的狀況,打開wifi
WifiMgr.getInstance(getContext()).openWifi();
}
//開始掃描
WifiMgr.getInstance(getContext()).startScan();
mScanResultList = WifiMgr.getInstance(getContext()).getScanResultList();
mScanResultList = ListUtils.filterWithNoPassword(mScanResultList);
if(mScanResultList != null){
mWifiScanResultAdapter = new WifiScanResultAdapter(getContext(),mScanResultList);
lv_result.setAdapter(mWifiScanResultAdapter);
lv_result.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//單擊選中指定的網絡
ScanResult scanResult = mScanResultList.get(position);
Log.i(TAG, "###select the wifi info ======>>>" + scanResult.toString());
//1.鏈接網絡
String ssid = Constant.DEFAULT_SSID;
ssid = scanResult.SSID;
WifiMgr.getInstance(getContext()).openWifi();
WifiMgr.getInstance(getContext()).addNetwork(WifiMgr.createWifiCfg(ssid, null, WifiMgr.WIFICIPHER_NOPASS));
//2.發送UDP通知信息到 文件接收方 開啓ServerSocketRunnable
mUdpServerRuannable = createSendMsgToServerRunnable(WifiMgr.getInstance(getContext()).getIpAddressFromHotspot());
AppContext.MAIN_EXECUTOR.execute(mUdpServerRuannable);
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
對於ListUtils.filterWithNoPassword是將掃描的結果進行過濾,過濾掉有密碼的掃描結果。
lv_result.setOnItemClickListener回調的方法是鏈接指定的熱點來造成一個局域網。文件傳輸的大前提條件就已經造成了。
到這裏文件發送端和文件接收端的初始化環境也就搭建起來了。
文件傳輸模塊
文件傳輸模塊的核心代碼就只有4個類,Transferable, BaseTransfer, FileSender, FileReceiver。
Transferable是接口。
BaseTransfer, FileSender, FileReceiver是類。
對於文件發送端,每個文件發送對應一個FileSender,而對於文件接收端,每個文件的接收對應一個FileReceiver。
而FileSender,FileReceiver是繼承自 抽象類BaseTransfer的。 BaseTransfer是實現了Transferable接口。
下面是4個類圖的關係:
在Transferable接口中定義了4個方法,分別是初始化,解析頭部,解析主體,結束。解析頭部和解析主體分別對應上面說的自定義協議的header和body。初始化是爲每一次文件傳輸作初始化工做,而結束是爲每一次文件傳輸作結束工做,好比關閉一些資源流,Socket等等。
而BaseTransfer就只是實現了Transferable, 裏面封裝了一些常量。沒有實現具體的方法,具體的實現是FileSender,FileReceiver。
代碼詳情:
Transferable
BaseTransfer
FileSender
FileReceiver
總結
端到端的文件傳輸就分析到這裏,主要是Ap熱點的操做,Wifi的操做,Socket通訊來實現文件的傳輸。可是這裏的Socket用到的不是異步IO,是同步IO。因此會引發阻塞。好比在FileSender中的暫停文件傳輸pause方法調用以後,會引發FileReceiver中文件傳輸的阻塞。若是你對異步IO有興趣,你也能夠去實現一下。
對於端對端的核心代碼都是在 io.github.mayubao.kuaichuan.core 包下面。
這是我在github上面的項目連接 https://github.com/mayubao/KuaiChuan
web端的文件傳輸
所謂的Web端的文件傳輸是指文件發送端做爲一個Http服務器,提供文件接收端來下載。這種文件傳輸方式是文件發送端必須安裝應用,而文件接收端只須要有瀏覽器便可。
效果圖
文件發送端
文件接收端
在android應用端架設微型Http服務器來實現文件的傳輸。這裏能夠用ftp來實現,爲何不用ftp呢?由於沒有縮略圖,這是重點!
web端的文件傳輸的核心重點:
文件發送端熱點的開啓(參考端對端的熱點操做類 APMgr.java)
文件發送端架設Http服務器。
Android端的Http服務器
Android上微型Http服務器(Socket實現),結合上面的效果圖分析。主要解決三種Http url的請求形式就好了,由上面的文件接收端的效果圖能夠看出來(文件接收端是去訪問文件發送端的Http服務器),大體能夠分爲三種連接:
Index主頁連接 http://hostname:port
Image連接 http://hostname:port/image/xxx.xxx
Download連接 http://hostname:port/download/xxx.xxx
下面用Socket來實如今Android上面的微型Http服務器的。
關於Http協議,我簡單的描述一下Http協議。對於Http協議,就是」請求-回覆(響應)「的這種通訊模式。客戶端發出請求,服務器根據請求,返回一個回覆(響應)給客戶端。
Http請求的大體分爲四個部分:
1. 請求行
2. 請求頭
3. 空行
4. 請求實體
Http響應的大體分爲四個部分:
1. 狀態行
2. 響應頭
3. 空行
4. 響應實體
Http請求(POST請求)的示例:
POST /image/index.html HTTP/1.1
Host: 127.0.0.1:7878
Connection: keep-alive
Content-Length: 247
Cache-Control: no-cache
Origin: chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcm
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"
mayubao
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"
123456
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.請求行(請求方式 + uri + http版本)
POST /image/index.html HTTP/1.1
1
2
2.請求頭
Host: 127.0.0.1:7878
Connection: keep-alive
Content-Length: 247
Cache-Control: no-cache
Origin: chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcm
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
1
2
3
4
5
6
7
8
9
10
11
3.空行
4.請求實體(對於GET請求通常沒有請求實體)
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"
mayubao
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw
Content-Disposition: form-data; name="username"
123456
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw--
1
2
3
4
5
6
7
8
9
10
Http響應的示例:
HTTP/1.0 200 OK
Cache-Control:public, max-age=86400
Content-Length:235
Content-Type:image/png
Date:Wed, 21 Dec 2016 08:20:54 GMT
請求實體
1
2
3
4
5
6
7
8
1.狀態行(Http版本 + 狀態 + 描述)
HTTP/1.0 200 OK
1
2
2.響應頭
HTTP/1.0 200 OK
Cache-Control:public, max-age=86400
Content-Length:235
Content-Type:image/png
Date:Wed, 21 Dec 2016 08:20:54 GMT
1
2
3
4
5
6
3.空行
4.響應實體
上面只是簡單的敘述了一下Http通常的請求-響應流程,還有對應請求,響應的結構。若是你想進一步瞭解http協議,請私下自行了解。
回到咱們的重點 AndroidMicroServer:
AndroidMicroServer是Http服務器的核心類,還有關聯到其餘的類,有IndexUriResHandler,ImageUriResHandler, DowloadUriResHandler。是AndroidMicroServer根據不一樣的Uri格式分配給指定的Handler去處理的。
UML的分析圖以下:
下面是AndroidMicroServer的源碼:
/**
* The micro server in Android
* Created by mayubao on 2016/12/14.
* Contact me 345269374@qq.com
*/
public class AndroidMicroServer {
private static final String TAG = AndroidMicroServer.class.getSimpleName();
/**
* the server port
*/
private int mPort;
/**
* the server socket
*/
private ServerSocket mServerSocket;
/**
* the thread pool which handle the incoming request
*/
private ExecutorService mThreadPool = Executors.newCachedThreadPool();
/**
* uri router handler
*/
private List<ResUriHandler> mResUriHandlerList = new ArrayList<ResUriHandler>();
/**
* the flag which the micro server enable
*/
private boolean mIsEnable = true;
public AndroidMicroServer(int port){
this.mPort = port;
}
/**
* register the resource uri handler
* @param resUriHandler
*/
public void resgisterResUriHandler(ResUriHandler resUriHandler){
this.mResUriHandlerList.add(resUriHandler);
}
/**
* unresigter all the resource uri hanlders
*/
public void unresgisterResUriHandlerList(){
for(ResUriHandler resUriHandler : mResUriHandlerList){
resUriHandler.destroy();
resUriHandler = null;
}
}
/**
* start the android micro server
*/
public void start(){
mThreadPool.submit(new Runnable() {
@Override
public void run() {
try {
mServerSocket = new ServerSocket(mPort);
while(mIsEnable){
Socket socket = mServerSocket.accept();
hanlderSocketAsyn(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
/**
* stop the android micro server
*/
public void stop(){
if(mIsEnable){
mIsEnable = false;
}
//release resource
unresgisterResUriHandlerList();
if(mServerSocket != null){
try {
// mServerSocket.accept(); //fuck ! fix the problem, block the main thread
mServerSocket.close();
mServerSocket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* handle the incoming socket
* @param socket
*/
private void hanlderSocketAsyn(final Socket socket) {
mThreadPool.submit(new Runnable() {
@Override
public void run() {
//1. auto create request object by the parameter socket
Request request = createRequest(socket);
//2. loop the mResUriHandlerList, and assign the task to the specify ResUriHandler
for(ResUriHandler resUriHandler : mResUriHandlerList){
if(!resUriHandler.matches(request.getUri())){
continue;
}
resUriHandler.handler(request);
}
}
});
}
/**
* create the requset object by the specify socket
*
* @param socket
* @return
*/
private Request createRequest(Socket socket) {
Request request = new Request();
request.setUnderlySocket(socket);
try {
//Get the reqeust line
SocketAddress socketAddress = socket.getRemoteSocketAddress();
InputStream is = socket.getInputStream();
String requestLine = IOStreamUtils.readLine(is);
SLog.i(TAG, socketAddress + "requestLine------>>>" + requestLine);
String requestType = requestLine.split(" ")[0];
String requestUri = requestLine.split(" ")[1];
// requestUri = URLDecoder.decode(requestUri, "UTF-8");
request.setUri(requestUri);
//Get the header line
String header = "";
while((header = IOStreamUtils.readLine(is)) != null){
SLog.i(TAG, socketAddress + "header------>>>" + requestLine);
String headerKey = header.split(":")[0];
String headerVal = header.split(":")[1];
request.addHeader(headerKey, headerVal);
}
} catch (IOException e) {
e.printStackTrace();
}
return request;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
AndroidMicroServer主要有兩個方法:
1. start (Http服務器的開啓)
2. stop (Http服務器的關閉,主要用來關閉ServerSocket和反註冊UriResHandler)
start方法 是Http服務器的入口
對於start方法:
/**
* start the android micro server
*/
public void start(){
mThreadPool.submit(new Runnable() {
@Override
public void run() {
try {
mServerSocket = new ServerSocket(mPort);
while(mIsEnable){
Socket socket = mServerSocket.accept();
hanlderSocketAsyn(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
開啓一個線程去執行ServerSocket, while循環去接收每個進來的Socket。 而hanlderSocketAsyn(socket)是異步處理每個進來的socket。
/**
* handle the incoming socket
* @param socket
*/
private void hanlderSocketAsyn(final Socket socket) {
mThreadPool.submit(new Runnable() {
@Override
public void run() {
//1. auto create request object by the parameter socket
Request request = createRequest(socket);
//2. loop the mResUriHandlerList, and assign the task to the specify ResUriHandler
for(ResUriHandler resUriHandler : mResUriHandlerList){
if(!resUriHandler.matches(request.getUri())){
continue;
}
resUriHandler.handler(request);
}
}
});
}
/**
* create the requset object by the specify socket
*
* @param socket
* @return
*/
private Request createRequest(Socket socket) {
Request request = new Request();
request.setUnderlySocket(socket);
try {
//Get the reqeust line
SocketAddress socketAddress = socket.getRemoteSocketAddress();
InputStream is = socket.getInputStream();
String requestLine = IOStreamUtils.readLine(is);
SLog.i(TAG, socketAddress + "requestLine------>>>" + requestLine);
String requestType = requestLine.split(" ")[0];
String requestUri = requestLine.split(" ")[1];
// //解決URL中文亂碼的問題
// requestUri = URLDecoder.decode(requestUri, "UTF-8");
request.setUri(requestUri);
//Get the header line
String header = "";
while((header = IOStreamUtils.readLine(is)) != null){
SLog.i(TAG, socketAddress + "header------>>>" + requestLine);
String headerKey = header.split(":")[0];
String headerVal = header.split(":")[1];
request.addHeader(headerKey, headerVal);
}
} catch (IOException e) {
e.printStackTrace();
}
return request;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
對於每個進來的Socket:
經過createRequest(socket)來建立一個Request對象,對應一個Http Request對象。在createRequest(socket)中如何去從socket中去讀取每一行呢?對於每個Http請求的每一行都是以’\r\n’字節結尾的。只要判斷讀取字節流的時候判斷連續的兩個字節是以’\r\n’結尾的就是一行結尾的標識。詳情請查看IOStreamUtils.java
根據請求行的path,分配給對應的Uri處理對象去處理,而所對應uri如何獲取,是從Socket的Inputsream讀取Http Request的請求行中讀取出來的。對於ResUriHandler,是一個接口。主要根據請求行的uri 分配給對應的ResUriHandler去處理。 ResUriHandler的實現類是對應給出響應的處理類。
注意:可參考上面的UML的類圖分析
ResUriHandler有三個實現類分別對應上面分析的三種Uri格式:
IndexResUriHandler 處理髮送文件列表的顯示
ImageResUriHandler 處理文件圖片
DownloadResUriHandler 處理文件下載
總結
AndroidMicroServer是架設在Android平臺上面的一個微型HttpServer, 是根據快傳項目的具體需求來實現的。巧妙的利用ResUriHandler來處理不一樣的uri。注意這不是通常通用的HttpServer, 以前有想過在Github上面去找一些Server端的代碼來進行開發,發現代碼關聯太多,並且不容易定製,因此纔會萌生本身用ServerSocket來實現符合本身需求的HttpServer。
對於HttpServer的核心代碼都是在 io.github.mayubao.kuaichuan.micro_server包下面。 這是我在github上面的項目連接 https://github.com/mayubao/KuaiChuan