JEE Back-end

Rappels HTTP et Servlet

Une vue d'ensemble

Les contrôleurs

Les contrôleurs gérent la communication avec les clients.

  • contrôleurs Web pour les clients légers,
  • contrôleurs Rest pour les clients lourds.

La communication se fait en utilisant le protocole HTTP.

Dans cette partie ...

Nous allons brièvement rappeler le fonctionnement du protocole HTTP.

Ensuite nous verrons comment les Servlet permettent d'écrire des serveurs Web en Java.

Il ne sont pas directement manipulés en Spring mais ils seront présents sous le capot.

Rappels HTTP

Le protocole HTTP fonctionne sur un principe requête-réponse.

  • Le client envoie une requête au serveur
  • Le serveur renvoie sa réponse

Le protocole HTTP est stateless. C'est à dire qu'il ne conserve aucune information sur les requêtes précédentes.

HTTP Request

GET /toto.html HTTP/1.1
Host: localhost:8989
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: username-localhost-8888="2|1:0|10:1635022841...
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1	

Structure de la requête:

  • Request-line : Method Ressource HTTP-Version
  • Header : Suite lignes au format "clé : valeur"
  • Body : Suite d'octets (possiblement vide)

Le header contient les informations nécessaires pour décoder le body.

Plusieurs méthodes possibles : GET, POST, PUT, PATCH, DELETE ...

HTTP Response (1/2)

$curl -i -X GET http://www.google.com/

HTTP/1.1 200 OK
Date: Mon, 25 Oct 2021 22:36:26 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
Server: gws
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=511=As...ODLU; path=/; domain=.google.com; HttpOnly
Accept-Ranges: none
Vary: Accept-Encoding
Transfer-Encoding: chunked

<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage"...

HTTP Response (2/2)

Structure de la réponse:

  • Status-line : HTTP-Version Code Message
  • Header : Suite lignes au format "clé : valeur"
  • Body : Suite d'octets (possiblement vide)

Exemples:

  • 200 OK
  • 404 NOT FOUND
  • ...

HTTP dans le browser (1/3)

  • le navigateur établit une connexion avec le serveur web www.univ-gustave-eiffel.fr
  • il envoie la requête suivante:
    GET /universite/qui-sommes-nous/ HTTP/1.1
    Host: www.univ-gustave-eiffel.fr
    User-Agent: ...
    
  • Le serveur renvoie un réponse contenant la page à afficher.

HTTP dans le browser (2/3)

Quand on clique sur le bouton Compute, le navigateur envoie la requête POST suivante:

POST /App/rectangle HTTP/1.1
...
content-length : 17
content-type : application/x-www-form-urlencoded

height=15&width=5

HTTP dans le browser (3/3)

Les données du formulaire sont encodées dans le body de la requête.

POST /App/rectangle HTTP/1.1
...
height=15&width=5

La ressource du POST et les noms des champs viennent du formulaire.

<form action="/App/rectangle" method="post">
<h2>Rectangle area calculator</h2>
    <div><label for="h">Height:</label>
        <input id="h" type="text" name="height"></div>

    <div><label for="w">Width:</label>
        <input type="text" id="w" name="width"></div>

    <div><button type="submit">Compute</button></div>

Serveur WEB en JEE

La classe Servlet de JEE permet d'écrire le code qui transforme une requête HTTP en la réponse correspondante.

JEE donne une spécification que doivent respecter les serveurs Web qui exécutent ce code. La version courante de la spécification est Jakarta Servlet Specification, Version 5.0

Ils existent plusieurs serveurs web respectant la Jakarta Servlet Specification: Tomcat, JBoss, Glassfish ...

Par défaut, Spring utilisse Tomcat et c'est donc le serveur web avec lequel nous allons jouer pendant le TP.

Mon premier servlet

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        writer.println("<!DOCTYPE html><html><h1>Hello world!</h1></html>");
        writer.flush();
    }
}
  • HttpServletRequest représent la requête entrante et HttpServletResponse représente la réponse sortante.
  • L'annotation @WevServlet("/hello") spécifie la route des requêtes qui vont être traitées par ce Servlet.
  • Ce servlet ne traite que le requête HTTP GET. Pour les requêtes POST, il faudrait redéfinir doPost.

