在Android APP内部实现一个Http Server——NanoHttpd 简单剖析

前言
     

最近公司产品(门禁系统)由安卓这边来实现,需要和宇泛人脸识别终端进行对接,对接过程中说白了,也就是在互相的调接口(这也就是项目做完了敢这么说吧),其实刚刚开始接触这块东西的时候还是挺懵圈的,理顺宇泛的文档,发现好多东西(将人员信息上传至宇泛设备、设置是否允许宇泛设备开门、设置宇泛设备的识别回调地址),这些都是通过Http  post 方式提交数据就好了吗!刚开始看的时候啊,那给我乐的!

                                                     

     

正文


     你品“设置宇泛设备的识别回调地址”这句话,你细品,我看到这块文档的时候那是一脸懵圈,这也是我为什么此处标红的原因。

     对他的文档就是这样写的,我看到这块的第一想法就是让后台写个接口,接收人脸识别回调的数据,然后通过UDP或者TCP的形式将消息发送给我这边,和后台、产品一起商量了一天半左右吧,最后考虑到数据传输速度和数据安全方面的问题,还是决定在安卓这边接收数据。

                                                   

     在Android这边做的话,首先打开我的第二大脑(百度)搜索一下,结果还真的有,看了一下大概三种方法:

           1、golang:简单的了解一下,这个还是比较复杂的,需要配置golang的环境,再进行开发。

           2、AndServer:因为我这个项目中已经有AndServer服务了,是在Android这边开启了一个web端的服务器,为了避免第三方库冲突还是选择其他的吧。

                AndServer官方链接:https://blog.csdn.net/yanzhenjie1003/article/details/64090436/

          3、NanoHttpd:也正是我项目中用到的,这篇文章要讲的东西。

NanoHttpd
           官网:https://github.com/NanoHttpd/nanohttpd

      1、NanoHttpd依赖:compile 'org.nanohttpd:nanohttpd:2.2.0'

            使用时NanoHttpd还是比较简单的,只要在build.gradle文件中添加依赖,编译通过就可以了。

      2、开始撸代码吧:

                        

            我们自定义一个MyNanoHttpdServer继承NanoHttpd,实现构造方法:

   public MyNanoHttpdServer(int port) {
        super(port);
    }

           NanoHttpd中有默认有两个构造方法,我们只要实现其中一个构造方法就可以了,构造方法中传入一个参数(端口号),这个参数由个人自行定义(这里建议找一个不常用有好记的端口号)。看一下双参数的构造方法吧:

    public NanoHTTPD(String hostname, int port) {
        this.hostname = hostname;
        this.myPort = port;
        setTempFileManagerFactory(new DefaultTempFileManagerFactory());
        setAsyncRunner(new DefaultAsyncRunner());
    }

        这里就多了一个主机名称,没有必要去考虑那么多了,至于这个构造函数中调用的其他方法是做什么的我这里没有太深入的了解,待后期了解后进行补充!

        通过构造函数创建了MyNanoHttpServer之后,可以直接调用NanoHttpd中的start()方法开启Http Server,看一下start()函数吧(这里因为要创建一个Http Server 其中想必会有耗时操作,所以此处建议在子线程中调用start()函数)。

public void start(final int timeout, boolean daemon) throws IOException {
        this.myServerSocket = this.getServerSocketFactory().create();
        this.myServerSocket.setReuseAddress(true);
 
        ServerRunnable serverRunnable = createServerRunnable(timeout);
        this.myThread = new Thread(serverRunnable);
        this.myThread.setDaemon(daemon);
        this.myThread.setName("NanoHttpd Main Listener");
        this.myThread.start();
        while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
            try {
                Thread.sleep(10L);
            } catch (Throwable e) {
                // on android this may not be allowed, that's why we
                // catch throwable the wait should be very short because we are
                // just waiting for the bind of the socket
            }
        }
        if (serverRunnable.bindException != null) {
            throw serverRunnable.bindException;
        }
    }


        这里第一个参数是设置Http Server连接时超时时间用的,这个没什么好说的。

        第二个参数daemon是干什么用的呢?  简单的看了一下,第二个参数是设置是否启动一个线程来守护当前进程的。这个因个人所需进行设置即可。

       接下来就是很重要的一步了,接收数据,那么数据该怎么接收呢,看了一下,我们的MyNanoHttpdServer可以重写其父类的server(IHTTPSession session)函数来接收数据。

 public Response serve(IHTTPSession session) {
        Map<String, String> files = new HashMap<String, String>();
        Method method = session.getMethod();
        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
            try {
                session.parseBody(files);
            } catch (IOException ioe) {
                return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
            } catch (ResponseException re) {
                return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
            }
        }
 
        Map<String, String> parms = session.getParms();
        parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
        return serve(session.getUri(), method, session.getHeaders(), parms, files);
    }

      这里其实就是在帮咱们解析数据,session.getMethod()获取请求方法,因为本人接到的需求是对接别的公司的产品吗,所以看看人间的文档是用的什么请求方式请求的我这边的接口吧,哦,post请求。

       其实我现在关心的根本不是神木请求的问题,我现在看这个函数最下边的一个返回是:

