Skip to content

Instantly share code, notes, and snippets.

@laobubu
Last active March 23, 2024 05:28
Show Gist options
  • Save laobubu/d6d0e9beb934b60b2e552c2d03e1409e to your computer and use it in GitHub Desktop.
Save laobubu/d6d0e9beb934b60b2e552c2d03e1409e to your computer and use it in GitHub Desktop.
A very simple HTTP server in C, for Unix, using fork()

Pico HTTP Server in C

This is a very simple HTTP server for Unix, using fork(). It's very easy to use

How to use

  1. include header httpd.h
  2. write your route method, handling requests.
  3. call serve_forever("12913") to start serving on port 12913

See main.c, an interesting example.

To log stuff, use fprintf(stderr, "message");

View httpd.h for more information

based on http://blog.abhijeetr.com/2010/04/very-simple-http-server-writen-in-c.html

#include "httpd.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <signal.h>
#define CONNMAX 1000
static int listenfd, clients[CONNMAX];
static void error(char *);
static void startServer(const char *);
static void respond(int);
typedef struct { char *name, *value; } header_t;
static header_t reqhdr[17] = { {"\0", "\0"} };
static int clientfd;
static char *buf;
void serve_forever(const char *PORT)
{
struct sockaddr_in clientaddr;
socklen_t addrlen;
char c;
int slot=0;
printf(
"Server started %shttp://127.0.0.1:%s%s\n",
"\033[92m",PORT,"\033[0m"
);
// Setting all elements to -1: signifies there is no client connected
int i;
for (i=0; i<CONNMAX; i++)
clients[i]=-1;
startServer(PORT);
// Ignore SIGCHLD to avoid zombie threads
signal(SIGCHLD,SIG_IGN);
// ACCEPT connections
while (1)
{
addrlen = sizeof(clientaddr);
clients[slot] = accept (listenfd, (struct sockaddr *) &clientaddr, &addrlen);
if (clients[slot]<0)
{
perror("accept() error");
}
else
{
if ( fork()==0 )
{
respond(slot);
exit(0);
}
}
while (clients[slot]!=-1) slot = (slot+1)%CONNMAX;
}
}
//start server
void startServer(const char *port)
{
struct addrinfo hints, *res, *p;
// getaddrinfo for host
memset (&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo( NULL, port, &hints, &res) != 0)
{
perror ("getaddrinfo() error");
exit(1);
}
// socket and bind
for (p = res; p!=NULL; p=p->ai_next)
{
int option = 1;
listenfd = socket (p->ai_family, p->ai_socktype, 0);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
if (listenfd == -1) continue;
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) break;
}
if (p==NULL)
{
perror ("socket() or bind()");
exit(1);
}
freeaddrinfo(res);
// listen for incoming connections
if ( listen (listenfd, 1000000) != 0 )
{
perror("listen() error");
exit(1);
}
}
// get request header
char *request_header(const char* name)
{
header_t *h = reqhdr;
while(h->name) {
if (strcmp(h->name, name) == 0) return h->value;
h++;
}
return NULL;
}
//client connection
void respond(int n)
{
int rcvd, fd, bytes_read;
char *ptr;
buf = malloc(65535);
rcvd=recv(clients[n], buf, 65535, 0);
if (rcvd<0) // receive error
fprintf(stderr,("recv() error\n"));
else if (rcvd==0) // receive socket closed
fprintf(stderr,"Client disconnected upexpectedly.\n");
else // message received
{
buf[rcvd] = '\0';
method = strtok(buf, " \t\r\n");
uri = strtok(NULL, " \t");
prot = strtok(NULL, " \t\r\n");
fprintf(stderr, "\x1b[32m + [%s] %s\x1b[0m\n", method, uri);
if (qs = strchr(uri, '?'))
{
*qs++ = '\0'; //split URI
} else {
qs = uri - 1; //use an empty string
}
header_t *h = reqhdr;
char *t, *t2;
while(h < reqhdr+16) {
char *k,*v,*t;
k = strtok(NULL, "\r\n: \t"); if (!k) break;
v = strtok(NULL, "\r\n"); while(*v && *v==' ') v++;
h->name = k;
h->value = v;
h++;
fprintf(stderr, "[H] %s: %s\n", k, v);
t = v + 1 + strlen(v);
if (t[1] == '\r' && t[2] == '\n') break;
}
t++; // now the *t shall be the beginning of user payload
t2 = request_header("Content-Length"); // and the related header if there is
payload = t;
payload_size = t2 ? atol(t2) : (rcvd-(t-buf));
// bind clientfd to stdout, making it easier to write
clientfd = clients[n];
dup2(clientfd, STDOUT_FILENO);
close(clientfd);
// call router
route();
// tidy up
fflush(stdout);
shutdown(STDOUT_FILENO, SHUT_WR);
close(STDOUT_FILENO);
}
//Closing SOCKET
shutdown(clientfd, SHUT_RDWR); //All further send and recieve operations are DISABLED...
close(clientfd);
clients[n]=-1;
}
#ifndef _HTTPD_H___
#define _HTTPD_H___
#include <string.h>
#include <stdio.h>
//Server control functions
void serve_forever(const char *PORT);
// Client request
char *method, // "GET" or "POST"
*uri, // "/index.html" things before '?'
*qs, // "a=1&b=2" things after '?'
*prot; // "HTTP/1.1"
char *payload; // for POST
int payload_size;
char *request_header(const char* name);
// user shall implement this function
void route();
// some interesting macro for `route()`
#define ROUTE_START() if (0) {
#define ROUTE(METHOD,URI) } else if (strcmp(URI,uri)==0&&strcmp(METHOD,method)==0) {
#define ROUTE_GET(URI) ROUTE("GET", URI)
#define ROUTE_POST(URI) ROUTE("POST", URI)
#define ROUTE_END() } else printf(\
"HTTP/1.1 500 Not Handled\r\n\r\n" \
"The server has no handler to the request.\r\n" \
);
#endif
#include "httpd.h"
int main(int c, char** v)
{
serve_forever("12913");
return 0;
}
void route()
{
ROUTE_START()
ROUTE_GET("/")
{
printf("HTTP/1.1 200 OK\r\n\r\n");
printf("Hello! You are using %s", request_header("User-Agent"));
}
ROUTE_POST("/")
{
printf("HTTP/1.1 200 OK\r\n\r\n");
printf("Wow, seems that you POSTed %d bytes. \r\n", payload_size);
printf("Fetch the data using `payload` variable.");
}
ROUTE_END()
}
all: server
clean:
@rm -rf *.o
@rm -rf server
server: main.o httpd.o
gcc -o server $^
main.o: main.c httpd.h
gcc -c -o main.o main.c
httpd.o: httpd.c httpd.h
gcc -c -o httpd.o httpd.c
@sjm1982
Copy link

