The core of this chapter

  • What is theFull duplexAgreement, andHalf duplexWhat is the essential difference?
  • How to implement full-duplex protocolSynchronous grammarCall?
    • That is, send a message to the server via websocket and wait for its result. As shown below:

At the end of the article there are Go server and Web Typescript source ~

Suggest you see the officer first point to add collection, convenient later search ~

background

In many scenarios, the front-end uses Websocket to implement some long-link related functionality. However, on the Web side, the WebSocket interface sends and receives messages in two different methods, as follows:

  • WebSocket.onmessage
    • An event listener to be called when a message is received from the server.
  • WebSocket.send(data)
    • Enqueues data to be transmitted.

【 Websocket – Web APIs | MDN 】

So if you want to implement a request/response pattern above, for the sender, one way to call might look like this:

let conn = new w3cwebsocket(url)
conn.binaryType = "arraybuffer"
conn.onopen = () = > {
    console.info("websocket open - readyState:", conn.readyState)
    if (conn.readyState === w3cwebsocket.OPEN) {
        let req = JSON.stringify({ "seq": 1."msg": "hello world"}) conn.send(req) <-- request conn.onclose =(e: ICloseEvent) = > {
    console.debug("event[onclose] fired")
}

conn.onmessage = (evt: IMessageEvent) = > {
    let resp = JSON.parse(<string>evt.data)
    console.info(resp) <-- response}Copy the code

Request and response separate coding logic, uncomfortable ~~

So, here’s how to solve this problem. However, before the coding is implemented, let me introduce the key point: the difference between full-duplex and half-duplex communication.

Full duplex

  • Full-duplex: Two parties can simultaneously send messages to each other.
  • Half-duplex: Two-way communication is supported, but only one direction is transmitting information at a time.

Just understand the basic concepts, which can mean different things at different levels.

As we all know, Websocket and HTTP1. x are based on TCP/IP, and TCP is also a full-duplex communication protocol. Websocket is a full-duplex communication protocol, while HTTP1. x is a half-duplex protocol. And Websocket is an upgrade based on Http protocol.

Isn’t http1.x your own

The http1.x protocol is a request-response pattern. In a Request, the Respose must occur after the Request, and the Request and response packets cannot be transmitted over the network at the same time:

This is why http1.x is a half-duplex protocol.

From a coding point of view, the code is easy to write because requests and returns are sequential, with pseudo-logic as follows:

function request(req){
    conn.send(req)
    let resp = conn.read()
    return resp
}
Copy the code

However, in a full-duplex communication, there is no clear order and correlation between messages. The diagram below:

Although Websocket is a full-duplex communication protocol, there is no association information between different messages in its protocol. When the Web end receives a message, it cannot distinguish who is with whom. Therefore, we can define a simple upper-level business protocol as follows:

attribute type instructions
Sequence plastic Message number
Type The enumeration 1: request /2: response /3: notification
Message string The message content

This logically makes it possible to tag each message, as shown in the picture above:

format The message
Sequence: 1,Type: 1,Message: hello Request1
Sequence: 2,Type: 1,Message: world Request2
Sequence: 1,Type: 2,Message: ok Response1
Sequence: 2,Type: 2,Message: ok Response2
Sequence: 1,Type: 1,Message: test Request3
Sequence: 1,Type: 3,Message: test Notify3

This gives us the logic we want to associate a request with a response using the Sequence and Type attributes.

The Sequence increments when the client generates a message.

In actual combat

Typescript code

Step 1: Define relevant objects:

  • Message: indicates a service protocol
  • Request: Simulates Request caching
  • Response: Simulate the Response
export class Message {
    sequence: number = 0;
    type: number = 1; message? :string;
    from? :string; // sender
    constructor(message? :string) {
        this.message = message;
        this.sequence = seq.next ()export class Request {
    sendTime: number
    callback: (response: Message) = > void
    constructor(callback: (response: Message) => void) {
        this.sendTime = Date.now()
        this.callback = callback
    }
}

export class Response {
    success: boolean = falsemessage? : Messageconstructor(success: boolean, message? : Message) {
        this.success = success;
        this.message = message; }}Copy the code

Note that there is a callback method in the Request, which is key to implementing synchronous calls.

Step 2: Create the WebsocketClient object, create a Map called SendQ to hold the request, and implement a request method:

export class WebsocketClient {
    private sendq = new Map<number, Request>() <-- create mapasync request(data: Message): Promise<Response> {
        return new Promise((resolve, _) = > {
            let seq = data.sequence

            // asynchronous wait ack from server
            let callback = (msg: Message) = >// remove from sendq this.sendq.delete(seq) resolve(new Response(true, MSG))} this.sendq.set(seq, seq) New Request(callback)) <-- Request if (! This.send (json.stringify (data))) {<-- send a message resolve(new Response(false)}})} send(data: string): boolean { try { if (this.conn == null) { return false } this.conn.send(data) } catch (error) { return false } return true }Copy the code

If the Http request and response association is based on the order, then the core of the full-duplex request and response association is the sendq Map object, which is equivalent to the client side cache all the requests waiting for the response, which is a bit difficult to say.

In this request method, there are three main steps of logic:

  1. Create a callback
  2. The temporary Request
  3. Send a message

Step 3: Receive message processing, which is in the login method:

async login(): PromiseThe < {success: boolean} > {if (this.state == State.CONNECTED) {
        return { success: false}}this.state = State.CONNECTING
    return new Promise((resolve, _) = > {
        let conn = new w3cwebsocket(this.wsurl)
        conn.binaryType = "arraybuffer"
        let returned = false
        conn.onopen = () = > {
            console.info("websocket open - readyState:", conn.readyState)
            if (conn.readyState === w3cwebsocket.OPEN) {
                returned = true
                resolve({ success: true}}})// overwrite onmessage
        conn.onmessage = (evt: IMessageEvent) = > {
            try {
                let msg = new Message();
                Object.assign(msg, JSON.parse(<string>evt.data))
                if (msg.type == 2) {
                    let req = this.sendq.get(msg.sequence) <---- Reads requestif(req) {req.callback(MSG) <---- trigger callback}}else if (msg.type == 3) {
                    console.log(msg.message, msg.from)
                }
            } catch (error) {
                console.error(evt.data, error)
            }
        }

        conn.onerror = (error) = > {
            console.info("websocket error: ", error)
            if (returned) {
                resolve({ success: false })
            }
        }

        conn.onclose = (e: ICloseEvent) = > {
            console.debug("event[onclose] fired")
            this.onclose(e.reason)
        }
        this.conn = conn
        this.state = State.CONNECTED
    })
}
Copy the code

As you can see, there is no uniform order in sending and receiving messages in full duplex. Therefore, after parsing the Message object here, we will determine its type. If it is a Response Message, we will look for the Request Request in Sendq and call the callback method.

Go server code logic:

A message broadcast is mainly implemented on the server, and a response message is sent to the sender after completion. The main logic is in the Handle method, which is not detailed here. If you are interested, you can read the source code directly.

type Message struct {
	Sequence int    `json:"sequence,omitempty"`
	Type     int    `json:"type,omitempty"`
	Message  string `json:"message,omitempty"`
	From     string `json:"from,omitempty"`
}

func (m *Message) MarshalJSON(a) []byte {
	bts, _ := json.Marshal(m)
	return bts
}

func parseMessage(text string) *Message {
	var msg Message
	_ = json.Unmarshal([]byte(text), &msg)
	return &msg
}

// Broadcast a message
func (s *Server) handle(user string, text string) {
	logrus.Infof("recv message %s from %s", text, user)
	s.Lock()
	defer s.Unlock()
	msg := parseMessage(text)
	msg.From = user
	msg.Type = 3 //notify type
	notice := msg.MarshalJSON()
	for u, conn := range s.users {
		if u == user {
			continue
		}
		logrus.Infof("send to %s : %s", u, text)
		err := s.writeText(conn, notice)
		iferr ! =nil {
			logrus.Errorf("write to %s failed, error: %v", user, err)}} conn := s.sers [user] resp := Message{<-- create response package Sequence: msg.Sequence, <-- the Sequence number must be the same as in the request package Type:2.//response type
		Message:  "ok",
	}
	_ = s.writeText(conn, resp.MarshalJSON())
}

func (s *Server) writeText(conn net.Conn, message []byte) error {
	// Create text frame data
	f := ws.NewTextFrame(message)
	err := conn.SetWriteDeadline(time.Now().Add(s.options.writewait))
	iferr ! =nil {
		return err
	}
	return ws.WriteFrame(conn, f)
}
Copy the code

Demo sample

Finally, we get the result we want:

let resp = await cli.request(req)

// index.ts
const main = async() = > {let cli = new WebsocketClient("ws://localhost:8000"."ccc");
    let { success } = await cli.login()
    console.log("client login return -- ", success)

    let req = new Message("hello")
    let resp = await cli.request(req)
    console.log("client request", req, "return", resp.message)

    await sleep(5)
    cli.logout()
}

main()
Copy the code

The following output is displayed:

$ ts-node index.ts
websocket open - readyState: 1
client login return --  true
client request Message { sequence: 1, type: 1, message: 'hello' } return Message { sequence: 1, type: 2, message: 'ok' }
event[onclose] fired
connection closed due to Normal connection closure
Copy the code

The final summary

This paper introduces the concept and essential difference between full duplex and half duplex. At the same time, the synchronous call logic of request and response under full duplex communication is completed through business protocol and Promise.

Github source code: Klintcheng/Demo

Finally, the most wonderful knowledge is in this small book I wrote distributed IM principles and practice: building the INSTANT messaging cloud from zero to one.

Look at all of this, but not just a “like”.