Android端實現(xiàn)Onvif IPC開發(fā)(二)——在Android端搭建服務(wù)器模擬Onvif IP Camera

@[toc]

Android端實現(xiàn)Onvif IPC開發(fā):

【Android音視頻】Onvif-IPC開發(fā)(一)——gSoap-onvif移植到Android》
【Android音視頻】Onvif-IPC開發(fā)(二)——JAVA版本onvif服務(wù)器構(gòu)建Onvif-IP-Camera
【Android音視頻】Onvif IPC開發(fā)(三)——YUV格式深入淺出
[【Android音視頻】Onvif IPC開發(fā)(四)——Onvif移植Android架構(gòu)與補全方案(更新中...) ]

本篇內(nèi)容簡介:

由于搞這個項目時养涮,參考并閱讀了許多資料,可能存在相似卻未聲明的借鑒之處,請聯(lián)系我修改或聲明

本篇是上一文章移植失敗采取的第二方案猾普,通過在android搭建service惫叛,模擬成一個onvif協(xié)議對接的IPC端,在這之前,首先需要明白烘挫,onvif設(shè)備對接的流程或者說方式,接下來的文章內(nèi)容也是基于下面一條流程去實現(xiàn)柬甥。

  • 發(fā)現(xiàn)-->請求-->控制-->打開視頻預(yù)覽

一饮六、作為Server端實現(xiàn)被發(fā)現(xiàn)功能

IPC設(shè)備基于Onvif被發(fā)現(xiàn)其垄,首先要明白 WS-Discovery: 動態(tài)的探測可用服務(wù)并調(diào)用之

  • 這一功能的原理是,在同一網(wǎng)段中維持一個固定地址值的UDP廣播卤橄,以特定的xml指令進行請求和響應(yīng)绿满,即完成設(shè)備端的信息查詢和識別
  • IPC固定地址值:239.255.255.250 端口:3702,服務(wù)端的這個廣播地址是固定的
  • 流程是:client端發(fā)送Probe請求窟扑,server根據(jù)Probe請求響應(yīng)對應(yīng)的ProbeMatch返回供client端識別
  • 請求和返回數(shù)據(jù)可以通過抓包去查看模擬
  • 在android端實現(xiàn)Onvif IPC功能的軟件幾乎沒有棒口,我找了很久才找到一個國外實現(xiàn)的項目,感興趣的可以下載 :AndroidIPC_apk

具體實現(xiàn)

