29 | Practical combat (4): How to design a "drawing" program?

Today we continue with our drawing process. After completing the previous lecture, our drawing program is not only functional, but also supports offline editing and storage.

Today we start thinking about the server side.

Where do we start?

The first step we have to consider is the network protocol.

Network protocol

For simplicity, we will not consider the scenario of multi-tenancy with authorization for the time being. We will continue to practice this drawing program in the next chapter of server development and transform it into a multi-tenant.

In the browser, one browser page edits one document, and different pages edit different documents. So in our browser-side dom.js, you can see that our DOM model is a single-document design.

But obviously, the server side and the browser side are different. Even if there is no multi-tenancy, multiple documents cannot run. We might as well call QPaint's document drawing, so the functions of the server are basically the following:

Create a new drawing document;

Get drawing document;

Delete drawing document;

Create a new shape in the drawing document;

Get a shape in the drawing document;

Modify a shape in the drawing document, including moving the position and modifying the graphic style;

Modify the zorder order of a shape in the drawing document (not implemented on the browser side);

Delete a shape from the drawing document.

The complete network protocol is shown in the table below:

Where <Shape> is like this:

"path": {
    "points": [
        {"x": <X>, "y": <Y>},
        ...
    ],
    "close": <Boolean>,
    "style": <ShapeStyle>
}

or:

"line": {
    "pt1": {"x": <X>, "y": <Y>},
    "pt2": {"x": <X>, "y": <Y>},
    "style": <ShapeStyle>
}

or:

"rect": {
    "x": <X>,
    "y": <Y>,
    "width": <Width>,
    "height": <Height>,
    "style": <ShapeStyle>
}

or:

"ellipse": {
    "x": <X>,
    "y": <Y>,
    "radiusX": <RadiusX>,
    "radiusY": <RadiusY>,
    "style": <ShapeStyle>
}

Where <ShapeStyle> is like this:

{
    "lineWidth": <Width>,  // 线宽
    "lineColor": <Color>,  // 线型颜色
    "fillColor": <Color>   // 填充色
}

Among them, the possible values ​​of <ZorderOperation> are:

"top": Go to the top

"bottom": to the bottom

"front": One level forward

"back": one level back

Overall, this set of network protocols reflects its corresponding functional meanings relatively straightforwardly. We follow this set of network protocol definition paradigms:

Create objects: POST /objects

Modify object: POST /objects/<ObjectID>

Delete an object: DELETE /objects/<ObjectID>

Query objects: GET /objects/<ObjectID>

In fact, there is another list object, but we don’t use it here:

List all objects: GET /objects

List matching objects: GET /objects?key=value

In addition, there is another point that needs special attention when designing the network: friendliness towards retries.

Why do we have to give full consideration to retry friendliness? Because the network is unstable. This means that when a network request fails, in some scenarios you may not be able to determine the true status of the request.

In a small probability, the server may have performed the expected operation, but there was a network problem when returning it to the client. When you retry, you think it's just a retry, but in fact the same operation is performed twice.

The so-called retry friendliness means that if the same operation is executed twice, the execution result will be the same as if it was executed only once.

Read-only operations, such as querying objects or listing objects, are obviously retry-friendly.

Creating objects (POST /objects) is often implemented in a retry-unfriendly manner. If executed twice, two objects will be created. Let’s compare the difference between creating a new drawing and creating a new shape here:

POST /drawings
POST /drawings/<DrawingID>/shapes
Content-Type: application/json

{
    "id": <ShapeID>,
    <Shape>
}

As you can see, the ShapeID is passed in when creating a new shape, which means that the ShapeID is assigned by the client (browser). The advantage of this is that if the server has already created the object last time, it can return an error that the object already exists (we use status = 409 conflict to represent it).

No parameters are passed in when creating a new drawing, so no conflicts will occur. Repeated calls will create two new drawings.

Through the above analysis, we can think that creating a new shape is retry-friendly, but creating a drawing is not retry-friendly. So how to solve this problem? There are several possibilities:

