بيانات أندرويد والشبكات والواجهات

الشبكات باستخدام HttpURLConnection

18 دقيقة الدرس 5 من 12

الشبكات باستخدام HttpURLConnection

تتبادل كل تطبيقات Android الحديثة تقريبًا البيانات مع خادم بعيد — جلب خلاصة أخبار، ونشر إجراء مستخدم، وتنزيل صورة. قبل أن تصل إلى مكتبة عالية المستوى مثل Retrofit، تحتاج إلى فهم السباكة منخفضة المستوى التي تقوم عليها جميع عمليات HTTP في Android: الفئة java.net.HttpURLConnection. تأتي هذه الفئة مع مكتبة Java القياسية ولا تتطلب أي تبعيات إضافية، مما يجعلها الأداة المناسبة للطلبات الصغيرة أو سكريبتات البناء أو في أي مكان تريد فيه لا شيء زائدًا.

لماذا لا يمكنك إجراء عمليات الشبكة في الخيط الرئيسي

يُشغّل الخيط الرئيسي في Android واجهة المستخدم: يعالج أحداث اللمس، ويقيس طرق العرض ويرسمها، ويُشغّل استدعاءات دورة حياة Activity. أي عملية حجب على هذا الخيط — بما في ذلك استدعاء شبكي قد يستغرق مئات المللي ثانية — يجمّد الواجهة بأكملها. يُطبّق Android هذه القاعدة في وقت التشغيل: محاولة فتح مقبس على الخيط الرئيسي تُطلق استثناء NetworkOnMainThreadException.

NetworkOnMainThreadException هو تعطل كامل. ليس تحذير lint يمكنك تجاهله — تعمد Android إيقاف التطبيقات التي تحجب خيط UI بعمليات إدخال/إخراج الشبكة. كل استدعاء HTTP تكتبه يجب أن يعمل على خيط في الخلفية.

في هذا الدرس ستستخدم Thread خلفية بسيطة لإبقاء التركيز على HttpURLConnection نفسها. في الدرس 4 رأيت كيف تمنحك AsyncTask وHandlerThread وExecutorService تجريدات أنظف؛ في التطبيقات الحقيقية ستلف هذا الكود في أحد تلك الأنماط.

إعلان صلاحية الإنترنت

قبل أن يتمكن تطبيقك من فتح أي مقبس، يجب عليك إعلان صلاحية INTERNET في AndroidManifest.xml. على عكس الصلاحيات الخطرة (الكاميرا، جهات الاتصال)، هذه صلاحية عادية: يمنحها Android تلقائيًا ولا تحتاج إلى مطالبة في وقت التشغيل.

<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.INTERNET" />

نسيان هذا السطر يُنتج فشلاً صامتًا: تنجح عملية استدعاء openConnection() لكن كل استدعاء لـconnect() يُطلق java.net.SocketException: Permission denied. أضفها دائمًا قبل كتابة أي كود شبكي.

إجراء طلب GET خطوة بخطوة

يتبع طلب GET الأساسي مع HttpURLConnection ست خطوات: بناء URL، وفتح الاتصال، وتهيئته، والاتصال، وقراءة الاستجابة، وإغلاق الدفق. إليك مثالًا كاملاً مستقلًا يجلب حمولة JSON من API عام:

