Tornado framework tutorial

Tornado is widely used in Zhihu. When you open the web version of Zhihu with Chrome and use the developer tool to carefully observe the requests in the Network, you will find that there is a special request with a status code of 101. It uses the browser's websocket technology to establish a long connection with the back-end server to receive the notification messages actively pushed by the server. The backend server here uses the tornado server. Apart from providing websocket services, tornado server can also provide long connection services, HTTP short link services, UDP services, etc. Tornado server is open source by facebook and is widely used in the back end of handheld reading.

How to use such a powerful tornado framework? This article will lead readers to gradually and deeply learn how to use tornado as the basis of web server.

Hello, World

   
  1. import tornado.ioloop
  2. import tornado.web
  3. class MainHandler (tornado.web.RequestHandler):
  4. def get ( self ):
  5. self.write( "Hello, world")
  6. def make_app ():
  7. return tornado.web.Application([
  8. ( r"/", MainHandler),
  9. ])
  10. if __name__ == "__main__":
  11. app = make_app()
  12. app.listen( 8888)
  13. tornado.ioloop.IOLoop.current().start()

This is the official Hello, world instance, which executes Python hello Py, open the browser to access http://localhost:8888/ You can see the normal output of the server Hello, world.

An ordinary tornado web server usually consists of four components.

  1. The ioloop instance, which is a global tornado event loop, is the core engine of the server. In the example, tornado ioloop. IOLoop. Current () is the default tornado ioloop instance.
  2. App instance, which represents a completed back-end app. It will be connected to a server socket port to provide services. There can be multiple app instances in an ioloop instance. There is only one app instance in the example. In fact, multiple app instances can be allowed, but they are rarely used.
  3. handler class, which represents business logic. When we develop the server, we write a pile of handlers to serve client requests.
  4. Routing table, which connects the specified url rule with the handler to form a routing mapping table. When the request arrives, query the routing mapping table according to the requested access url to find the corresponding service handler.

The relationship between these four components is that an ioloop contains multiple apps (managing multiple service ports), an app contains a routing table, and a routing table contains multiple handlers. Ioloop is the core of the service engine. It is the engine, which is responsible for receiving and responding to client requests, driving the operation of business handlers, and executing scheduled tasks within the server.

When a request arrives, ioloop reads the request and unpacks it into an http request object, finds the routing table of the corresponding app on the socket, queries the handler attached in the routing table through the url of the request object, and then executes the handler. After the handler method is executed, it will generally return an object. Ioloop is responsible for packaging the object into an http response object and serializing it to the client.

The same ioloop instance runs in a single threaded environment.

Factorial service

Let's write a normal web server that will provide factorial services. That is to help us calculate n! Value of. The server will provide factorial cache, and the calculated ones will be saved. There is no need to recalculate next time. The advantage of using Python is that we don't have to be careful that the calculation result of factorial will overflow. Python integers can be infinite.

   
  1. # fact.py
  2. import tornado.ioloop
  3. import tornado.web
  4. class FactorialService ( object ): # Define a factorial service object
  5. def __init__ ( self ):
  6. self.cache = {} # Use a dictionary to record the factorials that have been calculated
  7. def calc ( self , n):
  8. if n in self.cache: # If there is a direct return
  9. return self.cache[n]
  10. s = 1
  11. for i in range( 1, n):
  12. s *= i
  13. self.cache[n] = s # Cache
  14. return s
  15. class FactorialHandler (tornado.web.RequestHandler):
  16. service = FactorialService() # new out factorial service object
  17. def get ( self ):
  18. n = int( self.get_argument( "n")) # Gets the parameter value of the url
  19. self.write( str( self.service.calc(n))) # Using factorial services
  20. def make_app ():
  21. return tornado.web.Application([
  22. ( r"/fact", FactorialHandler), # Register routing
  23. ])
  24. if __name__ == "__main__":
  25. app = make_app()
  26. app.listen( 8888)
  27. tornado.ioloop.IOLoop.current().start()

Execute Python fact Py, open the browser and type http://localhost:8888/fact?n=50 , you can see the browser output
6082818640342675608722521633212953768875528313792102400000000. If we do not provide the n parameter, visit http://localhost:8888/fact , you can see that the browser outputs 400: Bad Request, which tells you that the request is wrong, that is, one parameter is missing.

Using Redis
--
The above example is to store the cache in the local memory. If you change a port and a factorial service to access it through this new port, it needs to be recalculated for each n, because the local memory cannot be shared across processes and machines.

