Skip to content

cuistobal/42_Webserv

Repository files navigation

WebServ - Architecture Reactor Pattern

Vue d'ensemble

WebServ implémente un serveur HTTP/1.1 basé sur le Reactor Pattern avec I/O non-bloquant et multiplexage via epoll.


Architecture Globale

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
Loading

Event Loop - Reactor Pattern

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
Loading

Flux de Traitement des Requêtes

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])
Loading

Gestion des Stratégies de Réponse

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é"
Loading

Sélection Automatique de Stratégie

// 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());
}

Pattern DTO - ResponseContext

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"
Loading

Avantages:

  • ✅ Strategies testables isolément (mock facile)
  • ✅ Pas de couplage avec Config ou ClientRequest
  • ✅ Interface claire (on voit exactement quelles données sont utilisées)
  • ✅ Extensible (ajouter un champ ne casse pas les interfaces)

Design Patterns Utilisés

1. Reactor Pattern (Architecture globale)

  • 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

2. Strategy Pattern (Génération réponses)

  • 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

3. Builder Pattern (Construction objets)

  • ClientRequest: Builder avec fluent interface
  • ServerResponse: Builder avec sélection automatique de stratégie

4. Chain of Responsibility (Parsing HTTP)

  • StartLineHandlerHeadersHandlerBodyHandler
  • Chaque handler traite une partie et passe au suivant

5. Factory Pattern (Création stratégies)

  • ServerResponse::selectStrategy() crée la bonne stratégie selon l'URI

6. DTO Pattern (Découplage)

  • ResponseContext: Encapsule RequestContext + ServerContext
  • Découple strategies de Config et ClientRequest

Gestion des États

États du Client

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
Loading

États du Parsing HTTP

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
Loading

Gestion des Erreurs

Hiérarchie des Exceptions

std::exception
    └── ClientRequestException
            ├── ConnectionClosedException      // Client déconnecté
            ├── InvalidRequestException        // Requête malformée
            ├── TimeoutException               // Timeout dépassé
            └── PayloadTooLargeException       // Body trop large

Propagation dans le Reactor

sequenceDiagram
    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
Loading

Performance et Optimisations

Non-Blocking I/O

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
}

Gestion EPOLLOUT Dynamique

É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);
}

HTTP Pipelining

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());
        }
    }
};

Envois Partiels (Partial Sends)

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;
        }
    }
}

Configuration

Exemple de fichier de configuration

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;
}

Tests

Tests Unitaires Recommandés

# 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

Tests de Charge

# Apache Bench
ab -n 10000 -c 100 http://localhost:8080/

# Siege
siege -c 100 -t 30s http://localhost:8080/

Compilation et Exécution

# 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étriques du Projet

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)

Références


Auteurs

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)

About

A C++ webserver using HTTP/1.1 protocol

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors