كتابة الـ RESP

في المقالة اللي فاتت احنا كتبنا اول جزء من الـ RESP وهو ال Parsing والمسؤول عن تحويل الـ RESP الى الـ Value Object نقدر نعرف منه الـ Client بعت اي commands. في الجزء ده هنشوف ازاي ممكن نرد على الـ Client بـ RESP ونكتب الـ Writer.

بما اننا بنستخدم ال Value علشان نحول الـ RESP ل Go struct نقدر نفهم منه ايه المطلوب .. هنستخدم بردو الـ Value علشان نسجل فيه الرد اللي احنا هنرد بيه علي الـclient سواء String, Bulk or Array.

ف اول خطوة محتاجين نعملها اننا نكتب الـ Marshal اللي هيحول الـ Value لـ bytes عباره عن RESP response.

كتابة الـ Value Serializer

  • هنكتب الاول ميثود Marshal و اللي منها هنكلم الميثود الخاصه بكل type حسب نوع الـ Value
func (v Value) Marshal() []byte {
	switch v.typ {
	case "array":
		return v.marshalArray()
	case "bulk":
		return v.marshalBulk()
	case "string":
		return v.marshalString()
	case "null":
		return v.marshallNull()
	case "error":
		return v.marshallError()
	default:
		return []byte{}
	}
}

كالعاده، هنشوف الtyp نوعها ايه و منها هنكلم الميثود اللي بتتعامل مع النوع ده

Simple Strings

اول type هتكون الـ Simple String لانها سهله

func (v Value) marshalString() []byte {
	var bytes []byte
	bytes = append(bytes, STRING)
	bytes = append(bytes, v.str...)
	bytes = append(bytes, '\r', '\n')

	return bytes
}

بكل بساطه بنعمل byte array و بنضيف الـ String ثم الـ CRLF. وخلي بالك من غير الـ CRLF هيحصل مشكله لان RESP client مش هيفهم الريسبونس من غيره.

الـ Bulk String

func (v Value) marshalBulk() []byte {
	var bytes []byte
	bytes = append(bytes, BULK)
	bytes = append(bytes, strconv.Itoa(len(v.bulk))...)
	bytes = append(bytes, '\r', '\n')
	bytes = append(bytes, v.bulk...)
	bytes = append(bytes, '\r', '\n')

	return bytes
}

الـ Array

func (v Value) marshalArray() []byte {
	len := len(v.array)
	var bytes []byte
	bytes = append(bytes, ARRAY)
	bytes = append(bytes, strconv.Itoa(len)...)
	bytes = append(bytes, '\r', '\n')

	for i := 0; i < len; i++ {
		bytes = append(bytes, v.array[i].Marshal()...)
	}

	return bytes
}

لاحظ ان فيه حاله الـ Array احنا جوا ال loop بنكلم Marshal method علي ال Value Object علشان نحوله ايا كان نوعه ايه. وهو ده المقصود بالـ Recursion اللي ذكرناه في الجزء الاول واحنا بنعمل Parse.

الـ Null و الـ Error

واخيرا محتاجين الـ Null و الـ Error علشان لو هنرد علي الـ Client بان البيانات مش موجوده او فيه error.

func (v Value) marshallError() []byte {
	var bytes []byte
	bytes = append(bytes, ERROR)
	bytes = append(bytes, v.str...)
	bytes = append(bytes, '\r', '\n')

	return bytes
}

func (v Value) marshallNull() []byte {
	return []byte("$-1\r\n")
}

الـ Resp.go

وبكدا يبقي ملف الـ resp.go ده الجزء الزياده بالكامل اللي كتبناه

// Marshal Value to bytes
func (v Value) Marshal() []byte {
	switch v.typ {
	case "array":
		return v.marshalArray()
	case "bulk":
		return v.marshalBulk()
	case "string":
		return v.marshalString()
	case "null":
		return v.marshallNull()
	case "error":
		return v.marshallError()
	default:
		return []byte{}
	}
}

func (v Value) marshalString() []byte {
	var bytes []byte
	bytes = append(bytes, STRING)
	bytes = append(bytes, v.str...)
	bytes = append(bytes, '\r', '\n')

	return bytes
}

