Le protocole HTTP en quelques mots
- HTTP (HyperText Transport Protocole) : protocole applicatif utilisé sur le web et employé également pour la communication d'applications Android avec des serveurs
- Repose historiquement sur le protocole de transport TCP assurant la retransmission des paquets en cas de de perte
-
Différentes versions de HTTP :
- HTTP 0.9 : première version du protocole pour le World Wide Web développé par Tim Berners-Lee et Robert Caillau au CERN au début des années 1990
-
HTTP 1.0 : 1ère version faisant l'objet d'une description dans une RFC
- Introduction de l'en-tête Host dans la requête permettant d'indiquer le domaine du serveur (permet l'hébergement de plusieurs domaine sur un même serveur web)
- HTTP 1.1 : version introduisant les connexions persistantes permettant de réaliser plusieurs requêtes sur la même connexion TCP (en-tête Connection: Keep-Alive)
- HTTP 2 : nouvelle version majeure de HTTP (1995) basée sur le protocole SPDY de Google assurant la compression des données ainsi que le multiplexage de requêtes sur une seule connexion TCP
-
HTTP 3 : version en cours de normalisation basée sur le protocole QUIC de Google
- Utilisation du protocole UDP au lieu de TCP avec réimplantation applicative (en espace utilisateur) de mécanismes assurant la fiabilité de la transmission (codes correcteurs d'erreurs, retransmission...)
- Principal intérêt : réduction du temps d'établissement des connexions (appréciable sur les liens de communication à forte latence)
- Support du multi-homing (émission des paquets depuis plusieurs adresses IP)
Remarque : la 1ère version de HTTP est simple et peut être implantée "à la main" en utilisant des sockets TCP. Les versions suivantes (2 et 3) sont beaucoup plus complexes, l'utilisation d'une bibliothèque implantant le protocole est incontournable.
Page pour en savoir plus sur le protocole HTTP
Exemple d'utilisation de HttpURLConnection
Réalisation d'une activité manipulant des compteurs d'entiers avec l'API web countapi.xyz.
Support de deux opérations :
- Récupération de la valeur d'un compteur
- Incrémentation de la valeur d'un compteur
Implantation de méthodes statiques réalisant ces deux opérations avec HttpURLConnection :
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.webcounter; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** Definition of useful methods to handle the counter API from CountApi.xyz on Android * We use the HttpURLConnection API froom the java.net package */ public class CountApiUtils { public static final int BUFFER_SIZE = 2048; public static final Charset CHARSET = StandardCharsets.UTF_8; public static final String NAMESPACE = "coursand"; /** This call does not follow the REST philosophy since it supports the GET method; * whereas the GET method should not induce server-side modifications */ public static final String INCREMENT_URL = "https://api.countapi.xyz/hit/:namespace/:key"; public static final String GET_URL = "https://api.countapi.xyz/get/:namespace/:key"; public static JSONObject makeHttpRequestWithJSONResult(String url) throws IOException, JSONException { URL u = new URL(url); HttpURLConnection connection = (HttpURLConnection)u.openConnection(); connection.connect(); if (connection.getResponseCode() == 200) { try (InputStream input = connection.getInputStream()) { // Accumulate all the data in memory ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[BUFFER_SIZE]; for (int r = input.read(buffer); r != -1; r = input.read(buffer)) baos.write(buffer, 0, r); // write into the baos the read buffer byte[] data = baos.toByteArray(); // Convert using the default charset to a string String dataStr = new String(data, 0, data.length, CHARSET); // Parse the string as JSON JSONObject json = new JSONObject(dataStr); return json; } } else throw new IOException("HTTP error " + connection.getResponseCode()); } public static class CounterApiException extends Exception { public CounterApiException(Throwable cause) { super(cause); } } private static String makeURL(String base, String key) { String url = base.replace(":namespace", NAMESPACE).replace(":key", key); return url; } /** Increment a counter and return the value of the incremented counter */ public static int incrementCounter(String key) throws CounterApiException { try { JSONObject result = makeHttpRequestWithJSONResult(makeURL(INCREMENT_URL, key)); return result.getInt("value"); } catch (IOException|JSONException e) { throw new CounterApiException(e); } } /** Fetch the value of a counter (return -1 if the counter does not exist) */ public static int fetchCounter(String key) throws CounterApiException { try { JSONObject result = makeHttpRequestWithJSONResult(makeURL(GET_URL, key)); if (result.get("value") == null) return -1; // non existing value return result.getInt("value"); } catch (IOException|JSONException e) { throw new CounterApiException(e); } } }
Ensuite nous réalisons l'activité WebCounterActivity utilisant les deux méthodes. Les méthodes réalisées sont synchrones et bloquent donc lors de la communication HTTP : nous les appelons depuis la méthode doInBackground d'une AsyncTask afin qu'elles soient exécutées dans une thread secondaire.
⚠ La classe telle qu'elle est implantée ne supporte pas la rotation par destruction et recréation d'activité
- Les données du TextView de résultat sont perdues
- Une AsyncTask démarrant juste avant la rotation et se terminant sous le règne de la nouvelle instance d'activité causerait des soucis
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.webcounter; import android.Manifest; import android.os.AsyncTask; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import java.util.Date; import fr.upem.coursand.R; import fr.upem.coursand.launcher.ActivityMetadata; /** Activity handling a web counter with 2 operations: get and increment * TODO: support screen rotation (the activity is destroyed and restarted) */ @ActivityMetadata(permissions={Manifest.permission.INTERNET}) public class WebCounterActivity extends AppCompatActivity { private EditText counterKeyEditText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_counter); counterKeyEditText = findViewById(R.id.counterKeyView); } public void appendText(String message) { TextView tv = findViewById(R.id.counterResultView); tv.append(new Date().toString() + ": " + message); } private String getKey() { String key = counterKeyEditText.getText().toString().trim(); if (key.isEmpty()) { Toast.makeText(this, "Key must not be empty", Toast.LENGTH_SHORT).show(); return null; } else return key; } /** Get the counter value */ public void onGetClick(View v) { AsyncTask<String, Void, Integer> task = new AsyncTask<String, Void, Integer>() { private CountApiUtils.CounterApiException exception = null; private String key; @Override protected Integer doInBackground(String... strings) { this.key = strings[0]; int result = -1; try { result = CountApiUtils.fetchCounter(this.key); } catch (CountApiUtils.CounterApiException e) { exception = e; } return result; } @Override protected void onPostExecute(Integer result) { if (result == -1 && exception == null) appendText("Key " + key + " does not exist\n"); else if (result == -1 && exception != null) appendText("Exception encountered: " + exception.toString() + "\n"); else appendText(key + "=" + result + "\n"); } }; String key = getKey(); if (key != null) task.execute(key); } /** Get the counter value */ public void onIncrementClick(View v) { AsyncTask<String, Void, Integer> task = new AsyncTask<String, Void, Integer>() { private CountApiUtils.CounterApiException exception = null; private String key; @Override protected Integer doInBackground(String... strings) { this.key = strings[0]; int result = -1; try { result = CountApiUtils.incrementCounter(this.key); } catch (CountApiUtils.CounterApiException e) { exception = e; } return result; } @Override protected void onPostExecute(Integer result) { if (result == -1 && exception != null) appendText("Exception encountered: " + exception.toString() + "\n"); else appendText(key + " incremented to " + result + "\n"); } }; String key = getKey(); if (key != null) task.execute(key); } }
Exemple d'utilisation de OkHttp
- OkHttp : bibliothèque HTTP cliente développée par Square
- Support de communications en mode synchrone et asynchrone (avec utilisation de fonctions de rappel)
Réalisation d'une activité réalisant une requête HTTP paramétrable
- Choix possible de la méthode HTTP à utiliser (GET ou POST)
- Spécification de l'URL à contacter
- Indication de champs d'en-têtes pour la requête
- Indication des différents champs avec leur valeur
-
Choix du type d'encodage du corps de la requête :
- multipart/form-data : les champs sont encodés comme pour les éléments d'un email ; permet le transfert de fichiers binaires pour des valeurs de champs
- application/x-www-form-urlencoded : système d'encodage utilisé pour les URLs (également employable pour le corps d'une requête)
- JSON : le corps de la requête est sous la forme de JSON en UTF-8
- Utilisation de préférences pour conserver la dernière requête réalisée (champs pré-remplis)
Utilisation de OkHttp en mode asynchrone :
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.webrequest; import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import java.io.IOException; import java.util.HashMap; import java.util.Map; import fr.upem.coursand.R; import fr.upem.coursand.launcher.ActivityMetadata; import okhttp3.Call; import okhttp3.Callback; import okhttp3.FormBody; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; /** An activity allowing to do customized HTTP requests */ @ActivityMetadata(permissions={Manifest.permission.INTERNET}) public class WebRequestActivity extends AppCompatActivity { /** Value to use to specify where to put a joined file */ public static final String FILE_PLACEHOLDER = "@file"; private EditText urlView; private EditText methodView; private EditText headersView; private RadioGroup encodingView; private EditText bodyView; private TextView fileExplanationView; private TextView resultView; // If a file is joined private Uri fileUri; // current HTTP call private Call currentCall; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_request); // assign the fields (should be more easier using Kotlin or the ButterKnife library urlView = findViewById(R.id.urlView); methodView = findViewById(R.id.methodView); headersView = findViewById(R.id.headersView); encodingView = findViewById(R.id.encodingView); bodyView = findViewById(R.id.requestBodyView); fileExplanationView = findViewById(R.id.fileExplanationView); bodyView = findViewById(R.id.requestBodyView); resultView = findViewById(R.id.requestResultView); // prefill the fields using the preferences SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); urlView.setText(prefs.getString("url", "")); methodView.setText(prefs.getString("method", "POST")); headersView.setText(prefs.getString("headers", "")); switch(prefs.getString("encoding", "urlEncoded")) { case "urlEncoded": encodingView.check(R.id.urlEncodedRadioButton); break; case "formdata": encodingView.check(R.id.formDataRadioButton); break; case "json": encodingView.check(R.id.jsonRadioButton); break; case "raw": encodingView.check(R.id.rawRadioButton); break; } bodyView.setText(prefs.getString("body", "")); // if a file is joined with ACTION_SEND if (getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_SEND)) { Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); if (uri != null) { fileUri = uri; fileExplanationView.setText("Use @file as value to include " + fileUri); fileExplanationView.setVisibility(View.VISIBLE); } } } /** Extract the map of parameters in a string */ public Map<String, String> extractParams(String s) { Map<String, String> params = new HashMap<>(); for (String line: s.split("\n")) { String[] components = line.split(":", 2); if (components.length == 2) { params.put(components[0].trim(), components[1].trim()); } } return params; } /** Build the request using the values of fields */ protected Request buildRequest() { Request.Builder rb = new Request.Builder() .url(urlView.getText().toString()); RequestBody body = null; // to be built // add headers for (Map.Entry<String, String> entry: extractParams(headersView.getText().toString()).entrySet()) rb.addHeader(entry.getKey(), entry.getValue()); // manage encoding for the body String encoding = null; switch(encodingView.getCheckedRadioButtonId()) { case R.id.urlEncodedRadioButton: encoding = "urlEncoded"; FormBody.Builder fbb = new FormBody.Builder(); for (Map.Entry<String, String> entry: extractParams(bodyView.getText().toString()).entrySet()) fbb.add(entry.getKey(), entry.getValue()); body = fbb.build(); break; case R.id.formDataRadioButton: encoding = "formdata"; MultipartBody.Builder mbb = new MultipartBody.Builder() .setType(MultipartBody.FORM); for (Map.Entry<String, String> entry: extractParams(bodyView.getText().toString()).entrySet()) if (entry.getValue().equals(FILE_PLACEHOLDER) && fileUri != null) { // special treatment to join a file // convert Uri to File mbb.addFormDataPart(entry.getKey(), "file", new UriRequestBody(getContentResolver(), fileUri)); } else { mbb.addFormDataPart(entry.getKey(), entry.getValue()); } body = mbb.build(); break; case R.id.jsonRadioButton: encoding = "json"; JSONObject json = new JSONObject(extractParams(bodyView.getText().toString())); body = RequestBody.create(MediaType.parse("application/json"), json.toString()); break; case R.id.rawRadioButton: encoding = "raw"; body = RequestBody.create(MediaType.parse("text/plain"), bodyView.getText().toString()); break; } // save into the preferences SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); editor.putString("url", urlView.getText().toString()); editor.putString("method", methodView.getText().toString()); editor.putString("encoding", encoding); editor.putString("body", bodyView.getText().toString()); editor.commit(); return rb.build(); } /** When one click on the send button */ public void onSendClick(View v) { // cancel the possible previous call if (currentCall != null) currentCall.cancel(); currentCall = null; // Build the request Request r = buildRequest(); OkHttpClient client = new OkHttpClient(); currentCall = client.newCall(r); currentCall.enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { runOnUiThread( () -> { // the call failed resultView.setText("HTTP call failed with exception " + e); currentCall = null; }); } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { runOnUiThread( () -> { String r = "Result code: " + response.code() + "\n"; // add the headers for (kotlin.Pair<?,?> header: response.headers()) r += header.getFirst().toString() + ": " + header.getSecond().toString() + "\n"; r += "\n"; // read the body try { r += "\n" + response.body().string(); // could be dangerous if the body is too largge for memory } catch (IOException e) { r += "Cannot read the body due to exception " + e; } resultView.setText(r); currentCall = null; }); } }); } @Override protected void onStop() { super.onStop(); // cancel the current call if not null if (currentCall != null) { currentCall.cancel(); currentCall = null; } } }
Exemple d'utilisation de websockets avec OkHttp
- Websocket : protocole de communication bidirectionnel avec initialisation à l'aide d'une communication HTTP
- Avantage par rapport à une connexion TCP brute : meilleure compatibilité avec de possibles pare-feux applicatifs
- Envoi et réception possible dans les deux directions de messages binaires (fiabilité et ordre des messages garantis)
Réalisation d'une activité avec envoi et réception de messages texte (encodage UTF-8) avec le protocole Websocket (utilisation de la bibliothèque OkHttp)
Content not available