Android 基于UDP协议的Socket通信
1、Java中实现UDP的重要的类
1.1 DatagramPacket类:数据报文
(1)定义概念
此类表示数据报包。
数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
(2)构造方法
构造函数名称 | 含义 |
---|---|
DatagramPacket(byte[] buf, int length) | 接收构造函数, 用来接收长度为 length 的数据包 |
DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 发送构造函数,构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号 |
(3)主要方法
- getAddress()返回接收或发送此数据报文的机器的 IP 地址。
- getData()返回接收的数据或发送出的数据。
- getLength()返回发送出的或接收到的数据的长度。
- getPort()返回接收或发送该数据报文的远程主机端口号。
1.2 DatagramSocket类:数据报套接字
(1)定义概念
此类表示用来发送和接收数据报包的套接字。
数据报套接字是包投递服务的发送或接收点。每个在数据报套接字上发送或接收的包都是单独编址和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。
在 DatagramSocket 上总是启用 UDP 广播发送。为了接收广播包,应该将 DatagramSocket 绑定到通配符地址。在某些实现中,将 DatagramSocket 绑定到一个更加具体的地址时广播包也可以被接收。
(2)构造函数总结
构造函数名称 含义
DatagramSocket() 构造数据报套接字并将其绑定到本地主机上任何可用的端口。
DatagramSocket(int port) 创建数据报套接字并将其绑定到本地主机上的指定端口。
DatagramSocket(int port, InetAddress laddr) 创建数据报套接字,将其绑定到指定的本地地址。
(3)重要方法摘要
方法名称 | 含义 |
---|---|
void close() | 关闭此数据报套接字。 |
void connect(InetAddress address, int port) | 将套接字连接到此套接字的远程地址。 |
boolean isClosed() | 返回是否关闭了套接字。 |
void receive(DatagramPacket p) | 从此套接字接收数据报包。 |
void send(DatagramPacket p) | 从此套接字发送数据报包。 |
DatagramSocket(int port, InetAddress laddr) | 创建数据报套接字,将其绑定到指定的本地地址。 |
1.3 InetAddress类
(1)定义概念
此类表示互联网协议 (IP) 地址。
IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。InetAddress 的实例包含 IP 地址,还可能包含相应的主机名(取决于它是否用主机名构造或者是否已执行反向主机名解析)。
(2)创建方法
注意,创建此类事通过类方法而获取,并非构造方法。
方法名称 | 含义 |
---|---|
static InetAddress getByAddress(byte[] addr) | 在给定原始 IP 地址的情况下,返回 InetAddress 对象。 |
static InetAddress getByAddress(String host, byte[] addr) | 根据提供的主机名和 IP 地址创建 InetAddress。 |
static InetAddress getByName(String host) | 在给定主机名的情况下确定主机的 IP 地址。 |
2、发送、接收UDP报文步骤讲解
- 同一端如果既要发送消息又想接收消息,一定要将发送的socket和接收的socket分开。不能用一个socket既接收信息,又发送信息,这样一定会造成阻塞。
- 接收线程中,一定要有 while(true){ if(标志何时结束) 代码块 } 这样的方式,不然,在接收信息时,一定会造成阻塞,就是只能接收到一次信息。
2.1 数据报发送解析
以下步骤都在发送线程内,需要注意的是发送UDP报文逻辑单一,顺序执行完毕线程即可结束,不涉及到后台等待的需求,所以执行完后即可关闭套接字连接。
发送步骤
- 构造DatagramSocket对象
- 根据发送IP 来创建InetAddress对象
- 根据InetAddress对象、发送端口号、发送数据 来创建发送的DatagramPacket数据包对象
- 调用DatagramSocket对象的send(datagramPacket) 方法,发送UDP报文
- 调用DatagramSocket对象的close() 关闭套接字连接
对应以上步骤,代码展示(仅为部分重要代码):
Byte[] buf="hello android! ".getBytes();
DatagramSocket sendSocket = new DatagramSocket();
InetAddress serverAddr = InetAddress.getByName(SEND_IP);
DatagramPacket outPacket = new DatagramPacket(buf, buf.length,serverAddr, SEND_PORT);
sendSocket.send(outPacket);
sendSocket.close();
SEND_IP即是你想要发送的地址,SEND_PORT端口号则要选用闲置端口就是向8000~9000这样的端口号。
2.2 数据报接收 解析
原理
以下步骤都在接收线程内,同发送线程大致相同,但需要注意这两者本质的区别:发送线程里的逻辑执行一遍即可结束,但是接收线程需要在后台待定等待接收UDP报文,不可执行一遍就结束!相当于在一个界面中,可多次创建发送线程用来发送报文,但是接收线程只需在界面初始化时创建,从而一直监听报文接收(若重新进入界面,逻辑如上)。
所以,在接收线程内部需要用到循环,在循环内部调用套接字对象的receive()
方法来接收UDP报文。注意套接字对象的连接关闭,发送线程中单一的逻辑,执行完发送过程即可关闭连接,但是在接收线程中使用了循环,所以需要用一个全局标识量来控制循环,若界面退出或销毁则将标示值置为false,这样接收线程即可结束,再关闭套接字连接。
接收步骤
- 需要根据接收端口号 构造DatagramSocket对象
- 创建发送的DatagramPacket数据包对象
- 调用DatagramSocket对象的receive(datagramPacket) 方法,接收UDP报文
对应以上步骤,代码展示(仅为部分重要代码):
DatagramSocket receiveSocket = new DatagramSocket(RECEIVE_PORT);
while(listenStatus){
byte[] inBuf= new byte[1024];
DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
receiveSocket.receive(inPacket); i if(!inPacket.getAddress().equals(serverAddr)){
throw new IOException("未知名的报文");
}
receiveInfo = inPacket.getData();
receiveHandler.sendEmptyMessage(1);
}
3.界面收发UDP报文完整代码
public class TextActivity extends AppCompatActivity{
/*
* Data
* */
private final static String SEND_IP = "27.18.140.100"; //发送IP
private final static int SEND_PORT = 8989; //发送端口号
private final static int RECEIVE_PORT = 8080; //接收端口号
private boolean listenStatus = true; //接收线程的循环标识
private byte[] receiveInfo; //接收报文信息
private byte[] buf;
private DatagramSocket receiveSocket;
private DatagramSocket sendSocket;
private InetAddress serverAddr;
private SendHandler sendHandler = new SendHandler();
private ReceiveHandler receiveHandler = new ReceiveHandler();
/*
* UI
* */
private TextView tvMessage;
private Button btnSendUDP;
class ReceiveHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
tvMessage.setText("接收到数据了" + receiveInfo.toString());
Toast.makeText(TextActivity.this, "接收到数据了", Toast.LENGTH_SHORT).show();
}
}
class SendHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
tvMessage.setText("UDP报文发送成功");
Toast.makeText(TextActivity.this, "成功发送", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text);
btnSendUDP = (Button) findViewById(R.id.btn_send);
tvMessage = (TextView) findViewById(R.id.tv_show);
btnSendUDP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//点击按钮则发送UDP报文
new UdpSendThread().start();
}
});
//进入Activity时开启接收报文线程
new UdpReceiveThread().start();
}
@Override
protected void onDestroy() {
super.onDestroy();
//停止接收线程,关闭套接字连接
listenStatus = false;
receiveSocket.close();
}
/*
* UDP数据发送线程
* */
public class UdpSendThread extends Thread
{
@Override
public void run()
{
try
{
buf="i am an android developer, hello android! ".getBytes();
// 创建DatagramSocket对象,使用随机端口
sendSocket = new DatagramSocket();
serverAddr = InetAddress.getByName(SEND_IP);
DatagramPacket outPacket = new DatagramPacket(buf, buf.length,serverAddr, SEND_PORT);
sendSocket.send(outPacket);
sendSocket.close();
sendHandler.sendEmptyMessage(1);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
/*
* UDP数据接收线程
* */
public class UdpReceiveThread extends Thread
{
@Override
public void run()
{
try
{
sendSocket = new DatagramSocket(RECEIVE_PORT);
serverAddr = InetAddress.getByName(SEND_IP);
while(listenStatus)
{
byte[] inBuf= new byte[1024];
DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
sendSocket.receive(inPacket);
if(!inPacket.getAddress().equals(serverAddr)){
throw new IOException("未知名的报文");
}
receiveInfo = inPacket.getData();
receiveHandler.sendEmptyMessage(1);
}
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
4. 优雅解决UDP报文接收不到问题
4.1 发送与接收DatagramSocket需不同
此点很重要,之前在网上找的例子就将接收、发送线程中的DatagramSocket共用同一个全局变量,这会直接导致应用程序无法接收到UDP报文!
首先接收、发送UDP的逻辑分别在两个不同的线程中的run() 方法里,根据线程的启动去调用它们。注意两个线程存在的生命周期:
-
接收线程 在一个界面中(不退出界面的情况下)只会被创建并启动一次,即线程一直存在于后台等待接收UDP报文,此时它的DatagramSocket对象也是要一直存在的;
-
发送线程 在需要的情况下会多次被重复创建并启动,它的每次启动都会去创建DatagramSocket对象,发送完报文后会立即关闭掉DatagramSocket的连接。
若两个线程共用一个DatagramSocket对象,接收线程开启后,DatagramSocket对象存在于后台,此时发送一次报文后,DatagramSocket对象会被关闭连接,这样应用程序就无法再接收到UDP报文了。所以,这两个线程的DatagramSocket对象需独立不相同!
4.2 退出界面需关闭接收Socket连接
在理解了上一点后,在不退出界面的情况下应用程序可以很好的收发UDP报文,再思考全面一点。考虑用户在使用中退出界面又重新回到界面,重点还是放在DatagramSocket对象上,一般出错大多是出在这个上面。
-
发送线程执行完逻辑后会关闭DatagramSocket连接,线程结束。
-
接收线程是一直运行在后台,除非循环标识量置为False,接收线程才会结束。
如果不对接收线程中的DatagramSocket对象进行处理,在退出界面又重新回到界面时,接收线程会被重新创建,之前创建的DatagramSocket对象连接未关闭,此时再重新创建,便会出现异常,导致应用程序无法接收到UDP报文。异常如下:
java.net.BindException: bind failed: EADDRINUSE (Address already in use)
很明显,显示地址被占用。所以界面退出时应对接收线程的回收及Socket对象的连接关闭,修改代码如下:
@Override
protected void onDestroy() {
super.onDestroy();
//停止接收线程,关闭套接字连接
listenStatus = false;
receiveSocket.close();
}
上一篇: Photoshop 简洁红色圣诞壁纸制作
下一篇: MySql Study Notes