Last active
November 1, 2022 00:02
-
-
Save bluebrown/0f03cb0971fbf5f73f7cc92e6ef98767 to your computer and use it in GitHub Desktop.
Basic tcp server handling http requests written in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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