Skip to content

Instantly share code, notes, and snippets.

@bluebrown
Last active November 1, 2022 00:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bluebrown/0f03cb0971fbf5f73f7cc92e6ef98767 to your computer and use it in GitHub Desktop.
Save bluebrown/0f03cb0971fbf5f73f7cc92e6ef98767 to your computer and use it in GitHub Desktop.
Basic tcp server handling http requests written in Go
package main
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"strconv"
"strings"
)
func main() {
if err := listenAndServeTCP(context.TODO(), ":8080"); err != nil {
log.Fatalln(err)
}
}
// runs a tcp server and returns an error if it could not bind the address
// the server listener is closed and the function returns if the context is done
func listenAndServeTCP(ctx context.Context, address string) error {
// do nothing if the context is already done
if ctx.Done() != nil {
return ctx.Err()
}
// create a new listener
listener, err := net.Listen("tcp", address)
if err != nil {
return err
}
// accept connection in a separate goroutine so that
// we can select between the context and the connection channel
connChan := make(chan net.Conn)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
log.Println("error accepting connection:", err)
continue
}
connChan <- conn
}
}()
log.Printf("listening on %s", address)
// handle the connection dispatched by the accept loop
// or cancel if the context is done
for {
select {
case <-ctx.Done():
return listener.Close()
case conn := <-connChan:
go func() {
log.Printf("new connection: %s", conn.RemoteAddr())
if err := connectionHandler(ctx, conn); err != nil {
log.Printf("error while handling connection: %s", err.Error())
}
if err := conn.Close(); err != nil {
log.Printf("error while closing the connection: %s", err.Error())
}
}()
}
}
}
type request struct {
Method string
URI string
Protocol string
Headers map[string][]string
Body io.Reader
}
func connectionHandler(ctx context.Context, conn net.Conn) error {
req, err := readRequest(ctx, conn)
if err != nil {
conn.Write([]byte("HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n"))
return err
}
log.Printf("request: %v", req)
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n"))
return err
}
func readRequest(ctx context.Context, conn net.Conn) (request, error) {
// create a reader to be able to read lines easily
r := bufio.NewReader(conn)
// create a request to capture the various parts read from the connection
req := request{Headers: map[string][]string{}}
// read the first line which is not formed like a regular header
// it actually contains the method, uri and protocol. i.e.
// GET / HTTP/1.1
info, err := r.ReadString('\n')
if err != nil {
return req, err
}
info = strings.TrimSpace(info)
// the info should consist of 3 parts
parts := strings.SplitN(string(info), " ", 3)
if len(parts) != 3 {
return req, errors.New("invalid request info")
}
req.Method = parts[0]
req.URI = parts[1]
req.Protocol = parts[2]
// read the header
for {
s, err := r.ReadString('\n')
if err != nil {
return req, err
}
s = strings.TrimSpace(s)
if len(s) == 0 {
break
}
parts := strings.SplitN(s, ":", 2)
if len(parts) != 2 {
return req, fmt.Errorf("malformed header")
}
req.Headers[parts[0]] = append(req.Headers[parts[0]], strings.TrimSpace(parts[1]))
}
// check if there is a content-length header
if len(req.Headers["Content-Length"]) == 0 {
return req, nil
}
// and get the content-length
contentLength, err := strconv.ParseInt(req.Headers["Content-Length"][0], 10, 64)
if err != nil {
return req, err
}
// if there is a content-length, read from the scanner until the
// body has reached the limit
buf := new(bytes.Buffer)
if _, err := io.CopyN(buf, r, contentLength); err != nil {
return req, err
}
req.Body = buf
return req, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment