Cómo utilizar java.net.URLConnection para activar y manejar solicitudes HTTP

Resuelto BalusC asked hace 14 años • 12 respuestas

java.net.URLConnectionAquí se pregunta con bastante frecuencia sobre el uso de , y el tutorial de Oracle también lo es. conciso al respecto.

Básicamente, ese tutorial solo muestra cómo activar una solicitud GET y leer la respuesta. No explica por ningún lado cómo usarlo para, entre otros, realizar un POST , configurar encabezados de solicitud, leer encabezados de respuesta, manejar cookies, enviar un formulario HTML, cargar un archivo, etc.

Entonces, ¿cómo puedo utilizarlo java.net.URLConnectionpara activar y manejar solicitudes HTTP "avanzadas"?

BalusC avatar May 08 '10 13:05 BalusC
Aceptado

Primero, un descargo de responsabilidad de antemano: los fragmentos de código publicados son todos ejemplos básicos. Necesitarás manejar IOExceptionmensajes triviales RuntimeExceptioncomo NullPointerExceptiony ArrayIndexOutOfBoundsExceptionconsortes tú mismo.

En caso de que esté desarrollando para Android en lugar de Java, tenga en cuenta también que desde la introducción del nivel 28 de API, las solicitudes HTTP de texto sin cifrar están deshabilitadas de forma predeterminada . Se le recomienda utilizar HttpsURLConnection, pero si es realmente necesario, se puede habilitar el texto sin cifrar en el Manifiesto de la aplicación.


Preparando

Primero necesitamos saber al menos la URL y el juego de caracteres. Los parámetros son opcionales y dependen de los requisitos funcionales.

String url = "http://example.com";
String charset = "UTF-8";  // Or in Java 7 and later, use the constant: java.nio.charset.StandardCharsets.UTF_8.name()
String param1 = "value1";
String param2 = "value2";
// ...

String query = String.format("param1=%s&param2=%s",
    URLEncoder.encode(param1, charset),
    URLEncoder.encode(param2, charset));

Los parámetros de consulta deben estar en name=valueformato y estar concatenados por &. Normalmente, también codificaría en URL los parámetros de consulta con el juego de caracteres especificado usando URLEncoder#encode().

Es String#format()sólo por conveniencia. Lo prefiero cuando necesito el operador de concatenación de cadenas +más de dos veces.


Activar una solicitud HTTP GET con (opcionalmente) parámetros de consulta

Es una tarea trivial. Es el método de solicitud predeterminado.

URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...

Cualquier cadena de consulta debe concatenarse con la URL mediante ?. El Accept-Charsetencabezado puede indicarle al servidor en qué codificación se encuentran los parámetros. Si no envía ninguna cadena de consulta, puede omitir el Accept-Charsetencabezado. Si no necesita configurar ningún encabezado, incluso puede utilizar el URL#openStream()método de acceso directo.

InputStream response = new URL(url).openStream();
// ...

De cualquier manera, si el otro lado es un HttpServlet, entonces doGet()se llamará a su método y los parámetros estarán disponibles para HttpServletRequest#getParameter().

Para fines de prueba, puede imprimir el cuerpo de la respuesta en la salida estándar como se muestra a continuación:

try (Scanner scanner = new Scanner(response)) {
    String responseBody = scanner.useDelimiter("\\A").next();
    System.out.println(responseBody);
}

Activar una solicitud HTTP POST con parámetros de consulta

Establecer URLConnection#setDoOutput()implícitamente trueel método de solicitud en POST. El HTTP POST estándar como lo hacen los formularios web es de un tipo application/x-www-form-urlencodeden el que la cadena de consulta se escribe en el cuerpo de la solicitud.

URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true); // Triggers POST.
connection.setRequestProperty("Accept-Charset", charset);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + charset);

try (OutputStream output = connection.getOutputStream()) {
    output.write(query.getBytes(charset));
}

InputStream response = connection.getInputStream();
// ...

