- Overheads: while you can easier build a server in NodeJS, Java or Go, they also come with overheads, which are particularly painful if you envisage and application on a small Linux machine or Docker image (be it minimal Ubuntu or Debian, Alpine, Raspberry PI or similar);
- Speed and Control: of the processes at the closest to the machine level, without redundancies, at the maximum speed of execution. In other wards, when you do precisely what you aim and what you aim only;
- You are a positive weirdo who also happens to love plain C and the “purest way” the coding could be done;
- and, my favourite, the artistic combination of the previous three :)
✎ You can see the source of the server.c here.
* * *
Now let’s get to the code. Put it simply, you ought to implement the following basic functionalities:
❶ | Listen to a port and read from/write to sockets; |
❷ | Parse the buffer (request) you've just read from the socket; |
❸ | Process the request (via a router) and respond to the client (browser); |
❹ | Close the interaction (flush buffers and clean memory, if allocated). |
Hence, the main function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | int main() { struct _request REQ; LISTEN: listenOn(8080, &REQ); // listen to port 8080 parseRequest(&REQ); listReqInfo(&REQ); // print info on the request router(&REQ); close(REQ.conn); goto LISTEN; } |
To handle the HTTP request, I use a data structure comprising key-value substructures for headers and querystring fields, as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct keyValue { char *item, *value, *filename; }; #define BUF_SIZE 2048 struct _request { int conn, total, qTotal, socket, port; char *method, *URI, *protocol, methCode, *body; // methcode 0-GET 1-POST X-Others struct keyValue hdr[20]; // headers struct keyValue fld[20]; // fields of query string size_t MemLen, MemTotal; char buffer[BUF_SIZE], ioBuffer[BUF_SIZE]; // For reading File and Post }; |
The BUF_SIZE tells the compiler how big the i/o buffers must be, in bytes. Alternatively, you may request malloc/calloc memory allocation, which is specifically appropriate when you handle large (say POST) requests or process the requests concurrently, in parallel (through threads, fork() or exec()).
1. Listen to the port
Here comes the listenOn function, which take the port as parameter. It sets (initialise) the server and starts listening to the port/socket for a new request from the client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void listenOn(int port, struct _request *H) { // return socket static int nSocket = -1; if(nSocket < 0) { int option = 1; struct sockaddr_in address; if((nSocket = socket(AF_INET, SOCK_STREAM, 0)) <0 ) ERR("> SOCKET CREATION"); memset((char *)&address, '\0', sizeof(address)); address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(port); if(setsockopt(nSocket, SOL_SOCKET,(SO_REUSEPORT | SO_REUSEADDR), (char*)&option, sizeof(option)) < 0) ERR("SOCKET SET OPTIONS"); if(bind(nSocket, (struct sockaddr *) &address, sizeof(address)) <0) ERR("> BINDING ERROR"); H->socket = nSocket; H->port = port; printf("> Server started on port %d.\n", port); } int newSocket; if(listen(nSocket, 10) <0) ERR("server: listen"); // 10 = pending backlogs if((newSocket = accept(nSocket, NULL, NULL)) < 0) ERR("SOCKET [server] ACCEPT"); H->conn = newSocket; } |
2. Parse the HTTP request
When the request arrives, it can be read and parsed, which is performed by the parseRequest function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | void parseRequest(struct _request *H) { char *buf = readSocket(H), *qs; H->total = 0; H->qTotal = 0; if((qs = strstr(buf,"\r\n\r\n")) && *(qs+4)) { *qs = '\0'; H->body = qs+4; } else H->body = NULL; H->method = buf; buf = strchr(buf, ' '); *buf++ = '\0'; H->URI = buf; buf = strchr(buf, ' '); *buf++ = '\0'; H->protocol = buf; buf = strchr(buf, '\r'); *buf = '\0'; buf+=2; if(strcmp(H->method,"GET")==0) H->methCode = '\0'; else if(strcmp(H->method,"POST")==0) H->methCode = '\1'; else H->methCode = '\2'; if(qs = strchr(H->URI, '?')) { *qs++ = '\0'; getQueries(H, qs); } char *key = buf, *val; while(val = strchr(buf, ':')) { *val = '\0'; val+=2; if(buf = strchr(val, '\r')) { *buf = '\0'; buf+=2; } H->hdr[H->total].item = key; H->hdr[H->total].value = val; H->hdr[H->total].filename = NULL; H->total++; if(buf) key = buf; else break; } if(H->methCode == '\1') { //--- handle Post printf("POST Length=%s\nPOST Type=%s\n", getHeader(H, "Content-Length"), getHeader(H, "Content-Type")); char *CL = getHeader(H, "Content-Length"), *CT = getHeader(H, "Content-Type"); if(*CT == 'a'){ //--- app www-form-url if(H->body) getQueries(H, H->body); else readXForm(H, CT, (size_t)atoi(CL)); } else if(*CT == 't') { //--- text/plain if(!H->body) readXForm(H, CT, (size_t)atoi(CL)); } else if(*CT == 'm') { //--- Multipart if(!CL || !CT) ERR("Post request without Length or Type headers.\n"); readMultipart(H, CT, (size_t)atoi(CL), &handleFile); } } } |
This function uses readSocket(), getQueries(), readMultipart() and readXForm() functions.
1 2 3 4 5 6 | char *readSocket(struct _request *H) { int vRead = read(H->conn, H->buffer, BUF_SIZE); H->buffer[vRead] = '\0'; if(vRead < 0) ERR("> CANNOT READ FROM SOCKET"); printf("\n\e[42m\e[30m Socket #%d (%d bytes) \e[39m\e[49m\n", H->conn, vRead); return H->buffer; } |
The technical reason to read from socket in a separate function is twofold:
- you'll use it in other processes, like communication client-server within the authentication (login);
- or adapt this fragment in case of dynamically allocated buffer for reading large requests, as well as for reading the requests in chunks. In this case the buffer ought to be free()-d on exit.
1 2 3 4 5 6 7 8 9 10 11 12 | void getQueries(struct _request *H, char *q) { char *eq, *am; while(eq = strchr(q, '=')) { *eq++ = '\0'; if(am = strchr(eq, '&')) { *am = '\0'; addInReqList(H, q, eq, NULL); q = am+1; } else addInReqList(H, q, eq, NULL); } } |
The handling of the querystring in separate function allows using it in both GET and POST requests of X-Form type.
The function addInReqList() adds items to the query list:
1 2 3 4 5 6 7 | void addInReqList(struct _request *H, char *item, char *value, char *filename) { int t = H->qTotal; H->fld[t].item = item; H->fld[t].value = urlDecode(value); H->fld[t].filename = filename; H->qTotal++; } |
In both GET and POST requests, the items are url-encoded (aka %-encoded) and, thus, must be decoded. Here is the urlDecode() function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | char *urlDecode(char *src) { char x, y, *ret = src, *dst = src, Fst = 'a'-'A', Sec = 'A' - 10; while(*src) { if(*src == '%' && (x = src[1]) && (y = src[2]) && isxdigit(a) && isxdigit(b)) { x -= (x >= 'a') ? Fst: ( (x >= 'A') ? Sec: '0' ); y -= (y >= 'a') ? Fst: ( (y >= 'A') ? Sec: '0' ); *dst++ = 16*x + y; src+=3; } else if(*src == '+') { *dst++ = ' '; src++; } else *dst++ = *src++; } *dst = '\0'; return ret; } |
⚑ | The functions readMultipart() and readXForm() are discussed separately here, since handling multipart requests is a whole big deal apart. |
As an utility function, you may use listReqInfo() to see how the request has been parsed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | void listReqInfo(struct _request *H) { printf("\nMethod \e[32m%s\e[0m (%d) URI: \e[32m%s\e[0m Query: \e[32m%d\e[0m Protocol: \e[32m%s\e[0m Body: \e[32m%s\e[0m\n", H->method, H->methCode, H->URI, H->qTotal, H->protocol, H->body?"EXISTS":"Empty"); if(H->qTotal > 0) { printf("\nQUERY STRINGs (%d):\n\n", H->qTotal); for(int i=0; i < H->qTotal; i++) printf("\t(%d) %s = \e[32m%s\e[0m (%s)\n", i, H->fld[i].item, H->fld[i].value, H->fld[i].filename?H->fld[i].filename:"-"); } printf("\nHEADERS LIST (%d):\n\n", H->total); for(int i=0; i<H->total; i++) { printf("\t(%d) %s: \e[32m%s\e[0m\n", i, H->hdr[i].item, H->hdr[i].value); } /* ------------- Testing get some headers ------------- */ const char *test[] = { "Host", "Connection", "Content-Type", "Content-Length" }; printf("\nGET HEADERS:\n\n"); for(int i=0; i<4; i++) { char *n = getHeader(H, test[i]); printf("\tHeader \"%s\": \e[32m%s\e[0m\n", test[i], n?n:"None"); } if(H->methCode == '\1' && H->body) { const char *n = getHeader(H, "Content-Type"); // if 't' text/plain if(n && *n == 't') printf("\nPOST BODY:\n\e[32m%s\e[0m\n", H->body); } fflush(stdout); } |
By calling listReqInfo(), you must have the following terminal screen:

3. Router
The router allows you to route the request's processing and respond back, depending on the URI, querystring, data supplied by POST, authentication and authorisation, and other data to analyse.
1 2 3 4 5 | void router(struct _request *H) { if(strcmp(H->URI,"/")==0 ) serveFile(H, "res/index.html"); else if(strcmp(H->URI,"/info")==0) dynamicHTML(H); else serveFile(H, H->URI+1); } |
This version of router provides only 3 branches:
- serves the file index.html when you enter localhost:8080 in your browser (or curl);
- produce and serve an HTML containing information about the last HTTP request;
- attempts to serve the file indicated in the URI or return "no file found". For example, localhost:8080/res/readme.txt will try to serve the file readme.txt from the subfolder res.
Once the only checked parameter is the URI, such a router will respond regardless of the request method (GET or POST).
The router() uses two other functions: serveFile() and dynamicHTML():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void serveFile(struct _request *H, const char *name) { int file, rs, sk = H->conn; char *Buf = H->ioBuffer; if((file = open(name, 0, S_IREAD))<0) { respond(sk, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nFile ["); respond(sk, name); respond(sk, "] not found."); printf("File %s not found.", name); return; } char *ext = (char*)name + strlen(name) - 1; while(ext > name && *ext != '.') ext--; respond(sk, "HTTP/1.1 200 OK\r\nContent-Type: "); if(ext == name) respond(sk, "text/plain"); else if( Lk(ext, ".js")) respond(sk, "text/javascript"); else if( Lk(ext, ".css.html")) { respond(sk, "text/"); respond(sk, ext+1); } else if( Lk(ext, ".jpeg.png.gif.bmp.webp.ico")) { respond(sk, "image/"); respond(sk, ext+1); } else if( Lk(ext, ".xml.pdf")) { respond(sk, "application/"); respond(sk, ext+1); } else respond(sk, "text/plain"); respond(sk, "\r\n\r\n"); while((rs = read(file, Buf, BUF_SIZE)) > 0) write(sk, Buf, rs); close(file); } |
The Lk function ("look if") is used to check if the file extension is part of the enumerated extensions:
1 2 3 4 5 6 7 8 9 | char Lk(const char *ce, const char *src) { do { const char *p = ce; while(*p) if(*p != *src) break; else { p++; src++; } if(*p=='\0' && (*src=='\0' || *src=='.')) return '\1'; while(*src && *src != '.') src++; } while(*src); return '\0'; } |
Here is the dynamicHTML function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void dynamicHTML(struct _request *H) { int sk = H->conn; respond(sk, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"); respond(sk, "<html><head><link rel='stylesheet' type='text/css' href='res/theme.css'>"); respond(sk, "</head><body><h1>Server Info:</h1>"); respond(sk, "Method: <b>"); respond(sk, H->method); respond(sk, "</b>, URI: <b>"); respond(sk, H->URI); respond(sk, "</b>, Protocol: <b>"); respond(sk, H->protocol); respond(sk, "</b><br>"); respond(sk, "<h3>Headers:</h3>"); respond(sk, "<table cellSpacing=0 cellPadding=6><tr><th>Header</th><th>Value</th></tr>"); for(int i=0; i<H->total; i++) { respond(sk, "<tr><td>"); respond(sk, H->hdr[i].item); respond(sk, "</td><td>"); respond(sk, H->hdr[i].value); respond(sk, "</td><tr>"); } respond(sk, "</table><br>"); if(H->qTotal > 0) { respond(sk, "<h3>Queries:</h3>"); respond(sk, "<table cellSpacing=0 cellPadding=6><tr><th>Item</th><th>Value</th></tr>"); for(int i=0; i < H->qTotal; i++) { respond(sk, "<tr><td>"); respond(sk, H->fld[i].item); respond(sk, "</td><td>"); respond(sk, H->fld[i].value); respond(sk, "</td><tr>"); } } respond(sk, "</table><br>"); if(H->methCode == '\1' && H->body) { respond(sk, "<hr>POST body: <b>"); respond(sk, H->body); respond(sk, "</b>"); } respond(sk, "</body></html>"); } |
You must expect the following terminal screen:

Here and above we use the respond() function to write back into the socket:
1 | void respond(int sk, const char *st) { write(sk, st, strlen(st)); } |
Here are the utility functions getHeader() and getQueryItem():
1 2 3 4 5 6 7 8 9 10 | char *getHeader(struct _request *Headers, const char *item) { for(int i=0; i<Headers->total; i++) if(strcmp(Headers->hdr[i].item, item)==0) return Headers->hdr[i].value; return NULL; } char *getQueryItem(struct _request *Headers, const char *item) { for(int i=0; i<Headers->qTotal; i++) if(strcmp(Headers->fld[i].item, item)==0) return Headers->fld[i].value; return NULL; } |
*The source of the server.c is here.
No comments:
Post a Comment