Un webserver non è altro che un server che "parla" il protocollo HTTP e risponde alle richieste secondo qualche logica. Da un lato c'è la parte di gestione delle connessioni - il server sta in ascolto su una porta TCP (tipicamente la porta 80) e "parla" con i client secondo il protocollo HTTP (di cui puoi trovare le specifiche fondamentalmente in questa RFC).
Il protocollo HTTP funziona a "richieste" e "risposte" secondo un protocollo stile telnet (anche qui, i dettagli stanno nelle RFC); il client fa richieste (ad esempio, GET /), il server risponde con uno status code (classici il 200 - OK; 404 - Not found e compagnia) ed eventualmente i dati corrispondenti.
Dall'altro lato, c'è la parte di con che logica gestire le richieste; un server HTTP semplice e che serve solo pagine statiche ha una directory (web root) in cui va a pescare i file che fornisce ai client. Nel momento in cui un client fa una richiesta (tipo GET /directory/nomefile.txt) il server va a vedere se nella web root c'è questo file, e in tal caso ne fornisce il contenuto al client.
Server web "veri" ovviamente sono molto più complessi - forniscono funzionalità di URL rewriting, gestione di più virtual host nello stesso server, pagine di errore personalizzate, e soprattutto pagine dinamiche, ovvero, in base a qualche logica (tipicamente l'estensione della pagina memorizzata nella web root, ma non solo) un certo file non viene fornito al client così com'è, ma viene eseguito o interpretato in qualche maniera e il risultato di questa elaborazione viene servito al client.

Il client, dall'altro lato, è un programma che apre una connessione a server web usando un socket; anche in questo caso deve seguire le specifiche del protocollo HTTP nel fare le richieste ai web server. In genere comunque è un lavoro molto più semplice - non devi gestire più connessioni in contemporanea e puoi implementare giusto il codice per creare le richieste che ti servono (tipicamente giusto GET).