Nota: siempre que desee enviar un formulario HTML mediante programación, no olvide incluir los name=valuepares de cualquier <input type="hidden">elemento en la cadena de consulta y, por supuesto, también el name=valuepar del <input type="submit">elemento que desea "presionar" mediante programación (porque (que generalmente se usa en el lado del servidor para distinguir si se presionó un botón y, de ser así, cuál).

También puedes lanzar lo obtenido URLConnectiony HttpURLConnectionusarlo en su HttpURLConnection#setRequestMethod()lugar. Pero si estás intentando usar la conexión para la salida, aún debes configurarla URLConnection#setDoOutput()en true.

HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...

De cualquier manera, si el otro lado es un HttpServlet, entonces doPost()se llamará a su método y los parámetros estarán disponibles para HttpServletRequest#getParameter().


Realmente activando la solicitud HTTP

Puede activar la solicitud HTTP explícitamente con URLConnection#connect(), pero la solicitud se activará automáticamente cuando desee obtener información sobre la respuesta HTTP, como el uso del cuerpo de la respuesta, URLConnection#getInputStream()etc. Los ejemplos anteriores hacen exactamente eso, por lo que la connect()llamada es superflua.


Recopilación de información de respuesta HTTP

  1. Estado de respuesta HTTP :

Necesitas un HttpURLConnectionaquí. Lanzalo primero si es necesario.

    int status = httpConnection.getResponseCode();
  1. Encabezados de respuesta HTTP :

     for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
         System.out.println(header.getKey() + "=" + header.getValue());
     }
    
  2. Codificación de respuesta HTTP :

Cuando Content-Typecontiene un charsetparámetro, entonces el cuerpo de la respuesta probablemente esté basado en texto y entonces nos gustaría procesar el cuerpo de la respuesta con la codificación de caracteres especificada del lado del servidor.

    String contentType = connection.getHeaderField("Content-Type");
    String charset = null;

    for (String param : contentType.replace(" ", "").split(";")) {
        if (param.startsWith("charset=")) {
            charset = param.split("=", 2)[1];
            break;
        }
    }

    if (charset != null) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) {
            for (String line; (line = reader.readLine()) != null;) {
                // ... System.out.println(line)?
            }
        }
    } else {
        // It's likely binary content, use InputStream/OutputStream.
    }

manteniendo la sesión

La sesión del lado del servidor suele estar respaldada por una cookie. Algunos formularios web requieren que haya iniciado sesión y/o que se le realice un seguimiento mediante una sesión. Puede utilizar la CookieHandlerAPI para mantener las cookies. Debe preparar un CookieManagercon un CookiePolicyde ACCEPT_ALLantes de enviar todas las solicitudes HTTP.

// First set the default cookie manager.
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));

// All the following subsequent URLConnections will use the same cookie manager.
URLConnection connection = new URL(url).openConnection();
// ...

connection = new URL(url).openConnection();
// ...

connection = new URL(url).openConnection();
// ...

Tenga en cuenta que se sabe que esto no siempre funciona correctamente en todas las circunstancias. Si le falla, lo mejor es recopilar y configurar manualmente los encabezados de las cookies. Básicamente, necesita tomar todos Set-Cookielos encabezados de la respuesta del inicio de sesión o de la primera GETsolicitud y luego pasarlos a través de las solicitudes posteriores.

// Gather all cookies on the first request.
URLConnection connection = new URL(url).openConnection();
List<String> cookies = connection.getHeaderFields().get("Set-Cookie");
// ...

// Then use the same cookies on all subsequent requests.
connection = new URL(url).openConnection();
for (String cookie : cookies) {
    connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
}
// ...

Está split(";", 2)[0]ahí para deshacerse de los atributos de las cookies que son irrelevantes para el lado del servidor como expires, pathetc. Alternativamente, también puede usarlos cookie.substring(0, cookie.indexOf(';'))en lugar de split().


Modo de transmisión

De HttpURLConnectionforma predeterminada, almacenará en búfer todo el cuerpo de la solicitud antes de enviarla, independientemente de si usted mismo ha establecido una longitud de contenido fija usando connection.setRequestProperty("Content-Length", contentLength);. Esto puede causar OutOfMemoryExceptionmensajes de correo electrónico cada vez que envía simultáneamente solicitudes POST grandes (por ejemplo, carga de archivos). Para evitar esto, le gustaría configurar el archivo HttpURLConnection#setFixedLengthStreamingMode().

httpConnection.setFixedLengthStreamingMode(contentLength);

Pero si realmente no se conoce la duración del contenido de antemano, entonces puede utilizar el modo de transmisión fragmentada configurando el modo HttpURLConnection#setChunkedStreamingMode()correspondiente. Esto establecerá el Transfer-Encodingencabezado HTTP chunked, lo que obligará a que el cuerpo de la solicitud se envíe en fragmentos. El siguiente ejemplo enviará el cuerpo en fragmentos de 1 KB.

httpConnection.setChunkedStreamingMode(1024);

Agente de usuario

Puede suceder que una solicitud devuelva una respuesta inesperada, mientras funciona bien con un navegador web real . El lado del servidor probablemente esté bloqueando solicitudes según el User-Agentencabezado de la solicitud. De forma predeterminada, lo URLConnectionconfigurará Java/1.6.0_19donde la última parte es obviamente la versión JRE. Puede anular esto de la siguiente manera:

connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); // Do as if you're using Chrome 41 on Windows 7.