注意點:

  • 此處為demo辜膝,在項目中最好將這個廣播添加到Android service中進行

  • android需要使用組播MulticastSocket實現(xiàn)udp搜索功能无牵,此處需要權(quán)限:

      // 允許應(yīng)用程序訪問WIFI網(wǎng)卡的網(wǎng)絡(luò)信息 
      <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
      // 允許應(yīng)用程序訪問有關(guān)的網(wǎng)絡(luò)信息
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
      //android 組播功能權(quán)限
      <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
    
  • 回傳的值可以通過在assets中寫好,通過%s代傳值去方便實現(xiàn)厂抖,以下為簡便寫死的

    • 獲取服務(wù)端IP地址:

         private String getlocalip() {
            WifiManager wifiManager = (WifiManager) mApplication.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
            WifiInfo info = wifiManager.getConnectionInfo();
            String ipaddress = null;
            if (info != null && info.getNetworkId() > -1) {
                int i = info.getIpAddress();
                ipaddress = String.format(Locale.ENGLISH, "%d.%d.%d.%d", i & 0xff, i >> 8 & 0xff, i >> 16 & 0xff, i >> 24 & 0xff);
                return ipaddress;
            } else if ((ipaddress = Utilities.getLocalIpAddress(true)) != null) {
                return ipaddress;
            }
            return "no found";
        }
      
    • 初始化數(shù)據(jù):此處用于返回對應(yīng)的服務(wù)端IP地址值和端口茎毁,service需要返回的url,USER_NAME和USER_PASSWORD用于鑒權(quán)使用忱辅,這邊寫死了

         private void initData() {           
            serverIp = getlocalip();
            Log.e("ipserver", "IP addresss:" + serverIp);
            devicesBack = mApplication.getDevicesBack();
            devicesBack.setIpAddress(serverIp);
            devicesBack.setProt(serverPort);
            devicesBack.setUserName(DevicesInfo.USER_NAME);
            devicesBack.setPsw(DevicesInfo.USER_PASSWORD);
            devicesBack.setServiceUrl("http://" + serverIp + ":8080/onvif/device_service");
            mApplication.setDevicesBackBean(devicesBack);
            Log.e("Description", "setDevicesBackBean 1: " + devicesBack.toString());    
        }
      
        public class DevicesBackBean {
            /**
             * 用戶名/密碼
             */
            private String userName;
        
            public String getEncodertype() {
                return encodertype;
            }
        
            public void setEncodertype(String encodertype) {
                this.encodertype = encodertype;
            }
        
            private String encodertype;
            private String psw;
            //IP地址
            private String ipAddress;
            /**
             * serviceUrl,uuid 通過廣播包搜索設(shè)備獲取
             */
            private String serviceUrl;
            private String uuid;
            /**
             * getCapabilities
             */
            private String mediaUrl;
            private String ptzUrl;
            private String imageUrl;
            private String eventUrl;
            private String analyticsUrl;
        
            private String source_with = "1920";
            private String source_height = "1080";
            private String encoder_with = "1920";
            private String encoder_height = "1080";
        
            private String frameRateLimit = "25";
            private String bitrateLimit = "10000";
            private String prot = "8080";
            private String media_timeout = "PT30S";
        
            public String getRtsp_port() {
                return rtsp_port;
            }
        
            public void setRtsp_port(String rtsp_port) {
                this.rtsp_port = rtsp_port;
            }
        
            private String rtsp_port = "8086";
        
            public String getRtsp_stream() {
                return rtsp_stream;
            }
        
            public void setRtsp_stream(String rtsp_stream) {
                this.rtsp_stream = rtsp_stream;
            }
        
            private String rtsp_stream = "";//---/main.h264
        
            public String getUserName() {
                return userName;
            }
        
            public void setUserName(String userName) {
                this.userName = userName;
            }
        
            public String getPsw() {
                return psw;
            }
        
            public void setPsw(String psw) {
                this.psw = psw;
            }
        
            public String getIpAddress() {
                return ipAddress;
            }
        
            public void setIpAddress(String ipAddress) {
                this.ipAddress = ipAddress;
            }
        
            public String getServiceUrl() {
                return serviceUrl;
            }
        
            public void setServiceUrl(String serviceUrl) {
                this.serviceUrl = serviceUrl;
            }
        
            public String getUuid() {
                return uuid;
            }
        
            public void setUuid(String uuid) {
                this.uuid = uuid;
            }
        
            public String getMediaUrl() {
                return mediaUrl;
            }
        
            public void setMediaUrl(String mediaUrl) {
                this.mediaUrl = mediaUrl;
            }
        
            public String getPtzUrl() {
                return ptzUrl;
            }
        
            public void setPtzUrl(String ptzUrl) {
                this.ptzUrl = ptzUrl;
            }
        
            public String getImageUrl() {
                return imageUrl;
            }
        
            public void setImageUrl(String imageUrl) {
                this.imageUrl = imageUrl;
            }
        
            public String getEventUrl() {
                return eventUrl;
            }
        
            public void setEventUrl(String eventUrl) {
                this.eventUrl = eventUrl;
            }
        
            public String getAnalyticsUrl() {
                return analyticsUrl;
            }
        
            public void setAnalyticsUrl(String analyticsUrl) {
                this.analyticsUrl = analyticsUrl;
            }
        
            public String getSource_with() {
                return source_with;
            }
        
            public void setSource_with(String source_with) {
                this.source_with = source_with;
            }
        
            public String getSource_height() {
                return source_height;
            }
        
            public void setSource_height(String source_height) {
                this.source_height = source_height;
            }
        
            public String getEncoder_with() {
                return encoder_with;
            }
        
            public void setEncoder_with(String encoder_with) {
                this.encoder_with = encoder_with;
            }
        
            public String getEncoder_height() {
                return encoder_height;
            }
        
            public void setEncoder_height(String encoder_height) {
                this.encoder_height = encoder_height;
            }
        
            public String getFrameRateLimit() {
                return frameRateLimit;
            }
        
            public void setFrameRateLimit(String frameRateLimit) {
                this.frameRateLimit = frameRateLimit;
            }
        
            public String getBitrateLimit() {
                return bitrateLimit;
            }
        
            public void setBitrateLimit(String bitrateLimit) {
                this.bitrateLimit = bitrateLimit;
            }
        
            public String getProt() {
                return prot;
            }
        
            public void setProt(String prot) {
                this.prot = prot;
            }
        
            public String getMedia_timeout() {
                return media_timeout;
            }
        
            public void setMedia_timeout(String media_timeout) {
                this.media_timeout = media_timeout;
            }
        
            @Override
            public String toString() {
                return "DevicesBackBean{" +
                        "userName='" + userName + '\'' +
                        ", psw='" + psw + '\'' +
                        ", ipAddress='" + ipAddress + '\'' +
                        ", serviceUrl='" + serviceUrl + '\'' +
                        ", uuid='" + uuid + '\'' +
                        ", mediaUrl='" + mediaUrl + '\'' +
                        ", ptzUrl='" + ptzUrl + '\'' +
                        ", imageUrl='" + imageUrl + '\'' +
                        ", eventUrl='" + eventUrl + '\'' +
                        ", analyticsUrl='" + analyticsUrl + '\'' +
                        ", source_with='" + source_with + '\'' +
                        ", source_height='" + source_height + '\'' +
                        ", encoder_with='" + encoder_with + '\'' +
                        ", encoder_height='" + encoder_height + '\'' +
                        ", frameRateLimit='" + frameRateLimit + '\'' +
                        ", bitrateLimit='" + bitrateLimit + '\'' +
                        ", prot='" + prot + '\'' +
                        ", media_timeout='" + media_timeout + '\'' +
                        '}';}}
      
        public class DevicesInfo {
            public static final String USER_NAME = "***";
            public static final String USER_PASSWORD = "***";
            public static final String GET_MEDIA = "/onvif/Media";
            public static final String GET_PTZ = "/onvif/PTZ";
            public static final String GET_ANALYTICS = "/onvif/Analytics";
            public static final String GET_DEVICE_SERVICE = "/onvif/device_service";
            public static final String GET_EVENTS = "/onvif/Events";
            public static final String GET_IMAGING = "/onvif/Imaging";
        //    public static final String GET_StreamUri = "";
        //    public static  final String USER_NAME="";
        }
      
      • 線程實現(xiàn):

        class UdpB extends Thread {
            @Override
            public void run() {
                MulticastSocket socket = null;
                InetAddress address = null;
                try {
                    socket = new MulticastSocket(3702);
                    address = InetAddress.getByName("239.255.255.250");
                    socket.joinGroup(address);
                    DatagramPacket packet;
                    Log.e(TAG, "receiver packet");           // 接收數(shù)據(jù)
                    byte[] rev = new byte[1024 * 6];
                    while (flag) {
                        packet = new DatagramPacket(rev, rev.length);
                        socket.receive(packet);
        
                      String receiver = new String(packet.getData()).trim();  //不加trim七蜘,則會打印出512個byte,后面是亂碼
                      Log.e(TAG, "get data = " + receiver);
                      Log.e(TAG, "socket = " + socket.getInetAddress() + "  " + socket.getLocalSocketAddress() + "  " + socket.getLocalAddress() + "  " + socket.getPort());
                      Log.e(TAG, "socket = " + packet.getAddress() + "  " + packet.getSocketAddress() + "  " + packet.getPort() + "  ");
                      //解析Probe請求信息墙懂,此處主要要提取client端的uuid用于返回驗證
                      DiscoveryReqHeader discoveryReqHeader = OnvifXmlResolver.getProbeResponse(receiver).getDiscoveryReqHeader();
                      String reqUuid = discoveryReqHeader.getaMessageId();
                      String a_action = discoveryReqHeader.getaAction();
        
                      Log.e(TAG, "reqUuid: " + reqUuid + "  a_action: " + a_action);
                      if (receiver.contains("Envelope")) {
                          devicesBack = mApplication.getDevicesBack();
                          devicesBack.setUuid(reqUuid);
                          mApplication.setDevicesBackBean(devicesBack);
                          Log.e(TAG, "setDevicesBackBean: " + devicesBack.toString());
                          //發(fā)送數(shù)據(jù)包
                          //返回對應(yīng)的ProbeMatch
                          String sendBack = Utilities.generateDeviceProbeMatch(reqUuid, serverIp, Utilities.getUrnUuid(getApplicationContext()), Utilities
                                  .getMessageId());
                          byte[] buf = sendBack.getBytes();
                          Log.e(TAG, "send packet: " + sendBack);
                          packet = new DatagramPacket(buf, buf.length, packet.getAddress(), packet.getPort());
                          socket.send(packet);
                      }
                  }
              } catch (IOException e) {
                  mHander.sendEmptyMessage(111);
              }
              //退出組播
              try {
                  socket.leaveGroup(address);
                  socket.close();
              } catch (IOException e) {
                  mHander.sendEmptyMessage(111);
              }}}
        
    • 返回ProbeMatch

        /**
          * generate a soap request for probe onvif device
          */
         public static String generateDeviceProbeMatch(String uuid_req, String address_local, String urn_uuid, String messageId) {
        //                  if(!ObjectCheck.validString(uuid)) {
        //                           return "";
        //                  }
                  StringBuffer sb;
                  sb = new StringBuffer();
                  sb.append("<?xml version=\"1.0\"  encoding=\"UTF-8\" ?>\r\n");
                  sb.append("<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:SOAP-ENC=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:xsi=\"http://www" +
                          ".w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " +
                          "xmlns:wsdd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:dn=\"http://www.onvif" +
                          ".org/ver10/network/wsdl\">\r\n");
                  sb.append("       <SOAP-ENV:Header>\r\n");
                  sb.append("                <wsa:MessageID>urn:uuid" + urn_uuid + "</wsa:MessageID>\r\n");
                  sb.append("                <wsa:RelatesTo>" + uuid_req + "</wsa:RelatesTo>\r\n");
                  sb.append("                <wsa:ReplyTo SOAP-ENV:mustUnderstand=\"true\">\r\n");
                  sb.append("                    <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>\r\n");
                  sb.append("                </wsa:ReplyTo>\r\n");
                  sb.append("                <wsa:To SOAP-ENV:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>\r\n");
                  sb.append("                <wsa:Action SOAP-ENV:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches</wsa:Action>\r\n");
                  sb.append("                <wsdd:AppSequence InstanceId=\"0\" MessageNumber=\"5\"></wsdd:AppSequence>\r\n");
                  sb.append("       </SOAP-ENV:Header>\r\n");
                  sb.append("       <SOAP-ENV:Body>\r\n");
                  sb.append("                <wsdd:ProbeMatches>\r\n");
                  sb.append("                         <wsdd:ProbeMatch>\r\n");
                  sb.append("                                  <wsa:EndpointReference>\r\n");
                  sb.append("                                           <wsa:Address>urn:uuid:" + urn_uuid + "</wsa:Address>\r\n");
                  sb.append("                                  </wsa:EndpointReference>\r\n");
                  sb.append("                                  <wsdd:Types>dn:NetworkVideoTransmitter</wsdd:Types>\r\n");
                  sb.append("                                   <wsdd:Scopes>onvif://www.onvif.org/Profile/Streaming onvif://www.onvif.org/type/video_encoder onvif://www.onvif" +
                          ".org/type/audio_encoder onvif://www.onvif.org/hardware/ONVIF-Emu onvif://www.onvif.org/name/ONVIF-Emu onvif://www.onvif.org/location/Default</wsdd:Scopes> \r\n");
                  sb.append("                                  <wsdd:XAddrs>http://" + address_local + ":8080/onvif/device_service</wsdd:XAddrs>\r\n");
                  sb.append("                                  <wsdd:MetadataVersion>10</wsdd:MetadataVersion>\r\n");
                  sb.append("                         </wsdd:ProbeMatch>\r\n");
                  sb.append("                </wsdd:ProbeMatches>\r\n");
                  sb.append("       </SOAP-ENV:Body>\r\n");
                  sb.append("</SOAP-ENV:Envelope>");
                  return sb.toString();
         }
      
    • 請求的Probe抓包:

        <?xml version="1.0" encoding="utf-8"?>
        <Envelope xmlns="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
            <Header>
                <wsa:MessageID xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">uuid:d9305f63-5027-4edb-b1f1-58c463b5419a</wsa:MessageID>
                <wsa:To xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>
                <wsa:Action xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action>
            </Header>
            <Body>
                <Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
                    <Types>tds:Device</Types>
                    <Scopes/>
                </Probe>
            </Body>
        </Envelope>
      
    • 其中OnvifXmlResolver為xml解析橡卤,感興趣的可以看我的文章: SAX解析XML文件,通過解析相應(yīng)節(jié)點去獲取需要的內(nèi)容损搬,以下都會有用到碧库,這個代碼會在下一篇文章總給出

    • 完成以上內(nèi)容既可以被搜索到了,干凈用工具測試一下巧勤,發(fā)現(xiàn)OK嵌灰!

