Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
@debuti

This comment has been minimized.

Copy link

@debuti debuti commented Nov 22, 2018

Hello! What license is this code using?

Regards

@serefercelik

This comment has been minimized.

Copy link

@serefercelik serefercelik commented Feb 20, 2019

How can i payload? Payload is not working..

@sjm1982

This comment has been minimized.

Copy link

@sjm1982 sjm1982 commented Mar 13, 2019

thank you so much

@foxweb

This comment has been minimized.

Copy link

@foxweb foxweb commented Mar 26, 2019

\r\n? Unix?

@foxweb

This comment has been minimized.

Copy link

@foxweb foxweb commented Mar 27, 2019

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

@RomanStone

This comment has been minimized.

Copy link

@RomanStone 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

This comment has been minimized.

Copy link

@Minssc Minssc commented Jun 10, 2019

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

@chienminglo

This comment has been minimized.

Copy link

@chienminglo chienminglo commented Sep 11, 2019

How can i payload? Payload is not working..

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

@thuan-olli

This comment has been minimized.

Copy link

@thuan-olli thuan-olli commented Oct 8, 2019

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

This comment has been minimized.

Copy link

@chientranse chientranse commented Dec 18, 2019

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

@glenkleidon

This comment has been minimized.

Copy link

@glenkleidon glenkleidon commented Mar 3, 2020

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

This comment has been minimized.

Copy link

@renehorstmann renehorstmann commented Oct 20, 2020

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

This comment has been minimized.

Copy link

@mesaglio mesaglio commented Oct 29, 2020

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

This comment has been minimized.

Copy link

@glenkleidon glenkleidon commented Oct 30, 2020

@laobubu

This comment has been minimized.

Copy link
Owner Author

@laobubu laobubu commented Nov 8, 2020

@VillainMan

This comment has been minimized.

Copy link

@VillainMan VillainMan 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

This comment has been minimized.

Copy link

@glenkleidon glenkleidon commented Feb 15, 2021

@Kepsz

This comment has been minimized.

Copy link

@Kepsz 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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment