NanoHttpd 安卓HTTP Sever建立

原创文章,转载请注明出处: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协议有如下几个特点:

  1. 无连接:处理完请求并且得到应答后即断开链接
  2. 媒体独立:任何类型的数据都可通过http协议完成
  3. 无状态:不记录前面的状态信息

从这些特点可以看出http协议具有传输速度快,应答速度快,传输方便等特点。
下面再看看http协议客户端发送请求的格式:


发送请求的格式

图上的描述已经足够清晰了,还有服务器发送响应的格式:


服务器响应的格式

http协议中我们最常用到的请求就是get和post,所以我们的http server基本上实现这两个方法即可实现文件的上传和下载了。其中get请求通常用于请求指定的页面信息并返回实体主体。post请求用于提交表单或者上传文件。其他的请求方法网络上有详细内容。http的相应头和状态码等内容网络上有更加详细的内容了,这里也就不再赘述。

http服务器

既然要在安卓中建立一个http服务器,那么采用java来建立即可,基本的思路就是建立java的serversocket来等待连接,连接后服务器不断接受客户端的数据并且进行处理,由于采用的http的协议,对数据的处理及回复也需采用http协议的内容。从零构建一个http的服务器具有一定的难度,要简化处理就先利用nanohttpd来构建服务器。

前期准备

  1. 先在官网上将nanohttpd下载到本地,解压缩以后进入文件夹,使用mvn compile和 man package(我的电脑是Linux)将自动编译构建jar文件,jar文件在core文件夹下的target文件夹内。
  2. 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的角度而言已经达到了目的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 一、概念(载录于:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436...
    yuantao123434阅读 8,326评论 6 152
  • 1.OkHttp源码解析(一):OKHttp初阶2 OkHttp源码解析(二):OkHttp连接的"前戏"——HT...
    隔壁老李头阅读 20,799评论 24 176
  • Http协议详解 标签(空格分隔): Linux 声明:本片文章非原创,内容来源于博客园作者MIN飞翔的HTTP协...
    Sivin阅读 5,201评论 3 82
  • 那些遗忘在角落的时光, 总在某一情境中被拾起, 也许是一本书, 也许是一句话, 也许是一张桌子,一个沙发。 突然,...
    三毛和她阅读 341评论 0 3