Lập trình ứng dụng cho giao thức UDP

2. Lớp DatagramPacket

Các datagram UDP đưa rất ít thông tin vào datagram IP. Header UDP chỉ  đưa tám byte vào header IP. Header UDP bao gồm số hiệu cổng nguồn và đích, chiều dài của dữ liệu và header UDP, tiếp đến là một checksum tùy chọn. Vì mỗi cổng được biểu diễn bằng hai byte nên tổng số cổng UDP trên một host sẽ là 65536. Chiều dài cũng được biểu diễn bằng hai byte nên số byte trong datagram tối đa sẽ là 65536 trừ đi tám 8 byte dành cho phần thông tin header.

Trong Java, một datagram UDP được biểu diễn bởi lớp DatagramPacket:

•  public final class DatagramPacket extends Object

Lớp này cung cấp các phương thức để nhận và thiết lập các địa chỉ nguồn, đích từ header IP, nhận và thiết lập các thông tin về cổng nguồn và đích, nhận và thiết lập độ dài dữ liệu. Các trường thông tin còn lại không thể truy nhập được từ mã Java thuần túy.

DatagramPacket sử dụng các constructor khác nhau tùy thuộc vào gói tin  được sử dụng để gửi hay nhận dữ liệu.

2.1. Các constructor để nhận datagram

Hai constructor tạo ra các đối tượng DatagramSocket mới để nhận dữ liệu từ mạng:

•  public DatagramPacket(byte[] b, int length)

•  public DatagramPacket(byte[] b, int offset, int length)

Khi một socket nhận một datagram, nó lưu trữ phần dữ liệu của datagram  ở trong vùng đệm b bắt đầu tại vị trí b[0] và tiếp tục cho tới khi gói tin được lưu trữ hoàn toàn hoặc cho tới khi lưu trữ hết length byte. Nếu sử dụng constructor thứ hai, thì dữ liệu được lưu trữ bắt đầu từ vị trí b[offset]. Chiều dài của b phải nhỏ hơn hoặc bằng b.length-offset. Nếu ta xây dựng một DatagramPacket có chiều dài vượt quá chiều dài của vùng đệm thì constructor sẽ đưa ra ngoại lệ IllegalArgumentException.  Đây là kiểu ngoại lệ RuntimeException nên chương trình của ta không cần thiết phải đón bắt ngoại lệ này.

Ví dụ, xây dựng một DatagramPacket để nhận dữ liệu có kích thước lên tới 8912 byte

byte b[]=new byte[8912];

DatagramPacket dp=new DatagramPacket(b,b.length);

2.2. Constructor để gửi các datagram

Bốn constructor tạo các đối tượng DatagramPacket mới để gửi dữ liệu trên mạng:

  • public DatagramPacket(byte[] b, int length, InetAddress dc, int port)
  • public DatagramPacket(byte[] b, int offset, int length, InetAddress dc, int port)
  • public DatagramPacket(byte[] b, int length, SocketAddress dc, int port)
  • public DatagramPacket(byte[] b, int offset, int length, SocketAddress dc, int port)

Mỗi constructor tạo ra một DatagramPacket mới để được gửi đi tới một host khác. Gói tin được điền đầy dữ liệu với chiều dài là length byte bắt đầu từ vị trí offset hoặc vị trí 0 nếu offset không được sử dụng.

Ví dụ để gửi đi một xâu ký tự đến một host khác như sau:

String s=”This is an example of UDP Programming”;

byte[] b= s.getBytes();

try{

InetAddress dc=InetAddress.getByName(“www.vnn.vn”);

int port =7;

DatagramPacket dp=new DatagramPacket(b,b.length,dc,port);

//Gửi gói tin

}

catch(IOException e){

System.err.println(e);

}

Công việc khó khăn nhất trong việc tạo ra một đối tượng DatagramPacket chính là việc chuyển đổi dữ liệu thành một mảng byte. Đoạn mã trên chuyển đổi một xâu ký tự thành một mảng byte để gửi dữ liệu đi

2.3. Các phương thức nhận các thông tin từ DatagramPacket

DatagramPacket có sáu phương thức để tìm các phần khác nhau của một datagram: dữ liệu thực sự cộng với một số trường header. Các phương thức này thường được sử dụng cho các datagram nhận được từ mạng.