return serve(session.getUri(), method, session.getHeaders(), parms, files);

       是吧,人家已经解析好了,然后用调用了另外一个函数,将解析到的相关信息全都作为参数传进去了是吧,那我此时此刻特别不想重写上边的方法了,我就想重写上边的函数最后一行调用的另外一个serve(... ...)函数,看看这个函数吧,只要这个函数是public修饰的,而且没有被final修饰,那么我就会得宠,额,不不不,是得逞。

       看一下这个方法里边都有些啥吧:

public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
        return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
    }

        只不过这个方法在用的时候画了一条线,说明什么呢,这个方法已经过时了,那和我又有什么关系呢,我只要能用就可以了,对吧。人家给你一姐解析好了你要用到的数据,还封装了现有的方法供你进行重写使用,那还别扭啥啊,这又何乐而不为呢是不是。

        接下来看看我自己重写的serve函数中的大体逻辑吧,因为此处涉及和其他公司进行对接,为了避免泄漏我这边不该泄漏的东西,就不贴出来了,简单的了解一下就好,你们自己做的时候,简单的看一下,自然就会明白。

     首先我们要先判断一下请求方法,是不是post请求对吧:

    method.equals(Method.POST)

        直接对比就好了,Method是NanoHTTPD类中的枚举,可以直接调用。请求方法比对完了之后,就开始要取出客户端请求时的参数了。   

String personData = files.get("postData");

        files里边就把客户端请求的信息全都封装成一个map数组,因为这里我们用的是post请求吗,所以这里我们就去获取post过来的数据,files.get("postData"),获取到数据之后,每个人要做的工作就不一样了。好了,大概也就是这样了,其实里边并没有太多的东西,毕竟是使用别人已经封装好的库吗,还是很简单的。

        简单的把我这边自己写的代码粘出来看一下吧,解析的部分就因人而异了:

import com.blankj.utilcode.util.StringUtils;
import com.tehike.studydemo.util.WriteLogToFile;
 
import java.io.IOException;
import java.util.Map;
 
import fi.iki.elonen.NanoHTTPD;
 
/**
 * <pre>
 * Created by small_world
 * QQ:1529442917  mail:Caokx@tehike.com
 * Time :2020/1/3 14:30
 * data :
 * <pre>
 * ********************************我佛慈悲**************************************
 * **                              _oo0oo_                               **
 * **                             o8888888o                              **
 * **                             88" . "88                              **
 * **                             (| -_- |)                              **
 * **                             0\  =  /0                              **
 * **                           ___/'---'\___                            **
 * **                        .' \\\|     |// '.                          **
 * **                       / \\\|||  :  |||// \\                        **
 * **                      / _ ||||| -:- |||||- \\                       **
 * **                      | |  \\\\  -  /// |   |                       **
 * **                      | \_|  ''\---/''  |_/ |                       **
 * **                      \  .-\__  '-'  __/-.  /                       **
 * **                    ___'. .'  /--.--\  '. .'___                     **
 * **                 ."" '<  '.___\_<|>_/___.' >'  "".                  **
 * **                | | : '-  \'.;'\ _ /';.'/ - ' : | |                 **
 * **                \  \ '_.   \_ __\ /__ _/   .-' /  /                 **
 * **            ====='-.____'.___ \_____/___.-'____.-'=====             **
 * **                              '=---='                               **
 * ************************************************************************
 * **                佛祖保佑      镇类之宝       永无bug                **
 * ************************************************************************                                    丶
 */
 
public class MyNanoHttpdServer extends NanoHTTPD {
 
    //实现父类的构造方法
    public MyNanoHttpdServer(int port) {
        super(port);
    }
 