二、在Android上搭建一個Server用于接收和響應(yīng)Client請求

服務(wù)端這邊主要由開源框架spydroid-ipcamera改動得來颅悉,項目地址在文章頭部給出沽瞭,讀者可以下載閱讀,其實改起來很簡單剩瓶,下面先對該項目簡單分析:
在閱讀下面分析時驹溃,最好在下載該項目,進行簡單閱讀或?qū)Ρ乳喿x延曙,這樣會更明了豌鹤,我本人在寫博客時更提倡讀者自己動手,而不是直接給個現(xiàn)成的demo
這個工程只要有大體的實現(xiàn)思維搂鲫,改動起來其實很容易

分析spydroid-ipcamera實現(xiàn):

  1. 該項目分別搭建了rstp server和一個本地的http web server傍药,我們這一篇文章主要分析CustomHttpServer和改動這個類
    • CustomHttpServer繼承TinyHttpServer類(簡便封裝的Server端磺平,基于org.apache.http框架)魂仍,注意此處拐辽,我們在使用的時候,往往會出現(xiàn)V4包版本沖突的問題擦酌,這是由于google在新版本的v4包不再使用這個框架的緣故俱诸,故此我們只要在app gradle中聲明

        android {
            useLibrary 'org.apache.http.legacy'
        }
      
    • 在TinyHttpServer中,方法addRequestHandler赊舶,學過java web的都知道睁搭,此處為解析請求內(nèi)容的結(jié)構(gòu)體

        //在TinyHttpServer中: 
        /** 
         * You may add some HttpRequestHandler to modify the default behavior of the server.
         * @param pattern Patterns may have three formats: * or *<uri> or <uri>*
         * @param handler A HttpRequestHandler
         */ 
        protected void addRequestHandler(String pattern, HttpRequestHandler handler) {
            mRegistry.register(pattern, handler);
        }
      
        //在CustomHttpServer中:
        @Override
        public void onCreate() {
            super.onCreate();
            mDescriptionRequestHandler = new DescriptionRequestHandler();
            addRequestHandler("/spydroid.sdp*", mDescriptionRequestHandler);//.sdp請求
            addRequestHandler("/request.json*", new CustomRequestHandler());//此處為json請求,這個搞android的應(yīng)該都會证逻,不做簡述
        }
      
        //.sdp請求: SDP會話描述協(xié)議:為會話通知碍岔、會話邀請和其它形式的多媒體會話初始化等目的提供了多媒體會話描述壁袄。
                    會話目錄用于協(xié)助多媒體會議的通告,并為會話參與者傳送相關(guān)設(shè)置信息锌唾。 SDP 即用于將這種信息傳輸?shù)浇邮斩恕?              SDP 完全是一種會話描述格式――它不屬于傳輸協(xié)議 ――它只使用不同的適當?shù)膫鬏攨f(xié)議,包括會話通知協(xié)議 (SAP) 夺英、會話初始協(xié)議(SIP)
                    晌涕、實時流協(xié)議 (RTSP)、 MIME 擴展協(xié)議的電子郵件以及超文本傳輸協(xié)議 (HTTP)痛悯。SDP 的設(shè)計宗旨是通用性余黎,
                    它可以應(yīng)用于大范圍的網(wǎng)絡(luò)環(huán)境和應(yīng)用程序,而不僅僅局限于組播會話目錄
      
    • 在DescriptionRequestHandler處理網(wǎng)絡(luò) request 和 response载萌, request我們在android中經(jīng)常寫惧财,現(xiàn)在讓我們來寫一次response把

        /** 
         * Allows to start streams (a session contains one or more streams) from the HTTP server by requesting 
         * this URL: http://ip/spydroid.sdp (the RTSP server is not needed here). 
         **/
        class DescriptionRequestHandler implements HttpRequestHandler {
      
            private final SessionInfo[] mSessionList = new SessionInfo[MAX_STREAM_NUM];
      
            private class SessionInfo {
                public Session session;
                public String uri;
                public String description;
            }
      
            public DescriptionRequestHandler() {
                for (int i=0;i<MAX_STREAM_NUM;i++) {
                    mSessionList[i] = new SessionInfo();
                }
            }
      
            public synchronized void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException {
                Socket socket = ((TinyHttpServer.MHttpContext)context).getSocket();
                String uri = request.getRequestLine().getUri();
                int id = 0;
                boolean stop = false;
      
                try {
      
                    // A stream id can be specified in the URI, this id is associated to a session
                    List<NameValuePair> params = URLEncodedUtils.parse(URI.create(uri),"UTF-8");
                    uri = "";
                    if (params.size()>0) {
                        for (Iterator<NameValuePair> it = params.iterator();it.hasNext();) {
                            NameValuePair param = it.next();
                            if (param.getName().equalsIgnoreCase("id")) {
                                try {   
                                    id = Integer.parseInt(param.getValue());
                                } catch (Exception ignore) {}
                            }
                            else if (param.getName().equalsIgnoreCase("stop")) {
                                stop = true;
                            }
                        }   
                    }
      
                    params.remove("id");
                    uri = "http://c?" + URLEncodedUtils.format(params, "UTF-8");
      
                    if (!uri.equals(mSessionList[id].uri)) {
      
                        mSessionList[id].uri = uri;
      
                        // Stops all streams if a Session already exists
                        if (mSessionList[id].session != null) {
                            boolean streaming = isStreaming();
                            mSessionList[id].session.syncStop();
                            if (streaming && !isStreaming()) {
                                postMessage(MESSAGE_STREAMING_STOPPED);
                            }
                            mSessionList[id].session.release();
                            mSessionList[id].session = null;
                        }
      
                        if (!stop) {
                            
                            boolean b = false;
                            if (mSessionList[id].session != null) {
                                InetAddress dest = InetAddress.getByName(mSessionList[id].session.getDestination());
                                if (!dest.isMulticastAddress()) {
                                    b = true;
                                }
                            }
                            if (mSessionList[id].session == null || b) {
                                // Parses URI and creates the Session
                                mSessionList[id].session = UriParser.parse(uri);
                                mSessions.put(mSessionList[id].session, null);
                            } 
      
                            // Sets proper origin & dest
                            mSessionList[id].session.setOrigin(socket.getLocalAddress().getHostAddress());
                            if (mSessionList[id].session.getDestination()==null) {
                                mSessionList[id].session.setDestination(socket.getInetAddress().getHostAddress());
                            }
                            
                            // Starts all streams associated to the Session
                            boolean streaming = isStreaming();
                            mSessionList[id].session.syncStart();
                            if (!streaming && isStreaming()) {
                                postMessage(MESSAGE_STREAMING_STARTED);
                            }
      
                            mSessionList[id].description = mSessionList[id].session.getSessionDescription().replace("Unnamed", "Stream-"+id);
                            Log.v(TAG, mSessionList[id].description);
                            
                        }
                    }
      
                    final int fid = id; final boolean fstop = stop;
                    response.setStatusCode(HttpStatus.SC_OK);
                    EntityTemplate body = new EntityTemplate(new ContentProducer() {
                        public void writeTo(final OutputStream outstream) throws IOException {
                            OutputStreamWriter writer = new OutputStreamWriter(outstream, "UTF-8");
                            if (!fstop) {
                                writer.write(mSessionList[fid].description);
                            } else {
                                writer.write("STOPPED");
                            }
                            writer.flush();
                        }
                    });
                    body.setContentType("application/sdp; charset=UTF-8");
                    response.setEntity(body);
      
                } catch (Exception e) {
                    mSessionList[id].uri = "";
                    response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
                    Log.e(TAG,e.getMessage()!=null?e.getMessage():"An unknown error occurred");
                    e.printStackTrace();
                    postError(e,ERROR_START_FAILED);
                }
      
            }
      
        }
      
    • 在這里CustomHttpServer和CustomRtspServer,都是基于android的service扭仁,運行方法也一樣:

        //1. 啟動服務(wù)可缚,別忘了在清單文件中聲明
        this.startService(new Intent(this,CustomHttpServer.class));
      
        //2. bindService
        bindService(new Intent(this,CustomHttpServer.class), mHttpServiceConnection, Context.BIND_AUTO_CREATE);
        
        //3. unbindService
        if (mHttpServer != null) mHttpServer.removeCallbackListener(mHttpCallbackListener);
            unbindService(mHttpServiceConnection);
      
        //// Kills HTTP server
        this.stopService(new Intent(this,CustomHttpServer.class));
        
        private ServiceConnection mHttpServiceConnection = new ServiceConnection() {
      
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mHttpServer = (CustomHttpServer) ((TinyHttpServer.LocalBinder)service).getService();
                mHttpServer.addCallbackListener(mHttpCallbackListener);
                mHttpServer.start();
            }
      
            @Override
            public void onServiceDisconnected(ComponentName name) {}
      
        };
        
        //在callback中處理相應(yīng)的View顯示和刷新
        private TinyHttpServer.CallbackListener mHttpCallbackListener = new TinyHttpServer.CallbackListener() {
      
            @Override
            public void onError(TinyHttpServer server, Exception e, int error) {
                // We alert the user that the port is already used by another app.
                if (error == TinyHttpServer.ERROR_HTTP_BIND_FAILED ||
                        error == TinyHttpServer.ERROR_HTTPS_BIND_FAILED) {
                    String str = error==TinyHttpServer.ERROR_HTTP_BIND_FAILED?"HTTP":"HTTPS";
                    new AlertDialog.Builder(SpydroidActivity.this)
                    .setTitle(R.string.port_used)
                    .setMessage(getString(R.string.bind_failed, str))
                    .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                        public void onClick(final DialogInterface dialog, final int id) {
                            startActivityForResult(new Intent(SpydroidActivity.this, OptionsActivity.class),0);
                        }
                    })
                    .show();
                }
            }
      
            @Override
            public void onMessage(TinyHttpServer server, int message) {
                if (message==CustomHttpServer.MESSAGE_STREAMING_STARTED) {
                    if (mAdapter != null && mAdapter.getHandsetFragment() != null) 
                        mAdapter.getHandsetFragment().update();
                    if (mAdapter != null && mAdapter.getPreviewFragment() != null)  
                        mAdapter.getPreviewFragment().update();
                } else if (message==CustomHttpServer.MESSAGE_STREAMING_STOPPED) {
                    if (mAdapter != null && mAdapter.getHandsetFragment() != null) 
                        mAdapter.getHandsetFragment().update();
                    if (mAdapter != null && mAdapter.getPreviewFragment() != null)  
                        mAdapter.getPreviewFragment().update();
                }
            }
      
        };
      