La réponse vue par le navigateur ? (1/2)

Il faut bien se souvenir que le body de notre réponse n'est qu'une suite d'octets. Le navigateur regarde dans les headers de la réponse (i.e., Content-Type) pour savoir comment l'interpréter.

Quand il n'y pas d'information, il devine:

  • Firefox devine que c'est du HTML,
  • Chrome l'affiche comme du texte.

La réponse vue par le navigateur ? (2/2)

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html ; charset=utf-8");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.println("<!DOCTYPE html><html><h1>Hello world!</h1></html>");
        writer.flush();
    }
}
  • La première ligne sert à indiquer dans le header de la réponse que le body est du HTML encodé en UTF-8.
  • La seconde ligne dit à notre Servlet d'écrire le body encodé en UTF-8.
  • Il faut les deux lignes !

Mais où est le serveur dans tout cela ?

  • Il faut installer un serveur compatible avec la Jakarta Servlet Specification. Dans notre cas (et pour Spring par défaut), nous utiliserons Tomcat 10.
  • Il faut démarrer le serveur !
  • Il faut compiler notre servlet et le packager dans un App.war (similaire au jar).
  • Il faut copier le war dans le dossier webapps de Tomcat.
  • Enfin on peut se connecter à Tomcat sur localhost:8080 et en demandant la resource /App/hello, on parle avec notre HelloServlet.

Les servlets doivent être thread-safe.

La spécification dit qu'une seule Servlet est instanciée par le serveur (i.e. Tomcat ici).

Donc toutes les requêtes sur la route /hello sont traitées par le même objet HelloServlet.

Par contre, les requêtes vont être traitées dans des threads différents!

Donc l'objet HelloServlet doit être thread-safe.

Tomcat 10

Exemple non thread-safe

@WebServlet("/hellocount")
public class HelloCountServletWrong extends HttpServlet {
    private int count = 0;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        count++;
        response.setContentType("text/html ; charset=utf-8");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.println("<!DOCTYPE html><html><h1>Hello world! ("+count+"th times)</h1></html>");
        writer.flush();
    }

}	

Data-race sur le champ count!

Exemple thread-safe

@WebServlet("/hellocount")
public class HelloCountServlet extends HttpServlet {

    private final Object lock = new Object();
    private int count = 0;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        var countSnaptshot=0;
        synchronized (lock){
            countSnaptshot=++count;
        }
        response.setContentType("text/html ; charset=utf-8");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.println("<!DOCTYPE html><html><h1>Hello world!"+ 
      		"("+countSnaptshot+"th times)"+     
        	"</h1></html>");
        writer.flush();
    }

}

Pourquoi faire un snapshot ne pas garder le lock pendant l'appel à writer.println ?

HTTP est stateless

Stateless veut dire qu'aucune information sur les requêtes précédentes n'est stockée par le protocole.
Toute est dans la requête !

Alors comment s'avoir:

  • si un utilisateur est authentifié ?
  • quels sont les objets du panier d'un utilisateur ?
  • ...

Cookies !

On utilise Cookie qui est un champ des headers qui est une suite d'association clé=valeur séparées par des point-virgules.



Si dans une réponse HTTP, on inclue un header un champ:

...
Set-Cookie: montokenamoi=AZADAZEZFSDFSD
...

Le navigateur web qui a reçu cette réponse, mettra ce cookie dans le champ dans ses prochaines requêtes vers cette application.

...
Cookie: montokenamoi=AZADAZEZFSDFSD
...

En pratique ...

Le Servlet garde l'association entre les tokens qu'il a déjà donnés et les infos de ce visiteur.
Soit dans une Hashmap du Servlet soit en BD (sur une déploiement distribué pas le choix BD).

Quand un requête arrive:

  • Si elle a un token dans ses cookies et que le token est connu, on retrouve les infos du visiteurs!
  • Si elle n'a pas de token, on tire au hasard un nouveau token par exemple avec UUID.randomUUID jusqu'à avoir un token qu'on n'a pas déjà donné et on le met le champ Cookie de la réponse.
  • Si elle a un token dans ses cookies et que le token est inconnu, on lui donne un nouveau token comme si c'était un nouveau client.

Pourquoi pas des tokens séquentiels ?