本文的理论和代码摘录于《疯狂Java讲义》[url]http://book.51cto.com/art/201203/322540.htm[/url],测试部分的截图是本人增加的。

1.UDP协议和TCP协议简单对比如下。
TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
UDP协议:不可靠,差错控制开销较小,传输大小限制在64KB以下,不需要建立连接。

2.使用DatagramSocket发送接收UDP数据

2.1 理论
JDK使用DatagramSocket代表UDP协议的Socket,DatagramSocket类似于码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报。
而DatagramPacket代表数据报,类似于集装箱。码头的作用就是负责发送、接收集装箱,而DatagramSocket的作用则是发送、接收DatagramPacket。

使用DatagramSocket发送数据报时,DatagramSocket并不知道将该数据报发送到哪里,而是由DatagramPacket自身决定数据报的目的地。就像码头并不知道每个集装箱的目的地,码头只是将这些集装箱发送出去,而集装箱本身包含了该集装箱的目的地。

发送数据是通过DatagramSocket的send()方法实现的,send()方法根据数据报的目的地址来寻径以传送数据报。

在接收数据之前,应该生成一个DatagramPacket对象,给出接收数据的字节数组及其长度。然后调用DatagramSocket的receive()方法等待数据报的到来,receive()将一直等待(该方法会阻塞调用该方法的线程),直到收到一个数据报为止。

当接收到一个DatagramPacket对象后,如果想向该数据报的发送者"反馈"一些信息,但由于UDP协议是面向非连接的,所以接收者并不知道每个数据报由谁发送过来,但程序可以调用DatagramPacket的如下3个方法来获取发送者的IP地址和端口。
InetAddress getAddress()
int getPort()
SocketAddress getSocketAddress()

2.2 编码
下面程序使用DatagramSocket实现了Server/Client结构的网络通信。本程序的服务器端使用循环1000次来读取DatagramSocket中的数据报,每当读取到内容之后便向该数据报的发送者送回一条信息。服务器端程序代码如下。

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UdpServer {
	public static final int PORT = 30000;
	// 定义每个数据报的最大大小为4KB
	private static final int DATA_LEN = 4096;
	// 定义接收网络数据的字节数组
	byte[] inBuff = new byte[DATA_LEN];
	// 以指定字节数组创建准备接收数据的DatagramPacket对象
	private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
	// 定义一个用于发送的DatagramPacket对象
	private DatagramPacket outPacket;
	// 定义一个字符串数组,服务器端发送该数组的元素
	String[] books = new String[] { "疯狂Java讲义", "轻量级Java EE企业应用实战",
			"疯狂Android讲义", "疯狂Ajax讲义" };

	public void init() throws IOException {
		// 创建DatagramSocket对象
		DatagramSocket socket = new DatagramSocket(PORT);

		// 采用循环接收数据
		for (int i = 0; i < 1000; i++) {
			// 读取Socket中的数据,读到的数据放入inPacket封装的数组里
			socket.receive(inPacket);
			// 判断inPacket.getData()和inBuff是否是同一个数组
			//getData()的API设计显得有些多余--直接访问传给 DatagramPacket构造器的字节数组实参即可,无须调用该方法
			System.out.println(inBuff == inPacket.getData());
			// 将接收到的内容转换成字符串后输出
			System.out.println(new String(inBuff, 0, inPacket.getLength()));
			// 从字符串数组中取出一个元素作为发送数据
			byte[] sendData = books[i % 4].getBytes();
			// 以指定的字节数组作为发送数据,以刚接收到的DatagramPacket的
			// 源SocketAddress作为目标SocketAddress创建DatagramPacket
			outPacket = new DatagramPacket(sendData, sendData.length,
					inPacket.getSocketAddress());
			// 发送数据
			socket.send(outPacket);
		}
	}

	public static void main(String[] args) throws IOException {
		new UdpServer().init();
	}
}




