UDP وDatagramSocket
بروتوكول مخطط البيانات للمستخدم (UDP) هو البروتوكول الثاني الرئيسي في طبقة النقل إلى جانب TCP. حيث يمنحك TCP تدفق بايتات موثوقًا ومرتبًا وقائمًا على الاتصال، يمنحك UDP مخططات بيانات سريعة وعديمة الاتصال وبجهد أفضل مبذول. فهم الاثنين معًا — ومعرفة متى يكون كل منهما الأداة المناسبة — أمر أساسي لأي مطور Java متمرس يبني أنظمة شبكية.
ما هو UDP فعلًا
يُرسل UDP مخططات بيانات فردية: حزم مكتفية بذاتها تحمل عنوان الوجهة ومنفذ المصدر ومنفذ الوجهة ومجموعًا تدقيقيًا وحمولة. تُوجَّه كل مخططة بيانات بشكل مستقل. لا يضمن البروتوكول وصول الحزمة، ولا وصولها مرة واحدة، ولا وصولها بالترتيب. لا يوجد تأسيس اتصال، ولا حالة اتصال، ولا إعادة إرسال.
حمل UDP صغير جدًا. رأس UDP لا يتجاوز 8 بايت فقط. بينما رأس TCP يتراوح بين 20 و60 بايت، ويضيف تأسيس الاتصال في TCP (المصافحة الثلاثية) رحلة ذهاب وإياب كاملة على الأقل قبل أن تنتقل أي بيانات. للحركة المرورية عالية التردد والحساسة للزمن التي يمكنها تحمل خسارة عرضية، يتفوق UDP في الأداء الخام.
متى تختار UDP على TCP
UDP هو الخيار الصحيح عندما تهم السرعة وانخفاض زمن الاستجابة أكثر من الموثوقية، أو عندما يمكن للتطبيق التعامل مع الحزم المفقودة أو غير المرتبة بشكل أفضل مما يستطيع نظام التشغيل. تشمل حالات الاستخدام الكلاسيكية:
- الصوت والفيديو في الوقت الفعلي (VoIP، مؤتمرات الفيديو، البث المباشر) — إطار متدهور قليلًا أفضل من تدفق متوقف ينتظر إعادة الإرسال.
- الألعاب متعددة اللاعبين عبر الإنترنت — تحديث الموضع من 50 ميلي ثانية مضت لا قيمة له؛ أرسل تحديثًا جديدًا بدلًا من إعادة إرسال القديم.
- DNS — الاستفسارات والردود تناسب حزمة واحدة؛ العميل ببساطة يعيد المحاولة إن لم يصله رد.
- DHCP وSNMP وTFTP وNTP — بروتوكولات بسيطة بما يكفي لإدارة موثوقيتها الخاصة، أو حيث يتفوق انخفاض الحمل على الخسارة العرضية.
- المقاييس والبيانات التشغيلية — فقدان لقطة عداد أحيانًا مقبول؛ إغراق الشبكة بحمل TCP غير مقبول.
- البث الشامل والبث المتعدد — يدعم UDP إرسال مخطط بيانات واحد لمستقبلين متعددين؛ بينما TCP نقطة إلى نقطة فحسب.
QUIC (بروتوكول HTTP/3) مبني على UDP. صمّم Google بروتوكول QUIC — وسيلة النقل لـ HTTP/3 — فوق UDP، ونفّذ موثوقيته وتعدد إرساله الخاص. يتيح ذلك تجنّب حجب بداية الخط في TCP مع توصيل تدفقات مرتبة حيثما دعت الحاجة. حركة مرور الإنترنت الحديثة تعتمد بشكل متزايد على UDP لهذا السبب تحديدًا.
DatagramSocket وDatagramPacket في Java
تكشف Java عن UDP من خلال صنفين في java.net:
DatagramSocket — المقبس الذي يُرسل ويستقبل مخططات البيانات. اربطه بمنفذ محلي للاستقبال؛ اتركه غير مربوط (أو اترك نظام التشغيل يعيّن منفذًا) للإرسال فقط.
DatagramPacket — حاوية مخطط بيانات واحد: مصفوفة بايت وإزاحة وطول، واختياريًا InetAddress ومنفذ (للحزم الصادرة).
بناء خادم UDP للصدى
يربط الخادم بمنفذ، ويدور في حلقة استقبال الحزم، ويرد على كل منها إلى عنوان المرسل ومنفذه — اللذان يوفرهما UDP تلقائيًا في الحزمة المستقبَلة.
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpEchoServer {
private static final int PORT = 9000;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket(PORT)) {
System.out.println("UDP echo server listening on port " + PORT);
byte[] buffer = new byte[BUFFER_SIZE];
while (true) {
DatagramPacket request = new DatagramPacket(buffer, buffer.length);
socket.receive(request); // يتوقف حتى وصول مخطط بيانات
String received = new String(
request.getData(), 0, request.getLength()
);
System.out.println("Received: " + received
+ " from " + request.getAddress() + ":" + request.getPort());
// رد الصدى: استخدام العنوان والمنفذ من الحزمة المستقبَلة
DatagramPacket response = new DatagramPacket(
request.getData(),
request.getLength(),
request.getAddress(),
request.getPort()
);
socket.send(response);
}
}
}
}
خصّص دائمًا مخزنًا جديدًا للاستقبال — أو أعد ضبط طول الحزمة. بعد socket.receive(packet) يُضبط length للحزمة على عدد البايتات المستقبَلة فعليًا، وقد يكون أصغر من حجم المخزن. إن أعدت استخدام نفس DatagramPacket في استدعاء الاستقبال التالي دون إعادة ضبط طوله إلى الحجم الكامل للمخزن، سيقرأ receive() فقط في تلك الشريحة الأصغر ويقطع الحزم الأكبر بصمت. نادِ packet.setLength(buffer.length) في بداية كل دورة.
بناء عميل UDP
ينشئ العميل مقبسًا غير مربوط (يعيّن نظام التشغيل منفذًا عابرًا كمصدر)، يُرسل مخطط بيانات إلى عنوان الخادم ومنفذه، ثم يستقبل الصدى.
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UdpEchoClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 9000;
private static final int TIMEOUT_MS = 3_000;
public static void main(String[] args) throws Exception {
String message = "Hello, UDP!";
byte[] sendBuffer = message.getBytes();
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(TIMEOUT_MS); // تجنّب الانتظار إلى الأبد
InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
DatagramPacket request = new DatagramPacket(
sendBuffer, sendBuffer.length, serverAddress, SERVER_PORT
);
socket.send(request);
byte[] receiveBuffer = new byte[1024];
DatagramPacket response = new DatagramPacket(receiveBuffer, receiveBuffer.length);
socket.receive(response); // يتوقف حتى الرد أو انتهاء المهلة
String reply = new String(
response.getData(), 0, response.getLength()
);
System.out.println("Echo: " + reply);
}
}
}
معالجة فقدان الحزم في كود التطبيق
نظرًا لأن UDP لا يوفر إعادة الإرسال، فإن أي موثوقية يحتاجها التطبيق يجب تنفيذها فوق البروتوكول. النمط القياسي هو الإرسال مع مهلة وإعادة المحاولة:
import java.io.IOException;
import java.net.*;
public class UdpReliableSender {
private static final int MAX_RETRIES = 3;
private static final int TIMEOUT_MS = 2_000;
public static byte[] sendWithRetry(
DatagramSocket socket,
DatagramPacket request,
int responseBufferSize) throws IOException {
byte[] responseBuffer = new byte[responseBufferSize];
DatagramPacket response = new DatagramPacket(responseBuffer, responseBuffer.length);
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
socket.setSoTimeout(TIMEOUT_MS);
socket.send(request);
socket.receive(response);
return response.getData(); // نجاح
} catch (SocketTimeoutException e) {
System.err.printf("Attempt %d timed out, retrying...%n", attempt);
}
}
throw new IOException("No response after " + MAX_RETRIES + " attempts");
}
}
البث الشامل والبث المتعدد
يدعم UDP شكلين من التوصيل من واحد إلى كثيرين لا يستطيع TCP توفيرهما. البث الشامل (Broadcast) يُرسل مخطط بيانات لكل مضيف في شبكة فرعية. البث المتعدد (Multicast) يُرسل إلى مجموعة محددة بعنوان IP من الفئة D (224.0.0.0 – 239.255.255.255)؛ يستقبل الحزم فقط المضيفون الذين انضموا إلى المجموعة.
import java.net.*;
// مُرسل البث الشامل (تفعيله بـ setBroadcast)
public class BroadcastSender {
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
String msg = "Service available on port 8080";
byte[] data = msg.getBytes();
// 255.255.255.255 = بث محدود
InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(
data, data.length, broadcastAddress, 4567
);
socket.send(packet);
System.out.println("Broadcast sent.");
}
}
}
للبث المتعدد، استخدم MulticastSocket (صنف فرعي من DatagramSocket) واستدعِ joinGroup() في المستقبلين:
import java.net.*;
public class MulticastReceiver {
public static void main(String[] args) throws Exception {
InetAddress group = InetAddress.getByName("230.0.0.1");
try (MulticastSocket socket = new MulticastSocket(5000)) {
socket.joinGroup(group);
byte[] buffer = new byte[256];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
System.out.println("Multicast message: "
+ new String(packet.getData(), 0, packet.getLength()));
socket.leaveGroup(group);
}
}
}
المفاضلات الرئيسية التي يجب تذكرها
- لا ضمان للترتيب — يمكن أن تصل الحزم خارج التسلسل. إن كان الترتيب مهمًا، أضف أرقام تسلسل في الحمولة ورتّب عند المستقبل.
- لا ضمان للتسليم — نفّذ إعادة الإرسال أو اقبل الخسارة، وفقًا لحالة الاستخدام.
- لا تحكم في التدفق أو التحكم في الازدحام — مُرسل سريع يمكنه إغراق مستقبل بطيء أو رابط ضيق. أنت المسؤول عن تحديد المعدل.
- الحد الأقصى لحجم مخطط البيانات — حد حمولة UDP هو 65,507 بايت (65,535 ناقص رأسي IP وUDP). لنقل البيانات الكبيرة بشكل موثوق، جزّئ على مستوى التطبيق أو استخدم TCP.
- سلامة الخيوط —
DatagramSocket ليس آمنًا للخيوط. استخدم مقبسًا واحدًا لكل خيط، أو تزامن بشكل صريح.
فكّر في NIO لخوادم UDP عالية الإنتاجية. يتيح java.nio.channels.DatagramChannel استخدام إدخال/إخراج غير متزامن ومحددًا، وتعدد آلاف النظراء على خيط واحد — وهو النهج ذاته الذي يشغّل خوادم الألعاب عالية الأداء والبنية التحتية للبث.
الخلاصة
يتاجر UDP الموثوقية بالسرعة. تمنح DatagramSocket وDatagramPacket في Java وصولًا مباشرًا وقليل الحمل إلى هذا البروتوكول. استخدم UDP عندما يهم زمن الاستجابة أكثر من ضمان التسليم، أو عندما تناسب الحمولة حزمة واحدة، أو عند الحاجة للبث الشامل أو المتعدد. نفّذ أي موثوقية ضرورية — إعادة إرسال، ترتيب، إزالة تكرار — في طبقة تطبيقك، واضبط SO_TIMEOUT على كل استدعاء استقبال لمنع الانتظار إلى أجل غير مسمى.