•  public InetAddress getAddress()

Phương thức getAddress() trả về một đối tượng InetAddress chứa địa chỉ IP của host ở xa. Nếu datagram được nhận từ Internet, địa chỉ trả về chính là địa chỉ của máy đã gửi datagram (địa chỉ nguồn). Mặt khác nếu datagram được tạo cục bộ để được gửi tới máy ở xa, phương thức này trả về địa chỉ của host mà datagram được đánh địa chỉ.

• public int getPort()

Phương thức getPort() trả về một số nguyên xác  định cổng trên host  ở xa. Nếu datagram được nhận từ Internet thì cổng này là cổng trên host đã gửi gói tin đi.

•  public SocketAddress()

Phương thức này trả về một đối tượng SocketAddress chứa địa chỉ IP và số hiệu cổng của host ở xa.

• public byte[] getData()

Phương thức getData() trả về một mảng byte chứa dữ liệu từ datagram. Thông thường cần phải chuyển các byte này thành một dạng dữ liệu khác trước khi chương trình xử lý dữ liệu. Một cách để thực hiện điều này là chuyển đổi mảng byte thành một  đối tượng String sử dụng constructor sau đây:

•  public String(byte[] buffer,String encoding)

Tham số đầu tiên, buffer, là mảng các byte chứa dữ liệu từ datagram. Tham số thứ hai cho biết cách thức mã hóa xâu ký tự. Cho trước một DatagramPacket dp  được nhận từ mạng, ta có thể chuyển đổi nó thành xâu ký tự như sau:

String s=new String(dp.getData(),”ASCII”);

Nếu datagram không chứa văn bản, việc chuyển đổi nó thành dữ liệu Java khó khăn hơn nhiều. Một cách tiếp cận là chuyển  đổi mảng byte  được trả về bởi phương thức getData() thành luồng ByteArrayInputStream bằng cách sử dụng constructor này:

•  public ByteArrayInputStream(byte[] b, int offset, int length)

b là mảng byte được sử dụng như là một luồng nhập InputStream

• public int getLength()

Phương thức getLength() trả về số bytes dữ liệu có trong một datagram.

• public getOffset()

Phương thức này trả về vị trí trong mảng được trả về bởi phương thức getData() mà từ đó dữ liệu trong datagram xuất phát.

Các phương thức thiết lập giá trị cho các trường thông tin

Sáu constructor ở trên là đủ để tạo lập ra các datagram. Tuy nhiên, Java cung cấp một số phương thức để thay đổi dữ liệu, địa chỉ của máy ở xa, và cổng trên máy ở xa sau khi datagram đã được tạo ra. Trong một số trường hợp việc sử dụng lại các DatagramPacket đã có sẵn sẽ nhanh hơn việc tạo mới các đối tượng này.

•  public void setData(byte[] b):

Phương thức này thay đổi dữ liệu của datagram

•  public void setData(byte[] b, int offset, int length)

Phương thức này đưa ra giải pháp để gửi một khối lượng dữ liệu lớn. Thay vì gửi toàn bộ dữ liệu trong mảng, ta có thể gửi dữ liệu trong từng đoạn của mảng tại mỗi thời điểm.

Ví dụ đoạn mã sau đây sẽ gửi dữ liệu theo từng đoạn 512 byte:

int offset=0;

DatagramPacket dp=new DatagramPacket(b,offset,512);

int bytesSent=0;

while(bytesSent<b.length)

{

ds.send(dp);

bytesSent+=dp.getLength();

int bytesToSend=b.length-bytesSent;

int size=(bytesToSend>512)?512:bytesToSend;

dp.setData(b,byteSent,512);

}

•  public void setAddress(InetAddress dc)

Phương thức setAddress() thay đổi địa chỉ của máy mà ta sẽ gửi gói tin tới. Điều này sẽ cho phép ta gửi cùng một datagram đến nhiều nơi nhận.

•  public void setPort(int port)

Phương thức này thay đổi số hiệu cổng gửi tới của gói tin.

•  pubic void setAddress(SocketAddress sa)

• public void setLength(int length)