import android.os.Handler; import android.os.Looper; import android.util.Log; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; public class NetworkHelper { private static final String TAG = "NetworkHelper"; /** جلب عنوان URL المُعطى على خيط خلفي؛ تسليم النتيجة على الخيط الرئيسي. */ public static void fetchGet(String urlString, Callback callback) { new Thread(() -> { String result = null; String error = null; HttpURLConnection conn = null; try { // 1. بناء URL وفتح الاتصال URL url = new URL(urlString); conn = (HttpURLConnection) url.openConnection(); // 2. تهيئة الاتصال conn.setRequestMethod("GET"); conn.setConnectTimeout(10_000); // 10 ثوانٍ لإنشاء TCP conn.setReadTimeout(15_000); // 15 ثانية لاستقبال البيانات conn.setRequestProperty("Accept", "application/json"); // 3. الاتصال والتحقق من رمز حالة HTTP int statusCode = conn.getResponseCode(); Log.d(TAG, "HTTP " + statusCode + " from " + urlString); if (statusCode == HttpURLConnection.HTTP_OK) { // 200 // 4. قراءة دفق النجاح result = readStream(conn.getInputStream()); } else { // 4b. قراءة دفق الخطأ للحصول على تفاصيل تشخيصية InputStream errStream = conn.getErrorStream(); error = (errStream != null) ? readStream(errStream) : "HTTP " + statusCode; } } catch (IOException e) { error = e.getMessage(); Log.e(TAG, "Network error", e); } finally { // 5. قطع الاتصال دائمًا if (conn != null) conn.disconnect(); } // 6. التسليم على الخيط الرئيسي final String finalResult = result; final String finalError = error; new Handler(Looper.getMainLooper()).post(() -> { if (finalResult != null) { callback.onSuccess(finalResult); } else { callback.onError(finalError); } }); }).start(); } private static String readStream(InputStream is) throws IOException { StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); } } return sb.toString(); } public interface Callback { void onSuccess(String body); void onError(String message); } }

بعض التفاصيل تستحق الانتباه:

  • setConnectTimeout مقابل setReadTimeout: يتحكم مهلة الاتصال في المدة التي ينتظرها نظام التشغيل لإتمام المصافحة الثلاثية TCP. يتحكم مهلة القراءة في المدة التي ينتظر فيها البيانات بعد إنشاء الاتصال. كلاهما يُعيّن إلى الصفر (لانهائي) افتراضيًا إذا لم تُحددهما، مما يعني أن خيط الخلفية قد يحجب إلى الأبد على خادم معطوب.
  • getResponseCode() يُطلق الطلب: استدعاء هذه الطريقة هو النقطة التي يُرسل فيها HttpURLConnection الطلب فعليًا وينتظر رؤوس الاستجابة. الاستدعاءات قبل هذه النقطة تُهيئ كائن الاتصال فحسب.
  • دفق الخطأ: عندما يُعيد الخادم 4xx أو 5xx، يكون الجسم على getErrorStream() لا على getInputStream(). قراءته تتيح لك إظهار رسائل خطأ API مفيدة للمطور.
  • disconnect() في finally: هذا يُغلق المقبس الأساسي. تخطيه يُسرّب موارد نظام التشغيل، خاصة على الشاشات المزدحمة التي تُجري طلبات كثيرة.

إرسال طلب POST بجسم JSON

لطلب POST يجب تهيئة ثلاثة أشياء إضافية: تعيين الطريقة إلى "POST"، وتفعيل الإخراج بـsetDoOutput(true)، وكتابة بيانات الجسم إلى getOutputStream(). لن يُرسل الاتصال حتى تقرأ رمز الاستجابة.

public static void postJson(String urlString, String jsonBody, Callback callback) { new Thread(() -> { HttpURLConnection conn = null; try { URL url = new URL(urlString); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(10_000); conn.setReadTimeout(15_000); conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); conn.setRequestProperty("Accept", "application/json"); conn.setDoOutput(true); // يُفعّل كتابة جسم الطلب // كتابة الجسم byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8); conn.getOutputStream().write(bodyBytes); conn.getOutputStream().flush(); int status = conn.getResponseCode(); String result = (status >= 200 && status < 300) ? readStream(conn.getInputStream()) : readStream(conn.getErrorStream()); final String finalResult = result; new Handler(Looper.getMainLooper()).post(() -> callback.onSuccess(finalResult)); } catch (IOException e) { new Handler(Looper.getMainLooper()).post(() -> callback.onError(e.getMessage())); } finally { if (conn != null) conn.disconnect(); } }).start(); }
عيّن Content-Type قبل استدعاء getOutputStream(). بمجرد أن تبدأ في كتابة الجسم، يُثبّت HttpURLConnection رؤوس الطلب. تعيين الرؤوس بعد هذه النقطة لن يكون له أي تأثير بصمت.

قراءة رمز الاستجابة ومعالجة الأخطاء

تُخبرك رموز حالة HTTP بالنتيجة الدلالية لطلب. يجب أن يتعامل كودك على الأقل مع ثلاث فئات:

  • نجاح 2xx — اقرأ getInputStream() للحصول على الجسم.
  • خطأ العميل 4xx (404 غير موجود، 401 غير مُصرح، 422 غير قابل للمعالجة) — طلبك كان خاطئًا؛ اقرأ getErrorStream() للحصول على رسالة API وأظهرها للمطور أو المستخدم.
  • خطأ الخادم 5xx — الخادم معطوب؛ أعد المحاولة مع تراجع أسّي، أو تدهور بأمان.
HTTP_OK ليس رمز النجاح الوحيد. 201 Created هو الاستجابة القياسية لطلب POST ناجح يُنشئ موردًا. تحقق دائمًا من النطاق الكامل 2xx (status >= 200 && status < 300) بدلاً من الترميز الصارم == 200.

تسليم النتائج إلى واجهة المستخدم

يعمل استدعاء الشبكة على خيط خلفي، لكن جميع تحديثات العرض — تعيين النص، إظهار شريط التقدم، التنقل — يجب أن تحدث على الخيط الرئيسي. الطريقة القانونية للقفز إلى الخلف هي new Handler(Looper.getMainLooper()).post(runnable). إذا كنت داخل Activity، يمكنك استخدام الاختصار المعادل runOnUiThread(runnable).

// داخل Activity fetchGet("https://api.example.com/users/1", new NetworkHelper.Callback() { @Override public void onSuccess(String body) { // يعمل على الخيط الرئيسي — آمن لتحديث الواجهة textView.setText(body); } @Override public void onError(String message) { Toast.makeText(MainActivity.this, "Error: " + message, Toast.LENGTH_LONG).show(); } });

HTTPS وحركة المرور النصية الواضحة

اعتبارًا من Android 9 (API 28)، يُحظر حركة المرور النصية الواضحة (HTTP العادي) افتراضيًا. يجب أن يستخدم تطبيقك HTTPS لجميع عناوين URL الإنتاجية. أثناء التطوير يمكنك السماح مؤقتًا بالنص الواضح لمضيف تصحيح محدد بإضافة network_security_config.xml، لكن لا تشحن هذا التكوين إلى الإنتاج أبدًا.

الخلاصة

كل استدعاء HTTP في Android يعيش على خيط خلفي — NetworkOnMainThreadException أمر غير قابل للتفاوض. تمنحك HttpURLConnection تحكمًا دقيقًا في طريقة الطلب والرؤوس والمهل والجسم. الحلقة الأساسية هي: فتح، وتهيئة، واستدعاء getResponseCode()، وقراءة الدفق المناسب، وقطع الاتصال في كتلة finally، وإرسال النتيجة إلى الخيط الرئيسي عبر Handler. بمجرد أن تشعر بالارتياح مع هذه السباكة، يفحص الدرس التالي Retrofit، الذي يُؤتمت هذا النمط المعياري مع الحفاظ على جميع المفاهيم الأساسية ذاتها.