转载请注明出处:https://blog.csdn.net/mythmayor/article/details/97106109
一、网络编程
在开始介绍UDP通信之前,我想先简单介绍一下网络编程的概念。
1.什么是网络编程
网络编程就是用来实现网络互连的不同计算机上运行的程序间可以进行数据交换。
2.网络编程三要素
(1)IP
IP即互联网协议(Internet Protocol)。每台网络终端在网络中都有一个独立的地址,我们在网络中传输数据就是使用这个地址。 IP是每个设备在网络中的唯一标识。
(2)协议
协议是为计算机网络中进行数据交换而建立的规则、标准或约定的集合。目前常用的有UDP协议和TCP协议。
UDP
- 用户数据报协议(User Datagram Protocol)
- 面向无连接,数据不安全,速度快。不区分客户端与服务端。
TCP
- 传输控制协议(Transmission Control Protocol)
- 面向连接(三次握手),数据安全,速度略低。分为客户端和服务端。
- 三次握手: 客户端先向服务端发起请求, 服务端响应请求, 传输数据。
(3)端口号
端口号是每个程序在设备上的唯一标识。每个网络程序都需要绑定一个端口号,传输数据的时候除了确定发到哪台机器上,还要明确发到哪个程序。端口号范围从0-65535。编写网络应用就需要绑定一个端口号,尽量使用1024以上的,1024以下的基本上都被系统程序占用了。
常用端口
- mysql: 3306
- oracle: 1521
- web: 80
- tomcat: 8080
- QQ: 4000
- feiQ: 2425
二、UDP简介
UDP即用户数据报协议(User Datagram Protocol)。是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,IETF RFC 768 是UDP的正式规范。UDP在IP报文的协议号是17。
UDP协议与TCP协议一样用于处理数据包,在OSI模型中,两者都位于传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用UDP协议。UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但即使在今天UDP仍然不失为一项非常实用和可行的网络传输层协议。
UDP协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。每一个数据包的前8个字节用来包含报头信息,剩余字节则用来包含具体的传输数据。
UDP是OSI参考模型中一种无连接的传输层协议,它主要用于不要求分组顺序到达的传输中,分组传输顺序的检查与排序由应用层完成,提供面向事务的简单不可靠信息传送服务。UDP 协议基本上是IP协议与上层协议的接口。UDP协议适用端口分别运行在同一台设备上的多个应用程序。
UDP提供了无连接通信,且不对传送数据包进行可靠性保证,适合于一次传输少量数据,UDP传输的可靠性由应用层负责。常用的UDP端口号有:53(DNS)、69(TFTP)、161(SNMP),使用UDP协议包括:TFTP、SNMP、NFS、DNS、BOOTP。
UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序,如DNS、TFTP、SNMP等。
总的来说,UDP有以下几个特点:面向无连接,数据不安全,速度快。不区分客户端与服务端。
三、UDP实战
1.创建DatagramSocket对象
我们知道,DatagramSocket一共有四个构造方法。
- public DatagramSocket()
- public DatagramSocket(SocketAddress bindaddr)
- public DatagramSocket(int port)
- public DatagramSocket(int port, InetAddress laddr)
常用的有下面两种方式:
(1)构造函数绑定固定端口号(不建议使用)
DatagramSocket mUdpSocket = new DatagramSocket(60000);
这样很简单的一句代码就能构造出DatagramSocket对象,同时会绑定60000端口,不过这样比较容易出现端口占用的问题,所以并不建议使用这种方式。
(2)先创建对象,再绑定固定端口号(建议使用)
//解决报出异常:java.net.BindException: bind failed: EADDRINUSE (Address already in use)
if (mUdpSocket == null) {
mUdpSocket = new DatagramSocket(null);
mUdpSocket.setReuseAddress(true);
//mUdpSocket.bind(new InetSocketAddress(60000));
}
这样方式创建DatagramSocket对象会在一定程度上杜绝端口占用的问题,但很遗憾,还是有可能会出现这种情况,但是相对于第1种方式显然更加推荐这种方式。用这种方式的话如果想要彻底解决端口被占用的问题,我个人比较推荐的是可以在某一端口区间内进行绑定,例如60000-60050区间,就是说比如60000端口已经被占用,那么则绑定下一个端口,即60001端口。
(3)直接创建对象,绑定随机端口号(建议使用)
if (mUdpSocket == null) {
mUdpSocket = new DatagramSocket();
mUdpSocket.setReuseAddress(true);
}
通过看源码我们不难发现,空构造函数创建DatagramSocket对象的时候会默认走绑定方法,这时候绑定的就是随机端口号了,这样的话就不会再出现端口被占用的问题了。
(4)如何选择是否要绑定固定端口号
对于UDP通信而言,必须要有一端绑定固定端口号,这样才能建立起UDP通信的桥梁。如果你是要绑定固定端口号的一端,则建议使用第二种方式,如果你是不需要绑定固定端口号的一端,则建议使用第三种方式。
2.UDP接收端Receive
//创建socket相当于创建码头
DatagramSocket socket = new DatagramSocket(60000);
//创建packet相当于创建集装箱
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
while(true) {
//接收货物
socket.receive(packet);
byte[] arr = packet.getData();
int len = packet.getLength();
String ip = packet.getAddress().getHostAddress();
System.out.println(ip + ":" + new String(arr,0,len));
}
3.UDP发送端Send
//创建socket相当于创建码头
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while(true) {
String str = sc.nextLine();
if("quit".equals(str)) {
break;
}
//创建packet相当于创建集装箱
DatagramPacket packet = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("127.0.0.1"), 60000);
//发货
socket.send(packet);
}
socket.close();
4.UDP广播与单播
UDP分为广播与单播。这两个概念也比较容易理解,广播即针对网络中所有工作站进行消息的发送,UDP的广播地址是固定的,为255.255.255.255,也就是说,在发消息的时候发到这个地址上,则完成了UDP广播。理解了广播,单播就好说了,向网络中某一个特定的地址(例如192.168.1.120)发送消息的过程就称作单播。举个简单的例子,以我们的社交软件为例,如果你向某个群里面发送消息即可以理解为广播,如果你单独找某个朋友聊天即可以理解为单播。
5.UDP通信的原理
上面我们已经说到,如果要建立UDP通信的连接,必须要有一端绑定固定端口号。
现在我们不妨假设A端与B端进行通信,并且A端绑定了固定端口号60000。那么A与B是如何建立起连接并完成通信的呢?
由于A已经绑定了固定端口号,那么这时候B就可以向该端口号发送广播消息了(address=255.255.255.255, port=60000),当然可以携带一些数据。如果A收到该广播并进行了数据的验证,这时候就可以建立起连接了,因为此时A已经知道了B的地址和端口号。A就可以向B发送消息,B也就知道了A的地址与端口号,于是就建立起通信的连接了。
6.UDP通信的实例代码
我这里会以绑定随机端口号为例,即上述所说的B端。为了使我这端UDP发送和接收所用的端口号一致,我会在发送和接收消息时使用同一个DatagramSocket对象。
(1)UdpHelper
这个类主要用于获取DatagramSocket实例及该实例的销毁等。
package com.mythmayor.udptest.utils;
import java.net.DatagramSocket;
/**
* Created by mythmayor on 2019/6/21.
* UDP通信基类,获取DatagramSocket对象
*/
public class UdpHelper {
private static DatagramSocket mUdpSocket;
public static DatagramSocket getUdpSocket() {
try {
//1.构造函数,不建议用
//mUdpSocket = new DatagramSocket(MyConstant.DEFAULT_PORT);
//2.绑定指定端口
/*if (mUdpSocket == null) {//解决报出异常:java.net.BindException: bind failed: EADDRINUSE (Address already in use)
mUdpSocket = new DatagramSocket(null);
mUdpSocket.setReuseAddress(true);
//mUdpSocket.bind(new InetSocketAddress(MyConstant.DEFAULT_PORT));
mUdpSocket.bind(null);
}*/
//3.绑定随机端口
if (mUdpSocket == null) {
mUdpSocket = new DatagramSocket();
mUdpSocket.setReuseAddress(true);
}
} catch (Exception e) {
e.printStackTrace();
}
return mUdpSocket;
}
public static void destroyUdpSocket() {
if (mUdpSocket != null && mUdpSocket.isConnected()) {
mUdpSocket.disconnect();
mUdpSocket.close();
mUdpSocket = null;
}
UdpSendHelper.destroy();
UdpReceiveHelper.destroy();
}
}
(2)UdpReceiveHelper
这个类主要用于UDP消息的接收。使用方法如下:
UdpReceiveHelper.receive(new UdpReceiveListener() {
@Override
public void onReceive(DatagramPacket packet) {
String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
//TODO
}
});
package com.mythmayor.udptest.utils;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.HandlerThread;
import com.mythmayor.udptest.MyApplication;
import com.mythmayor.udptest.itype.UdpReceiveListener;
import java.net.DatagramPacket;
/**
* Created by mythmayor on 2019/6/20.
* UDP通信消息接收工具类
*/
public class UdpReceiveHelper extends UdpHelper {
private static Handler mHandler;
private static boolean isRunning;
private static UdpReceiveListener mListener;
private static Runnable mRunnable = new Runnable() {
@Override
public void run() {
try {
while (isRunning) {
byte data[] = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
getUdpSocket().receive(packet);
mListener.onReceive(packet);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
public static String getCurrentIP() {
//获取wifi服务
WifiManager wifiManager = (WifiManager) MyApplication.getContext().getSystemService(Context.WIFI_SERVICE);
//判断wifi是否开启
if (!wifiManager.isWifiEnabled()) {
wifiManager.setWifiEnabled(true);
}
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int ipAddress = wifiInfo.getIpAddress();
String ip = intToIp(ipAddress);
return ip;
}
public static String intToIp(int i) {
return (i & 0xFF) + "." +
((i >> 8) & 0xFF) + "." +
((i >> 16) & 0xFF) + "." +
(i >> 24 & 0xFF);
}
public static void receive(UdpReceiveListener listener) {
isRunning = true;
mListener = listener;
HandlerThread thread = new HandlerThread("MyHandlerThread");
thread.start();
mHandler = new Handler(thread.getLooper());
mHandler.post(mRunnable);
}
public static void destroy() {
try {
isRunning = false;
mHandler.removeCallbacks(mRunnable);
mHandler = null;
mListener = null;
mRunnable = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
(3)UdpSendHelper
这个类主要用于UDP消息的发送。使用方法如下:
UdpSendHelper.send(“255.255.255.255”, 60000, “sendData”);
package com.mythmayor.udptest.utils;
import java.net.DatagramPacket;
import java.net.InetAddress;
/**
* Created by mythmayor on 2019/6/20.
* UDP通信消息发送工具类
*/
public class UdpSendHelper extends UdpHelper {
public static void send(String host, final int port, final String sendData) {
try {
final InetAddress address = InetAddress.getByName(host);
//mServerAddress = InetAddress.getByName(etip.getText().toString());//单播UDP
//mServerAddress = InetAddress.getByName(MyConstant.BROADCAST_IP);//广播UDP
new Thread(new Runnable() {
@Override
public void run() {
try {
byte data[] = sendData.getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
getUdpSocket().send(packet);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void destroy() {
}
}