2. Starting with the request
Requests and responses
When we do network programming, we always use request and response . For example, if process A needs to pass some data to process B, we can say that process A sent a request to process B. Sometimes a request is sent and we don’t care about the rest; but more often, we care about the result of the processing of the data by the target process, and we want to process the result further. In this case, we can ask the target process to send a request to the source process and pass the result to the source process after it has received the sent data and processed it accordingly. We call this behavior a response.
It is not enough to have a request and a response. Consider the following scenario:
Since the process of sending the request and the process of processing the response are located in two different methods, the context generated by the process of sending the request is not available when processing the response.
To solve this problem, we propose the concept of session. We believe that each request must correspond to a response, and a session id is used to uniquely identify a conversation, and the session id is passed between the request and the response. If some context is used in processing the response, it can be stored with the session id as the primary key, and the required context is retrieved by the session id during response processing. during response processing.
I wrote a simple example below, where the
send method sends data to the remote process, the two arguments are the remote process handle and the data; when the process receives the data it calls back the
on_receive_message method, the two arguments are the sender process handle and the data;
request_handler is the request handler function, all request_handler` is the request handler function, which will be called for all requests.
send_request function takes three parameters: the remote process handle, the request data and the callback function. Each call to
send_request generates a session id, and stores the callback function with the session id as the primary key. When a response is received, the session id is used to find the callback function, and the callback function is called. This completes a session.
In this example, we use the js feature of using the callback function as the context. The real context, the
ctx variable, is stored implicitly as the upper value of the callback function.
3. Introducing the problem
With a session, we can easily send requests and responses. For example, we can send a request and receive a response like this:
The target process can then receive the request and return the response:
However, there are often some inconveniences in using callback functions in this way. What if you need to send a request to another process in the request handler and get the return value and then respond?
We cannot return
on_receive_message as the return value of
request_handler, and we cannot pass the response of the request. To solve this problem, we can make some changes:
Instead of passing the response by the return value, a callback function is now passed with an additional parameter, and the response is passed by calling the callback function. This solves the problem.
However, this is still not very convenient, especially when multiple calls are involved, and the callback function needs to be passed through the parameters. Also, in many cases, a remote call may not always succeed, and we want to be able to handle exceptions when errors occur. So in addition to callbacks, it is common to pass in a function called an exception callback to specifically handle exceptions. If both callbacks and exception callbacks are passed in layers, this makes the code difficult to maintain and the methods containing the remote calls difficult to encapsulate into a common library.
To solve this paradox, js introduced the concept of Promise. A “promise” is a promise that some action will be performed in the future. The biggest change is that it moves the passing of callback functions from parameters to return values. A Promise object means an unfinished job that is promised to be completed in the future, and expects a callback function. When the work is completed at some point in the future, the callback function is called. This approach solves the problem described above perfectly.
Let’s look at the basic usage:
Constructs a Promise object using a function. This function takes two arguments: a callback and an exception callback. We say that a Promise object means an unfinished job, so the callback function is called when the job completes, and the exception callback function is called when the job fails. After constructing a Promise object, call
Promise.prototype.then to set the callback function and call
Promise.prototype.catch to set the exception callback function.
We can call the
then method of a Promise object multiple times to set multiple callbacks. These callbacks form a chain of callbacks, and the return value of the previous function becomes the argument of the next function:
These callback functions can also return Promise objects, forming nested Promise calls:
The idea is the same, think of a Promise object as an unfinished task, and pass the result of the task when it completes by calling a callback function.
The above example uses Promise in this way:
Promise.resolve automatically determines if an object is a Promise object, and if it is, it waits for it to be ready before calling it back, or if it is not, it calls it back directly.
As you can see, there is no need to pass the callback function in the argument, but instead the caller returns the Promise object and sets the callback function from it after receiving the Promise object. Even if there are multiple calls, you only need to return the Promise object one after another. This solves this problem perfectly.
Another powerful feature of Promise is exception handling. As mentioned above, in addition to calling
Promise.prototype.then to set the callback function, you can also call
Promise.prototype.catch to set the exception callback. Both callbacks and exception callbacks form a callback chain. When an exception occurs, the exception callback will be executed instead; and if the exception callback is fault tolerant, the callback will be executed instead.
Promise.prototype.then supports both callbacks and exception callbacks, as shown above, the first argument is a callback and the second argument is an exception callback. If you uncomment
throw err, it will print
error occurred again: cannot be odd at the end.
I was first introduced to Deferred using Python’s twisted library. Deferred is actually available in jQuery, and they are practically the same. The essence and core idea of Deferred is the same as Promise, but with a different presentation.
Again, let’s start with a simple example:
You can see that Deferred and Promise are quite similar.
Deferred.addCallback is equivalent to
Deferred.addErrback is equivalent to
Promise.prototype.catch . The big difference is that instead of using a function constructor, Deferred calls
Deferred.errback directly to tell it that its work has completed or failed.
Deferred’s callback chain and nested Deferred are the same as Promise, so here’s an example of the same:
As an example, here I have used Deferred to implement
Deferred’s exception handling is basically the same as Promise, so I won’t go into it here. See the documentation for details.
I first came across Deferred when I was using Python twisted. At the time, I was using Python2, and there was no concurrency. It’s not as convenient as a concurrent process, but it solves the problem of repeated callbacks in server programming and improves maintainability of the code. Later, I came across Promise when I was using js, and found that they were similar. Of course, as technology evolves, these will slowly be replaced by concurrent processes. However, it is worth learning from their clever design ideas.