Utilice la cadena User-Agent de un navegador reciente .


Manejo de errores

Si el código de respuesta HTTP es 4nn(Error del cliente) o 5nn(Error del servidor), es posible que desee leerlo HttpURLConnection#getErrorStream()para ver si el servidor ha enviado alguna información de error útil.

InputStream error = ((HttpURLConnection) connection).getErrorStream();

Si el código de respuesta HTTP es -1, entonces algo salió mal con la conexión y el manejo de la respuesta. La HttpURLConnectionimplementación en JRE más antiguos tiene algunos errores a la hora de mantener vivas las conexiones. Es posible que desee desactivarlo configurando la http.keepAlivepropiedad del sistema en false. Puede hacer esto mediante programación al comienzo de su aplicación:

System.setProperty("http.keepAlive", "false");

Subiendo archivos

Normalmente usarías multipart/form-datacodificación para contenido POST mixto (datos binarios y de caracteres). La codificación se describe con más detalle en RFC2388 .

String param = "value";
File textFile = new File("/path/to/file.txt");
File binaryFile = new File("/path/to/file.bin");
String boundary = Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
String CRLF = "\r\n"; // Line separator required by multipart/form-data.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

try (
    OutputStream output = connection.getOutputStream();
    PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);
) {
    // Send normal param.
    writer.append("--" + boundary).append(CRLF);
    writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
    writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
    writer.append(CRLF).append(param).append(CRLF).flush();

    // Send text file.
    writer.append("--" + boundary).append(CRLF);
    writer.append("Content-Disposition: form-data; name=\"textFile\"; filename=\"" + textFile.getName() + "\"").append(CRLF);
    writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF); // Text file itself must be saved in this charset!
    writer.append(CRLF).flush();
    Files.copy(textFile.toPath(), output);
    output.flush(); // Important before continuing with writer!
    writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.

    // Send binary file.
    writer.append("--" + boundary).append(CRLF);
    writer.append("Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + binaryFile.getName() + "\"").append(CRLF);
    writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(binaryFile.getName())).append(CRLF);
    writer.append("Content-Transfer-Encoding: binary").append(CRLF);
    writer.append(CRLF).flush();
    Files.copy(binaryFile.toPath(), output);
    output.flush(); // Important before continuing with writer!
    writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.

    // End of multipart/form-data.
    writer.append("--" + boundary + "--").append(CRLF).flush();
}

Si el otro lado es un HttpServlet, entonces doPost()se llamará a su método y las partes estarán disponibles HttpServletRequest#getPart()(nota, por lo tanto no getParameter() , ¡y así sucesivamente!). Sin embargo, el getPart()método es relativamente nuevo y se introdujo en Servlet 3.0 (Glassfish 3, Tomcat 7, etc.). Antes de Servlet 3.0, su mejor opción era utilizar Apache Commons FileUpload para analizar una multipart/form-datasolicitud. Consulte también esta respuesta para ver ejemplos de los enfoques FileUpload y Servelt 3.0.


Tratar con sitios HTTPS que no son de confianza o mal configurados

En caso de que esté desarrollando para Android en lugar de Java, tenga cuidado : la siguiente solución puede salvarle el día si no ha implementado los certificados correctos durante el desarrollo. Pero no deberías usarlo para la producción. En estos días (abril de 2021), Google no permitirá que su aplicación se distribuya en Play Store si detecta un verificador de nombre de host inseguro; consulte https://support.google.com/faqs/answer/7188426.

A veces necesitas conectar una URL HTTPS, tal vez porque estás escribiendo un raspador web. En ese caso, es probable que te encuentres con alguien javax.net.ssl.SSLException: Not trusted server certificateen algunos sitios HTTPS que no mantienen sus certificados SSL actualizados, o con alguien java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] founden javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_namealgunos sitios HTTPS mal configurados.

El siguiente inicializador de ejecución única staticen su clase de raspador web debería ser HttpsURLConnectionmás indulgente con esos sitios HTTPS y, por lo tanto, ya no generar esas excepciones.

static {
    TrustManager[] trustAllCertificates = new TrustManager[] {
        new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null; // Not relevant.
            }
            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
                // Do nothing. Just allow them all.
            }
            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
                // Do nothing. Just allow them all.
            }
        }
    };

    HostnameVerifier trustAllHostnames = new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true; // Just allow them all.
        }
    };

    try {
        System.setProperty("jsse.enableSNIExtension", "false");
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCertificates, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(trustAllHostnames);
    }
    catch (GeneralSecurityException e) {
        throw new ExceptionInInitializerError(e);
    }
}

Ultimas palabras

