2
\$\begingroup\$

I'm new to Go and wanted to implement a web server using system calls to get a better feel for how everything works.

I'm looking for feedback on idiomatic go especially error handling and using pointers.

I'm not particularly concerned about the completeness of the code. The primary goal was to learn syscalls in go.

package main

// Simple, single-threaded server using system calls instead of the net library.
//
// Omitted features from the go net package:
//
// - TLS
// - Most error checking
// - Only supports bodies that close, no persistent or chunked connections
// - Redirects
// - Deadlines and cancellation
// - Non-blocking sockets

import (
    "bufio"
    "errors"
    "flag"
    "io"
    "log"
    "net"
    "net/textproto"
    "os"
    "strconv"
    "strings"
    "syscall"
)

// netSocket is a file descriptor for a system socket.
type netSocket struct {
    // System file descriptor.
    fd int
}

func (ns netSocket) Read(p []byte) (int, error) {
    if len(p) == 0 {
        return 0, nil
    }
    n, err := syscall.Read(ns.fd, p)
    if err != nil {
        n = 0
    }
    return n, err
}

func (ns netSocket) Write(p []byte) (int, error) {
    n, err := syscall.Write(ns.fd, p)
    if err != nil {
        n = 0
    }
    return n, err
}

// Creates a new netSocket for the next pending connection request.
func (ns *netSocket) Accept() (*netSocket, error) {
    // syscall.ForkLock doc states lock not needed for blocking accept.
    nfd, _, err := syscall.Accept(ns.fd)
    if err == nil {
        syscall.CloseOnExec(nfd)
    }
    if err != nil {
        return nil, err
    }
    return &netSocket{nfd}, nil
}

func (ns *netSocket) Close() error {
    return syscall.Close(ns.fd)
}

// Creates a new socket file descriptor, binds it and listens on it.
func newNetSocket(ip net.IP, port int) (*netSocket, error) {
    // ForkLock docs state that socket syscall requires the lock.
    syscall.ForkLock.Lock()
    // AF_INET = Address Family for IPv4
    // SOCK_STREAM = virtual circuit service
    // 0: the protocol for SOCK_STREAM, there's only 1.
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
    if err != nil {
        return nil, os.NewSyscallError("socket", err)
    }
    syscall.ForkLock.Unlock()

    // Allow reuse of recently-used addresses.
    if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
        syscall.Close(fd)
        return nil, os.NewSyscallError("setsockopt", err)
    }

    // Bind the socket to a port
    sa := &syscall.SockaddrInet4{Port: port}
    copy(sa.Addr[:], ip)
    if err = syscall.Bind(fd, sa); err != nil {
        return nil, os.NewSyscallError("bind", err)
    }

    // Listen for incoming connections.
    if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
        return nil, os.NewSyscallError("listen", err)
    }

    return &netSocket{fd: fd}, nil
}

type request struct {
    method string // GET, POST, etc.
    header textproto.MIMEHeader
    body   []byte
    uri    string // The raw URI from the request
    proto  string // "HTTP/1.1"
}

func parseRequest(c *netSocket) (*request, error) {
    b := bufio.NewReader(*c)
    tp := textproto.NewReader(b)
    req := new(request)

    // First line: parse "GET /index.html HTTP/1.0"
    var s string
    s, _ = tp.ReadLine()
    sp := strings.Split(s, " ")
    req.method, req.uri, req.proto = sp[0], sp[1], sp[2]

    // Parse headers
    mimeHeader, _ := tp.ReadMIMEHeader()
    req.header = mimeHeader

    // Parse body
    if req.method == "GET" || req.method == "HEAD" {
        return req, nil
    }
    if len(req.header["Content-Length"]) == 0 {
        return nil, errors.New("no content length")
    }
    length, err := strconv.Atoi(req.header["Content-Length"][0])
    if err != nil {
        return nil, err
    }
    body := make([]byte, length)
    if _, err = io.ReadFull(b, body); err != nil {
        return nil, err
    }
    req.body = body
    return req, nil
}

func main() {
    ipFlag := flag.String("ip_addr", "127.0.0.1", "The IP address to use")
    portFlag := flag.Int("port", 8080, "The port to use.")
    flag.Parse()

    ip := net.ParseIP(*ipFlag)
    port := *portFlag
    socket, err := newNetSocket(ip, port)
    defer socket.Close()
    if err != nil {
        panic(err)
    }

    log.Print("===============")
    log.Print("Server Started!")
    log.Print("===============")
    log.Print()
    log.Printf("addr: http://%s:%d", ip, port)

    for {
        // Block until incoming connection
        rw, e := socket.Accept()
        log.Print()
        log.Print()
        log.Printf("Incoming connection")
        if e != nil {
            panic(e)
        }

        // Read request
        log.Print("Reading request")
        req, err := parseRequest(rw)
        log.Print("request: ", req)
        if err != nil {
            panic(err)
        }

        // Write response
        log.Print("Writing response")
        io.WriteString(rw, "HTTP/1.1 200 OK\r\n"+
            "Content-Type: text/html; charset=utf-8\r\n"+
            "Content-Length: 20\r\n"+
            "\r\n"+
            "<h1>hello world</h1>")
        if err != nil {
            log.Print(err.Error())
            continue
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ I edited your question because the sentence I rephrased made the question sound off-topic (which I don't believe it is.) If you feel the edit is in error please feel free to revert it or possibly rephrase on your own. \$\endgroup\$ – AGirlHasNoName Feb 17 at 13:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.