23、网络请求之HttpURLConnection:GET与POST请求、设置超时、读取响应流

网络请求,说白了就是让你的App跟服务器说上话。

Android里做网络请求的方式有好几种,但HttpURLConnection是官方最基础、最原生的一套API。我个人习惯在简单项目或者不想引入第三方库时,直接用这个。它轻量、可控,而且从Android 2.3开始,官方就推荐用它替代HttpClient了。

核心要点:HttpURLConnection是Java标准库的一部分,Android对它做了封装。它支持HTTP/HTTPS,能处理GET、POST,还能设置超时、读取响应流。你想想看,这些不就是网络请求最核心的几件事吗?

23.1 知识体系总览

先给你一张图,把本章的知识结构理清楚。我每次讲网络请求,都会先画这个图,心里有谱了再动手写代码。

HttpURLConnection GET请求 POST请求 设置超时 拼接URL参数 设置请求方法为GET 读取响应流 设置请求方法为POST 设置Content-Type 写入请求体 connectTimeout readTimeout 超时异常处理 响应流读取 → 关闭连接 → 异常处理

23.2 GET请求:从服务器拿数据

GET请求,说白了就是把参数拼在URL后面,问服务器要东西。比如你要查天气,URL里带上城市名就行。

我记得刚入行那会儿,有个同事写GET请求时忘了对参数做URL编码,结果参数里带了个空格,服务器直接返回400。嗯,这里要注意:所有参数都必须用URLEncoder.encode()编码

// 一个标准的GET请求示例
URL url = new URL("https://api.example.com/data?city=" + URLEncoder.encode("北京", "UTF-8") + "&page=1");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

// 设置请求方法,默认就是GET,但显式写出来更清晰
connection.setRequestMethod("GET");

// 设置请求头,模拟浏览器行为
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
connection.setRequestProperty("Accept", "application/json");

// 设置超时(后面会细讲)
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);

// 发起连接
connection.connect();

// 检查响应码
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
    // 读取响应流
    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    String line;
    StringBuilder response = new StringBuilder();
    while ((line = reader.readLine()) != null) {
        response.append(line);
    }
    reader.close();
    // 这里response.toString()就是服务器返回的数据
} else {
    // 处理错误
    System.out.println("请求失败,响应码:" + responseCode);
}

// 最后一定要关闭连接
connection.disconnect();

我的小技巧:我在项目中习惯把读取响应流封装成一个工具方法,传入InputStream,返回String。这样不管GET还是POST,复用起来都方便。另外,记得在finally块里关闭连接,防止内存泄漏。

23.3 POST请求:往服务器发数据

POST请求跟GET不一样,数据是放在请求体里的,不是拼在URL上。你想想看,登录、提交表单、上传文件,这些场景都得用POST。

我曾经踩过一个坑:POST请求忘了设置Content-Type,服务器不知道我发的是什么格式的数据,直接返回415(Unsupported Media Type)。所以,Content-Type必须明确指定

URL url = new URL("https://api.example.com/login");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

// 设置为POST请求
connection.setRequestMethod("POST");

// 关键:允许输出,因为我们要往服务器写数据
connection.setDoOutput(true);

// 设置请求头
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
connection.setRequestProperty("Accept", "application/json");

// 设置超时
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);

// 构建请求体数据(JSON格式)
String jsonBody = "{\"username\":\"admin\",\"password\":\"123456\"}";

// 写入请求体
OutputStream os = connection.getOutputStream();
os.write(jsonBody.getBytes("UTF-8"));
os.flush();
os.close();

// 发起连接(其实调用getOutputStream后会自动连接,但显式调用更规范)
connection.connect();

// 读取响应
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    String line;
    StringBuilder response = new StringBuilder();
    while ((line = reader.readLine()) != null) {
        response.append(line);
    }
    reader.close();
    System.out.println("登录成功:" + response.toString());
} else {
    System.out.println("登录失败,响应码:" + responseCode);
}

connection.disconnect();

注意:POST请求的setDoOutput(true)必须在connect()之前调用。我见过有人把顺序搞反了,结果写入数据时直接抛异常。另外,请求体数据不要太大,否则要考虑分块传输。

23.4 设置超时:别让用户等太久

网络请求最怕什么?怕卡死。用户点了个按钮,转圈转了30秒还没反应,换你你也烦。所以,超时设置是必须的。

HttpURLConnection提供了两个超时参数:

参数 作用 建议值
connectTimeout 建立连接的超时时间。如果服务器连不上,到这个时间就放弃。 5-10秒
readTimeout 读取数据的超时时间。如果服务器响应慢,到这个时间就放弃。 10-15秒

