Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Notes

Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

1 Event-driven Programming paradigm

1.1 Single thread


Single threaded or sequential programming is in which requests are processed
by a single thread. If the thread is doing some blocking task like IO then the
program can’t handle any other requests until the request is processed.
Example of single threaded server
import time

from http import server


from socketserver import ThreadingMixIn

class RequestHandler(server.SimpleHTTPRequestHandler):

def do_GET(self):
start_time = time.time()
time.sleep(5)
end_time = time.time()
self.send_response(200, "OK")
self.end_headers()
response = (
"Started at {}, Ended at {} difference {}".format(
start_time, end_time, end_time - start_time
)
)
self.wfile.write(response.encode(’utf-8’))
return

with server.HTTPServer(
(’localhost’, 8000), RequestHandler
) as http_server:
http_server.serve_forever()

1.2 Multi-Threaded
Converting above program to a multithreaded version will help. A multi-
threaded program will handle each HTTP request in a new thread and thus
it can handle mutiple requests at the same time.

import time

from http import server


from socketserver import ThreadingMixIn

1
class RequestHandler(server.SimpleHTTPRequestHandler):

def do_GET(self):
time.sleep(40)
end_time = time.time()
self.send_response(200, "OK")
self.end_headers()
response = "Ended at {}".format(end_time)
self.wfile.write(response.encode(’utf-8’))
return

class ThreadedHTTPServer(ThreadingMixIn, server.HTTPServer):


daemon_threads = True

with ThreadedHTTPServer(
(’localhost’, 8000), RequestHandler
) as http_server:
http_server.serve_forever()

Compared to the single threaded approach, the program with multithreaded


will be able to achieve more performance because the server will spawn a new
thread for each new request. And thus, when the thread is blocking, it can
handle another request during that time.
But here is a problem. When bunch of new requests are coming (usually
higher than the core of processor), Blocking thread is actually blocking the
CPU core.
Compare the threaded example using Apache Benchmark tool.

ab -n 100 -c 100 -s 120 http://localhost:8000

In the above, command -n is a short-hand for Number of requests to be


fired. -c is a concurrent requests the tool will fire at the same time. -s is a short
hand for total number of seconds after which the tool will timeout a request.
The threaded server, a maximum number of concurrent requests handled by
the server will be somewhere between (total number of cores * 1.5 ) + 1. The
server can’t handle other concurrent requests at the same time. The server will
process those requests in batches.

2
1.3 Event-driven
Now, if we convert above example into event-driven asynchronous code then it
will look as below.

import time

from twisted.internet import reactor


from twisted.internet.task import deferLater
from twisted.web.resource import Resource
from twisted.web.server import Site, NOT_DONE_YET

class BusyPage(Resource):
isLeaf = True

def _delayedResponse(self, request):


end_time = time.time()
response = "Ended at {}".format(end_time)
request.write(response.encode("utf-8"))
request.finish()

def render_GET(self, request):


d = deferLater(reactor, 40, lambda: request)
d.addCallback(self._delayedResponse)
return NOT_DONE_YET

factory = Site(BusyPage())
reactor.listenTCP(8000, factory)
reactor.run()

Compare the performance of the above code by Apache benchmarking tool.


Below is the same command we had used earlier to compare the multi-
threaded approach.

ab -n 100 -c 100 -s 120 http://localhost:8000

The output statistics of both the command will be different. The asyn-
chronous approach will be able to handle all the requests at the same time.
Reason behind this is when the request is coming, the IO thread is trying to
process the request. During that time, if it is blocked the code is handled by
event-driven approach and the thread is again available for entertaining another
request. The processing of request is again resumed when the duration of IO is
completed and returned.

3
2 Introduction to Twisted programming
2.1 History
Twisted was invented by Glyph Lefkowitz in the year October 22, 2002 which
is almost 16 years before. It is an old giant.

4
2.2 Current situation
At present, the Twisted framework is developed and managed by its community.
Latest stable version is 18.9.x. The Twisted framework is licensed under MIT
license. Twisted is based on event-driven programming paradigm.

2.3 Sockets
Twisted programming supports all the TCP, UDP, SSL/TSL based sockets. It
also supports the Unix sockets.

2.4 Protocol
Twisted supports many application layer protocols. Such as HTTP, XMPP,
IMAP, SSH, IRC, FTP and many more.

2.5 Applications/Frameworks based on Twisted


• Buildbot [?]
• Omgle video chat service [?]

• Twilio a cloud telephony provider [?]


• Twitch.tv a popular Game livestream video sharing [?]
• Scrapy a framework for scrapping webpages [?]

And many more are based on Twisted!