接下來實現(xiàn)我們的Onvif Server:

首先選取我們要用的,TinyHttpServer是我們需要的斋枢,參照CustomHttpServer寫一個OnvifHttpServer,其中web訪問我們不做帘靡,可以去掉

  1. 自定義一個OnvifHttpServer集成TinyHttpServer,對照CustomHttpServer實現(xiàn)相應(yīng)方法瓤帚,Onvif Client請求端請求的方式是post,請求方式我們就假裝不知道描姚,我們定義Request pattern為所有格式即可,即: "/*"

     @Override
      public void onCreate() {
               super.onCreate();
               mDescriptionRequestHandler = new DescriptionRequestHandler();
               mDescriptionOnvifHandler = new DescriptionOnvifHandler();
               addRequestHandler("/*", mDescriptionOnvifHandler);
               addRequestHandler("/request.json*", new CustomRequestHandler());
      }
    
  2. 實現(xiàn)基于Onvif的請求解析DescriptionRequestHandler戈次,返回的格式為:"application/soap+xml; charset=UTF-8"轩勘,接口有很多,要想實現(xiàn)標準工具播放怯邪,要實現(xiàn)很多很多的接口绊寻,下面簡單介紹幾個,感興趣的可以自己去抓包標準設(shè)備完成:

    • GetDeviceInformation:獲取設(shè)備信息

    • GetCapabilities :獲取設(shè)備性能

    • GetProfiles獲取設(shè)備權(quán)限

    • GetStreamUri 獲取設(shè)備流媒體服務(wù)地址和相應(yīng)信息

        /**
         * this URL for onvif request.
         **/
        class DescriptionOnvifHandler implements HttpRequestHandler {
                 private final IpcServerApplication mApplication;
      
                 public DescriptionOnvifHandler() {
                          mApplication = (IpcServerApplication) getApplication();
                 }
      
                 public void handle(HttpRequest request, HttpResponse response, HttpContext arg2) throws HttpException, IOException {
                          if (request.getRequestLine().getMethod().equals("POST")) {//onvif請求為post
                                   // Retrieve the POST content
                                   final String url = URLDecoder.decode(request.getRequestLine().getUri());
                                   Log.e(TAG, "DescriptionRequestHandler------------------:url " + url);
                                   HttpEntityEnclosingRequest post = (HttpEntityEnclosingRequest) request;
                                   byte[] entityContent = EntityUtils.toByteArray(post.getEntity());
                                   String content = new String(entityContent, Charset.forName("UTF-8"));
      
                                   //此處需要用到響應(yīng)的你想給請求設(shè)備的信息,我作為一個全局的bean類保存澄步,此處可以實現(xiàn)為序列化到本地或者sp保存都行
                                   DevicesBackBean devicesBack = mApplication.getDevicesBack();
                                   LogUtils.e(TAG, "DescriptionRequestHandler :" + devicesBack.toString());
      
                                   String backS = null;
                                   //接下來就是返回請求了冰蘑,這里可以解析請求的包,即XML數(shù)據(jù)村缸,判斷相應(yīng)為什么請求祠肥,這個地方就體現(xiàn)了,模擬Onvif ipc
                                   //的尿性之處梯皿,請求接口相當繁多復(fù)雜仇箱,蛋疼的一匹,這樣實現(xiàn)終非正道东羹,如果要完成一個標識的Onvif IPC端還是要采取我第
                                   //一篇文章的移植才是正軌剂桥,這里簡要實現(xiàn)幾個接口,作為參考属提,大家有問題可以私信我一起溝通
                                   if (content.contains("GetDeviceInformation")) {
                                            backS = Utilities.getPostString("GetDeviceInformationReturn.xml", false, getApplicationContext(), mApplication.getDevicesBack());
                                            LogUtils.e(TAG, "DescriptionRequestHandler :" + mApplication.getDevicesBack().toString());
                                   } else if (content.contains("GetProfiles")) {//需要鑒權(quán)
                                            if (needProfiles) {
                                                     boolean isAuthTrue = DigestUtils.doAuthBack(content);
                                                     if (!isAuthTrue) {
                                                              doBackFail(response);
                                                              return;
                                                     }
                                            }                                           
                                            backS = Utilities.getPostString("getProfilesReturn.xml", true, getApplicationContext(), mApplication.getDevicesBack());                                         
                                            LogUtils.e("DescriptionRequestHandler", "-------getProfilesReturn--" + backS);                                                                                                
                                   }else {
                                            doBackFail(response);
                                            return;
                                   }
                                   LogUtils.e(TAG, "getProfilesReturn backS:" + backS);
                                   // Return the response
                                   final String finalBackS = backS;
                                   ByteArrayEntity body = new ByteArrayEntity(finalBackS.getBytes());
                                   ByteArrayInputStream is = (ByteArrayInputStream) body.getContent();
                                   response.setStatusCode(HttpStatus.SC_OK);
                                   body.setContentType("application/soap+xml; charset=UTF-8");
                                   response.setEntity(body);
                          }
                 }
        }
      
  3. 下面給一個我抓包的請求:

    • getProfiles和getProfilesBack:

        <?xml version="1.0" encoding="utf-8"?>
        <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
            <s:Header>
                <Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
                    <UsernameToken>
                        <Username>%s</Username>
                        <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</Password>
                        <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</Nonce>
                        <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</Created>
                    </UsernameToken>
                </Security>
            </s:Header>
            <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
                <GetProfile xmlns="http://www.onvif.org/ver10/media/wsdl">
                    <ProfileToken>%s</ProfileToken>
                </GetProfile>
            </s:Body>
        </s:Envelope>
      
        <?xml version="1.0" encoding="utf-8" ?>
        <SOAP-ENV:Envelope xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
            <SOAP-ENV:Body><trt:GetProfilesResponse><trt:Profiles fixed="true" token="Profile1">
                        <tt:Name>Profile1</tt:Name>
                        <tt:VideoSourceConfiguration token="VideoSourceToken">
                            <tt:Name>VideoSourceConfig</tt:Name><tt:UseCount>1</tt:UseCount><tt:SourceToken>VideoSource_1</tt:SourceToken><tt:Bounds height="%s" width="%s" x="0" y="0"/>
                        </tt:VideoSourceConfiguration>
                        <tt:VideoEncoderConfiguration token="VideoEncoderToken_1">
                            <tt:Name>VideoEncoder_1</tt:Name><tt:UseCount>1</tt:UseCount><tt:Encoding>%s</tt:Encoding><tt:Resolution>
                                <tt:Width>%s</tt:Width><tt:Height>%s</tt:Height>
                            </tt:Resolution>
                            <tt:Quality>44.0</tt:Quality>
                            <tt:RateControl><tt:FrameRateLimit>%s</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>%s</tt:BitrateLimit>
                            </tt:RateControl>
                            <tt:H264><tt:GovLength>100</tt:GovLength><tt:H264Profile>Baseline</tt:H264Profile>
                            </tt:H264>
                            <tt:Multicast>
                                <tt:Address>
                                    <tt:Type>IPv4</tt:Type><tt:IPv4Address>0.0.0.0</tt:IPv4Address><tt:IPv6Address />
                                </tt:Address>
                                <tt:Port>0</tt:Port>
                                <tt:TTL>0</tt:TTL>
                                <tt:AutoStart>false</tt:AutoStart>
                            </tt:Multicast>
                            <tt:SessionTimeout>PT30S</tt:SessionTimeout>
                        </tt:VideoEncoderConfiguration>
                        <tt:PTZConfiguration token="PTZToken">
                            <tt:Name>PTZ</tt:Name>
                            <tt:UseCount>1</tt:UseCount>
                            <tt:NodeToken>PTZNODETOKEN</tt:NodeToken>
                            <tt:DefaultAbsolutePantTiltPositionSpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace</tt:DefaultAbsolutePantTiltPositionSpace>
                            <tt:DefaultAbsoluteZoomPositionSpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace</tt:DefaultAbsoluteZoomPositionSpace>
                            <tt:DefaultRelativePanTiltTranslationSpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace</tt:DefaultRelativePanTiltTranslationSpace>
                            <tt:DefaultRelativeZoomTranslationSpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace</tt:DefaultRelativeZoomTranslationSpace>
                            <tt:DefaultContinuousPanTiltVelocitySpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace</tt:DefaultContinuousPanTiltVelocitySpace>
                            <tt:DefaultContinuousZoomVelocitySpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace</tt:DefaultContinuousZoomVelocitySpace>
                            <tt:DefaultPTZSpeed>
                                <tt:PanTilt space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace" x="0.100000" y="0.100000"/>
                                <tt:Zoom space="http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace" x="1.000000"/>
                            </tt:DefaultPTZSpeed>
                            <tt:DefaultPTZTimeout>PT1S</tt:DefaultPTZTimeout>
                            <tt:PanTiltLimits>
                                <tt:Range>
                                    <tt:URI>http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace</tt:URI>
                                    <tt:XRange>
                                        <tt:Min>-INF</tt:Min><tt:Max>INF</tt:Max>
                                    </tt:XRange>
                                    <tt:YRange>
                                        <tt:Min>-INF</tt:Min><tt:Max>INF</tt:Max>
                                    </tt:YRange>
                                </tt:Range>
                            </tt:PanTiltLimits>
                            <tt:ZoomLimits>
                                <tt:Range>
                                    <tt:URI>http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace</tt:URI>
                                    <tt:XRange>
                                        <tt:Min>-INF</tt:Min>
                                        <tt:Max>INF</tt:Max>
                                    </tt:XRange>
                                </tt:Range>
                            </tt:ZoomLimits>
                        </tt:PTZConfiguration>
                    </trt:Profiles>
                </trt:GetProfilesResponse>
            </SOAP-ENV:Body>
        </SOAP-ENV:Envelope>
      
  4. 返回端給出的代碼只需要把相應(yīng)的需要返回的信息替代到里面字符串即可

  5. 關(guān)于鑒權(quán)方面渊额,Onvif的鑒權(quán)有2中,WS-username token和Digest垒拢,可參考文章:Onvif協(xié)議及其在Android下的實現(xiàn)官方格式為

     Digest=B64Encode(SHA1(B64ENCODE(Nonce)+Date+Password))
     //nonce只是一個16位隨機數(shù)即可
     //Sha-1: MessageDigest md = MessageDigest.getInstance("SHA-1");
     //date:參考值:"2013-09-17T09:13:35Z";  由客戶端請求給出
     //此處我的不便摘要旬迹,詳細可參考文章:https://blog.csdn.net/yanjiee/article/details/18809107
     public String getPasswordEncode(String nonce, String password, String date) {  
         try {  
             MessageDigest md = MessageDigest.getInstance("SHA-1");  
             byte[] b1 = Base64.decode(nonce.getBytes(), Base64.DEFAULT);  
             byte[] b2 = date.getBytes(); // "2013-09-17T09:13:35Z";  
             byte[] b3 = password.getBytes();  
             byte[] b4 = new byte[b1.length + b2.length + b3.length];  
             md.update(b1, 0, b1.length);  
             md.update(b2, 0, b2.length);  
             md.update(b3, 0, b3.length);  
             b4 = md.digest();  
             String result = new String(Base64.encode(b4, Base64.DEFAULT));  
             return result.replace("\n", "");  
         } catch (Exception e) {  
             e.printStackTrace();  
             return "";  
         }  
     }  
       
     public String getNonce() {  
         String base = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";  
         Random random = new Random();  
         StringBuffer sb = new StringBuffer();  
         for (int i = 0; i < 24; i++) {  
             int number = random.nextInt(base.length());  
             sb.append(base.charAt(number));  
         }  
         return sb.toString();  
     }  
       
     private void createAuthString() {  
         SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'",  
                 Locale.CHINA);  
         mCreated = df.format(new Date());  
         mNonce = getNonce();  
         mAuthPwd = getPasswordEncode(mNonce, mCamera.password, mCreated);  
     }  
    

