Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Peer-to-Peer Replication on Android

jsh33hy
February 19, 2020

Peer-to-Peer Replication on Android

Building synchronized peer-to-peer systems on Android with Network Service Discovery (NSD) and CouchbaseLite (1.X)

- Problem statement: building a POS on Android
- Architectural constraints: Android OS, multiple devices, no single point of failure, must work offline
- Peer-to-Peer technologies available in Android
- Overview of the technologies we are using: Network Service Discovery and CouchbaseLite

jsh33hy

February 19, 2020
Tweet

Other Decks in Programming

Transcript

  1. Android Point of Sale Consumer grade hardware Easy to set

    up Support between 1 and 20 devices in a store 5
  2. Android Point of Sale Consumer grade hardware Easy to set

    up Support between 1 and 20 devices in a store No single point of failure 6
  3. Android Point of Sale Consumer grade hardware Easy to set

    up Support between 1 and 20 devices in a store No single point of failure Must work offline (no outbound internet) 7
  4. Peer-to-Peer How do you discover other nodes? How do you

    transmit data to other nodes? How do you structure your network? 11
  5. Network Service Discovery (NSD) mDNSResponder created by Apple (Bonjour) 21

    https://github.com/andriydruk/Bonjou Added to Android in API 16 (4.1)
  6. Network Service Discovery (NSD) mDNSResponder created by Apple (Bonjour) 22

    https://github.com/andriydruk/Bonjou Added to Android in API 16 (4.1) Supported by any device with a network card
  7. Network Service Discovery (NSD) mDNSResponder created by Apple (Bonjour) 23

    https://github.com/andriydruk/Bonjou Added to Android in API 16 (4.1) Supported by any device with a network card Multicast DNS and DNS-SD
  8. Define our Network Service serviceInfo = new NsdServiceInfo(); // _<protocol>._<transportlayer>

    serviceInfo.setServiceType("_p2pnsd._tcp."); // The name is subject to change based on conflicts // with other services advertised on the same network. serviceInfo.setServiceName("P2P_NSD_"+uuid); // Preferred** service port serviceInfo.setPort(3334); // Requires API Level 21+ serviceInfo.setAttribute("key1", "Some text value"); serviceInfo.setAttribute("key2", "Some other text value"); 27
  9. Define our Network Service serviceInfo = new NsdServiceInfo(); // _<protocol>._<transportlayer>

    serviceInfo.setServiceType("_p2pnsd._tcp."); // The name is subject to change based on conflicts // with other services advertised on the same network. serviceInfo.setServiceName("P2P_NSD_"+uuid); // Preferred** service port serviceInfo.setPort(3334); // Requires API Level 21+ serviceInfo.setAttribute("key1", "Some text value"); serviceInfo.setAttribute("key2", "Some other text value"); 28
  10. Define our Network Service serviceInfo = new NsdServiceInfo(); // _<protocol>._<transportlayer>

    serviceInfo.setServiceType("_p2pnsd._tcp."); // The name is subject to change based on conflicts // with other services advertised on the same network. serviceInfo.setServiceName("P2P_NSD_"+uuid); // Preferred** service port serviceInfo.setPort(3334); // Requires API Level 21+ serviceInfo.setAttribute("key1", "Some text value"); serviceInfo.setAttribute("key2", "Some other text value"); 29
  11. Define our Network Service serviceInfo = new NsdServiceInfo(); // _<protocol>._<transportlayer>

    serviceInfo.setServiceType("_p2pnsd._tcp."); // The name is subject to change based on conflicts // with other services advertised on the same network. serviceInfo.setServiceName("P2P_NSD_"+uuid); // Preferred** service port serviceInfo.setPort(3334); // Requires API Level 21+ serviceInfo.setAttribute("key1", "Some text value"); serviceInfo.setAttribute("key2", "Some other text value"); 30
  12. Register our Service registrationListener = new NsdManager.RegistrationListener() { @Override public

    void onServiceRegistered(NsdServiceInfo NsdServiceInfo) { // Save the service name. Android may have changed it in order to // resolve a conflict, so update the name you initially requested // with the name Android actually used. serviceName = NsdServiceInfo.getServiceName(); } @Override public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {} @Override public void onServiceUnregistered(NsdServiceInfo arg0) {} @Override public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {} }; 31
  13. Register our Service registrationListener = new NsdManager.RegistrationListener() { @Override public

    void onServiceRegistered(NsdServiceInfo NsdServiceInfo) { // Save the service name. Android may have changed it in order to // resolve a conflict, so update the name you initially requested // with the name Android actually used. serviceName = NsdServiceInfo.getServiceName(); } @Override public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {} @Override public void onServiceUnregistered(NsdServiceInfo arg0) {} @Override public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {} }; 32 registrationListener = new NsdManager.RegistrationListener() { @Override public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) { // Save the service name. Android may have changed it in order to // resolve a conflict, so update the name you initially requested // with the name Android actually used. serviceName = NsdServiceInfo.getServiceName(); } //... }; nsdManager = Context.getSystemService(Context.NSD_SERVICE); nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
  14. Discover Services discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 34
  15. Discover Services discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 35 discoveryListener = new NsdManager.DiscoveryListener() { @Override public void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) { // A service was found! Do something with it. if (!service.getServiceType().equals("_p2pnsd._tcp.")) { // This is not our service type, ignore it. } else if (service.getServiceName().equals(serviceName)) { // This is the service we are broadcasting, ignore it } else if (service.getServiceName().contains("P2P_NSD")){ nsdManager.resolveService(service, resolveListener); } } //.. };
  16. Discover Services discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 39 discoveryListener = new NsdManager.DiscoveryListener() { @Override public void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) { // A service was found! Do something with it. if (!service.getServiceType().equals("_p2pnsd._tcp.")) { // This is not our service type, ignore it. } else if (service.getServiceName().equals(serviceName)) { // This is the service we are broadcasting, ignore it } else if (service.getServiceName().contains("P2P_NSD")){ nsdManager.resolveService(service, resolveListener); } } //.. };
  17. Resolve Service discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 40 resolveListener = new NsdManager.ResolveListener() { @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { // handle error } @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { // start replicating data to our peer String url = serviceInfo.getHost().getHostAddress()+":"+serviceInfo.getPort(); startReplications(url); } };
  18. Setting it up discoveryListener = new NsdManager.DiscoveryListener() { @Override public

    void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 42 couchManager = new Manager(new AndroidContext(context), Manager.DEFAULT_OPTIONS); couchDb = couchManager.getDatabase(DB_NAME); // init peer to peer replication listener (This is the service we advertised earlier) LiteListener listener = new LiteListener(couchManager, 3334); Thread thread = new Thread(listener); thread.start();
  19. Setting it up discoveryListener = new NsdManager.DiscoveryListener() { @Override public

    void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 43 couchManager = new Manager(new AndroidContext(context), Manager.DEFAULT_OPTIONS); couchDb = couchManager.getDatabase(DB_NAME); // init peer to peer replication listener (This is the service we advertised earlier) LiteListener listener = new LiteListener(couchManager, 3334); Thread thread = new Thread(listener); thread.start();
  20. Setting it up discoveryListener = new NsdManager.DiscoveryListener() { @Override public

    void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 44 couchManager = new Manager(new AndroidContext(context), Manager.DEFAULT_OPTIONS); couchDb = couchManager.getDatabase(DB_NAME); // init peer to peer replication listener (This is the service we advertised earlier) LiteListener listener = new LiteListener(couchManager, 3334); Thread thread = new Thread(listener); thread.start();
  21. Starting Replications 46 resolveListener = new NsdManager.ResolveListener() { @Override public

    void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { // handle error } @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { // start replicating data to our peer String url = serviceInfo.getHost().getHostAddress()+":"+serviceInfo.getPort(); startReplications(url); } };
  22. Starting Replications 47 private void startReplications(String syncUrl){ Replication push =

    couchDB.createPushReplication(syncUrl); push.setContinuous(true); push.start(); Replication pull = couchDB.createPullReplication(syncUrl); pull.setContinuous(true); pull.start(); }
  23. Starting Replications 48 private void startReplications(String syncUrl){ Replication push =

    couchDB.createPushReplication(syncUrl); push.setContinuous(true); push.start(); Replication pull = couchDB.createPullReplication(syncUrl); pull.setContinuous(true); pull.start(); }
  24. Writing Data discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 49 // In NoSQL everything is basically a key : value pair Map<String, Object> properties = new HashMap<>(); properties.put("TYPE", "Employee"); properties.put("name", "Jeremiah"); // Create a new document Document document = couchDb.createDocument(); // Save the document to the database document.putProperties(properties); { "TYPE": "Employee", "name": "Jeremiah" }
  25. Writing Data discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 50 // In NoSQL everything is basically a key : value pair Map<String, Object> properties = new HashMap<>(); properties.put("TYPE", "Employee"); properties.put("name", "Jeremiah"); // Create a new document Document document = couchDb.createDocument(); // Save the document to the database document.putProperties(properties); { "TYPE": "Employee", "name": "Jeremiah" }
  26. Writing Data discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 51 // In NoSQL everything is basically a key : value pair Map<String, Object> properties = new HashMap<>(); properties.put("TYPE", "Employee"); properties.put("name", "Jeremiah"); // Create a new document Document document = couchDb.createDocument(); // Save the document to the database document.putProperties(properties); { "TYPE": "Employee", "name": "Jeremiah" }
  27. Writing Data discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 52 // In NoSQL everything is basically a key : value pair Map<String, Object> properties = new HashMap<>(); properties.put("TYPE", "Employee"); properties.put("name", "Jeremiah"); // Create a new document Document document = couchDb.createDocument(); // Save the document to the database document.putProperties(properties); { "TYPE": "Employee", "name": "Jeremiah" }
  28. Writing Data discoveryListener = new NsdManager.DiscoveryListener() { @Override public void

    onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 53 // In NoSQL everything is basically a key : value pair Map<String, Object> properties = new HashMap<>(); properties.put("TYPE", "Employee"); properties.put("name", "Jeremiah"); // Create a new document Document document = couchDb.createDocument(); // Save the document to the database document.putProperties(properties); { "TYPE": "Employee", "name": "Jeremiah" }
  29. Listening for Changes discoveryListener = new NsdManager.DiscoveryListener() { @Override public

    void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 54 // Create a view emloyeeView = couchDb.getView("EMPLOYEE_VIEW"); emloyeeView.setMap((doc, emitter) -> { if (doc.get("TYPE").equals("Employee")) { emitter.emit(doc); } }); // Create live query Query query = emloyeeView.createQuery(); employeeLiveQuery = query.toLiveQuery(); employeeLiveQuery.addChangeListener(event -> { // TODO: Handle Changes }); employeeLiveQuery.start()
  30. Listening for Changes discoveryListener = new NsdManager.DiscoveryListener() { @Override public

    void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 55 // Create a view emloyeeView = couchDb.getView("EMPLOYEE_VIEW"); emloyeeView.setMap((doc, emitter) -> { if (doc.get("TYPE").equals("Employee")) { emitter.emit(doc); } }); // Create live query Query query = emloyeeView.createQuery(); employeeLiveQuery = query.toLiveQuery(); employeeLiveQuery.addChangeListener(event -> { // TODO: Handle Changes }); employeeLiveQuery.start()
  31. Listening for Changes discoveryListener = new NsdManager.DiscoveryListener() { @Override public

    void onDiscoveryStarted(String regType) {} @Override public void onServiceFound(NsdServiceInfo service) {} // @Override public void onServiceLost(NsdServiceInfo service) {} @Override public void onDiscoveryStopped(String serviceType) {} @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { } }; 56 // Create a view emloyeeView = couchDb.getView("EMPLOYEE_VIEW"); emloyeeView.setMap((doc, emitter) -> { if (doc.get("TYPE").equals("Employee")) { emitter.emit(doc); } }); // Create live query Query query = emloyeeView.createQuery(); employeeLiveQuery = query.toLiveQuery(); employeeLiveQuery.addChangeListener(event -> { // TODO: Handle Changes }); employeeLiveQuery.start()