2
\$\begingroup\$

At heart, this program is just the basic "FizzBuzz" program. However, the program was pushed further, by making it a client-server via Unix Socket:

  1. Server run, and listening on a socket.
  2. Client run with an argument, a file containing a JSON one query or more. The client opens the file and takes the queries.
  3. A typical JSON query would look like this: {"id":"first attempt","from":0,"to":15,"fizz":"Mango","buzz":"Avocado"}\n and is sent through the unix socket file to the server.
  4. The server receives the JSON message perform the FizzBuzz, and send an answer back to client (also JSON form). {"first attempt":["MangoAvocado",1,2,"Mango",4,"Avocado","Mango",7,8,"Mango","Avocado",11,"Mango",13,14,"MangoAvocado"]}
  5. Client receives the JSON message and printout the JSON elements:

Here is the server.go:

package main

import (
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"
    "encoding/json"
)


//thanks to https://mholt.github.io/json-to-go/
type input struct {
    ID   string `json:"id"`
    From int    `json:"from"`
    To   int    `json:"to"`
    Fizz string `json:"fizz"`
    Buzz string `json:"buzz"`
}

type output struct {
    Output []interface{} `json:"output"`
}

func nice(inputBytes []byte)[]byte{
    
    var in input
    json.Unmarshal(inputBytes, &in)

    out := output{
        Output: []interface{}{},
    }

    for i := in.From; i <= in.To; i++ {
        if i % 3 == 0 {
            if i % 5 == 0{
                out.Output = append(out.Output, in.Fizz + in.Buzz)
            }else{
                out.Output = append(out.Output, in.Fizz)
            }
        }else if i % 5 == 0{
            out.Output = append(out.Output, in.Buzz)
        }else{
            out.Output = append(out.Output, i)
        }
    }
    d, _ := json.Marshal(out.Output)
    d = append([]byte("\":"), d...)
    d = append([]byte(in.ID), d...)
    d = append([]byte("{\""), d...)
    d = append(d, "}"...)
    return d
}


func echoServer(c net.Conn) {
    for {
        buf := make([]byte, 512)
        nr, err := c.Read(buf)
        if err != nil {
            return
        }

        data := buf[0:nr]
        println("Server got:", string(data))
        data = nice(data)
        _, err = c.Write(data)
        if err != nil {
            log.Fatal("Writing client error: ", err)
        }
    }
}

func main() {
    log.Println("Starting echo server")
    ln, err := net.Listen("unix", "/tmp/go.sock")
    if err != nil {
        log.Fatal("Listen error: ", err)
    }

    sigc := make(chan os.Signal, 1)
    signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
    go func(ln net.Listener, c chan os.Signal) {
        sig := <-c
        log.Printf("Caught signal %s: shutting down.", sig)
        ln.Close()
        os.Exit(0)
    }(ln, sigc)

    for {
        fd, err := ln.Accept()
        if err != nil {
            log.Fatal("Accept error: ", err)
        }

        go echoServer(fd)
    }
}

and the client.go:

package main

import (
    "fmt"
    "os"
    "bufio"
    "io"
    "log"
    "net"
    "time"
)

func reader(r io.Reader) {
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf[:])
        if err != nil {
            return
        }
        println("Client got:", string(buf[0:n]))
    }
}

