Java网络编程(UDP)
在文章http://blog.csdn.net/xiaolangfanhua/article/details/52761615中介绍了TCP方式的网络编程步骤。网络通讯的方式除了TCP方式以外,还有一种实现的方式就是UDP方式。UDP(User Datagram Protocol),中文意思是用户数据报协议,方式类似于发短信息,是一种物美价廉的通讯方式,使用该种方式无需建立专用的虚拟连接,由于无需建立专用的连接,所以对于服务器的压力要比TCP小很多,所以也是一种常见的网络编程方式。但是使用该种方式最大的不足是传输不可靠,当然也不是说经常丢失,就像大家发短信息一样,理论上存在收不到的可能,这种可能性可能是1%,反正比较小,但是由于这种可能的存在,所以平时我们都觉得重要的事情还是打个电话吧(类似TCP方式),一般的事情才发短信息(类似UDP方式)。网络编程中也是这样,必须要求可靠传输的信息一般使用TCP方式实现,一般的数据才使用UDP方式实现。
在Java API中,实现UDP方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:
1、DatagramSocket
DatagramSocket类实现“网络连接”,包括客户端网络连接和服务器端网络连接。虽然UDP方式的网络通讯不需要建立专用的网络连接,但是毕竟还是需要发送和接收数据,DatagramSocket实现的就是发送数据时的发射器,以及接收数据时的监听器的角色。类比于TCP中的网络连接,该类既可以用于实现客户端连接,也可以用于实现服务器端连接。
2、DatagramPacket
DatagramPacket类实现对于网络中传输的数据封装,也就是说,该类的对象代表网络中交换的数据。在UDP方式的网络编程中,无论是需要发送的数据还是需要接收的数据,都必须被处理成DatagramPacket类型的对象,该对象中包含发送到的地址、发送到的端口号以及发送的内容等。其实DatagramPacket类的作用类似于现实中的信件,在信件中包含信件发送到的地址以及接收人,还有发送的内容等,邮局只需要按照地址传递即可。在接收数据时,接收到的数据也必须被处理成DatagramPacket类型的对象,在该对象中包含发送方的地址、端口号等信息,也包含数据的内容。和TCP方式的网络传输相比,IO编程在UDP方式的网络编程中变得不是必须的内容,结构也要比TCP方式的网络编程简单一些。
下面介绍一下UDP方式的网络编程中,客户端和服务器端的实现步骤,以及通过基础的示例演示UDP方式的网络编程在Java语言中的实现方式。
UDP 客户端:
UDP客户端编程涉及的步骤也是4个部分:建立连接、发送数据、接收数据和关闭连接。
1、建立连接
其中UDP方式的建立连接和TCP方式不同,只需要建立一个连接对象即可,不需要指定服务器的IP和端口号码。实现的代码为:
DatagramSocket ds = new DatagramSocket();
这样就建立了一个客户端连接,该客户端连接使用系统随机分配的一个本地计算机的未用端口号。在该连接中,不指定服务器端的IP和端口,所以UDP方式的网络连接更像一个发射器,而不是一个具体的连接。
2、发送数据
在UDP方式的网络编程中,IO技术不是必须的,在发送数据时,需要将需要发送的数据内容首先转换为byte数组,然后将数据内容、服务器IP和服务器端口号一起构造成一个DatagramPacket类型的对象,这样数据的准备就完成了,发送时调用网络连接对象中的send方法发送该对象即可。例如将字符串“Hello”发送到IP是127.0.0.1,端口号是10001的服务器,则实现发送数据的代码如下:
String s = “Hello”;
String host = “127.0.0.1”;
int port = 10001;
//将发送的内容转换为byte数组
byte[] b = s.getBytes();
//将服务器IP转换为InetAddress对象
InetAddress server = InetAddress.getByName(host);
//构造发送的数据包对象
DatagramPacket sendDp = new DatagramPacket(b,b.length,server,port);
//发送数据
ds.send(sendDp);
在该示例代码中,不管发送的数据内容是什么,都需要转换为byte数组,然后将服务器端的IP地址构造成InetAddress类型的对象,在准备完成以后,将这些信息构造成一个DatagramPacket类型的对象,在UDP编程中,发送的数据内容、服务器端的IP和端口号,都包含在DatagramPacket对象中。在准备完成以后,调用连接对象ds的send方法把DatagramPacket对象发送出去即可。
3、接收数据
当数据发送出去以后,就可以接收服务器端的反馈信息了。接收数据在Java语言中的实现是这样的:首先构造一个数据缓冲数组,该数组用于存储接收的服务器端反馈数据,该数组的长度必须大于或等于服务器端反馈的实际有效数据的长度。然后以该缓冲数组为基础构造一个DatagramPacket数据包对象,最后调用连接对象的receive方法接收数据即可。接收到的服务器端反馈数据存储在DatagramPacket类型的对象内部。实现接收数据以及显示服务器端反馈内容的示例代码如下:
//构造缓冲数组
byte[] data = new byte[1024];
//构造数据包对象
DatagramPacket received = new DatagramPacket(data,data.length);
//接收数据
ds.receive(receiveDp);
//输出数据内容
byte[] b = receiveDp.getData(); //获得缓冲数组
int len = receiveDp.getLength(); //获得有效数据长度
String s = new String(b,0,len);
System.out.println(s);
在该代码中,首先构造缓冲数组data,这里设置的长度1024是预估的接收到的数据长度,要求该长度必须大于或等于接收到的数据长度,然后以该缓冲数组为基础,构造数据包对象,使用连接对象ds的receive方法接收反馈数据,由于在Java语言中,除String以外的其它对象都是按照地址传递,所以在receive方法内部可以改变数据包对象receiveDp的内容,这里的receiveDp的功能和返回值类似。数据接收到以后,只需要从数据包对象中读取出来就可以了,使用DatagramPacket对象中的getData方法可以获得数据包对象的缓冲区数组,但是缓冲区数组的长度一般大于有效数据的长度,换句话说,也就是缓冲区数组中只有一部分数据是反馈数据,所以需要使用DatagramPacket对象中的getLength方法获得有效数据的长度,则有效数据就是缓冲数组中的前有效数据长度个内容,这些才是真正的服务器端反馈的数据的内容。
4、关闭连接
虽然UDP方式不建立专用的虚拟连接,但是连接对象还是需要占用系统资源,所以在使用完成以后必须关闭连接。关闭连接使用连接对象中的close方法即可,实现的代码如下:
ds.close();
UDP服务器端
UDP方式网络编程的服务器端实现和TCP方式的服务器端实现类似,也是服务器端监听某个端口,然后获得数据包,进行逻辑处理以后将处理以后的结果反馈给客户端,最后关闭网络连接,下面依次进行介绍。
1、监听端口
首先UDP方式服务器端网络编程需要建立一个连接,该连接监听某个端口,实现的代码为:
DatagramSocket ds = new DatagramSocket(10010);
由于服务器端的端口需要固定,所以一般在建立服务器端连接时,都指定端口号。例如该示例代码中指定10010端口为服务器端使用的端口号,客户端端在连接服务器端时连接该端口号即可。
2、接收数据
服务器端接收客户端发送过来的数据,其接收的方法和客户端接收的方法一致,其中receive方法的作用类似于TCP方式中accept方法的作用,该方法也是一个阻塞方法,其作用是接收数据。
接收到客户端发送过来的数据以后,服务器端对该数据进行逻辑处理,然后将处理以后的结果再发送给客户端,在这里发送时就比客户端要麻烦一些,因为服务器端需要获得客户端的IP和客户端使用的端口号,这个都可以从接收到的数据包中获得。示例代码如下:
//获得客户端的IP
InetAddress clientIP = receiveDp.getAddress();
//获得客户端的端口号
Int clientPort = receiveDp.getPort();
3、结果反馈
使用以上代码,就可以从接收到的数据包对象receiveDp中获得客户端的IP地址和客户端的端口号,这样就可以在服务器端中将处理以后的数据构造成数据包对象,然后将处理以后的数据内容反馈给客户端了,反馈数据的具体步骤与客户端发送数据的步骤类似。
4、关闭连接
ds.close();
下面通过一个简单的示例演示UDP网络编程的基本使用。该示例的功能是实现将客户端程序的系统时间发送给服务器端,服务器端接收到时间以后,向客户端反馈字符串“OK”。
客户端代码:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Date;
/*
* 简单的UDP客户端,实现向服务器端发送系统时间功能
*/
public class SimpleUDPClient {
public static void main(String[] args) {
// TODO Auto-generated method stub
DatagramSocket ds=null; // 连接对象
DatagramPacket sendDp=null; // 发送数据包对象
DatagramPacket receiveDp=null; // 接收数据包对象
String serverHost="127.0.0.1"; // 服务器IP
int serverPort=10010; // 服务器端口号
try{
// 建立连接
ds=new DatagramSocket();
// 初始化发送数据
Date d=new Date(); // 系统当前时间
String content=d.toString(); // 转换为字符串
byte[] data=content.getBytes();// 所有的发送内容都必须转换为byte型数组
// 初始化发送包对象
InetAddress address=InetAddress.getByName(serverHost);
sendDp=new DatagramPacket(data, data.length,address,serverPort);
// 发送
ds.send(sendDp);
// 初始化接收数据
byte[] b=new byte[1024]; // 接收数据缓冲数组,这个长度应大于接收数据包的长度
receiveDp=new DatagramPacket(b, b.length); // 用接收数据缓冲数组初始化接收数据包对象
// 接收
ds.receive(receiveDp);
// 读取反馈内容并输出
byte[] response=receiveDp.getData();
int len=receiveDp.getLength();
String s=new String(response, 0, len);
System.out.println("服务器端反馈为:"+s);
}catch(Exception e1){
e1.printStackTrace();
}finally {
try{
// 关闭连接
ds.close();
}catch(Exception e2){
e2.printStackTrace();
}
}
}
}
服务器端代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/*
* 简单UDP服务器端,实现功能是输出客户端发送数据,
* 反馈字符串“OK”给客户端
*/
public class SimpleRUDPServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
DatagramSocket ds=null; // 连接对象
DatagramPacket sendDp=null; // 发送数据包对象
DatagramPacket receiveDp=null; // 接收数据包对象
final int port=10010; // 端口
try{
// 建立连接,监听端口
ds=new DatagramSocket(port);
System.out.println("服务器端已启动:");
// 初始化接收数据
byte[] b=new byte[1024];
receiveDp=new DatagramPacket(b, b.length);
// 接收
ds.receive(receiveDp);
// 读取客户端请求并输出
InetAddress clientIP=receiveDp.getAddress(); // 该连接的客户端的IP
int clientPort=receiveDp.getPort(); // 客户端端口
byte[] data=receiveDp.getData(); // 请求内容
int len=receiveDp.getLength();
System.out.println("客户端IP:"+clientIP);
System.out.println("客户端端口:"+clientPort);
System.out.println("客户端请求内容:"+new String(data,0,len));
// 发送反馈
String response="OK";
byte[] bData=response.getBytes();
sendDp=new DatagramPacket(bData, bData.length,clientIP,clientPort);
// 发送
ds.send(sendDp);
}catch(Exception e1){
e1.printStackTrace();
}finally {
try{
ds.close();
}catch(Exception e3){
e3.printStackTrace();
}
}
}
}
支持多次数据交换和多客户端
以下介绍如何通过一次连接进行多次数据交换以及服务器如何支持多个客户端。
在解决该问题以前,需要特别指出的是UDP方式的网络编程由于不建立虚拟的连接,所以在实际使用时和TCP方式存在很多的不同,最大的一个不同就是“无状态”。该特点指每次服务器端都收到信息,但是这些信息和连接无关,换句话说,也就是服务器端只是从信息是无法识别出是谁发送的,这样就要求发送信息时的内容需要多一些,这个在后续的示例中可以看到。
下面是实现客户端多次发送以及服务器端支持多个数据包同时处理的程序结构,实现的原理和TCP方式类似,在客户端将数据的发送和接收放入循环中,而服务器端则将接收到的每个数据包启动一个专门的线程进行处理。实现的代码如下:
1、客户端代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Date;
/*
* 简单的UDP客户端,实现向服务器端发送系统时间功能
* 改程序发送3次数据到服务器端
*/
public class MulUDPClient {
public static void main(String[] args) {
// TODO Auto-generated method stub
DatagramSocket ds=null; // 连接对象
DatagramPacket sendDp; // 发送数据包对象
DatagramPacket receiveDp; // 接收数据包对象
String serverHost="127.0.0.1"; // 服务器IP
int serverPort=10012; // 服务器端口号
try{
// 建立连接
ds=new DatagramSocket();
// 初始化
InetAddress address=InetAddress.getByName(serverHost);
byte[] b=new byte[1024];
receiveDp=new DatagramPacket(b, b.length);
System.out.println("客户端准备完成");
// 循环30次,每次间隔0.01秒
for(int i=0;i<30;i++){
// 初始化发送数据
Date d=new Date();
String content=d.toString();
byte[] data=content.getBytes();
// 初始化发送包对象
sendDp=new DatagramPacket(data, data.length,address,serverPort);
// 发送
ds.send(sendDp);
// 延迟
Thread.sleep(10);
// 接收
ds.receive(receiveDp);
// 读取反馈内容,并输出
byte[] response=receiveDp.getData();
int len=receiveDp.getLength();
String s=new String(response, 0, len);
System.out.println("服务器端反馈:"+s);
}
}catch(Exception e1){
e1.printStackTrace();
}finally {
try{
ds.close();
}catch(Exception e2){
e2.printStackTrace();
}
}
}
}
2、服务器端代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/*
* 可以并发处理数据包的服务器
* 功能为:显示客户端发送的内容,并向客户端反馈字符串“OK”
*/
public class MulUDPServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
DatagramSocket ds=null;
DatagramPacket receiveDp;
final int PORT=10012;
byte[] b=new byte[1024];
receiveDp=new DatagramPacket(b, b.length);
try{
ds=new DatagramSocket(PORT);
System.out.println("服务器端已启动:");
while(true){
ds.receive(receiveDp);
new LogicThread(ds,receiveDp);
}
}catch(Exception e1){
e1.printStackTrace();
}finally {
try{
ds.close();
}catch(Exception e3){
e3.printStackTrace();
}
}
}
}
class LogicThread extends Thread{
DatagramSocket ds;
DatagramPacket dp;
public LogicThread (DatagramSocket ds,DatagramPacket dp) {
this.ds=ds;
this.dp=dp;
start();
}
public void run(){
try{
byte [] data=dp.getData();
int len=dp.getLength();
InetAddress clientAddress=dp.getAddress();
int clientPort=dp.getPort();
System.out.println("客户端IP:"+clientAddress.getHostAddress());
System.out.println("客户端端口号:"+clientPort);
System.out.println("客户端发送内容:"+new String(data,0,len));
byte [] b="OK".getBytes();
DatagramPacket sendDp=new DatagramPacket(b, b.length,clientAddress,clientPort);
ds.send(sendDp);
}catch(Exception e2){
e2.printStackTrace();
}
}
}