-
Notifications
You must be signed in to change notification settings - Fork 0
/
producer.py
201 lines (152 loc) · 6.27 KB
/
producer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import argparse
import BaseHTTPServer
import threading
class Consumers(object):
"""Thread-safe wrapper around the set of registered consumers.
"""
def __init__(self):
self.lock = threading.Lock()
# Guarded by self.lock.
self.registered = set()
def exists(self, consumer):
"""Check if the consumer exists. Returns whether the consumer exists.
"""
with self.lock:
return consumer in self.registered
def exists_and_add(self, consumer):
"""Check if the consumer already exists and add if missing. Returns whether the
consumer already exists.
"""
with self.lock:
if consumer in self.registered:
return True
self.registered.add(consumer)
return False
def reset(self):
"""Resets the state by deleting all registered consumers.
An unfortunate functionality required by the fact that Python's standard HTTP server
framework does not allow access to a handler's instance state, forcing Consumers to be
stored as static state in RegistrationHandler.
"""
with self.lock:
self.registered.clear()
class Server(object):
"""Toy streaming server. Produces a stream of UDP packets for registered consumers.
The following is the HTTP server interface:
PUT /
Register a consumer. The request's client IP address and the provided port are used to
register that consumer.
GET /
Check registration status. The request's client IP address and the provided port are used
to check the status of the consumer. If the reply's status code is 200, the consumer is
registered. If the reply's status code is 404, no such consumer is registered.
DELETE /
Deregister a consumer. The request's client IP address and the provided port are used to
deregister that consumer.
In the commandline examples below, nc is used to register a consumer listening at port
6714 and then deregister it.
Register a consumer:
$ nc SERVER PORT
PUT / HTTP/1.1
Content-Length: 4
6714
HTTP/1.1 200 OK
Check the consumer exists:
$ nc SERVER PORT
GET / HTTP/1.1
Content-Length: 4
6714
HTTP/1.1 200 OK
Deregister the consumer:
$ nc SERVER PORT
DELETE / HTTP/1.1
Content-Length: 4
6714
HTTP/1.1 200 OK
Check the consumer doesn't exist:
$ nc SERVER PORT
GET / HTTP/1.1
Content-Length: 4
6714
HTTP/1.1 404 Not Found
"""
@staticmethod
def parse_args():
"""Parses Server args from the arg string."""
parser = argparse.ArgumentParser(description='Parse Server args.')
parser.add_argument('-p', '--port', type=int, default=8080,
help='Set the port to listen for UDP packets')
return parser.parse_args()
class RegistrationHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""A handler for registering new consumers.
New consumers of the streaming server should PUT. To deregister, existing consumers
should DELETE. The only vaild resource path is /. The payload must contain a port number
of the consumer.
The class has static state. Python's standard HTTP server framework does not allow
access to a handler's instance state.
Thread-safe.
"""
consumers = Consumers()
# Changes the error message format. Breaks encapsulation. Not pretty but this is the
# way that BaseHTTPRequestHandler works.
error_message_format = '%(code)d %(explain)s: %(message)s\n'
error_content_type = 'text/plain'
def get_port(self):
"""Get the port from the body. Return a tuple (bool, port) where the first
part of the tuple indicates whether a port was found.
"""
content_length = int(self.headers.getheader('Content-Length', 0))
port_string = self.rfile.read(content_length)
if port_string.isdigit():
return (True, int(port_string))
return (False, -1)
def path_supported(self):
"""Returns whether the path is supported.
Only / is supported. Mapping paths to handlers is manual in this framework and not
necessary for a toy server.
"""
if self.path.strip() == '/':
return True
return False
def do_PUT(self):
if not self.path_supported():
self.send_error(404, 'Path {} not supported'.format(self.path))
return
(port_provided, port) = self.get_port()
if not port_provided:
self.send_error(400, 'Port not provided in body')
return
consumer = (self.client_address[0], port)
if self.consumers.exists_and_add(consumer):
self.send_error(400, 'Consumer {} already exists'.format(consumer))
return
self.send_response(200)
def do_GET(self):
if not self.path_supported():
self.send_error(404, 'Path {} not supported'.format(self.path))
return
(port_provided, port) = self.get_port()
if not port_provided:
self.send_error(400, 'Port not provided in body')
return
consumer = (self.client_address[0], port)
if self.consumers.exists(consumer):
self.send_response(200)
return
self.send_error(404, 'Consumer {} not registered'.format(consumer))
def __init__(self, port):
"""Constructs an HTTP Server listening at the provided port."""
address = ('localhost', port)
self.httpd = BaseHTTPServer.HTTPServer(address, Server.RegistrationHandler)
def run(self):
"""Run the server forever."""
Server.RegistrationHandler.consumers.reset()
self.httpd.serve_forever()
def shutdown(self):
"""Shut down and clean up the server."""
self.httpd.shutdown()
self.httpd.server_close()
if __name__ == '__main__':
args = Server.parse_args()
server = Server(args.port)
server.run()