原创文章,转载请注明出处:NanoHttpd 安卓HTTP Sever建立:http://www.jianshu.com/p/ef6279a429d4
前言
近来,接到了一个任务,要在手机搭建一个HTTP Server ,用于文件的分享。即手机端的应用启动以后,电脑可以通过局域网的手机ip地址连接上手机,并且下载手机分享的文件。功能很方便,并且由于是http server的关系,最后应该能实现多设备连接到同一部手机上下载文件。同时,技术利用局域网的wifi或者手机热点来实现,这就使得传输效率非常高(突然想到,后面应该增加一个功能,使没有安装该软件的安卓手机可以使用手机浏览器连接该http server下载该软件),因为先是练手,并且也可以先了解建立http server的流程,对http协议、http server与客户端的交互进行分析,故没有采用data binding等设计模式相关的内容,避免被无关因素干扰了程序的开发过程。
HTTP协议
TCP/IP协议的内容无需多言,读者可以自行了解。http是客户端与服务器之间发起请求和应答的标准,服务器根据客户端发送的不同的http请求并进行处理,处理后向客户端发送应答消息。它基于TCP/IP协议来传递各式各样的数据。
在编写http server的过程中,也许你也会有如此疑问,http不过是一个协议罢了,如果我们使用自定义的http server(或许只需叫做server)来传递数据,同时使用自定义的http client(同理,只需叫client)来进行客户端和服务器端的交互以及数据的传输,我们其实并不需要使用http协议,我们可以写个安卓程序,使用socket和serversocket的程序,用不同的设备运行这个程序,利用我们自己定好的标识符即可实现交互和传输了。认真想一想,我们的设备难道必须要安装这个程序才可以运行使用吗,并且对于pc,无法安装该安卓程序,我们所要实现的数据传输不就无法实现了嘛?这样想一想,我们就应该使用已经成熟的,大家公认可用并且获得普遍支持的协议来实现应用,这样我们的应用才更加的具有普遍性,对不同的设备具有同样的兼容性。
有些偏题了,现在再回到http协议的内容:
http协议有如下几个特点:
- 无连接:处理完请求并且得到应答后即断开链接
- 媒体独立:任何类型的数据都可通过http协议完成
- 无状态:不记录前面的状态信息
从这些特点可以看出http协议具有传输速度快,应答速度快,传输方便等特点。
下面再看看http协议客户端发送请求的格式:
图上的描述已经足够清晰了,还有服务器发送响应的格式:
http协议中我们最常用到的请求就是get和post,所以我们的http server基本上实现这两个方法即可实现文件的上传和下载了。其中get请求通常用于请求指定的页面信息并返回实体主体。post请求用于提交表单或者上传文件。其他的请求方法网络上有详细内容。http的相应头和状态码等内容网络上有更加详细的内容了,这里也就不再赘述。
http服务器
既然要在安卓中建立一个http服务器,那么采用java来建立即可,基本的思路就是建立java的serversocket来等待连接,连接后服务器不断接受客户端的数据并且进行处理,由于采用的http的协议,对数据的处理及回复也需采用http协议的内容。从零构建一个http的服务器具有一定的难度,要简化处理就先利用nanohttpd来构建服务器。
前期准备
- 先在官网上将nanohttpd下载到本地,解压缩以后进入文件夹,使用mvn compile和 man package(我的电脑是Linux)将自动编译构建jar文件,jar文件在core文件夹下的target文件夹内。
- android studio中新建工程,在工程的依赖关系中引入jar包,引入的jar包就可用于构建http server,基本就是建立一个新的类,继承于NanoHTTPD
正式开始
主界面
主界面布局简单,简单描述一下即可,不贴代码了。主界面使用了一个TextView,该TextView用于启动应用后显示本机的ip地址和自定的http server的端口号,其他主机可以使用该ip地址和端口号进行远程连接。
//获取IP地址
public static String getLocalIpStr(Context context){
WifiManager wifiManager=(WifiManager)context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo=wifiManager.getConnectionInfo();
return intToIpAddr(wifiInfo.getIpAddress());
}
private static String intToIpAddr(int ip){
return (ip & 0xFF)+"."
+ ((ip>>8)&0xFF) + "."
+ ((ip>>16)&0xFF) + "."
+ ((ip>>24)&0xFF);
}
HTTP Server
这里我们开始正式的构建http server ,首先建立一个服务器类继承于NanoHTTPD,实现NanoHTTPD中的基本方法就可以完成我们所要的操作。
public class FileServer extends NanoHTTPD{
public static final int DEFAULT_SERVER_PORT= com.example.zjt.nanohttpexample.Status.MY_PORT;//为8080
public static final String TAG = FileServer.class.getSimpleName();
//根目录
private static final String REQUEST_ROOT = "/";
private List<SharedFile> fileList;//用于分享的文件列表
public FileServer(List<SharedFile> fileList){
super(DEFAULT_SERVER_PORT);
this.fileList = fileList;
}
//当接受到连接时会调用此方法
public Response serve(IHTTPSession session){
if(REQUEST_ROOT.equals(session.getUri())||session.getUri().equals("")){
return responseRootPage(session);
}
return responseFile(session);
}
//对于请求根目录的,返回分享的文件列表
public Response responseRootPage(IHTTPSession session){
StringBuilder builder = new StringBuilder();
builder.append("<!DOCTYPER html><html><body>");
builder.append("<ol>");
for(int i = 0 , len = fileList.size(); i < len ; i++){
File file = new File(fileList.get(i).getPath());
if(file.exists()){
//文件及下载文件的链接,定义了一个文件类,这里使用getPath方法获得路径,使用getName方法获得文件名
builder.append("<li> <a href=\""+file.getPath()+"\">"+file.getName()+"</a></li>");
}
}
builder.append("<li>分享文件数量: "+fileList.size()+"</li>");
builder.append("</ol>");
builder.append("</body></html>\n");
//回送应答
return Response.newFixedLengthResponse(String.valueOf(builder));
}
//对于请求文件的,返回下载的文件
public Response responseFile(IHTTPSession session){
try {
//uri:用于标示文件资源的字符串,这里即是文件路径
String uri = session.getUri();
//文件输入流
FileInputStream fis = new FileInputStream(uri);
// 返回OK,同时传送文件,为了安全这里应该再加一个处理,即判断这个文件是否是我们所分享的文件,避免客户端访问了其他个人文件
return Response.newFixedLengthResponse(Status.OK,"application/octet-stream",fis,fis.available());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return response404(session,null);
}
//页面不存在,或者文件不存在时
public Response response404(IHTTPSession session,String url) {
StringBuilder builder = new StringBuilder();
builder.append("<!DOCTYPE html><html>body>");
builder.append("Sorry,Can't Found" + url + " !");
builder.append("</body></html>\n");
return Response.newFixedLengthResponse(builder.toString());
}
}
要实现服务器上面这些就已足够了,后面还有一些是选择分享的文件。
文件选择器
文件选择器就是用于选择要分享的文件,这个地方真的是个坑,一开始选择的路径是错误的,后面查了好多资料最后在网上找到了一份可靠的实现,原博主已经忘记是谁了,感谢。
代码:
//
public class FileChooser extends AppCompatActivity {
private final int FILE_SELECT_CODE = 1;//文件选择的代码
private Button chooser;//点击按钮调用系统的选择器来选择文件
private Uri uri = null;
private ListView fileListView;
private FileAdapter fileAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.filechooser);
initView();
initListener();
}
private void initView() {
chooser = findViewById(R.id.chooser);
fileListView = findViewById(R.id.filelist);
fileAdapter = new FileAdapter(Status.fileLists, this);
fileListView.setAdapter(fileAdapter);
}
private void initListener() {
chooser.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showFileChooser();
}
});
}
//文件选择
private void showFileChooser() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");//任意文件都可以分享
intent.addCategory(Intent.CATEGORY_OPENABLE);
//调用系统的文件选择器
startActivityForResult(Intent.createChooser(intent, "请选择分享的文件"), FILE_SELECT_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case FILE_SELECT_CODE:
if (resultCode == RESULT_OK) {
uri = data.getData();
SharedFile sharedFile = new SharedFile();
sharedFile.setPath(FileUtils.getPath(this,uri));//这个地方就是坑了,直接选择文件的路径在7.0的手机和7.1的手机上面都是不对的
sharedFile.setFilename(path2Name(FileUtils.getPath(this,uri)));
sharedFile.setSharedtime(getTime());
Status.fileLists.add(sharedFile); //分享文件列表是全局的,所以文件选择器可以向其中添加文件,服务器也可以从其中读取文件信息。
fileAdapter.notifyDataSetChanged();
}
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
//将文件完整路径转化为文件名
private String path2Name(String path){
String name=null;
int len=path.length();
if(path.charAt(len-1)=='/'){
int a=-1,b=0;
while((a=path.indexOf('/',b+1))>0){
if(a==len-1)break;
b=a;
}
name = path.substring(b+1,len);
}else {
int a=-1,b=0;
while((a=path.indexOf('/',b+1))>0){
if(a==len-1)break;
b=a;
}
name = path.substring(b+1,len);
}
return name;
}
//分享文件的时间
private String getTime(){
Date date = new Date();
return date.toString();
}
}
文件工具类:
public class FileUtils {
/**
* 返回文件本地绝对路径
*
* @param context
* @param uri
* @return path of the selected image file from gallery
*/
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
//文件本地绝对路径
// check here to KITKAT or new version
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/"
+ split[1];
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] { split[1] };
return getDataColumn(context, contentUri, selection,
selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context
* The context.
* @param uri
* The Uri to query.
* @param selection
* (Optional) Filter used in the query.
* @param selectionArgs
* (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri,
String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = { column };
try {
cursor = context.getContentResolver().query(uri, projection,
selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri
* The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri
.getAuthority());
}
/**
* @param uri
* The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri
.getAuthority());
}
/**
* @param uri
* The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri
.getAuthority());
}
/**
* @param uri
* The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri
.getAuthority());
}
}
结尾语
通过以上这些,我们就完成了一个简单的手机服务器,这个手机服务器简单的实现了文件的分享,其他的设备使用浏览器就可以连接上这个手机服务器并且下载分享的文件,当然这里面还有许多可以修改扩展的地方,但从建立一个http server的角度而言已经达到了目的。