func (v Value) marshalBulk() []byte {
	var bytes []byte
	bytes = append(bytes, BULK)
	bytes = append(bytes, strconv.Itoa(len(v.bulk))...)
	bytes = append(bytes, '\r', '\n')
	bytes = append(bytes, v.bulk...)
	bytes = append(bytes, '\r', '\n')

	return bytes
}

func (v Value) marshalArray() []byte {
	len := len(v.array)
	var bytes []byte
	bytes = append(bytes, ARRAY)
	bytes = append(bytes, strconv.Itoa(len)...)
	bytes = append(bytes, '\r', '\n')

	for i := 0; i < len; i++ {
		bytes = append(bytes, v.array[i].Marshal()...)
	}

	return bytes
}

func (v Value) marshallError() []byte {
	var bytes []byte
	bytes = append(bytes, ERROR)
	bytes = append(bytes, v.str...)
	bytes = append(bytes, '\r', '\n')

	return bytes
}

func (v Value) marshallNull() []byte {
	return []byte("$-1\r\n")
}

كدا احنا خلصنا 90% من الـ Writer و مش فاضل بس غير اننا نرد بيه علي الـ Client من الـ IO. ولكن خلينا الاول ناخد امثله علي الكود بتاعنا ده و نشوف هيشتغل ازاي و نشوف الـ Output.

امثله علي الـ Marshal

Simple String

لو هنرد مثلا بـ String Ok

v := Value{typ: "string", str: "Ok"}
fmt.Println(string(v.Marshal()))

شكل الريسبونس هيبقي كدا

+Ok\r\n

Bulk String

لو هنرد بـ Bulk String

v := Value{typ: "bulk", bulk: "Hello World"}
fmt.Println(string(v.Marshal()))

شكل الريسبونس هيبقي كدا

$11\r\nHello World\r\n

Array

لو هنرد بـ Array

v := Value{typ: "array", array: []Value{
	Value{typ: "string", str: "Hello"},
	Value{typ: "string", str: "World"},
}}
fmt.Println(string(v.Marshal()))

شكل الريسبونس هيبقي كدا

*2\r\n$5\r\nHello\r\n$5\r\nWorld\r\n

وبكدا يبقي احنا جاهزين بس نكتب الـ bytes اللي بناخدها من الـ Marshal method للـ Writer.

كتابة الـ Writer

و علشان نعمل writer ف الموضوع بسيط خالص .. كل اللي محتاجينه Writer struct و بياخد io.Writer

type Writer struct {
	writer io.Writer
}

func NewWriter(w io.Writer) *Writer {
	return &Writer{writer: w}
}

وبعدين بنعمل method بتاخد Value و بتكتب الـ bytes اللي بتاخدها من Marshal method علي الـ Writer

func (w *Writer) Write(v Value) error {
	var bytes = v.Marshal()

	_, err := w.writer.Write(bytes)
	if err != nil {
		return err
	}

	return nil
}

وعلشان نستخدمه علشان مثلا نرد بـ Ok علي Redis commands كل اللي محتاجنيه هيكون

writer := NewWriter(conn)
writer.Write(Value{typ: "string", str: "OK"})

الخاتمه

كدا احنا خلصنا الـ RESP serialize and deserialize و عملنا الReader والـ Writer اللي هيساعدونا بكل سهوله نرد علي الـ Client

اخيرا هحتاج نغير main.go و نخليها ترد عن طريق الـ Writer

package main

import (
	"fmt"
	"net"
)

func main() {
	fmt.Println("Listening on port :6379")

	// Create a new server
	l, err := net.Listen("tcp", ":6379")
	if err != nil {
		fmt.Println(err)
		return
	}

	// Listen for connections
	conn, err := l.Accept()
	if err != nil {
		fmt.Println(err)
		return
	}

	defer conn.Close()

	for {
		resp := NewResp(conn)
		value, err := resp.Read()
		if err != nil {
			fmt.Println(err)
			return
		}

		_ = value

		writer := NewWriter(conn)
		writer.Write(Value{typ: "string", str: "OK"})
	}
}

cli output

الجزء اللي جاي ان شاء الله، هنبدا نرد علي كوماند زي PING, SET, GET, HSET, HGET, and HGETALL و نشوف ازاي ممكن نعملهم implement في Go.