Slide 1

Slide 1 text

ZeroServices FELD Boris - PyconFR 2013 [email protected]

Slide 2

Slide 2 text

/me • FELD Boris - DevTools • Tornado FTW • @lothiraldan un peu partout sur le web • https://github.com/Lothiraldan/ ZeroServices

Slide 3

Slide 3 text

ZeroService • Librairie pour écrire des Services P2P • Python2.x • Very Alpha

Slide 4

Slide 4 text

ZeroServices Pourquoi ?

Slide 5

Slide 5 text

Besoins • AutoDiscovery • Multicast / Unicast • Découverte des pertes de pairs

Slide 6

Slide 6 text

Network is difficult

Slide 7

Slide 7 text

Peer To Peer is difficult

Slide 8

Slide 8 text

Choix technologiques

Slide 9

Slide 9 text

Pourquoi ØMQ ? • Sockets BSD trop bas niveau. • RabbitMQ... NOPE. • ØMQ FTW. • L’ioloop est packagée dans PyZMQ.

Slide 10

Slide 10 text

AutoDiscovery UDP Multicast

Slide 11

Slide 11 text

Communication Unicast / Multicast ZeroMQ

Slide 12

Slide 12 text

Découverte des pertes Heartbeating (to do)

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

Retour d’expérience

Slide 15

Slide 15 text

L’asynchrone c’est difficile

Slide 16

Slide 16 text

Tester de l’asynchrone c’est douloureux

Slide 17

Slide 17 text

Quelques trucs • Isoler au maximum la partie asynchrone • La tester avec des mocks • Mocker la partie asynchrone

Slide 18

Slide 18 text

Exemple class ZeroMQMediumTestCase(unittest.TestCase): def setUp(self): self.service = ServiceMock() self.ioloop = IOLoop.instance() self.medium = ZeroMQMedium(self.service, ioloop=self.ioloop) def testRegister(self): self.medium.register() self.ioloop.start() self.assertEqual(self.service.on_registration_message.call_count, 1)

Slide 19

Slide 19 text

Exemple II class BaseServiceTestCase(unittest.TestCase): def setUp(self): self.medium = TestMedium() self.service = BaseService(self.medium) def testOnRegistration(self): node_info = {'name': 'Service 1'} self.service.on_registration_message(node_info) self.assertEqual(self.medium.send_registration_answer.call_count, 1)

Slide 20

Slide 20 text

Les topologies réseaux est hétérogène

Slide 21

Slide 21 text

Vous allez toujours avoir des topologies de réseau tordues

Slide 22

Slide 22 text

Au choix • Firewall • NAT • VPN • DMZ • Pas d’UDP Multicast

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Traitez les quand ils arrivent

Slide 25

Slide 25 text

Pas de solutions magiques en P2P

Slide 26

Slide 26 text

Des techniques • Utiliser le port 443 ou 80 • UDP Hole Punching • Encapsuler le traffic dans HTTP

Slide 27

Slide 27 text

Une solution ? • Avoir un pair central • Qui forward le traffic

Slide 28

Slide 28 text

ZeroService is not perfect • Pas d’ack pour les messages • Pas d’heartbeat • Une socket par message sortant • UDP Multicast / ZeroConf • Marche en LAN. Pas en WAN.

Slide 29

Slide 29 text

Exemple

Slide 30

Slide 30 text

Le service de chat class ChatService(BaseService): def __init__(self, username): self.username = username super(ChatService, self).__init__(ZeroMQMedium(self, port_random=True)) def service_info(self): return {'name': self.username}

Slide 31

Slide 31 text

Le service de chat class ChatService(BaseService): def on_event(self, message_type, message): """Called when a multicast message is received """ msg = {'type': message_type} msg.update(message) self.send_to_all_clients(json.dumps(msg)) def on_message(self, message_type, **kwargs): """Called when an unicast message is received """ msg = {'type': message_type} msg.update(kwargs) self.send_to_all_clients(json.dumps(msg)) def on_new_node(self, node_info): """Called when a new peer joins """ msg = json.dumps({'type': 'user_join', 'id': node_info['node_id'], 'name': node_info['name']}) self.send_to_all_clients(msg) def on_node_close(self, node_info): """Called when a peer leaves """ msg = json.dumps({'type': 'user_leave', 'id': node_info['node_id'], 'name': node_info['name']}) self.send_to_all_clients(msg)

Slide 32

Slide 32 text

La partie Tornado class MainHandler(web.RequestHandler): def get(self): return self.render('chat.html', port=int(sys.argv[2]), name=sys.argv[1])

Slide 33

Slide 33 text

La partie Tornado clients = [] class WebSocketHandler(websocket.WebSocketHandler): def open(self): clients.append(self) for node_id, node in s.nodes_directory.items(): msg = json.dumps({'type': 'user_join', 'id': node_id, 'name': node['name']}) self.write_message(msg) def on_close(self): clients.remove(self) def on_message(self, message): message = json.loads(message) if message['type'] == 'message': msg = {'username': sys.argv[1], 'message': message['data']['message']} s.publish(str(message['type']), msg) elif message['type'] == 'direct_message': msg = {'from': sys.argv[1], 'message': message['data']['message']} s.send(message['data']['to'], msg, msg_type=str(message['type']))

Slide 34

Slide 34 text

Chat.py urls = [ (r"/", MainHandler), (r"/websocket", WebSocketHandler), (r"/static/(.*)", web.StaticFileHandler, {"path": "."})] if __name__ == '__main__': application = web.Application(urls, debug=True) application.listen(int(sys.argv[2])) try: ioloop.IOLoop.instance().add_timeout(time() + 1, s.medium.register) ioloop.IOLoop.instance().start() except KeyboardInterrupt: s.close() for client in clients: client.close()

Slide 35

Slide 35 text

La demo chat • 69 lignes de python • Pas de serveur central • API qui se veut simple

Slide 36

Slide 36 text

DEMO !

Slide 37

Slide 37 text

Questions ?