Android如何实现茄子快传
茄子快传是一款文件传输应用,相信大家都很熟悉这款应用,应该很多人用过用来文件的传输。它有两个核心的功能:
端到端的文件传输
Web端的文件传输
这两个核心的功能我们具体来分析一下!
端到端的文件传输
所谓的端到端的文件传输是指应用端发送到应用端(这里的应用端指Android应用端),这种文件传输方式是文件发送端和文件接收端必须安装应用。
效果图
文件发送方
文件接收方
简单的文件传输的话,我们可以用蓝牙,wifi直连,ftp这几种方式来进行文件的传输。但是:
蓝牙传输的话,速度太慢,而且要配对。相对比较麻烦。
wifi直连差不多跟蓝牙一样,但是速率很快,也要配对。
ftp可以实现文件的批量传输,但是没有文件的缩略图。
最初分析这个项目的时候就想着通过自定义协议的Socket的通信来实现,自定义的协议包括header + body的自定义协议, header部分包括了文件的信息(长度,大小,文件路径,缩略图), body部分就是文件。现在实现这一功能。(后序:后面开发《网页传》功能的时候,可以考虑这两个核心的功能都能用在Android架设微型Http服务器来实现。这是后话了。)
流程图
编码实现
两部设备文件传输是需要在一个局域网的条件下的,只有文件发送方连接上文件接收方的热点(搭建了一个局域网),这样文件发送方和文件接收方就在一个局域网里面,我们才可以进行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