三、當當當當當求类!奔垦,完成上面的服務(wù)框架搭建,啟動服務(wù)

可通過標準工具檢查到我們的設(shè)備啦尸疆,并且還能查詢到響應(yīng)的信息

  • 其中EP Address即在文章初始椿猎,udp返回中的getUrnUuid,為我根據(jù)android 特定機器碼生成的一個特定唯一的UUID
onvif_1.png

onvif_2.png

接下來我們要搭建RTSP服務(wù)器寿弱,即可以在標準工具中進行播放犯眠,請查看我的下一篇文章:《在Android端搭建RTSP服務(wù)器》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市症革,隨后出現(xiàn)的幾起案子筐咧,更是在濱河造成了極大的恐慌,老刑警劉巖噪矛,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件量蕊,死亡現(xiàn)場離奇詭異,居然都是意外死亡艇挨,警方通過查閱死者的電腦和手機残炮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缩滨,“玉大人势就,你說我怎么就攤上這事泉瞻。” “怎么了苞冯?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵袖牙,是天一觀的道長。 經(jīng)常有香客問我抱完,道長贼陶,這世上最難降的妖魔是什么刃泡? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任巧娱,我火速辦了婚禮,結(jié)果婚禮上烘贴,老公的妹妹穿的比我還像新娘禁添。我一直安慰自己,他們只是感情好桨踪,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布老翘。 她就那樣靜靜地躺著,像睡著了一般锻离。 火紅的嫁衣襯著肌膚如雪铺峭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天汽纠,我揣著相機與錄音卫键,去河邊找鬼。 笑死虱朵,一個胖子當著我的面吹牛莉炉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碴犬,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼絮宁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了服协?” 一聲冷哼從身側(cè)響起绍昂,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎偿荷,沒想到半個月后治专,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡遭顶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年张峰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棒旗。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡喘批,死狀恐怖撩荣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饶深,我是刑警寧澤餐曹,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站敌厘,受9級特大地震影響台猴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜俱两,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一饱狂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宪彩,春花似錦休讳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至活合,卻和暖如春雏婶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背白指。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工留晚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侵续。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓倔丈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親状蜗。 傳聞我的和親對象是個殘疾皇子需五,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法轧坎,內(nèi)部類的語法宏邮,繼承相關(guān)的語法,異常的語法缸血,線程的語...
    子非魚_t_閱讀 31,622評論 18 399
  • org.springframework.beans: org.springframework.beans.fact...
    過河卒sc閱讀 590評論 1 1
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理蜜氨,服務(wù)發(fā)現(xiàn),斷路器捎泻,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • 我近期的目標是提高個人和家庭收入飒炎,愿財富種子迅速開花結(jié)果。 與目標相關(guān)的種子: 一笆豁、給公公買紅棗郎汪、面粉寄給他赤赊,為他...
    歸韻閱讀 87評論 0 0
  • 昨天和楊濤開完會后我倆坐在教室里談了很久,從各自的生活狀況到對感情的看法煞赢。 最近一次和朋友這樣聊天抛计,也是在四個月前...
    一克松閱讀 195評論 0 0