func main() {

    if len(os.Args) <= 1 {
        fmt.Println("No args given. (expecting a filename)")
        return
    }

    if len(os.Args) > 2 {
        fmt.Println("Too many args (only expecting one)")
        return
    }

    var file, err = os.OpenFile(os.Args[1], os.O_RDONLY, 0644)
    if (err != nil){
        log.Fatal(err) //problem opening filename
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    msg := ""

    c, err := net.Dial("unix", "/tmp/go.sock")
    if err != nil {
        log.Fatal("Dial error", err)
    }
    defer c.Close()

    go reader(c)

    for scanner.Scan(){
        msg = scanner.Text()
        _, err = c.Write([]byte(msg))
        if err != nil {
            log.Fatal("Write error:", err)
            //break
            return
        }
        println("Client sent:", msg)
        time.Sleep(1e9) // this gives time for 
    }

    if err:=scanner.Err(); err != nil{
        log.Fatal(err)
    }

}

Say, we have this file named input:

{"id":"first entry","from":0,"to":15,"fizz":"Orca","buzz":"Seal"}
{"id":"second entry","from":-3,"to":8,"fizz":"Mango","buzz":"Avocado"}
{"id":"third one","from":1001,"to":1025,"fizz":"Coke","buzz":"Pepsi"}

Running go run server.go and go run client.go input will give these:

Client sent: {"id":"first entry","from":0,"to":15,"fizz":"Orca","buzz":"Seal"}

Client got: {"first entry":["OrcaSeal",1,2,"Orca",4,"Seal","Orca",7,8,"Orca","Seal",11,"Orca",13,14,"OrcaSeal"]}

Client sent: {"id":"second entry","from":-3,"to":8,"fizz":"Mango","buzz":"Avocado"}

Client got: {"second entry":["Mango",-2,-1,"MangoAvocado",1,2,"Mango",4,"Avocado","Mango",7,8]}

Client sent: {"id":"third one","from":1001,"to":1025,"fizz":"Coke","buzz":"Pepsi"}

Client got: {"third one":[1001,"Coke",1003,1004,"CokePepsi",1006,1007,"Coke",1009,"Pepsi","Coke",1012,1013,"Coke","Pepsi",1016,"Coke",1018,1019,"CokePepsi",1021,1022,"Coke",1024,"Pepsi"]}

This program works as expected but I am somehow not satisfied since I am new to both JSON and Go. How can I improve the code?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

The encoding/json package has Encoder and Decoder which you can take advantage of for operating on streams of JSON.

Redefining Output

In server.go you json.Marshal(out.Output), then manually build the JSON to add in.ID. You can add ID to Output to avoid this. Also instead of using []interface{} I would just define a Series []string

type Output struct {
    ID     string
    Series []string
}

Accepting net.Conn

You launch a goroutine after accepting from the listener. However, writing to the terminal isn't safe for concurrent use. Since this isn't a high throughput server, you don't need to handle each conn in parallel.

Also be sure to Close() the accepted connection.

Using Encoder and Decoder in server.go

d := json.NewDecoder(conn)
e := json.NewEncoder(conn)

Using these tools, you don't have to specify an arbitrary 512-byte buffer. Simply d.Decode(...) and e.Encode(...) to read and write JSON from the connection.

Using Encoder and Decider in client.go

Much the same as server.go, these simplify JSON stream handling (both for reading the input file and reading/writing from the connection).

inputd := json.NewDecoder(input)
connd := json.NewDecoder(conn)
conne := json.NewEncoder(conn)

First inputd.Decode(...) to read from the input file, then conne.Encode(...) to send it along the connection, finally connd.Decode(...) to get the server's response.

Minor notes

  • in client.go lines 20 & 62, should use fmt.Println instead of println
  • in client.go, time.Sleep() isn't needed since the server will respond immediately
  • in server.go, listening to os.Interrupt will catch Ctrl+C, so syscall.SIGTERM isn't needed
  • in server.go, when exiting you can os.Remove the socket, otherwise restarting the server will give an error because the socket already exists

Putting it all together

Updated code (gist)

common.go

package main

import "encoding/json"

type Input struct {
    ID   string
    From int
    To   int
    Fizz string
    Buzz string
}

// just used for pretty-printing to terminal
func (in Input) String() string {
    b, err := json.Marshal(in)
    if err != nil {
        panic(err)
    }
    return string(b)
}

type Output struct {
    ID     string
    Series []string
}

// just used for pretty-printing to terminal
func (out Output) String() string {
    b, err := json.Marshal(out)
    if err != nil {
        panic(err)
    }
    return string(b)
}

server.go

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "os/signal"
    "strconv"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("usage: ./server socket")
        os.Exit(1)
    }
    ln, err := net.Listen("unix", os.Args[1])
    if err != nil {
        log.Fatal(err)
    }
    defer ln.Close()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    go func() {
        <-c
        os.Remove(os.Args[1]) // clean up socket
        os.Exit(1)
    }()

    for {
        if err = accept(ln); err != nil {
            log.Println(err)
        }
    }
}

func accept(ln net.Listener) error {
    conn, err := ln.Accept()
    if err != nil {
        return err
    }
    defer conn.Close()
    return respond(conn)
}

func respond(conn net.Conn) error {
    d := json.NewDecoder(conn)
    e := json.NewEncoder(conn)
    for {
        // Read from client
        var in Input
        if err := d.Decode(&in); err == io.EOF {
            return nil
        } else if err != nil {
            return err
        }

        fmt.Println("recieved", in)

        // Write to client
        out := buildOutput(&in)
        if err := e.Encode(&out); err != nil {
            return err
        }

        fmt.Println("sent", out)
    }
}

func buildOutput(in *Input) Output {
    out := Output{
        ID: in.ID,
    }

    for i := in.From; i <= in.To; i++ {
        if i%3 == 0 && i%5 == 0 {
            out.Series = append(out.Series, in.Fizz+in.Buzz)
        } else if i%3 == 0 {
            out.Series = append(out.Series, in.Fizz)
        } else if i%5 == 0 {
            out.Series = append(out.Series, in.Buzz)
        } else {
            out.Series = append(out.Series, strconv.Itoa(i))
        }
    }

    return out
}

client.go

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Println("usage: ./client socket filename")
        os.Exit(1)
    }

    input, err := os.Open(os.Args[2])
    if err != nil {
        log.Fatal(err)
    }
    defer input.Close()

    inputd := json.NewDecoder(input)

    conn, err := net.Dial("unix", os.Args[1])
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    connd := json.NewDecoder(conn)
    conne := json.NewEncoder(conn)

    for {
        // Read from input
        var in Input
        if err = inputd.Decode(&in); err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }

        // Write to server
        if err = conne.Encode(&in); err != nil {
            log.Println(err)
            continue
        }

        fmt.Println("sent", in)

        // Read from server
        var out Output
        if err = connd.Decode(&out); err != nil {
            log.Println(err)
        }

        fmt.Println("recieved", out)
    }
}

Hope this helps!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.