3 Echo server and client example


3.1 Echo Server
\\Example echo_server.py
from twisted.internet import protocol, reactor

class EchoServer(protocol.Protocol):

def dataReceived(self, data):


print(data)
self.transport.write(data)

class EchoFactory(protocol.Factory):

def buildProtocol(self, addr):

5
return EchoServer()

reactor.listenTCP(8000, EchoFactory())
reactor.run()

3.2 Echo client


\\ Example echo_client.py
from twisted.internet import reactor, protocol

class EchoClient(protocol.Protocol):

def connectionMade(self):
request = "Hello, world!"
self.transport.write(request.encode("utf-8"))

def dataReceived(self, data):


print(data)
self.transport.loseConnection()

class EchoFactory(protocol.ClientFactory):

def buildProtocol(self, addr):


protocol = EchoClient()
protocol.factory = self
return protocol

def clientConnectionFailed(self, connector, reason):


reactor.stop()

def clientConnectionLost(self, connector, reason):


reactor.stop()

reactor.connectTCP("localhost", 8000, EchoFactory())


reactor.run()

6
4 Reactor
Reactor is the core of the event loop. It is responsible for waiting for the event
and notify the program when the event is occurred. Reactor is dependent on
platform specific libraries. Twisted will automatically choose default platform
specific library.
For under the hood details, In GNU/Linux platform it tries to fetch for epoll
[?]. If epoll [?] is not available, it will use poll [?]. All the POSIX compliant
platforms it will use poll. All other platforms such as Windows and Macintosh
it will use select reactor.
You have to register your callbacks to the reactor and have to start it. Once
the reactor is start, it will listen to the events and notifies to the program forever
or until reactor.stop is called.
In our echo server and echo client example, below code is representing the
reactor. The reactor.run() will start the event loop until the SIGINT or
reactor.stop() is called.

reactor.listenTCP(8000, MyFactory())
reactor.run()

5 Transport
Transport is responsible to provide the behaviour for transferring data to the
other end of the connection. The transport behaves differently according to
the connection for example, TCP or UDP will have different implementation of
transport. In echo client and echo server example below code is representing
Transport

self.transport.write()

All the transport implementations should be dependent on ITransport in-


terface. Below are the common methods of Transport

5.1 write
Should write data to the connection.

5.2 writeSequence
Should write list of strings to the connection.

5.3 loseConnection
Should close the connection after writing ending data.

7
5.4 getPeer
Should return remote address of the connection.

6 Protocol
Protocol will define the behaviour to process the network events in async man-
ner. Twisted does have in-built definition for many protocols such as HTTP,
Telnet, DNS etc. Base class is protocol.Protocol.
In echo server and echo client example, below code is of Protocol.

\\ Client
class EchoClient(protocol.Protocol):

def connectionMade(self):
request = "Hello, world!"
self.transport.write(request.encode("utf-8"))

def dataReceived(self, data):


print(data)
self.transport.loseConnection()

\\ Server
class EchoServer(protocol.Protocol):

def dataReceived(self, data):


print(data)
self.transport.write(data)

All the protocol classes should implement IProtocol interface. Common


methods are below

6.1 makeConnection
Should create a connection.

6.2 connectionMade
Should be called when a new connection is made.

6.3 dataReceived
Should be called when any data is received from the other end of the circuit.

8
6.4 connectionLost
Should be called when the connection is terminated.

7 Protocol Factory
The factory will produce the instance of Protocol when the new connection is
made. Protocol will be garbage collected when the collection is called. The
Factory will store configuration details for Protocol instances.
According to our echo server and echo client example, below is the code of
portable factory.

\\ Server
class EchoFactory(protocol.Factory):

def buildProtocol(self, addr):


return EchoServer()

\\ Client
class EchoFactory(protocol.ClientFactory):

def buildProtocol(self, addr):


protocol = EchoClient()
protocol.factory = self
return protocol

def clientConnectionFailed(self, connector, reason):


reactor.stop()

def clientConnectionLost(self, connector, reason):


reactor.stop()

The factory is following IProtocolFactory interface. There client imple-


mentation of common factory methods is ClientFactory. Common methods
are as per below

7.1 buildProtocol
should create the instance of Protocol class.
From the above example, purpose of protocol factories might not be clear in
your mind. For that reason, we will conver another example.

\\chat_server.py
from twisted.internet.protocol import Factory

9
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor

class ChatProtocol(LineReceiver):

def __init__(self, factory):


self.factory = factory

def connectionMade(self):
self.factory.users.append(self)

def lineReceived(self, line):


for user in self.factory.users:
if user != self:
user.transport.write(line+b"\r\n")