Phương thức này thay đổi số byte dữ liệu có thể đặt trong vùng đệm.

3. Lớp DatagramSocket

Để gửi hoặc nhận một DatagramPacket, bạn phải mở một DatagramSocket. Trong Java, một datagram socket  được tạo ra và  được truy xuất thông qua  đối tượng DatagramSocket

public class DatagramSocket extends Object

Tất cả các datagram được gắn với một cổng cục bộ, cổng này được sử dụng để lắng nghe các datagram đến hoặc được đặt trên các header của các datagram sẽ gửi đi. Nếu ta viết một client thì không cần phải quan tâm đến số hiệu cổng cục bộ là bao nhiêu DatagramSocket  được sử dụng  để gửi và nhận các gói tin UDP. Nó cung cấp các phương thức để gửi và nhận các gói tin, cũng như xác định một giá trị timeout khi sử dụng phương pháp vào ra không phong tỏa (non blocking I/O), kiểm tra và sửa đổi kích thước tối đa của gói tin UDP, đóng socket.

Các phương thức

• void close(): đóng một liên kết và giải phóng nó khỏi cổng cục bộ.

•  void connect(InetAddress remote_address, int remote_port): kết nối tới một tới một đối tượng InetAddress và một port.

• InetAddress getInetAddress():phương thức này trả về địa chỉ remote mà socket kết nối tới, hoặc giá trị null nếu không tồn tại liên kết.

• InetAddress getLocalAddress(): trả về địa chỉ cục bộ

• Int getSoTimeOut() trả về giá trị tùy chọn timeout của socket. Giá trị này xác định thời gian mà thao tác đọc sẽ phong tỏa trước khi nó đưa ra ngoại lệ InterruptedException. Ở chế độ mặc định, giá trị này bằng 0, chỉ ra rằng vào ra không phong tỏa được sử dụng.

•  void receive(DatagramPacket dp) throws IOException:phương thức  đọc một gói tin UDP và lưu nộ dung trong packet xác định.

•  void send(DatagramSocket dp) throws IOException:phương thức gửi một gói tin

•  void setSoTimeOut(int timeout): thiết lập giá trị tùy chọn của socket.

4. Nhận các gói tin

Trước khi một ứng dụng có thể đọc các gói tin UDP được gửi bởi các máy ở xa, nó phải gán một socket với một cổng UDP bằng cách sử dụng DatagramSocket, và tạo ra một DatagramPacket sẽ đóng vai trò như là một bộ chứa cho dữ liệu của gói tin UDP. Hình vẽ dưới đây chỉ ra mối quan hệ giữa một gói tin UDP với các lớp Java khác nhau được sử dụng để xử lý nó và các ứng dụng thực tế.

Khi một  ứng dụng muốn  đọc các gói tin UDP, nó gọi phương thức DatagramSocket.receive(), phương thức này sao chép gói tin UDP vào một DatagramPacket xác định. Xử lý nội dung nói tin và tiến trình lặp lại khi cần.

DatagramPacket dp=new DatagramPacket(new byte[256],256);

DatagramSocket ds=new DatagramSocket(2000);

boolean finished=false;

while(!finished)

{

ds.receive(dp);

//Xử lý gói tin

}

ds.close();

Khi xử lý gói tin ứng dụng phải làm việc trực tiếp với một mảng byte. Tuy nhiên nếu ứng dụng là đọc văn bản thì ta có thể sử dụng các lớp từ gói vào ra để chuyển đổi giữa mảng byte và luồng stream và reader. Bằng cách gắn kết luồng nhập ByteArrayInputStream  với nội dung của một datagram và sau đó kết nối với một kiểu luồng khác, khi đó bạn có thể truy xuất tới nội dung của gói UDP một cách dễ dàng. Rất nhiều người lập trình thích dùng các luồng vào ra I/O  để xử lý dữ liệu, bằng cách sử dụng luồng DataInputStream hoặc BufferedReader để truy xuất tới nội dung của các mảng byte.

Ví dụ, để gắn kết một luồng DataInputStream với nội dung của một DatagramPacket, ta sử dụng đoạn mã sau:

ByteArrayInputStream bis=new ByteArrayInputStream(dp.getData());