The client passes the id (same as creating a new shape above);

Client passes name;

The client passes uuid.

Of course, the essential differences between these three methods are not big. For example, if the client passes a name, and if the name is also used for reference in other subsequent operations, then the name is essentially the id.

Passing uuid can be considered as a general retry-friendly modification method. Here uuid has no actual meaning. You can understand it as the unique serial number of drawing or the unique serial number of network request. Of course, the performance of these two different network protocols will be slightly different, as follows:

POST /drawings
Content-Type: application/json

{
    "uuid": <DrawingUUID>
}
POST /drawings
Content-Type: application/json
X-Req-Uuid: <RequestUUID>

Modifying objects and deleting objects is often easier to do retry-friendly. But this is not absolute. For example, in our example "the order of modifying shapes", its network protocol is as follows:

POST /drawings/<DrawingID>/shapes/<ShapeID>
Content-Type: application/json

{
    "zorder": <ZorderOperation>
}

Among them, the possible values ​​of <ZorderOperation> are:

"top": Go to the top

"bottom": to the bottom

"front": One level forward

"back": one level back

When ZorderOperation is "front" or "back", executing it twice will cause the shape to move forward (or backward) 2 levels.

How to adjust?

There are two ways. One way is to express modification operations as absolute values ​​rather than relative values. For example, ZorderOperation of "front" or "back" is a relative value, but Zorder = 5 is an absolute value.

Another method is universal, which is to use the requested serial number (RequestUUID). This method has been used to create a new drawing above. It can also be used here:

POST /drawings/<DrawingID>/shapes/<ShapeID>
Content-Type: application/json
X-Req-Uuid: <RequestUUID>

{
    "zorder": <ZorderOperation>
}

Of course, there is an additional cost to using the request sequence number, because it means that the server must record all the request sequence numbers (RequestUUID) that have been successfully executed recently. When receiving a request with a request sequence number, check the sequence number. Whether the request has been successfully executed, a conflict will be reported if it has been executed.

In the design of network protocols, there is another business-related detail worth mentioning.

If you are careful, you may notice that the json representation of our Shape is different in the network protocol and localStorage storage format. In network protocols it is:

{
    "id": <ShapeID>,
    "path": {
        "points": [
            {"x": <X>, "y": <Y>},
            ...
        ],
        "close": <Boolean>,
        "style": <ShapeStyle>
    }  
}

And in localStorage is:

{
    "type": "path",
    "id": <ShapeID>,
    "points": [
        {"x": <X>, "y": <Y>},
        ...
    ],
    "close": <Boolean>,
    "style": <ShapeStyle>
}

From the perspective of Schema design of structured data, the implementation in localStorage is Schema-less and too arbitrary. This is because localStorage is only a local cache and has a relatively small impact, so we chose a mode that is as convenient as possible. In the future, network protocols may serve as open APIs for businesses and need to be treated with rigor.

version upgrade

In addition, this drawing program is just a DEMO program after all, so some common network protocol issues are not taken into consideration.

For example, from a longer-term perspective, network protocols often involve protocol version management issues. The network protocol is a set of open API interfaces. Once released, it is difficult to take it back. The compatibility of the protocol needs to be considered.

In order to facilitate future protocol upgrades, many network protocols will carry version numbers. for example:

POST /v1/objects
POST /v1/objects/<ObjectID>
DELETE /v1/objects/<ObjectID>
GET /v1/objects/<ObjectID>
GET /v1/objects?key=value

When incompatible changes occur in the protocol, we will tend to upgrade the version, such as upgrading to v2:

POST /v2/objects
POST /v2/objects/<ObjectID>
DELETE /v2/objects/<ObjectID>
GET /v2/objects/<ObjectID>
GET /v2/objects?key=value

This has some benefits:

The traffic of the old version can be gradually offline, allowing the two versions of the protocol to coexist for a period of time;

The new and old versions of the business servers can be independent of each other, and the front end is dispatched by nginx or other application gateways.