class ChatFactory(Factory):

def __init__(self):
self.users = []

def buildProtocol(self, addr):


return ChatProtocol(self)

reactor.listenTCP(8000, ChatFactory())
reactor.run()

Connect with chat server using multiple connections made by telnet pro-
gram. This will allow to communicate multiple clients as in chatting in group. In
that code, concurrent client instances are stored at Protocol Factory. Whenever
you have to store persistent information beyond the lifecycle of your socket, you
should store that in the Protocol Factory. Protocol will be created and garbage
collected with connection.

8 Deferreds
Deferreds are the core API provided by the Twisted. This API is provided for
writing a callbacks. A callback is a type of function which can be paused by an
event-loop and resumed when specific event is happended.
If you are assuming that just writing code in Twisted will make things asyn-

10
chronous then you are wrong. You have to wrap your blocking code into appro-
priate Deferred to make it event-driven.
We have already encountered with deferred API in our Twisted webserver
example.

\\Example from Twisted based HTTP server


d = deferLater(reactor, 40, lambda: request)
d.addCallback(self._delayedResponse)

In above example, d.addCallback is adding delayedResponse as an call-


back function. deferLater will fire the deferred function.

8.1 addCallback
Below is the example of addCallback and callback API

\\Example of addCallback and callback


from twisted.internet.defer import Deferred

def callback_func(result):
print(result)

d = Deferred()
d.addCallback(callback_func)
d.callback("Trigerred the callback!")

In the above example, the callback will fire the callback with the arguemtn
of string. The callbackfunction is added as a callback by the addCallback

8.2 addErrback
\\Example Errback
from twisted.internet.defer import Deferred

def errorback_func(failure):
print(failure)

d = Deferred()
d.addErrback(errorback_func)
d.errback("Triggered! Error!".encode("utf-8"))

11
In above example, the errback is responsible for triggering the error. The
addErrback is responsible for registering the callback.

8.3 addCallbacks
from twisted.internet.defer import Deferred

def callback_func(result):
print(result)

def errback_func(failure):
print(failure)

d = Deferred()
d.addCallbacks(callback_func, errback_func)
d.callback(b"Hello")
#d.errback(b"Hello")

The function addCallbacks will add a callback and errorback function in


single call.

8.4 Comparision
The addCallback or addErrrback will add callbacks at different level whereas
the addCallbacks will add callback at the same level.

9 Testing
Testing in Twisted is considered as tricky. Twisted has dedicated utility called
trial for running and managing the tests. The trial is based on pytest. You
can use trial for various purposes.

9.1 Testing echo server


from twisted.test import proto_helpers
from twisted.trial import unittest

from echo_server import EchoFactory

class TestEchoServer(unittest.TestCase):

12
def test_response(self):
factory = EchoFactory()
protocol = factory.buildProtocol(("localhost", 0))
transport = proto_helpers.StringTransport()

protocol.makeConnection(transport)
data = b"test\r\n"
protocol.dataReceived(data)
self.assertEqual(transport.value(), data)

Things to observe is trial.unittest.TestCase. This is the special class


provided by Twisted for testing.
Run below command to run the test
PYTHONPATH=${PWD} trial test_server

10 Deployments
10.1 twistd
twistd is an utility for running the Twisted code. twistd will manage facilities
like logging and running the service in daemonize mode.

10.2 Twisted Application Configuration (TAC)


This file is responsible for managing the twisted applicaiton and its state. This
file will be given as input to twistd application.

10.3 Example
We will consider the echo server example we observed earlier and try to wrap it
into tac file.

10.3.1 Echo server


\\Example echo_server.py
from twisted.internet import protocol, reactor

class EchoProtocol(protocol.Protocol):

def dataReceived(self, data):


self.transport.write(data)

class EchoFactory(protocol.Factory):

13
def buildProtocol(self, addr):
return EchoProtocol()

10.3.2 tac file


\\ echo_server.tac
import os
import sys

sys.path.append(os.path.abspath(os.path.dirname(__file__)))

from twisted.application import internet, service

from echo_app import EchoFactory

application = service.Application("echo")
echoService = internet.TCPServer(8000, EchoFactory())
echoService.setServiceParent(application)

10.3.3 Run
You can run the twisted application using below command

twistd -n -y echo_server.tac

References
[1] http://man7.org/linux/man-pages/man7/epoll.7.html
[2] http://man7.org/linux/man-pages/man2/poll.2.html
[3] https://buildbot.net/

[4] https://omegle.com
[5] https://www.twilio.com/
[6] https://www.twitch.tv/
[7] https://scrapy.org/

14

You might also like