Android茄子快传项目源码
茄子快传是一款文件传输应用,相信大家都很熟悉这款应用,应该很多人用过用来文件的传输。它有两个核心的功能:
- 端到端的文件传输
- Web端的文件传输
这两个核心的功能我们具体来分析一下!
端到端的文件传输
所谓的端到端的文件传输是指应用端发送到应用端(这里的应用端指Android应用端),这种文件传输方式是文件发送端和文件接收端必须安装应用。
效果图
文件发送方
文件发送方_1文件发送方_2
文件发送方_3
文件接收方
文件接收方_1简单的文件传输的话,我们可以用蓝牙,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.初始化热点 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
对于类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; }
注意**:这里扫描的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); } }); }
对于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服务器,提供文件接收端来下载。这种文件传输方式是文件发送端必须安装应用,而文件接收端只需要有浏览器即可。
效果图
文件发送端
开启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请求的大致分为四个部分:
- 请求行
- 请求头
- 空行
- 请求实体
Http响应的大致分为四个部分:
- 状态行
- 响应头
- 空行
- 响应实体
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.请求行(请求方式 + uri + http版本)
POST /image/index.html HTTP/1.1
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
3.空行
4.请求实体(对于GET请求一般没有请求实体)
------WebKitFormBoundaryLIr5t1rdtuD8Ztuw Content-Disposition: form-data; name="username" mayubao ------WebKitFormBoundaryLIr5t1rdtuD8Ztuw Content-Disposition: form-data; name="username" 123456 ------WebKitFormBoundaryLIr5t1rdtuD8Ztuw--
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.状态行(Http版本 + 状态 + 描述)
HTTP/1.0 200 OK
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
3.空行
4.响应实体
上面只是简单的叙述了一下Http一般的请求-响应流程,还有对应请求,响应的结构。如果你想进一步了解http协议,请私下自行了解。
回到我们的重点 AndroidMicroServer:
AndroidMicroServer是Http服务器的核心类,还有关联到其他的类,有IndexUriResHandler,ImageUriResHandler, DowloadUriResHandler。是AndroidMicroServer根据不同的Uri格式分配给指定的Handler去处理的。
UML的分析图如下:
AndroidMicroServer分析下面是AndroidMicroServer的源码:
/** * The micro server in Android * Created by mayubao on 2016/12/14. * Contact me [email protected] */ 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; } }
AndroidMicroServer主要有两个方法:
- start (Http服务器的开启)
- 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(); } } }); }
开启一个线程去执行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; }
对于每一个进来的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包下面。
资源均来自第三方,谨慎下载,前往第三方网站下载