Learning a programming language while doing a project would be much easier than simply learning the syntax or taking long courses and making notes. So this is my attempt at learning Go while creating a redis clone.
Note: I'll ignore error handling in most of the cases while including code here(even if go compiler doesn't like it), however the full code with better error handling is in my repo: manu156/redisClone
Link to previous part: Part 1
Parsing Commands
In the last part, we implemented some hackish way of parsing command "ping". Let's make it right. My first attempt was to use Scanner from bufio something like this (code inspired from the functions in bufio):
scanner := bufio.NewScanner(conn)
splitter := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
// Scan until space, marking end of word.
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if '\r' == r {
rn, _ := utf8.DecodeRune(data[i+1:])
if '\n' == rn {
return i + width, data[start:i], nil
}
}
}
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
// Request more data.
return start, nil, nil
}
for {
// Set the split function for the scanning operation.
scanner.Split(splitter)
// Validate the input
for scanner.Scan() {
// first scan would be size of array. and next scans will scan command and it's arguments
fmt.Printf("%s\n", scanner.Text())
}
// ... more code
}
The problem with this is we may not be able to parse large arguments so I thought to use conn.read() directly instead of wrapping it in some interface, it won't be neat though.
Better Code
I have added a struct to maintain buffer
type DataBuffer struct {
Buffer []byte // Data read from client
Size int // Size of data read from client in bytes
ReadPointer int // Number of bytes read/processed
}
Now my handleConnection is something like this
func handleConnection(conn net.Conn) {
// defer clouser of connection
lastProcessed := true
dataBuffer := DataBuffer{Buffer: make([]byte, BufferSize)}
for {
if lastProcessed {
// read new data into buffer
// ... code
}
// parse the command
commandArray, err := readCommand(conn, &dataBuffer)
if err {
return
}
// proccess and reply to client
if processCommand(conn, commandArray) {
return
}
if dataBuffer.ReadPointer < dataBuffer.Size {
// If some data is left in buffer probably another command then process it in next iteration
lastProcessed = false
// ... more code
}
}
}
And for readCommand, I really wanted to use the size provided in front of each datatype in parsing. I tried to read the size of command/argument first then I can read atleast this "size" amount of data to completely obtain next argument. (MaxReadIterations is just for a safety-check so that we never end up in a loop because of some error)
func readCommand(conn net.Conn, dataBuffer *DataBuffer) ([]string, bool) {
// get the size of array
arraySize, err := getDataSize(conn, dataBuffer)
if err || 0 == arraySize || arraySize > MaxArgumentSize {
return nil, true
}
cmdArray := make([]string, arraySize)
for arrayCounter := 0; arrayCounter < arraySize; arrayCounter++ {
// get the size of argument
argSize, getDataSizeErr := getDataSize(conn, dataBuffer)
if getDataSizeErr || 0 == argSize {
return nil, true
}
for iter := 0; iter <= MaxReadIterations; iter++ {
// If there is enough data in buffer to competely parse this argument then proceed else read more data
if dataBuffer.Size >= dataBuffer.ReadPointer+argSize+2 {
break
}
err = readDataIntoBuffer(conn, dataBuffer, true)
if err {
return nil, true
}
}
cmdArray[arrayCounter] = string((dataBuffer.Buffer)[dataBuffer.ReadPointer : dataBuffer.ReadPointer+argSize])
dataBuffer.ReadPointer += argSize + 2
}
return cmdArray, false
}
Remaining is just imperative code stuff. Now we can easily parse our commands something like this
func processCommand(conn net.Conn, cmdArray []string) bool {
if strings.ToLower(cmdArray[0]) == "ping" {
_, err := conn.Write([]byte("+PONG\r\n"))
if nil != err {
fmt.Println("error while pinging.", err)
return true
}
} else {
_, err := conn.Write([]byte("-1\r\n"))
if nil != err {
fmt.Println("error while pinging.", err)
return true
}
}
return false
}
Finally our code is much better and we can write code for each command (in the next blog ofc 😅).