Therefore, in this example, we will use Redis to cache the calculation results, so as to completely avoid repeated calculation. In addition, instead of returning plain text, we will return a json, and add a field name in the response to say whether this calculation comes from the cache or the fact. In addition, we provide default parameters. If the client does not provide N, it defaults to n=1.

   
  1. import json
  2. import redis
  3. import tornado.ioloop
  4. import tornado.web
  5. class FactorialService ( object ):
  6. def __init__ ( self ):
  7. self.cache = redis.StrictRedis( "localhost", 6379) # The cache has been changed to redis
  8. self.key = "factorials"
  9. def calc ( self , n):
  10. s = self.cache.hget( self.key, str(n)) # Save the calculation results with hash structure
  11. if s:
  12. return int(s), True
  13. s = 1
  14. for i in range( 1, n):
  15. s *= i
  16. self.cache.hset( self.key, str(n), str(s)) # Save results
  17. return s, False
  18. class FactorialHandler (tornado.web.RequestHandler):
  19. service = FactorialService()
  20. def get ( self ):
  21. n = int( self.get_argument( "n") or 1) # Parameter defaults
  22. fact, cached = self.service.calc(n)
  23. result = {
  24. "n": n,
  25. "fact": fact,
  26. "cached": cached
  27. }
  28. self.set_header( "Content-Type", "application/json; charset=UTF-8")
  29. self.write(json.dumps(result))
  30. def make_app ():
  31. return tornado.web.Application([
  32. ( r"/fact", FactorialHandler),
  33. ])
  34. if __name__ == "__main__":
  35. app = make_app()
  36. app.listen( 8888)
  37. tornado.ioloop.IOLoop.current().start()

When we visit again http://localhost:8888/fact?n=50 , you can see the browser output as follows
{"cached": false, "fact": 608281864034267560872252163321295376887552831379210240000000000, "n": 50}
, refresh again. The browser outputs {"cached": true, "fact": 6082818640342675608722521633212953768875528313792102400000000, "n": 50}. You can see that the cached field is programmed from true to false, indicating that the cache has indeed saved the calculation results. Let's restart the process,
Visit the connection again and observe the browser output. You can find that the cached result is still equal to true. This indicates that the cache result is no longer stored in local memory.

PI calculation service
--
Next, we will add a service to calculate the PI. There are many formulas for calculating the PI. We use it as the simplest one.

We provide a parameter n in the service as the accuracy index of PI. The larger n is, the more accurate the PI calculation is. Similarly, we also cache the calculation results in Redis server to avoid repeated calculation.

   
  1. # pi.py
  2. import json
  3. import math
  4. import redis
  5. import tornado.ioloop
  6. import tornado.web
  7. class FactorialService ( object ):
  8. def __init__ ( self , cache):
  9. self.cache = cache
  10. self.key = "factorials"
  11. def calc ( self , n):
  12. s = self.cache.hget( self.key, str(n))
  13. if s:
  14. return int(s), True
  15. s = 1
  16. for i in range( 1, n):
  17. s *= i
  18. self.cache.hset( self.key, str(n), str(s))
  19. return s, False
  20. class PiService ( object ):
  21. def __init__ ( self , cache):
  22. self.cache = cache
  23. self.key = "pis"
  24. def calc ( self , n):
  25. s = self.cache.hget( self.key, str(n))
  26. if s:
  27. return float(s), True
  28. s = 0.0
  29. for i in range(n):
  30. s += 1.0/( 2*i+ 1)/( 2*i+ 1)
  31. s = math.sqrt(s* 8)
  32. self.cache.hset( self.key, str(n), str(s))
  33. return s, False
  34. class FactorialHandler (tornado.web.RequestHandler):
  35. def initialize ( self , factorial):
  36. self.factorial = factorial
  37. def get ( self ):
  38. n = int( self.get_argument( "n") or 1)
  39. fact, cached = self.factorial.calc(n)
  40. result = {
  41. "n": n,
  42. "fact": fact,
  43. "cached": cached
  44. }
  45. self.set_header( "Content-Type", "application/json; charset=UTF-8")
  46. self.write(json.dumps(result))
  47. class PiHandler (tornado.web.RequestHandler):
  48. def initialize ( self , pi):
  49. self.pi = pi
  50. def get ( self ):
  51. n = int( self.get_argument( "n") or 1)
  52. pi, cached = self.pi.calc(n)
  53. result = {
  54. "n": n,
  55. "pi": pi,
  56. "cached": cached
  57. }
  58. self.set_header( "Content-Type", "application/json; charset=UTF-8")
  59. self.write(json.dumps(result))
  60. def make_app ():
  61. cache = redis.StrictRedis( "localhost", 6379)
  62. factorial = FactorialService(cache)
  63. pi = PiService(cache)
  64. return tornado.web.Application([
  65. ( r"/fact", FactorialHandler, { "factorial": factorial}),
  66. ( r"/pi", PiHandler, { "pi": pi}),
  67. ])
  68. if __name__ == "__main__":
  69. app = make_app()
  70. app.listen( 8888)
  71. tornado.ioloop.IOLoop.current().start()

Because both handlers need redis, we extract redis separately and pass it in through parameters. In addition, the Handler can pass parameters through the initialize function. When registering the route, it can pass any parameters by providing a dictionary. The key of the dictionary should correspond to the parameter name. We run Python PI Py, open the browser to access http://localhost:8888/pi?n=200 , you can see the browser output {"cached": false, "pi": 3.1412743276, "n": 1000}, which is very close to the PI.

More Python videos, source codes and materials are available for free

Reprinted to: https://zhuanlan.zhihu.com/p/37382503

Keywords: Python Tornado

Added by xwin on Sun, 16 Jan 2022 09:34:39 +0200