diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5511ff2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM golang:1.11 + +COPY . /go/src/gohomework + +ENTRYPOINT ["/go/src/gohomework/server/server"] + +EXPOSE 9090 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3dd5560 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +SRC_PATH = ./src +SERVER_PATH = ./src/server +SERVER_BIN = server +CLIENT_PATH = ./src/client +CLIENT_BIN = client + +DOCKER_BUILDER := golang:1.11 +BECOME := sudo -E +VERSION := $(shell cat VERSION) +DOCKER_IMAGE := gohomework:$(VERSION) +RUNNER = docker run --rm -v $(CURDIR):/go/src/gohomework/$(SERVER_PATH) +RUNNER += $(DOCKER_ENVS) -w /go/src/gohomework/$(SERVER_PATH) + +BUILDER = $(RUNNER) $(DOCKER_BUILDER) +PORT := 9090 + +.PHONY: test +test: + go test -coverprofile coverage.out -v ./... + +.PHONY: check +check: + goimports -e -l $(SRC_PATH) + golint -set_exit_status $(SRC_PATH) + go vet $(SERVER_PATH) + go vet $(CLIENT_PATH) + + +.PHONY: gohomework +tinyredis: + docker build -t gohomework . + +.PHONY: build +build: + go build -o $(SERVER_BIN) $(SERVER_FILE) + go build -o $(CLIENT_BIN) $(CLIENT_FILE) + +.PHONY: clean +clean: + $(BECOME) $(RM) $(SERVER_PATH)/server + $(BECOME) $(RM) $(CLIENT_PATH)/client + +.PHONY: cleancontainer +cleancontainer: + docker rm gohomework \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d96bbc --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# GoHomework +Implementation of the server-client solution for storing KV data lightweight analog of the Redis (https://redis.io/). +More in file [Task.pdf](https://github.com/Dubouski/GoHomework/blob/working-branch/Task.pdf) + +Use makefile for all actions + +## Testing: +**make check** +Run "go vet", "goimports", "golint" + +**make test** +Run tests and save the output to the "coverage.out" file. + + +## Work with application + +## Server +You may run server using arguments: +- '-p' or '--port' +>listening port, default is 9090; +- '-m' or '--mode' +>storage mode, default is "memory", alternate mode is "disk" (save to file "data.json"); +- '-v' or '--verbose' +>verbose mode, full log of the client requests. + +## Client +You may run client using arguments: +- '-p' or '--port' +>connect to port, default is 9090; +- '-h' or '--host' +>connect to ip address, default is 127.0.0.1; +- '--dump' +>dump the whole database to the JSON format on STDOUT (example:'[{"key": "YOUR_KEY", "value": "YOUR_VALUE"}]'). Save to file 'data.json'; +- '--restore' +>restore the database from the dumped file 'data.json'. + +## Commands: +updates one key at a time with the given value: +- set key value +returns tuple of the value and the key state. The state either present or absent: +- get key +removes one key at a time and returns the state of the resource: +- del key +returns all keys matching pattern, for example "h?llo" matches "hello", "hallo" and "hxllo": +- keys [pattern] +exit from app +- exit + + diff --git a/Task.pdf b/Task.pdf new file mode 100644 index 0000000..53f81b5 Binary files /dev/null and b/Task.pdf differ diff --git a/src/client/client.go b/src/client/client.go new file mode 100644 index 0000000..0b3d373 --- /dev/null +++ b/src/client/client.go @@ -0,0 +1,228 @@ +package client + +import ( + "bufio" + "fmt" + "io" + "net" + "os" + "strconv" + "strings" +) + +const ( + publish = "publish" + subscribe = "subscribe" + cmdExit = "EXIT" + dataJSON = "data.json" + + defaultPort = "9090" + defaultHost = "127.0.0.1" + + fPort = "--port" + port = "-p" + fHost = "--host" + host = "-h" + + dump = "--dump" + restore = "--restore" + + protocolTCP = "tcp" +) + +var ( + _mainPort = defaultPort + _mainHost = defaultHost + + _dump = false + _restore = false + + _subscriber = false //подписчик + _publisher = false //слушатель +) + +//для парсинга коммандной строки +func trimLastSymbol(s string) string { + if last := len(s) - 1; last >= 0 && s[last] == ',' { + s = s[:last] + return s + } + return s +} + +func parseArguments() { + + fmt.Println("Parse arguments") + var cmds = make(map[string]func(string)) + // заполняем команды + cmds[port] = func(s string) { + //номер порта + s = trimLastSymbol(s) + fmt.Printf("Parse -p=%s\r\n", s) + if port, err := strconv.Atoi(s); err == nil { + //Valid numbers for ports are: 0 to 2^16-1 = 0 to 65535 + //But user ports 1024 to 49151 + if port < 49152 && port > 1024 { + _mainPort = s + } + } else { + _mainPort = defaultPort + } + fmt.Printf("New port: %s\r\n", _mainPort) + } + cmds[fPort] = func(s string) { + s = trimLastSymbol(s) + fmt.Printf("Parse --port=%s\r\n", s) + if port, err := strconv.Atoi(s); err == nil { + //Valid numbers for ports are: 0 to 2^16-1 = 0 to 65535 + //But user ports 1024 to 49151 + if port < 49152 && port > 1024 { + _mainPort = s + } + } else { + _mainPort = defaultPort + } + fmt.Printf("New port: %s\r\n", _mainPort) + } + cmds[host] = func(s string) { + s = trimLastSymbol(s) + //путь к файлу для сохранения + fmt.Printf("Parse --h=%s\r\n", s) + // TODO add validate ip address + fmt.Printf("New IP address: %s\r\n", s) + _mainHost = s // update ip address + } + cmds[fHost] = func(s string) { + s = trimLastSymbol(s) + //путь к файлу с сохранением + fmt.Printf("--host=%s\r\n", s) + // TODO add validate ip address + fmt.Printf("New IP address: %s\r\n", s) + _mainHost = s // update ip address + } + cmds[dump] = func(s string) { + s = trimLastSymbol(s) + //путь к файлу с сохранением + fmt.Println("--dump") + _dump = true + } + cmds[restore] = func(s string) { + s = trimLastSymbol(s) + //путь к файлу с сохранением + fmt.Println("--restore") + _restore = true + } + + //анализ команд + for _, arg := range os.Args[1:] { + cmd := strings.Split(arg, "=") + + if len(cmd) > 2 { + fmt.Println(arg, "don't know... ") + continue + } + + // для красноречия + name := cmd[0] + param := "" + if trimLastSymbol(cmd[0]) == dump || trimLastSymbol(cmd[0]) == restore { + name = trimLastSymbol(cmd[0]) + param = "" + } else { + param = cmd[1] + } + + // ищем функцию + fn := cmds[name] + if fn == nil { + fmt.Println(name, "don't know... ") + continue + } + + // исполняем команду + fn(param) + } +} + +func main() { + + if len(os.Args) > 0 { + parseArguments() + } + + // connect to this socket + address := _mainHost + ":" + _mainPort + conn, err := net.Dial(protocolTCP, address) + + if err == nil && conn != nil { + + if _dump { + // send to socket dump command + fmt.Fprintf(conn, "dump\n") + // get file + file, _ := os.Create(dataJSON) + defer file.Close() + n, err := io.Copy(file, conn) + if err == io.EOF { + fmt.Println(err.Error()) + } + fmt.Println("Bytes received", n) + } else if _restore { + // send to socket restore command + fmt.Fprintf(conn, "restore ") + + file, errF := os.Open(dataJSON) // For read access. + if errF != nil { + fmt.Println("Unable to open file, " + errF.Error()) + } + defer file.Close() // make sure to close the file even if we panic. + n, error := io.Copy(conn, file) + if error != nil { + fmt.Printf("Send file error %s\r\n", error.Error()) + } + fmt.Println(n, "Bytes sent") + fmt.Fprintf(conn, "\r\n") + file.Close() + } else { + + for { + // read in input from stdin + if !_subscriber { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Text to send: ") + text, _ := reader.ReadString('\n') + if strings.ToUpper(strings.TrimRight(text, "\r\n")) == cmdExit { + //пользователь решил удалиться))) + break + } + + if strings.ToUpper(strings.TrimRight(text, "\r\n")) == publish { + _publisher = true + _subscriber = false + } + if strings.ToUpper(strings.TrimRight(text, "\r\n")) == subscribe { + _subscriber = true + _publisher = false + } + + // send to socket + fmt.Fprintf(conn, text+"\n") + } + + // listen for reply + if !_publisher { + message, _ := bufio.NewReader(conn).ReadString('\n') + fmt.Println("Message from client: " + message) + } + } + } + + } else { + fmt.Print("Didn't connect") + if err != nil { + fmt.Printf(", Error: %s\n\r", err.Error()) + } + } + defer conn.Close() +} + diff --git a/src/client/client_test.go b/src/client/client_test.go new file mode 100644 index 0000000..aacdae6 --- /dev/null +++ b/src/client/client_test.go @@ -0,0 +1,72 @@ +package client + +import ( + "os" + "testing" +) + +func TestTrimLastSymbol(t *testing.T) { + t.Log("\tTrim last comma test") + + tests := []struct { + input string + want string + }{ + {"9090,", "9090"}, + {"9090", "9090"}, + {"-p=9090, -h=127.0.0.1", "-p=9090, -h=127.0.0.1"}, + {",", ""}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + if trimLastSymbol(tt.input) == tt.want { + t.Log("\t[OK]\tShould get '" + tt.want + "'") + } else { + t.Error("\t[ERR]\tShould get '" + tt.want + "'") + } + }) + } +} + +func Test_parseArguments(t *testing.T) { + t.Log("Parse arguments test") + + tests := []struct { + name string + Args []string + port string + host string + dump bool + restore bool + }{ + {"no arguments", []string{"client.go"}, defaultPort, defaultHost, false, false}, + {"one wrong argument", []string{"client.go", "-pt=t"}, defaultPort, defaultHost, false, false}, + {"one argument", []string{"client.go", "--port=9091"}, "9091", defaultHost, false, false}, + {"port after 49152", []string{"client.go", "--port=50505"}, "9090", defaultHost, false, false}, + {"port after 49152", []string{"client.go", "-p=50505"}, "9090", defaultHost, false, false}, + {"port less 1024", []string{"client.go", "--port=1000"}, "9090", defaultHost, false, false}, + {"port less 1024", []string{"client.go", "-p=1000"}, "9090", defaultHost, false, false}, + {"two arguments", []string{"client.go", "-p=1000", "-h=127.0.0.2"}, defaultPort, "127.0.0.2", false, false}, + {"two arguments", []string{"client.go", "-p=9090", "--host=127.0.0.2"}, defaultPort, "127.0.0.2", false, false}, + {"three arguments", []string{"client.go", "-p=9091", "--host=127.0.0.3", "--dump"}, "9091", "127.0.0.3", true, false}, + {"four arguments", []string{"client.go", "-p=9092", "--host=127.0.0.4", "--dump", "--restore"}, "9092", "127.0.0.4", true, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + _mainPort = defaultPort + _mainHost = defaultHost + _dump = false + _restore = false + + os.Args = tt.Args + parseArguments() + if tt.host == _mainHost && tt.port == _mainPort && tt.dump == _dump && tt.restore == _restore { + t.Log("\t[OK]\t parse with " + tt.name) + } else { + t.Error("\t[ERR]\t parse with " + tt.name + " port: " + _mainPort) + } + }) + } +} + diff --git a/src/server/server.go b/src/server/server.go new file mode 100644 index 0000000..6ff6de3 --- /dev/null +++ b/src/server/server.go @@ -0,0 +1,602 @@ +package server + +import ( + "bufio" + "container/list" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "regexp" + "strconv" + "strings" + "sync" +) + +const ( + cmdSet = "SET" + cmdGet = "GET" + cmdDel = "DEL" + cmdKeys = "KEYS" + publish = "publish" + subscribe = "subscribe" + dump = "dump" + restore = "restore" + dataJSON = "data.json" + + defaultPort = "9090" + diskMode = "disk" + memoryMode = "memory" + + fMode = "--mode" + mode = "-m" + fPort = "--port" + port = "-p" + fVerbose = "--verbose" + verbose = "-v" + + protocolTCP = "tcp" +) + +var ( + _mainPort = defaultPort + _mainMode = true //mode=true - save in disk; mode=false - save in memory + _mainVerbose = false //verbose=false - without logging; verbose=true - with logging + + dataMap = map[string]string{} + + clients *list.List + publishAddr = list.New() + subscribeAddr = list.New() +) + +//for parsing CLI +func trimLastSymbol(s string) string { + if last := len(s) - 1; last >= 0 && s[last] == ',' { + s = s[:last] + //Info.Printf("Trimmed arguments: %s", s) + return s + } + return s +} + +func parseArguments() { + + Info.Println("Parse arguments") + var cmds = make(map[string]func(string)) + // заполняем команды + cmds[port] = func(s string) { + //номер порта + s = trimLastSymbol(s) + Info.Printf("Parse -p=%s\n", s) + //update client/server port + + //проверка на число + if port, err := strconv.Atoi(s); err == nil { + fmt.Printf("%q looks like a number.\n", s) + //Valid numbers for ports are: 0 to 2^16-1 = 0 to 65535 + //But user ports 1024 to 49151 + if port < 49152 && port > 1024 { + _mainPort = s + } + } else { + _mainPort = defaultPort + } + Info.Printf("New port: %s\r\n", _mainPort) + } + + cmds[fPort] = func(s string) { + + s = trimLastSymbol(s) + Info.Printf("Parse --port=%s\r\n", s) + //update client/server port + + //проверка на число + if port, err := strconv.Atoi(s); err == nil { + //Valid numbers for ports are: 0 to 2^16-1 = 0 to 65535 + //But user ports 1024 to 49151 + if port < 49152 && port > 1024 { + _mainPort = s + } + } else { + _mainPort = defaultPort + } + Info.Printf("New port: %s\r\n", _mainPort) + } + cmds[mode] = func(s string) { + s = trimLastSymbol(s) + Info.Printf("Parse -m=%s\r\n", s) + if s == memoryMode { + _mainMode = false + } else if s == diskMode { + _mainMode = true + } else { + Warning.Println("Argument 'mode' error, will be used 'disk'") + _mainMode = true + } + + if _mainMode == true { + Info.Printf("New mode work with data: %s", diskMode) + } else { + Info.Printf("New mode work with data: %s", memoryMode) + } + } + cmds[fMode] = func(s string) { + s = trimLastSymbol(s) + Info.Printf("Parse --mode=%s\r\n", s) + if s == memoryMode { + _mainMode = false + } else if s == diskMode { + _mainMode = true + } else { + Warning.Println("Argument 'mode' error, will be used 'disk'") + _mainMode = true + } + if _mainMode == true { + Info.Printf("New mode work with data: %s", diskMode) + } else { + Info.Printf("New mode work with data: %s", memoryMode) + } + } + cmds[verbose] = func(s string) { + Info.Println("Parse -v") + _mainVerbose = true + Info.Println("Work with logs") + } + cmds[fVerbose] = func(s string) { + Info.Println("Parse --verbose") + _mainVerbose = true + Info.Println("Work with all logs") + } + + //анализ команд + for _, arg := range os.Args[1:] { + cmd := strings.Split(arg, "=") + + if len(cmd) > 2 { + fmt.Println(arg, "don't know... ") + continue + } + + name := cmd[0] + param := "" + if trimLastSymbol(cmd[0]) == verbose || trimLastSymbol(cmd[0]) == fVerbose { + name = trimLastSymbol(cmd[0]) + param = "" + } else { + param = cmd[1] + } + + // ищем функцию + fn := cmds[name] + if fn == nil { + fmt.Println(name, "i don't know... ") + continue + } + + // исполняем команду + fn(param) + } +} + +// JSONWriter structure for writing to a file in JSON format +type JSONWriter struct { + mutex *sync.Mutex + fileName string +} + +func newJSONWriter(fileName string) *JSONWriter { + name := fileName + return &JSONWriter{mutex: &sync.Mutex{}, fileName: name} +} + +/** +if +f = True - SET +f = False - DEL +k - key in map +v - value in map +*/ +func (w *JSONWriter) update(k string, v string, f bool) { + + w.mutex.Lock() + + if f == true { + dataMap[k] = v + } else { + delete(dataMap, k) + } + + Info.Println(dataMap) + + if _mainMode { + //преобразуем в JSON формат + jsonString, err := json.Marshal(dataMap) + //Info.Println(jsonString) + if err != nil { + Error.Println(err) + } else { + //Info.Println(jsonString) + err = ioutil.WriteFile(w.fileName, jsonString, 0644) + if err != nil { + Error.Println(err) + } + } + } + w.mutex.Unlock() +} + +func (w *JSONWriter) get() map[string]string { + + w.mutex.Lock() + + // Open jsonFile + jsonFile, err := os.Open(w.fileName) + // if we os.Open returns an error then handle it + if err != nil { + Warning.Println(err) + } else { + Info.Println("Successfully Opened json file") + } + // defer the closing of our jsonFile so that we can parse it later on + defer jsonFile.Close() + + byteValue, _ := ioutil.ReadAll(jsonFile) + + var result map[string]string + err = json.Unmarshal([]byte(byteValue), &result) + + w.mutex.Unlock() + + if err != nil { + Warning.Println(err) + result = map[string]string{} + } else { + Info.Printf("JSON file: %s", result) + } + return result + +} + +var ( + //Trace for minor doing + Trace *log.Logger + //Info for user doing + Info *log.Logger + //Warning for case minor error + Warning *log.Logger + //Error for errors + Error *log.Logger +) + +func initLoggers( + + traceHandle io.Writer, + infoHandle io.Writer, + warningHandle io.Writer, + errorHandle io.Writer) { + + Trace = log.New(traceHandle, + "TRACE: ", + log.Ldate|log.Ltime|log.Lshortfile) + + Info = log.New(infoHandle, + "INFO: ", + log.Ldate|log.Ltime|log.Lshortfile) + + Warning = log.New(warningHandle, + "WARNING: ", + log.Ldate|log.Ltime|log.Lshortfile) + + Error = log.New(errorHandle, + "ERROR: ", + log.Ldate|log.Ltime|log.Lshortfile) +} + +// Println - only print string +func Println(l *log.Logger, s string) { + + if _mainVerbose { + l.Println(s) + } +} + +// Printf - print string with 1 argument +func Printf(l *log.Logger, s string, s2 interface{}) { + if _mainVerbose { + l.Printf(s, s2) + } +} + +func handleClient(socket net.Conn, writer JSONWriter) { + for { + buffer, err := bufio.NewReader(socket).ReadString('\n') + if err != nil { + Printf(Info, "User %s go away", socket.RemoteAddr()) + //fmt.Println("User go away") + + if subscribeAddr.Len() > 0 { + for j := subscribeAddr.Front(); j != nil; j = j.Next() { + if j.Value == socket.RemoteAddr().String() { + subscribeAddr.Remove(j) + } + } + //Printf(Info, "After s:%s\r\n", subscribeAddr) + //fmt.Println(subscribeAddr.Len()) + } + + if publishAddr.Len() > 0 { + for j := publishAddr.Front(); j != nil; j = j.Next() { + if j.Value == socket.RemoteAddr().String() { + publishAddr.Remove(j) + } + } + //Printf(Info, "After p:%s\r\n", publishAddr) + //fmt.Println(publishAddr.Len()) + } + + socket.Close() + return + + } + for i := clients.Front(); i != nil; i = i.Next() { + //fmt.Fprint(i.Value.(net.Conn), buffer) + + //обработка пришедшей команды + go parseInputMessage(writer, i.Value.(net.Conn), buffer) + } + } +} + +//This will be fixed in Go 1.12.)))))) +func parseInputMessage(jsonW JSONWriter, conn net.Conn, s string) { + Printf(Info, "input message: %s", s) + + s = strings.Trim(s, "\r\n") + line := strings.Split(s, " ") + + cmd := line[0] + cmd = strings.ToUpper(cmd) + switch cmd { + case cmdSet: + { + var error = false + Printf(Info, "command cmdSet: %s", cmd) + var key, value = "", "" + if len(line) > 1 { + key = line[1] + if len(line) > 2 { + value = strings.Trim(line[2], "\r\n") + } + if len(line) > 3 { + //TODO сообщить об ошибке + error = true + } + + } else { + key, value = "", "" + } + Info.Printf("\nk:%s v:%s", key, value) + + if error { + Println(Error, "ERROR: Syntax error\r\n") + //отправить данные клиенту + fmt.Fprint(conn, "ERROR: Syntax error\r\n") + } else { + //запись в JSON файл + jsonW.update(key, value, true) + Println(Trace, "Record was saved\r\n") + //оправить сообщение пользователю + fmt.Fprint(conn, "OK\r\n") + } + } + case cmdGet: + { + Printf(Info, "command cmdGet:%s", cmd) + if len(line) == 2 { + key := strings.Trim(string(line[1]), "\r\n") + Printf(Info, "key: %s", key) + _, ok := dataMap[key] + if ok { + //проверка c чтением из файла + if _mainMode { + value := jsonW.get()[key] + Printf(Trace, "Read value from file: %s\r\n", value) + //отправить клиенту + fmt.Fprint(conn, value+"\r\n") + } else { + //если ключ есть + value := dataMap[key] + Printf(Trace, "value:%s\r\n", value) + //отправить клиенту + fmt.Fprint(conn, value+"\r\n") + } + + } else { + //если ключа нет + fmt.Printf("\nvalue:(nil)") + //отправить клиенту + fmt.Fprint(conn, "(nil)\r\n") + } + } else { + //TODO сообщить об ошибке + fmt.Fprint(conn, "ERROR: wrong number of arguments (given "+string(len(line)-1)+" expected 1)\r\n") + } + } + case cmdDel: + { + //удаление по ключу + Printf(Info, "command cmdDel:%s", cmd) + if len(line) > 1 { + key := strings.Trim(string(line[1]), "\r\n") + _, ok := dataMap[key] + if ok { + + //update MAP and write to JSON file, because update map + jsonW.update(key, "nil", false) + + //TODO добавить удаление многих элементов + //отправить клиенту сколько удалено + fmt.Fprint(conn, "1\r\n") + } else { + //отправить клиенту сколько удалено + fmt.Fprint(conn, "0\r\n") + } + Printf(Info, "key:%s", key) + Printf(Info, "key in map:%t", ok) + + } else { + //TODO сообщить об ошибке + //отправить клиенту + fmt.Fprint(conn, "ERROR: wrong number of arguments for 'del' command\r\n") + } + } + case cmdKeys: + { + Printf(Info, "command cmdKeys:%s\r\n", cmd) + + if len(line) > 1 { + pattern := strings.Trim(string(line[1]), "\r\n") + Printf(Info, "pattern keys: %s", pattern) + + keys := make([]string, 0, len(dataMap)) + i := 0 + for key := range dataMap { + matched, err := regexp.MatchString(pattern, key) + if matched && err == nil { + Info.Printf("key: %s, pattern: %s", key, pattern) + keys = append(keys, key) + i++ + } + } + + Printf(Info, "all found keys: %s", keys) + if i > 0 { + fmt.Fprint(conn, strings.Join(keys, ",")+"\r\n") + } else { + fmt.Fprint(conn, "(nil)\r\n") + } + + } else { + //TODO сообщить об ошибке + //send client + fmt.Fprint(conn, "ERROR: wrong number of arguments for 'keys' command\r\n") + } + + } + case publish: + { + + if len(line) > 1 { + channel := strings.Trim(string(line[1]), "\r\n") + Printf(Info, "Publish channel: %s", channel) + Info.Printf("Command: %s, address of client: %s\r\n", cmd, conn.RemoteAddr().String()) + + publishAddr.PushFront(conn.RemoteAddr()) + if subscribeAddr.Len() > 0 { + for j := subscribeAddr.Front(); j != nil; j = j.Next() { + if j.Value == conn.RemoteAddr().String() { + subscribeAddr.Remove(j) + } + } + //Info.Printf("Address remove s:%s, rest len: %s\r\n", subscribeAddr, subscribeAddr.Len()) + } + } else { + //TODO сообщить об ошибке + //отправить клиенту + fmt.Fprint(conn, "ERROR: wrong number of arguments for 'publish' command\r\n") + } + + } + case subscribe: + { + Info.Printf("Command: %s, address of client: %s\r\n", cmd, conn.RemoteAddr().String()) + subscribeAddr.PushFront(conn.RemoteAddr()) + if publishAddr.Len() > 0 { + for j := publishAddr.Front(); j != nil; j = j.Next() { + if j.Value == conn.RemoteAddr().String() { + publishAddr.Remove(j) + } + } + Printf(Info, "After p:%s\r\n", publishAddr) + fmt.Println(publishAddr.Len()) + } + } + case dump: + { + Info.Printf("command cmdKeys:%s", cmd) + file, errF := os.Open(dataJSON) // For read access. + if errF != nil { + Error.Println("Unable to open file") + } + defer file.Close() // make sure to close the file even if we panic. + n, error := io.Copy(conn, file) + if error != nil { + Error.Printf("Send file error %s\r\n", error.Error()) + } + Info.Println(n, "Bytes sent") + file.Close() + conn.Close() + } + case restore: + { + Info.Printf("command cmdKeys:%s", cmd) + + data := line[1] + var result map[string]string + errUnm := json.Unmarshal([]byte(data), &result) + + if errUnm != nil { + Warning.Println(errUnm) + result = map[string]string{} + } else { + Info.Printf("JSON file: %s", result) + } + for k, v := range result { + jsonW.update(k, v, true) + } + } + default: + { + //отправить клиенту + fmt.Fprint(conn, "\r\n") + } + + } +} + +func main() { + + initLoggers(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr) //init loggers + + parseArguments() //parse Args + + Println(Info, "Server start") + clients = list.New() + + jsonWriter := newJSONWriter(dataJSON) + dataMap = jsonWriter.get() + + server, err := net.Listen(protocolTCP, ":"+_mainPort) + if err != nil { + Printf(Error, "Error: %s", err.Error()) + return + } + defer server.Close() //close connection + for { + client, err := server.Accept() + if err != nil { + Printf(Error, "Error: %s", err.Error()) + return + } + + Printf(Info, "New user connected %s\n", client.RemoteAddr()) + clients.PushBack(client) + go handleClient(client, *jsonWriter) + } + +} + diff --git a/src/server/server_test.go b/src/server/server_test.go new file mode 100644 index 0000000..9df17a1 --- /dev/null +++ b/src/server/server_test.go @@ -0,0 +1,234 @@ +package server + +import ( + "bufio" + "fmt" + "io/ioutil" + "net" + "os" + "testing" + "time" +) + +func TestTrimLastSymbol(t *testing.T) { + t.Log("\tTrim last comma test") + if trimLastSymbol("9090,") == "9090" { + t.Log("\t[OK]\tShould get '9090'") + } else { + t.Error("\t[ERR]\tShould get '9090'") + } + + if trimLastSymbol("9090") == "9090" { + t.Log("\t[OK]\tShould get '9090'") + } else { + t.Error("\t[ERR]\tShould get '9090'") + } + + if trimLastSymbol("-p=9090") == "-p=9090" { + t.Log("\t[OK]\tShould get '-p=9090'") + } else { + t.Error("\t[ERR]\tShould get '-p=9090'") + } + + if trimLastSymbol(",") == "" { + t.Log("\t[OK]\tShould get ''") + } else { + t.Error("\t[ERR]\tShould get ''") + } + +} + +func TestPrintln(t *testing.T) { + t.Log("\tLoggers test") + initLoggers(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr) + _mainVerbose = true + Println(Trace, "test println") + Println(Info, "test println") + Println(Warning, "test println") + Println(Error, "test println") + cmd := "test" + Printf(Trace, "%s printf", cmd) + Printf(Info, "%s printf", cmd) + Printf(Warning, "%s printf", cmd) + Printf(Error, "%s printf", cmd) + _mainVerbose = false +} + +func Test_newJsonWriter(t *testing.T) { + t.Log("New JSON writer test") + file, err := ioutil.TempFile(os.TempDir(), "temp.json") + + if err == nil { + + type args struct { + fileName string + } + tests := []struct { + name string + }{ + {"temp file"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newJSONWriter(file.Name()) + t.Log("\t[OK]\t temp file " + tt.name) + }) + } + } else { + t.Error("\t[ERR]\tCan't create temporary file") + } + defer os.Remove(file.Name()) +} + +func Test_parseArguments(t *testing.T) { + t.Log("Parse arguments test") + tests := []struct { + name string + Args []string + port string + mode bool + verbose bool + }{ + {"no arguments", []string{"server.go"}, "9090", true, false}, + {"wrong arguments 1", []string{"server.go", " -g=t"}, "9090", true, false}, + {"wrong arguments 2", []string{"server.go", "--port=t"}, "9090", true, false}, + {"one argument", []string{"server.go", "-p=9091"}, "9091", true, false}, + {"port after 49152", []string{"server.go", "-p=55000"}, "9090", true, false}, + {"port less 1024", []string{"server.go", "-p=1020"}, "9090", true, false}, + {"port after 49152", []string{"server.go", "--port=55000"}, "9090", true, false}, + {"port less 1024", []string{"server.go", "--port=1020"}, "9090", true, false}, + {"two arguments", []string{"server.go", "--port=9091,", "-m=disk"}, "9091", true, false}, + {"mode broke", []string{"server.go", "--port=9091,", "-m=broke_disk"}, "9091", true, false}, + {"mode broke", []string{"server.go", "--port=9091,", "--mode=broke_disk"}, "9091", true, false}, + {"three arguments", []string{"server.go", "--port=9092,", "--mode=memory,", "-v"}, "9092", false, true}, + {"different arguments", []string{"server.go", "--port=9090,", "--mode=disk,", "--verbose"}, "9090", true, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + _mainPort = defaultPort + _mainMode = true //mode=true - save in disk; mode=false - save in memory + _mainVerbose = false //verbose=false - without logging; verbose=true - with logging + + os.Args = tt.Args + parseArguments() + if tt.mode == _mainMode && tt.port == _mainPort && tt.verbose == _mainVerbose { + t.Log("\t[OK]\t parse with " + tt.name) + } else { + t.Error("\t[ERR]\t parse with " + tt.name + " port: " + _mainPort) + } + }) + } +} + +func mockClientConnectSet(t *testing.T, msg string) string { + conn, _ := net.Dial("tcp", "127.0.0.1:9090") + // send to socket + fmt.Fprintf(conn, msg) + // listen for reply + message, _ := bufio.NewReader(conn).ReadString('\n') + defer conn.Close() + return message +} + +func mockClientConnectGet(t *testing.T, msg string) string { + conn, _ := net.Dial("tcp", "127.0.0.1:9090") + // send to socket + fmt.Fprintf(conn, msg) + // listen for reply + message, _ := bufio.NewReader(conn).ReadString('\n') + time.Sleep(5 * time.Millisecond) + defer conn.Close() + return message +} + +func mockClientConnectDel(t *testing.T, msg string) string { + conn, _ := net.Dial("tcp", "127.0.0.1:9090") + // send to socket + fmt.Fprintf(conn, msg) + // listen for reply + message, _ := bufio.NewReader(conn).ReadString('\n') + defer conn.Close() + return message +} + +func mockClientConnectKeys(t *testing.T, msg string) string { + conn, _ := net.Dial("tcp", "127.0.0.1:9090") + fmt.Fprintf(conn, msg) + message, _ := bufio.NewReader(conn).ReadString('\n') + defer conn.Close() + return message +} + +func Test_main(t *testing.T) { + t.Log("CI test") + + set := "set kT0 v0\r\n" + set1 := "set kT1 v1\r\n" + set2 := "set kT2\r\n" + + os.Args = []string{"server.go", "-m=memory"} + go main() + time.Sleep(50 * time.Millisecond) + + if mockClientConnectSet(t, set) == "OK\r\n" { + t.Log("\t[OK]\t good response from 'SET kT0 v0'") + } else { + t.Error("\t[ERR]\t with response from 'SET kT0 v0'") + } + if mockClientConnectSet(t, set1) == "OK\r\n" { + t.Log("\t[OK]\t good response from 'SET kT1 v1'") + } else { + t.Error("\t[ERR]\t with response from 'SET kT1 v1'") + } + if mockClientConnectSet(t, set2) == "OK\r\n" { + t.Log("\t[OK]\t good response from 'SET kT2 v2'") + } else { + t.Error("\t[ERR]\t with response from 'SET kT2 v2'") + } + time.Sleep(50 * time.Millisecond) + + get := "get kT1 kt2 kt3 kT4\r\n" + get1 := "GET kT2\r\n" + get2 := "Get kT3\r\n" + + if mockClientConnectGet(t, get) != "v1\r\n" { + t.Log("\t[OK]\t good response from 'GET kT1'") + } else { + t.Error("\t[ERR]\t with response from 'GET kT1'") + } + if mockClientConnectGet(t, get1) == "\r\n" { + t.Log("\t[OK]\t good response from 'GET kT2'") + } else { + t.Error("\t[ERR]\t with response from 'GET kT2'") + } + if mockClientConnectGet(t, get2) == "(nil)\r\n" { + t.Log("\t[OK]\t good response from 'GET kT3'") + } else { + t.Error("\t[ERR]\t with response from 'GET kT3'") + } + time.Sleep(50 * time.Millisecond) + + del1 := "del kT3\r\n" + del2 := "del\r\n" + + if mockClientConnectDel(t, del1) == "0\r\n" { + t.Log("\t[OK]\t good response from 'del kT3'") + } else { + t.Error("\t[ERR]\t with response from 'del kT3'") + } + if mockClientConnectDel(t, del2) == "ERROR: wrong number of arguments for 'del' command\r\n" { + t.Log("\t[OK]\t good response from 'del'") + } else { + t.Error("\t[ERR]\t with response from 'del'") + } + + mockClientConnectDel(t, "DEL kT0\r\n") + if mockClientConnectKeys(t, "KEYS k*\r\n") == "kT1,kT2\r\n" { + t.Log("\t[OK]\t good response from 'DEL kT0' 'KEYS k*'") + } else { + t.Error("\t[ERR]\t with response from 'DEL kT0' 'KEYS k*'") + } + +} +