客户端采用循环不断地读取用户键盘输入,每当读取到用户输入的内容后就将该内容封装成DatagramPacket数据报,再将该数据报发送出去;接着把DatagramSocket中的数据读入接收用的DatagramPacket中(实际上是读入该DatagramPacket所封装的字节数组中)。客户端程序代码如下。


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class UdpClient {
	// 定义发送数据报的目的地
	public static final int DEST_PORT = 30000;
	public static final String DEST_IP = "127.0.0.1";
	// 定义每个数据报的最大大小为4KB
	private static final int DATA_LEN = 4096;
	// 定义接收网络数据的字节数组
	byte[] inBuff = new byte[DATA_LEN];
	// 以指定的字节数组创建准备接收数据的DatagramPacket对象
	private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
	// 定义一个用于发送的DatagramPacket对象
	private DatagramPacket outPacket = null;

	public void init() throws IOException {
		// 创建一个客户端DatagramSocket,使用随机端口
		DatagramSocket socket = new DatagramSocket();
		// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
		outPacket = new DatagramPacket(new byte[0], 0,
				InetAddress.getByName(DEST_IP), DEST_PORT);
		// 创建键盘输入流
		Scanner scan = new Scanner(System.in);
		// 不断地读取键盘输入
		while (scan.hasNextLine()) {
			// 将键盘输入的一行字符串转换成字节数组
			byte[] buff = scan.nextLine().getBytes();
			// 设置发送用的DatagramPacket中的字节数据
			outPacket.setData(buff);
			// 发送数据报
			socket.send(outPacket);
			// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
			socket.receive(inPacket);
			System.out.println(new String(inBuff, 0, inPacket.getLength()));
		}
	}

	public static void main(String[] args) throws IOException {
		new UdpClient().init();
	}
}




2.3 测试


下面我们测试一下,打开UdpServer,然后打开2个UdpClient。随便哪个Client都可以发消息给Server,同时会Server的回复。


用[url=http://technet.microsoft.com/en-us/sysinternals/bb896653.aspx]Process Explorer[/url]监控一下端口可以发现,Server开启了30000端口,而Client则开启了随机端口,这次Client1是49509,Client2是52535。


服务器和Client1、Client2都是没有连接的。


[img]http://dl2.iteye.com/upload/attachment/0096/0395/db85056b-1504-35c6-ae84-3c20195bf477.png[/img]


[img]http://dl2.iteye.com/upload/attachment/0096/0401/e2aa3205-dac3-3027-911a-00abc7773194.png[/img]



3.使用Socket发送接收TCP数据


对比之下,如果是TCP的话,就是一般的Socket和ServerSocket互通。


服务端


import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server
{
	public static void main(String[] args) 
		throws IOException
	{
		// 创建一个ServerSocket,用于监听客户端Socket的连接请求
		ServerSocket ss = new ServerSocket(30000);
		// 采用循环不断接受来自客户端的请求
		while (true) {
			// 每当接受到客户端Socket的请求,服务器端也对应产生一个Socket,并开启一个新线程负责和客户端通信
			final Socket s = ss.accept();
			new Thread(new Runnable(){

				@Override
				public void run() {
					// 将Socket对应的输出流包装成PrintStream
					PrintStream ps=null;
					try {
						ps = new PrintStream(s.getOutputStream());
						int i=9;
						//此处死循环是为了观察端口,不然下面的ps一关闭,就看不到端口情况了
						while (i==9) {
							ps.println("您好,您收到了服务器的新年祝福!");
						}
						ps.close();
						s.close();
					} catch (IOException e) {
						e.printStackTrace();
					}

				}

			}
			)
			.start();
		}
	}
}




客户端


import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;

public class Client
{
	public static void main(String[] args) 
		throws Exception
	{
		Socket socket = new Socket("127.0.0.1" , 30000);
		BufferedReader br = new BufferedReader(
		new InputStreamReader(socket.getInputStream()));
		int i=9;
		while (i==9) {
			String line = br.readLine();
			System.out.println("来自服务器的数据:" + line);
		}
		br.close();
		socket.close();
	}
}




我们也可以做个试验,开启1个Server,2个Client。


从图中可以发现,Server开启了30000端口,并且和Client的53170和53171建立了连接。同时Server的30000端口还在监听准备接受新的Client的到来。


[img]http://dl2.iteye.com/upload/attachment/0096/0393/50daaa78-4d4a-3e6f-86d6-e9e678548331.png[/img] 

[img]http://dl2.iteye.com/upload/attachment/0096/0399/d715c6de-edd8-3099-80f1-20350645e396.png[/img] 



我们做另一个测试,同时开启UdpServer,UdpClient,并同时开启(TCP)Server,Client,发现他们不会冲突,都正常工作呢。 

[img]http://dl2.iteye.com/upload/attachment/0096/0397/ce6f7f90-f18a-3cfb-8c2a-a339f9f7a12d.png[/img]


由此可以得到另外一个结论:TCP和UDP端口是独立的,他们之间没有关系。TCP可以有65536个端口,UDP同样也有65536个端口可用。这样一台计算机上总共就有131072个端口可用。