first implementation version

After talking about network protocols, we will start to consider the implementation of the server side. There are several possibilities when choosing what to do with your first implementation.

The first one is, of course, the conventional hold-back-ultimate-move mode. Directly do business architecture design, architecture review, coding, testing, and finally go online.

The second method is to make a Mock version of the server program.

What's the difference between the two?

The difference is that from the perspective of architectural design, server-side programs have many non-business-related general issues, such as high reliability and high availability.

High reliability means that data cannot be lost. Even if the server's hard drive breaks down, the data cannot be lost. This is nothing. Many services even require that even if there is a large-scale accident such as an earthquake at the computer room level, there should be no data loss.

High availability means that the service cannot have a single point of failure. If any one or even several servers are down, users must still be able to access them normally. Some services, such as Alipay, even require remote active-active across computer rooms. When one computer room fails, the entire business cannot be interrupted.

Without good infrastructure, it is not easy to make a good server program. So another option is to make a Mock version of the server program first.

Doesn't this increase the workload? has no meaning?

One is to allow teams to work in parallel. The basis for collaboration among different teams is the network protocol. A quickly built minimal version of the Mock server, which allows the front end to not wait for the back end. The backend can easily and conveniently conduct unit tests on network protocols, achieving high test coverage to ensure quality, and the progress is not affected by the frontend.

The second is to allow business logic to be connected in series as quickly as possible and quickly verify the validity of network protocols. If you find that the network protocol does not meet business needs during the process, you can adjust it in time.

So our first version of the server program is the Mock version. The Mock version does not need to consider too many issues in the server field. Its core value is serial business. Therefore, the Mock version of the server does not even need to rely on a database. All business logic can be directly based on the data structure in the memory.

code show as below:

https://github.com/qiniu/qpaint/tree/v29/paintdom

We will continue to complete the server side of the official layout drawing program in the actual combat of the server side development chapter later.

From an architectural perspective, this paintdom program is divided into two layers: Model layer and Controller layer.

Let's look at the Model layer first. Its source code is:

paintdom/shape.go

paintdom/drawing.go

The Model layer has nothing to do with the network and only contains pure core business logic. It implements a multi-document version of the drawing program. The logical structure is also a DOM tree, but it has one more layer than the browser:

Document => Drawing => Shape => ShapeStyle

QPaintDoc on the browser side corresponds to the Drawing here, not the Document here.

Let's take a look at the Controller layer again. Its source code is:

paintdom/service.go

The Controller layer implements network protocols. You may wonder why I regard the network protocol layer as the Controller layer, so where does the View layer go in MVC.

First of all, server-side programs do not need a display module in most cases, so there is no View layer. Why the network protocol layer can be regarded as the Controller layer is because it is responsible for accepting user input. It's just that user input is not the user interaction we understand every day, but an API request from an automation control (Automation) program.

Although the implementation of this paintdom program, there are some Go language-related knowledge points that are worth talking about, especially the parts related to network protocol implementation. However, I will not expand on it here. Interested students can learn the Go language on their own.

Generally speaking, the parts related to business logic are relatively easy to understand, so we won’t go into details here.

Conclusion

Today we focus on the network protocol of the "Paint" program and give some considerations for the design of conventional network protocols. The status of the network protocol is very critical. It is the user interface for coupling the front and back ends of a B/S or C/S program, and therefore is also a key point that affects the team's development efficiency.

How to stabilize network protocols early? How to enable front-end programmers to jointly debug with the server as early as possible? These are the areas we should focus on.

After clearly defining the network protocol, we gave the first server-side implementation version of the paintdom program that satisfies the network protocol we defined, which is used to connect business logic. This implementation version is a Mock program, which only focuses on business logic and does not care about the inherent high reliability and high availability requirements of the server program. We will continue to iterate on it in the next chapter of server-side development.

In the next lecture, we will connect this paintdom server program with our paintweb drawing program.

Guess you like

Origin blog.csdn.net/qq_37756660/article/details/134971530