Apache HttpComponents HttpClient es mucho más conveniente en todo esto :)

  • Tutorial de HttpClient
  • Ejemplos de HttpClient

Analizar y extraer HTML

Si todo lo que desea es analizar y extraer datos de HTML, entonces será mejor que utilice un analizador HTML como Jsoup .

  • ¿Cuáles son las ventajas y desventajas de los principales analizadores HTML en Java?
  • Cómo escanear y extraer una página web en Java
BalusC avatar May 08 '2010 06:05 BalusC

Cuando se trabaja con HTTP, casi siempre es más útil hacer referencia a ella HttpURLConnectionque a la clase base URLConnection(ya que URLConnectiones una clase abstracta cuando solicitas URLConnection.openConnection()una URL HTTP, eso es lo que obtendrás de todos modos).

URLConnection#setDoOutput(true)Luego, en lugar de confiar en establecer implícitamente el método de solicitud en POST , puede hacer httpURLConnection.setRequestMethod("POST")lo que algunos podrían encontrar más natural (y que también le permite especificar otros métodos de solicitud como PUT , DELETE , ...).

También proporciona constantes HTTP útiles para que puedas hacer:

int responseCode = httpURLConnection.getResponseCode();

if (responseCode == HttpURLConnection.HTTP_OK) {
Paal Thorstensen avatar Dec 06 '2011 02:12 Paal Thorstensen

Inspirándome en esta y otras preguntas sobre Stack Overflow, he creado un cliente http básico de código abierto mínimo que incorpora la mayoría de las técnicas que se encuentran aquí.

google-http-java-client también es un excelente recurso de código abierto.

David Chandler avatar Jun 13 '2012 16:06 David Chandler

Le sugiero que eche un vistazo al código en kevinsawicki/http-request , es básicamente un contenedor HttpUrlConnectionque proporciona una API mucho más simple en caso de que solo desee realizar las solicitudes ahora mismo o puede echar un vistazo a las fuentes ( no es demasiado grande) para ver cómo se manejan las conexiones.

Ejemplo: realizar una GETsolicitud con tipo de contenido application/jsony algunos parámetros de consulta:

// GET http://google.com?q=baseball%20gloves&size=100
String response = HttpRequest.get("http://google.com", true, "q", "baseball gloves", "size", 100)
        .accept("application/json")
        .body();
System.out.println("Response was: " + response);
fernandohur avatar Oct 24 '2014 21:10 fernandohur

Actualizar

El nuevo Cliente HTTP se envió con Java 9 pero como parte de un módulo Incubator llamado jdk.incubator.httpclient. Los módulos de incubadora son un medio para poner API no finales en manos de los desarrolladores mientras las API avanzan hacia su finalización o eliminación en una versión futura.

En Java 9, puedes enviar una GETsolicitud como:

// GET
HttpResponse response = HttpRequest
    .create(new URI("http://www.stackoverflow.com"))
    .headers("Foo", "foovalue", "Bar", "barvalue")
    .GET()
    .response();

Luego puedes examinar lo devuelto HttpResponse:

int statusCode = response.statusCode();
String responseBody = response.body(HttpResponse.asString());

Dado que este nuevo cliente HTTP está enjava.httpclient jdk.incubator.httpclientmódulo, debes declarar esta dependencia en tu module-info.javaarchivo:

module com.foo.bar {
    requires jdk.incubator.httpclient;
}
Ali Dehghani avatar Apr 29 '2016 08:04 Ali Dehghani

Hay dos opciones que puede utilizar con accesos a URL HTTP: GET / POST

OBTENER solicitud:

HttpURLConnection.setFollowRedirects(true); // Defaults to true

String url = "https://name_of_the_url";
URL request_url = new URL(url);
HttpURLConnection http_conn = (HttpURLConnection)request_url.openConnection();
http_conn.setConnectTimeout(100000);
http_conn.setReadTimeout(100000);
http_conn.setInstanceFollowRedirects(true);
System.out.println(String.valueOf(http_conn.getResponseCode()));

Solicitud de publicación:

HttpURLConnection.setFollowRedirects(true); // Defaults to true

String url = "https://name_of_the_url"
URL request_url = new URL(url);
HttpURLConnection http_conn = (HttpURLConnection)request_url.openConnection();
http_conn.setConnectTimeout(100000);
http_conn.setReadTimeout(100000);
http_conn.setInstanceFollowRedirects(true);
http_conn.setDoOutput(true);
PrintWriter out = new PrintWriter(http_conn.getOutputStream());
if (urlparameter != null) {
   out.println(urlparameter);
}
out.close();
out = null;
System.out.println(String.valueOf(http_conn.getResponseCode()));
Utkarsh Bhatt avatar Dec 18 '2014 12:12 Utkarsh Bhatt