DataInputStream dis=new DataInputStream(bis);

//đọc nội dung của gói tin UDP

5. Gửi các gói tin

Lớp DatagramSocket cũng được sử dụng để gửi các gói tin. Khi gửi gói tin, ứng dụng phải tạo ra một DatagramPacket, thiết lập địa chỉ và thông tin cổng, và ghi dữ liệu cần truyền vào mảng byte. Nếu muốn gửi thông tin phúc đáp thì ta cũng đã biết địa chỉ và số hiệu cổng của gói tin nhận được. Mỗi khi gói tin sẵn sàng để gửi, ta sử dụng phương thức send() của lớp DatagramSocket để gửi gói tin đi.

//Socket lắng nghe các gói tin đến trên cổng 2000

DatagramSocket socket = new DatagramSocket(2000);

DatagramPacket packet = new DatagramPacket (new byte[256], 256);

packet.setAddress ( InetAddress.getByName ( somehost ) );

packet.setPort ( 2000 );

boolean finished = false;

while !finished )

{

// Ghi dữ liệu vào vùng đệm buffer

………

socket.send (packet);

// Thực hiện hành động nào đó, chẳng hạn như đọc gói tin kháci hoặc kiểm tra xemor
// còn gói tin nào cần gửi đi hay không

………

}

socket.close();

6. Ví dụ minh họa giao thức UDP

Để minh họa các gói tin UDP được gửi và nhận như thế nào, chúng ta sẽ viết, biên dịch và chạy ứng dụng sau. Viết chương trình theo mô hình Client/Server để:

Client thực hiện các thao tác sau đây:

• Client gửi một xâu ký tự do người dùng nhập từ bàn phím cho server

• Client nhận thông tin phản hồi trở lại từ Server và hiển thị thông tin đó trên màn hình

Server thực hiện các thao tác sau:

• Server nhận xâu ký tự do client gửi tới và in lên màn hình

• Server biến đổi xâu ký tự thành chữ hoa và gửi trở lại  cho Client

import java.net.*;
import java.io.*;

public class UDPClient

{

public final static int CONG_MAC_DINH=9;

public static void main(String args[])

{

String hostname;

int port=CONG_MAC_DINH;

if(args.length>0){

hostname=args[0];

try{

port =Integer.parseInt(args[1]);

}

catch(Exception e){

}

}

else

{

hostname=”127.0.0.1″;

}

try{

InetAddress dc=InetAddress.getByName(hostname);

BufferedReader userInput=new BufferedReader(new InputStreamReader(System.in));

DatagramSocket ds =new DatagramSocket(port);

while(true){

String line=userInput.readLine();

if(line.equals(“exit”))break;

byte[] data=line.getBytes();

DatagramPacket  dp=new DatagramPacket(data,data.length,dc,port);

ds.send(dp);

dp.setLength(65507);

ds.receive(dp);

ByteArrayInputStream  bis  =new ByteArrayInputStream(dp.getData());

BufferedReader dis =new BufferedReader(new InputStreamReader(bis));

System.out.println(dis.readLine());

}

} catch(UnknownHostException e){

System.err.println(e);

} catch(IOException e) {

System.err.println(e);

}

}

}

import java.net.*;
import java.io.*;

public class UDPServer {

public final static int CONG_MAC_DINH=9;
public static void main(String args[]){

int port=CONG_MAC_DINH;

try{

port =Integer.parseInt(args[1]);

}

catch(Exception e){  }

try{

DatagramSocket ds =new DatagramSocket(port);

DatagramPacket dp=new DatagramPacket(new byte[65507],65507);

while(true){

ds.receive(dp);

ByteArrayInputStream  bis  =new ByteArrayInputStream(dp.getData());

BufferedReader dis =new BufferedReader(new InputStreamReader(bis));

String s=dis.readLine();

System.out.println(s);

s.toUpperCase();

dp.setData(s.getBytes());

dp.setLength(s.length());

dp.setAddress(dp.getAddress());

dp.setPort(dp.getPort());

ds.send(dp);

}

} catch(UnknownHostException e){

System.err.println(e);

} catch(IOException e){

System.err.println(e);

}

}

}

Chương trình Client/Server sử dụng đa tuyến đoạn