我个人习惯把connectTimeout设成5秒,readTimeout设成10秒。为什么?因为连接超时通常意味着网络不通或者服务器挂了,没必要等太久。而读取超时可能只是服务器处理慢,稍微多给点时间。

connection.setConnectTimeout(5000);  // 5秒
connection.setReadTimeout(10000);    // 10秒

避坑指南:我曾经在一个弱网环境下测试,发现即使设置了超时,有时候还是会卡住。后来排查发现,是因为我在主线程里做了网络请求,而Android 4.0以后不允许在主线程做网络操作。所以,网络请求一定要放在子线程里,配合Handler或者AsyncTask来更新UI。

23.5 读取响应流:拿到服务器返回的数据

不管是GET还是POST,最终都要读取服务器返回的数据。这里有个细节:响应流要按字符读取,而不是字节。因为服务器返回的通常是文本(JSON、XML、HTML),用BufferedReader包装一下,按行读取最方便。

嗯,这里还要注意编码问题。如果服务器返回的Content-Type里指定了charset,就用那个编码。如果没有指定,我一般默认用UTF-8,因为现在大部分API都用UTF-8。

// 通用的响应流读取方法
private String readResponse(HttpURLConnection connection) throws IOException {
    InputStream inputStream = null;
    BufferedReader reader = null;
    try {
        // 如果响应码是200,用getInputStream();否则用getErrorStream()
        if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
            inputStream = connection.getInputStream();
        } else {
            inputStream = connection.getErrorStream();
        }
        
        reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            response.append(line);
        }
        return response.toString();
    } finally {
        // 关闭资源
        if (reader != null) {
            try { reader.close(); } catch (IOException e) { e.printStackTrace(); }
        }
        if (inputStream != null) {
            try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); }
        }
    }
}

我的经验:读取响应流时,别忘了处理getErrorStream()。服务器返回4xx或5xx时,响应体里可能包含错误详情,用getErrorStream()才能读到。我之前有个项目,一直用getInputStream(),结果服务器返回400时直接抛异常,连错误信息都拿不到。

23.6 完整示例:GET与POST二合一

最后,给你一个完整的工具类示例。我在项目中就是这么用的,把GET和POST封装成两个静态方法,传入URL和参数,返回响应字符串。简洁、实用、可复用。

public class HttpUtil {
    
    private static final int CONNECT_TIMEOUT = 5000;
    private static final int READ_TIMEOUT = 10000;
    
    // GET请求
    public static String doGet(String urlStr, Map<String, String> params) throws IOException {
        // 拼接参数
        if (params != null && !params.isEmpty()) {
            StringBuilder sb = new StringBuilder(urlStr);
            sb.append("?");
            for (Map.Entry<String, String> entry : params.entrySet()) {
                sb.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
                  .append("=")
                  .append(URLEncoder.encode(entry.getValue(), "UTF-8"))
                  .append("&");
            }
            urlStr = sb.substring(0, sb.length() - 1); // 去掉最后一个&
        }
        
        HttpURLConnection connection = null;
        try {
            URL url = new URL(urlStr);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(CONNECT_TIMEOUT);
            connection.setReadTimeout(READ_TIMEOUT);
            connection.connect();
            
            return readResponse(connection);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
    
    // POST请求
    public static String doPost(String urlStr, String jsonBody) throws IOException {
        HttpURLConnection connection = null;
        try {
            URL url = new URL(urlStr);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
            connection.setConnectTimeout(CONNECT_TIMEOUT);
            connection.setReadTimeout(READ_TIMEOUT);
            
            // 写入请求体
            OutputStream os = connection.getOutputStream();
            os.write(jsonBody.getBytes("UTF-8"));
            os.flush();
            os.close();
            
            return readResponse(connection);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
    
    // 读取响应流(复用)
    private static String readResponse(HttpURLConnection connection) throws IOException {
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                inputStream = connection.getInputStream();
            } else {
                inputStream = connection.getErrorStream();
            }
            
            reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            return response.toString();
        } finally {
            if (reader != null) {
                try { reader.close(); } catch (IOException e) { e.printStackTrace(); }
            }
            if (inputStream != null) {
                try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        }
    }
}

好了,HttpURLConnection的核心用法就这些。GET、POST、超时、响应流,四个点串起来,你就能搞定大部分网络请求场景了。记住:子线程执行、设置超时、关闭连接,这三个习惯养成了,网络请求这块基本不会出大问题。


公众号:蓝海资料掘金营,微信deep3321