WebServ implémente un serveur HTTP/1.1 basé sur le Reactor Pattern avec I/O non-bloquant et multiplexage via epoll.
classDiagram
%% Core Reactor Pattern
class EventHandler {
<<interface>>
+handleRead() void
+handleWrite() void
+handleError() void
+getHandle() int
}
class Reactor {
-map~int, EventHandler*~ _handlers
-EpollManager _epollManager
-bool _running
+run() void
+registerHandler(handler: EventHandler*, events: uint32_t) void
+modifyHandler(fd: int, events: uint32_t) void
+removeHandler(fd: int) void
-dispatch(event: epoll_event) void
}
class EpollManager {
-int _epollFd
-int _maxEvents
+add(fd: int, events: uint32_t) bool
+modify(fd: int, events: uint32_t) bool
+remove(fd: int) bool
+wait(timeout: int) vector~epoll_event~
}
%% Concrete Handlers
class AcceptHandler {
-int _listenFd
-Server* _server
-Reactor* _reactor
+handleRead() void
+handleWrite() void
+handleError() void
+getHandle() int
}
class ClientHandler {
-Client* _client
-Server* _server
-Reactor* _reactor
-deque~string~ _writeQueue
-size_t _bytesSent
+handleRead() void
+handleWrite() void
+handleError() void
+enableWriting() void
+disableWriting() void
-generateResponse() void
-cleanup() void
}
%% Server & Client
class Server {
-int _serverFd
-Reactor* _reactor
-Config _config
-map~int, Client*~ _clients
+startWithReactor() void
+stopReactor() void
+addClientToMap(fd: int, client: Client*) void
+removeClientFromMap(fd: int) void
+getConfig() Config&
}
class Client {
-int _fd
-HttpParser _parser
-deque~ClientRequest~ _requestQueue
+receiveData(buffer: char*, len: size_t) void
+hasRequests() bool
+getRequest() ClientRequest&
+getFd() int
}
%% HTTP Processing
class HttpParser {
-ParseState _state
-StartLineHandler _startLineHandler
-HeadersHandler _headersHandler
-BodyHandler _bodyHandler
-IBodyStrategy* _bodyStrategy
+parse(buffer: char*, len: size_t) ParseResult
+isComplete() bool
+getRequest() ClientRequest
}
class ClientRequest {
-string _method
-string _uri
-string _version
-map~string, string~ _headers
-string _body
-bool _built
+withMethod(method: string) ClientRequest&
+withUri(uri: string) ClientRequest&
+withBody(body: string) ClientRequest&
}
class ServerResponse {
-unsigned short _statusCode
-string _statusMessage
-string _headers
-string _body
-IResponseStrategy* _strategy
-bool _ownsStrategy
+withRequest(request: ClientRequest&, config: Config&) ServerResponse&
+withError(exception: ClientRequestException&) ServerResponse&
+build() ServerResponse&
-selectStrategy() bool
}
%% Response Strategies
class IResponseStrategy {
<<interface>>
+execute(ctx: ResponseContext&, ...) bool
+getStrategyName() string
}
class StaticFileStrategy {
+execute(ctx: ResponseContext&, ...) bool
}
class CgiStrategy {
+execute(ctx: ResponseContext&, ...) bool
}
class ErrorStrategy {
+execute(ctx: ResponseContext&, ...) bool
}
class AutoIndexStrategy {
+execute(ctx: ResponseContext&, ...) bool
}
%% Response Context (DTO)
class ResponseContext {
+RequestContext request
+ServerContext server
+fromRequestAndConfig(req, cfg)$ ResponseContext
}
class RequestContext {
+string method
+string uri
+string httpVersion
+map~string, string~ headers
+string body
}
class ServerContext {
+string rootPath
+unsigned int maxBodySize
+bool autoindexEnabled
+string indexFile
+map~int, string~ errorPages
}
%% Relations
Reactor --> EpollManager : utilise
Reactor --> EventHandler : dispatche
EventHandler <|.. AcceptHandler : implémente
EventHandler <|.. ClientHandler : implémente
AcceptHandler --> Server : utilise
AcceptHandler --> Reactor : enregistre ClientHandler
ClientHandler --> Client : gère I/O
ClientHandler --> Server : utilise
ClientHandler --> Reactor : modifie events
Server --> Reactor : possède
Server --> Client : gère (1→*)
Client --> HttpParser : utilise
HttpParser --> ClientRequest : produit
ClientHandler --> ServerResponse : génère
ServerResponse --> IResponseStrategy : délègue
IResponseStrategy <|.. StaticFileStrategy : implémente
IResponseStrategy <|.. CgiStrategy : implémente
IResponseStrategy <|.. ErrorStrategy : implémente
IResponseStrategy <|.. AutoIndexStrategy : implémente
IResponseStrategy --> ResponseContext : utilise
ResponseContext *-- RequestContext : contient
ResponseContext *-- ServerContext : contient
sequenceDiagram
participant M as main()
participant S as Server
participant R as Reactor
participant E as EpollManager
participant AH as AcceptHandler
participant CH as ClientHandler
participant C as Client
participant P as HttpParser
participant SR as ServerResponse
participant ST as Strategy
%% Initialisation
M->>S: startWithReactor()
S->>S: createSocket() → listen_fd
S->>R: new Reactor()
S->>AH: new AcceptHandler(listen_fd, server, reactor)
S->>R: registerHandler(AcceptHandler, EPOLLIN)
R->>E: add(listen_fd, EPOLLIN)
%% Event Loop
loop Event Loop Infini
S->>R: run()
R->>E: wait(timeout) → events[]
alt Nouvelle Connexion (EPOLLIN sur listen_fd)
E-->>R: event{fd=listen_fd, EPOLLIN}
R->>R: dispatch(event)
R->>AH: handleRead()
AH->>AH: accept() → client_fd
AH->>C: new Client(client_fd)
AH->>CH: new ClientHandler(client, server, reactor)
AH->>R: registerHandler(ClientHandler, EPOLLIN|EPOLLRDHUP)
R->>E: add(client_fd, EPOLLIN|EPOLLRDHUP)
AH->>S: addClientToMap(client_fd, client)
else Données Client (EPOLLIN sur client_fd)
E-->>R: event{fd=client_fd, EPOLLIN}
R->>R: dispatch(event)
R->>CH: handleRead()
CH->>C: receiveData() → recv()
C->>P: parse(buffer, len)
alt Requête Complète
P-->>C: ParseResult::COMPLETE
C->>C: _requestQueue.push(request)
CH->>C: hasRequests() → true
CH->>C: getRequest() → ClientRequest
CH->>CH: generateResponse()
CH->>SR: new ServerResponse()
CH->>SR: withRequest(request, config)
SR->>SR: selectStrategy()
alt URI = /cgi-bin/*
SR->>ST: new CgiStrategy()
else URI ends with /
SR->>ST: new AutoIndexStrategy()
else Fichier statique
SR->>ST: new StaticFileStrategy()
end
CH->>SR: build()
SR->>ST: execute(ResponseContext)
ST-->>SR: body, headers, status
SR-->>CH: response string
CH->>CH: _writeQueue.push(response)
CH->>CH: enableWriting()
CH->>R: modifyHandler(fd, EPOLLIN|EPOLLOUT|EPOLLRDHUP)
R->>E: modify(client_fd, EPOLLIN|EPOLLOUT)
else Parsing en cours
P-->>C: ParseResult::PARSING
end
else Prêt à Écrire (EPOLLOUT sur client_fd)
E-->>R: event{fd=client_fd, EPOLLOUT}
R->>R: dispatch(event)
R->>CH: handleWrite()
CH->>CH: _writeQueue.front() + _bytesSent
CH->>CH: send(fd, data, MSG_DONTWAIT)
alt Envoi complet
CH->>CH: _writeQueue.pop()
CH->>CH: _bytesSent = 0
alt Queue vide
CH->>CH: disableWriting()
CH->>R: modifyHandler(fd, EPOLLIN|EPOLLRDHUP)
R->>E: modify(client_fd, EPOLLIN)
end
else Envoi partiel (EAGAIN)
CH->>CH: _bytesSent += sent
Note over CH: Attend prochain EPOLLOUT
end
else Erreur/Déconnexion (EPOLLERR | EPOLLHUP)
E-->>R: event{fd=client_fd, EPOLLERR}
R->>R: dispatch(event)
R->>CH: handleError()
CH->>CH: cleanup()
CH->>R: removeHandler(client_fd)
R->>E: remove(client_fd)
CH->>S: removeClientFromMap(client_fd)
CH->>C: delete client
CH->>CH: delete this
end
end
flowchart TD
Start([Client envoie requête]) --> Recv[recv dans handleRead]
Recv --> Parse[HttpParser::parse]
Parse --> ParseCheck{Parsing complet?}
ParseCheck -->|Non| WaitMore[Attendre plus de données]
WaitMore --> Start
ParseCheck -->|Oui| Queue[Ajouter à _requestQueue]
Queue --> GenResp[generateResponse]
GenResp --> CreateResp[new ServerResponse]
CreateResp --> WithReq[withRequest request, config]
WithReq --> SelectStrat[selectStrategy]
SelectStrat --> StratCheck{Type de requête?}
StratCheck -->|/cgi-bin/*| CGI[CgiStrategy]
StratCheck -->|URI ends with /| Auto[AutoIndexStrategy]
StratCheck -->|Fichier| Static[StaticFileStrategy]
StratCheck -->|Erreur| Error[ErrorStrategy]
CGI --> Build[build]
Auto --> Build
Static --> Build
Error --> Build
Build --> Execute[strategy→execute]
Execute --> FillResp[Remplir statusCode, headers, body]
FillResp --> ToString[Construire réponse HTTP complète]
ToString --> PushQueue[_writeQueue.push response]
PushQueue --> EnableWrite[enableWriting]
EnableWrite --> ModEpoll[Reactor::modifyHandler EPOLLOUT]
ModEpoll --> WaitWrite[Attendre EPOLLOUT]
WaitWrite --> HandleWrite[handleWrite appelé]
HandleWrite --> Send[send avec MSG_DONTWAIT]
Send --> SendCheck{Tout envoyé?}
SendCheck -->|Non| PartialSend[_bytesSent += sent]
PartialSend --> WaitWrite
SendCheck -->|Oui| PopQueue[_writeQueue.pop]
PopQueue --> QueueEmpty{Queue vide?}
QueueEmpty -->|Non| WaitWrite
QueueEmpty -->|Oui| DisableWrite[disableWriting]
DisableWrite --> ModEpollIn[Reactor::modifyHandler EPOLLIN]
ModEpollIn --> End([Attendre prochaine requête])
classDiagram
class IResponseStrategy {
<<interface>>
+execute(ctx: ResponseContext&, statusCode&, statusMessage&, headers&, body&) bool
+getStrategyName() string
}
class StaticFileStrategy {
-getMimeType(extension: string) string
-resolveFilePath(uri: string, root: string) string
+execute(ctx: ResponseContext&, ...) bool
}
class CgiStrategy {
-getInterpreter(extension: string) string
-setupEnvironment(ctx: ResponseContext) map~string, string~
-executeCgi(scriptPath: string, env: map, body: string) string
+execute(ctx: ResponseContext&, ...) bool
}
class AutoIndexStrategy {
-resolveDirectoryPath(uri: string, root: string) string
-generateHtml(path: string, uri: string) string
-formatSize(bytes: size_t) string
-formatTime(time: time_t) string
+execute(ctx: ResponseContext&, ...) bool
}
class ErrorStrategy {
-int _errorCode
-string _errorMessage
-generateDefaultErrorPage(code: int, msg: string) string
-loadCustomErrorPage(path: string) string
+execute(ctx: ResponseContext&, ...) bool
}
IResponseStrategy <|.. StaticFileStrategy
IResponseStrategy <|.. CgiStrategy
IResponseStrategy <|.. AutoIndexStrategy
IResponseStrategy <|.. ErrorStrategy
note for StaticFileStrategy "Sert fichiers .html, .css, .js, images\n20+ MIME types supportés\nGestion permissions et 404"
note for CgiStrategy "Exécute .py, .php, .pl, .sh, .rb\nfork + execve + socketpair(AF_UNIX, SOCK_STREAM)\nVariables CGI complètes"
note for AutoIndexStrategy "Génère listings HTML\nAffiche les entrées telles que renvoyées par le système (aucun tri garanti actuellement)\nAffichage taille et date"
note for ErrorStrategy "Pages d'erreur 4xx/5xx\nSupport pages personnalisées\nHTML par défaut stylé"
// Dans ServerResponse::selectStrategy()
if (uri.find("/cgi-bin/") != string::npos ||
uri.ends_with(".py") || uri.ends_with(".php")) {
setStrategy(new CgiStrategy());
}
else if (uri.ends_with("/")) {
if (config.autoindexEnabled()) {
setStrategy(new AutoIndexStrategy());
} else {
setStrategy(new StaticFileStrategy()); // index.html
}
}
else {
setStrategy(new StaticFileStrategy());
}Pour découpler les stratégies de Config et ClientRequest, on utilise un Data Transfer Object:
classDiagram
class ResponseContext {
+RequestContext request
+ServerContext server
+fromRequestAndConfig(req, cfg)$ ResponseContext
}
class RequestContext {
+string method
+string uri
+string httpVersion
+map~string, string~ headers
+string body
}
class ServerContext {
+string rootPath
+unsigned int maxBodySize
+bool autoindexEnabled
+string indexFile
+map~int, string~ errorPages
+map~string, string~ cgiInterpreters
}
ResponseContext *-- RequestContext : contient
ResponseContext *-- ServerContext : contient
note for ResponseContext "Factory method adapte\nConfig + ClientRequest\nvers structures simples"
note for RequestContext "Données HTTP uniquement\nPas de dépendances"
note for ServerContext "Configuration uniquement\nPas de dépendances"
Avantages:
- ✅ Strategies testables isolément (mock facile)
- ✅ Pas de couplage avec
ConfigouClientRequest - ✅ Interface claire (on voit exactement quelles données sont utilisées)
- ✅ Extensible (ajouter un champ ne casse pas les interfaces)
- Reactor: Gère l'event loop et dispatche les événements
- EventHandler: Interface pour tous les handlers
- AcceptHandler: Gère les nouvelles connexions
- ClientHandler: Gère I/O bidirectionnel des clients
- IResponseStrategy: Interface pour les stratégies de réponse
- StaticFileStrategy: Sert fichiers statiques
- CgiStrategy: Exécute scripts CGI
- AutoIndexStrategy: Génère listings de répertoires
- ErrorStrategy: Génère pages d'erreur
- ClientRequest: Builder avec fluent interface
- ServerResponse: Builder avec sélection automatique de stratégie
- StartLineHandler → HeadersHandler → BodyHandler
- Chaque handler traite une partie et passe au suivant
ServerResponse::selectStrategy()crée la bonne stratégie selon l'URI
- ResponseContext: Encapsule
RequestContext+ServerContext - Découple strategies de
ConfigetClientRequest
stateDiagram-v2
[*] --> Connected: accept()
Connected --> ReadingRequest: EPOLLIN
ReadingRequest --> ReadingRequest: recv() partial
ReadingRequest --> ProcessingRequest: Request complete
ProcessingRequest --> WritingResponse: Response ready
WritingResponse --> WritingResponse: send() partial (EAGAIN)
WritingResponse --> Connected: send() complete
WritingResponse --> [*]: EPOLLHUP/EPOLLERR
Connected --> [*]: Client disconnect
ReadingRequest --> [*]: EPOLLHUP/EPOLLERR
stateDiagram-v2
[*] --> PARSE_START_LINE
PARSE_START_LINE --> PARSE_HEADERS: Start line complete
PARSE_HEADERS --> PARSE_BODY: Headers complete
PARSE_BODY --> PARSE_COMPLETE: Body complete
PARSE_COMPLETE --> [*]
note right of PARSE_START_LINE
Parse: GET /index.html HTTP/1.1
end note
note right of PARSE_HEADERS
Parse: Host: localhost
Content-Length: 42
end note
note right of PARSE_BODY
Stratégie selon headers:
- ContentLengthStrategy
- ChunkedStrategy
- NoBodyStrategy
end note
std::exception
└── ClientRequestException
├── ConnectionClosedException // Client déconnecté
├── InvalidRequestException // Requête malformée
├── TimeoutException // Timeout dépassé
└── PayloadTooLargeException // Body trop largesequenceDiagram
participant CH as ClientHandler
participant C as Client
participant P as HttpParser
participant SR as ServerResponse
participant R as Reactor
CH->>C: receiveData()
C->>P: parse()
alt Exception levée
P-->>C: throw InvalidRequestException
C-->>CH: throw
CH->>SR: withError(exception)
SR->>SR: setStrategy(new ErrorStrategy(400))
SR->>SR: build()
CH->>CH: _writeQueue.push(errorResponse)
CH->>CH: enableWriting()
else Exception fatale
P-->>C: throw ConnectionClosedException
C-->>CH: throw
CH->>CH: cleanup()
CH->>R: removeHandler()
CH->>CH: delete this
end
Tous les sockets sont non-bloquants:
// Lors de accept()
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// Lors de send()
ssize_t sent = send(fd, data, len, MSG_DONTWAIT);
if (sent == -1 && errno == EAGAIN) {
// Normal, buffer plein, réessayer au prochain EPOLLOUT
}Évite le busy-wait à 100% CPU:
// Après génération réponse
enableWriting(); // Active EPOLLOUT
reactor->modifyHandler(fd, EPOLLIN | EPOLLOUT | EPOLLRDHUP);
// Après envoi complet
if (_writeQueue.empty()) {
disableWriting(); // Désactive EPOLLOUT
reactor->modifyHandler(fd, EPOLLIN | EPOLLRDHUP);
}Support de plusieurs requêtes simultanées par client:
class ClientHandler {
std::deque<std::string> _writeQueue; // Queue de réponses
void handleRead() {
// Parse requête 1
_writeQueue.push(response1);
// Parse requête 2 (pipelined)
_writeQueue.push(response2);
enableWriting();
}
void handleWrite() {
// Envoie réponses dans l'ordre FIFO
while (!_writeQueue.empty()) {
send(_writeQueue.front());
}
}
};Gestion du cas où send() n'envoie pas tout:
size_t _bytesSent = 0;
void handleWrite() {
std::string& data = _writeQueue.front();
const char* ptr = data.c_str() + _bytesSent;
size_t remaining = data.size() - _bytesSent;
ssize_t sent = send(fd, ptr, remaining, MSG_DONTWAIT);
if (sent > 0) {
_bytesSent += sent;
if (_bytesSent >= data.size()) {
_writeQueue.pop();
_bytesSent = 0;
}
}
}server {
listen 8080;
server_name localhost;
root /var/www/html;
index index.html;
client_max_body_size 10M;
location / {
autoindex on;
allowed_methods GET POST DELETE;
}
location /cgi-bin/ {
cgi_pass python3;
allowed_methods GET POST;
}
error_page 404 /errors/404.html;
error_page 500 /errors/500.html;
}# Test accept
curl http://localhost:8080/
# Test fichier statique
curl http://localhost:8080/index.html
# Test autoindex
curl http://localhost:8080/uploads/
# Test CGI
curl -X POST http://localhost:8080/cgi-bin/script.py -d "param=value"
# Test erreur 404
curl http://localhost:8080/nonexistent
# Test partial send (grande réponse)
curl http://localhost:8080/large_file.bin > /dev/null
# Test pipelining
printf "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nGET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n" | nc localhost 8080# Apache Bench
ab -n 10000 -c 100 http://localhost:8080/
# Siege
siege -c 100 -t 30s http://localhost:8080/# Compilation
make clean && make
# Exécution
./WebServ configs/default.conf
# Avec Valgrind (détection fuites mémoire)
valgrind --leak-check=full --show-leak-kinds=all ./WebServ configs/default.conf| Métrique | Valeur |
|---|---|
| Lignes de code | ~5000 |
| Fichiers source | 45+ |
| Classes | 25+ |
| Design patterns | 6 |
| Handlers Reactor | 3 (Accept, Client, CGI) |
| Stratégies réponse | 4 (Static, CGI, AutoIndex, Error) |
| MIME types supportés | 20+ |
| Interpréteurs CGI | 5 (Python, PHP, Perl, Shell, Ruby) |
- RFC 7230 - HTTP/1.1 Message Syntax
- RFC 3875 - CGI Specification
- Reactor Pattern - POSA2
- epoll man page
- Non-blocking I/O
Projet réalisé dans le cadre de l'école 42.
Architecture: Reactor Pattern avec I/O non-bloquant
Parsing: Incrémental avec Chain of Responsibility
Réponses: Strategy Pattern avec 4 stratégies
Découplage: DTO Pattern (ResponseContext)