    //正式开启服务
    public void start() {
        try {
            start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
 
            //向日志文件中写入接收消息的接口已经打开
            WriteLogToFile.info("The face recognition callback interface has been opened, the port number is 9999.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public Response serve(IHTTPSession session) {
        //这个就是之前分析,重写父类的一个参数的方法,
        //这里边已经把所有的解析操作已经在这里执行了
        return super.serve(session);
    }
 
    @Override
    public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
        //这就是上边的serve方法最后一行调用的那个过时的方法,这里简单的做个判断就好了
        if (!method.equals(Method.POST)) {//判断请求方式是否争取
            return newFixedLengthResponse("the request method is incoorect");
        }
        if (!StringUtils.equalsIgnoreCase(uri, "callBackUrl")) {//判断uri是否正确
            return newFixedLengthResponse("the request uri is incoorect");
        }
        String personData = files.get("postData");
        if (StringUtils.isEmpty(personData)) {//判断post过来的数据是否正确
            return newFixedLengthResponse("postData is null");
        }
        //判断完了开始解析数据,如果是你想要的数据,那么你就给返回一个正确的格式就好了
        //举个栗子:return newFixedLengthResponse("{\"result\":0,\"success\":true}");
        return super.serve(uri, method, headers, parms, files);
    }
}
       

 至于上边的newFixedLengthResponse("")函数是干啥用的呢,简单的了解一下吧:

   public static Response newFixedLengthResponse(String msg) {
        return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg);
    }

     点进去之后你会发现,他又调用了本类中,该函数本身的重载方法,返回了一个Status.OK,这个肯定就是200了,也就是说,只要是我们接收到数据之后,我们自己手动返回请求结果的时候,那么他底层就认为这个请求时成功的。

小结


        其实整个过程中自己也是看着NanoHttpd官方文档以及参考别人的博客,一边摸索学习,因为项目比较着急的原因,这里只是将简单的流程和原理在这里介绍了一下,后期可能在本篇文章的基础上扩展内容,也或许写一篇新的文章,因为毕竟觉得这块东西还是值得“深入”一下的

题外话


        对接其他公司的产品这已经是我们这个产品2.0版本干的事了,1.0版本对接人脸识别设备的时候(当时并没有考虑到后期使用外部的人脸识别设备,因为给保密机构做产品还是自己的东西用着放心对吧,但是谁能想到1.0版本用到的人脸识别设备用到的硬件的厂家因为之前的贸易战收到了影响,不能及时供货,那时候订单还是比较多的,所以就选择了外部产品这个应急之策),是通过收发TCP消息进行传输数据的(因为1.0版本对接的是公司同事自己研发的设备,所以这块的协议我们内部可以自行商议解决),我这边是自定义的http server(或许只需叫做server),同事的人脸识别设备是自定义的http client(同理,只需叫client)来进行客户端和服务器端的交互以及数据的传输,我们其实并不需要使用http协议,我们可以写个安卓程序,使用socket和serversocket的程序,用不同的设备运行这个程序,利用我们自己定好的标识符即可实现交互和传输了。

       讲一下Http的特点:

         无连接:处理完请求并且得到应答后即断开链接

         媒体独立:任何类型的数据都可通过http协议完成

         无状态:不记录前面的状态信息

结束语


        好了,到这里就结束了,好久没有写博客了,有些言语不对的地方还希望大家能够见谅,有什么不对的地方还希望大神能够多多指点、指教。这是我2020年的第一篇博客,最近几天可能闲下来了,可能整理一下简历(每年年底的时候我都会整理一次自己的简历,看看一年中自己成长了哪些,为自己定个方向),还可能会写几篇博客吧,毕竟自己学到的东西,一是为了能够做个笔记,二是分享给大家供大家参考。

       最后还是要说一句,有什么不对的地方希望能得到批评指正。


————————————————
版权声明:本文为CSDN博主「small小小世界world」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/small_and_smallworld/java/article/details/103393070

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,以下是一个简单Android应用程序,可以调用手机摄像头: 1. 首先,打开Android Studio,创建一个新的空白项目。 2. 在项目中创建一个新的Activity,命名为CameraActivity。 3. 打开CameraActivity.java文件,添加以下代码: ``` import android.content.Intent; import android.os.Bundle; import android.provider.MediaStore; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.Button; public class CameraActivity extends AppCompatActivity { private static final int REQUEST_IMAGE_CAPTURE = 1; private Button btnCamera; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera); btnCamera = findViewById(R.id.btn_camera); btnCamera.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dispatchTakePictureIntent(); } }); } private void dispatchTakePictureIntent() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { // The user has taken a photo and it is saved to the phone's gallery } } } ``` 4. 在res/layout文件夹中创建一个名为activity_camera.xml的布局文件,并添加以下代码: ``` <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".CameraActivity"> <Button android:id="@+id/btn_camera" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take a picture" android:layout_centerInParent="true"/> </RelativeLayout> ``` 5. 现在你可以运行你的应用程序,并在点击“Take a picture”按钮时调用摄像头。当用户拍照后,照片将保存在手机的图库中。 注意:在运行应用程序之前,请确保你的手机上已经安装了相机应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值