import java.net.*;
import java.io.*;

public abstract class UDPServer extends Thread  {

private int bufferSize;

protected DatagramSocket ds;

public UDPServer(int port, int bufferSize) throws SocketException  {

this.bufferSize=bufferSize;

this.ds=new DatagramSocket(port);

}

public UDPServer(int port)throws SocketException  {

this(port,8192);

}

public void run(){

byte[] buffer=new byte[bufferSize];

while(true)  {

DatagramPacket dp=new DatagramPacket(buffer,buffer.length);

try{

ds.receive(dp);

this.respond(dp);

} catch(IOException e) {

System.err.println(e);

}

}

}

public abstract void respond(DatagramPacket req);

}

Server Echo

import java.net.*;
import java.io.*;

public class UDPEchoServer extends UDPServer  {

public final static int DEFAULT_PORT=7;

public UDPEchoServer()throws SocketException  {

super(DEFAULT_PORT);

}

public void respond(DatagramPacket dp) {

try{

DatagramPacket outdp=new DatagramPacket(dp.getData(),dp.getLength(),dp.getAddress(),dp.getPort());

ds.send(outdp);

} catch(IOException e) {

System.err.println(e);

}

}

public static void main(String[] args)  {

try

{

UDPServer server=new UDPEchoServer();

server.start();

System.out.println(“Server dang da san sang lang nghe lien ket…”);

}  catch(SocketException e) {

System.err.println(e);

}

}

}

Client

import java.net.*;
import java.io.*;

public class  ReceiverThread extends Thread

{

private DatagramSocket ds;

private boolean stopped=false;

public ReceiverThread(DatagramSocket ds) throws SocketException  {

this.ds=ds;

}

public void halt(){

this.stopped=true;

}

public void run(){

byte buffer[]=new byte[65507];

while(true){

if(stopped) return;

DatagramPacket dp=new DatagramPacket(buffer,buffer.length);

try{

ds.receive(dp);

String s=new String(dp.getData(),0,dp.getLength());

System.out.println(s);

Thread.yield();

} catch(IOException e){

System.err.println(e);

}

}

}

}

import java.net.*;
import java.io.*;

public class  SenderThread extends Thread {

private InetAddress server;

private DatagramSocket ds;

private boolean stopped=false;

private int port;

public SenderThread(InetAddress address, int port) throws SocketException  {

this.server=address;

this.port=port;

this.ds=new DatagramSocket();

this.ds.connect(server,port);

}

public void halt(){

this.stopped=true;

}

public DatagramSocket getSocket(){

return this.ds;

}

public void run(){

try{

BufferedReader userInput=new BufferedReader(new InputStreamReader(System.in));

while(true)   {

if(stopped) return;

String line=userInput.readLine();

if(line.equals(“exit”))break;

byte[] data=line.getBytes();

DatagramPacket dp=new DatagramPacket(data,data.length,server,port);

ds.send(dp);

Thread.yield();

}

} catch(IOException e){

System.err.println(e);

}

}

}

Client Echo

import java.net.*;
import java.io.*;

public class  UDPEchoClient {

public final static int DEFAULT_PORT=7;

public static void main(String[] args)  {

String hostname=”localhost”;

int port= DEFAULT_PORT;

if(args.length>0)  {

hostname=args[0];

}

try{

InetAddress ia=InetAddress.getByName(args[0]);

SenderThread sender=new SenderThread(ia,DEFAULT_PORT);

sender.start();

ReceiverThread receiver=new ReceiverThread(sender.getSocket());

receiver.start();

} catch(UnknownHostException e){

System.err.println(e);

} catch(SocketException e) {

System.err.println(e);

}

}

}

7. Kết luận

Trong chương này, chúng ta đã thảo luận những khái niệm căn bản về giao thức UDP và so sánh nó với giao thức TCP. Chúng ta đã đề cập tới việc cài đặt các chương trình UDP trong Java bằng cách sử dụng hai lớp DatagramPacket và DatagramSocket. Một số chương trình mẫu cũng được giới thiệu để bạn đọc tham khảo và giúp hiểu sâu hơn về các vấn đề lý thuyết.

Các bài liên quan:
Lập trình mạng trong Java – Phần 8