The core of this chapter
- What is the
Full duplex
Agreement, andHalf duplex
What is the essential difference? - How to implement full-duplex protocol
Synchronous grammar
Call?- 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:
- Create a callback
- The temporary Request
- 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”.