sjm1982 commented Mar 13, 2019

thank you so much

@foxweb
Copy link

foxweb commented Mar 26, 2019

\r\n? Unix?

@foxweb
Copy link

foxweb commented Mar 27, 2019

Please check it out https://github.com/foxweb/pico 🌚

@RomanStone
Copy link

RomanStone commented Apr 18, 2019

\r\n? Unix?

YES. [CRLF] (HTTP protocol, no matter Unix or other)
See https://tools.ietf.org/html/rfc7230
2.1. Client/Server Messaging
and https://tools.ietf.org/html/rfc2616

@Minssc
Copy link

Minssc commented Jun 10, 2019

slots are not freed since process is forked. once CONNMAX is reached, the program will hang

@chienminglo
Copy link

How can i payload? Payload is not working..

it looks "char* t" declare twice so payload data is not point correctly

@thuan-olli
Copy link

How can i payload? Payload is not working..

it looks "char* t" declare twice so payload data is not point correctly

I think we should change as below:
t = strtok(NULL, "\r\n");
instead of t++
at the line 168 http.c file.

@chientranse
Copy link

@laobubu hello my friend, what is the license on using this code.

@glenkleidon
Copy link

Thanks to Abijeet way back in 2012 and to you laobubu. I have been re-introducing myself to C after 30 years or so using this project.
I have forked the GIST and started to a new enterprise style project - so its too big for a GIST GitHub - pico. This will work out of the box with VSCode on Linux.

A number of important things to note:

  1. When forking, the file handles for ACCEPT and SOCKET need to be closed depending on the Parent or Child. On the server side, it no longer needs the accepted FD close(clients[slot] ) and the client no longer needs the listening FD close(listenfd)
if ( fork()==0 )
{
	// I am now the client - close the listener: client doesnt need it
	close(listenfd);
	respond(slot);
	exit(0);
} else
{
	// I am still the server - close the accepted handle: server doesnt need it.
	close(clients[slot]);
}

If you dont do this, the server will maintain file handle on the listener making it hard to restart the service and if you are still debugging, it will leave behind TCP close waits

  1. As someone already pointed out, A way is needed to communicate back to he parent that the slot is no longer busy and can be re-used.
  2. HTTP1.1 is KEEP-ALIVE by default. So, if the client does not disconnect, they may want to send a new request on the same connection. I am not 100% this is working right - I will update if this needs fixing.

overall- pretty nice workflow! BTW -it was pretty far sited to use that routing method back in 2012! That is pretty much the standard approach now.

Thanks for the effort!

@renehorstmann
Copy link

There might be a little overflow error while reading the header:
if "recv" recieves the complete buffer size of 65535, "rcvd" will be set to 65535, so:

"buf[rcvd] = '\0';"

would cause an overflow.

@mesaglio
Copy link

Hi! Greate job.

It would be great, that you resolve a small memory leak from buf variable.

You can just add one line, at the end of respond function like

free(buf);

@glenkleidon
Copy link

glenkleidon commented Oct 30, 2020 via email

@laobubu
Copy link
Author

laobubu commented Nov 8, 2020 via email

@nanigav
Copy link

nanigav commented Feb 15, 2021

\r\n? Unix?

It's not about Unix. That is the standard for http. You can send http messages from any device, AFAIK the standard only specifies message formatting and TCP connections.

@glenkleidon
Copy link

glenkleidon commented Feb 15, 2021 via email

@Kepsz
Copy link

Kepsz commented May 6, 2021

Hi, thank you for this code!
I was able to get it working on a Raspberry, using VS2019 with remote debug. But there are 2 things that needs to be corrected for a successful compile.

First: in line 131, buf = malloc(65535); -malloc needs casting. This line should be:
buf = (char *) malloc(65535);

Second: there are variables declared in the header that causes "multiple declared" errors in the linker. This should be avoided by copiying those variables into the c file, while adding "extern" to them in the header.

So these should be in the first part of the c file:

char* method,    // "GET" or "POST"
* uri,       // "/index.html" things before '?'
* qs,        // "a=1&b=2"     things after  '?'
* prot;      // "HTTP/1.1"

char* payload;     // for POST
int      payload_size;

While line 12..18 in the header should be:

extern char* method,    // "GET" or "POST"
* uri,       // "/index.html" things before '?'
* qs,        // "a=1&b=2"     things after  '?'
* prot;      // "HTTP/1.1"

extern char* payload;     // for POST
extern int      payload_size;

@ruanhao
Copy link

ruanhao commented Sep 30, 2023

another thing to note: make sure to flush stdout before calling serve_forever.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment