UDP通信的流程中,接收端先监听某个端口,等待接收其它设备发来的数据包。发送端准备好数据包后,发送给接收端。接收端只需一个接收线程就可接收多个客户端发来的数据包。一个应用可以同时包含发送端和接收端。
这里通过一个例子介绍如何代码如何编写。这个例子是一个Android应用和一个JavaSE程序通过UDP通信。双方都同时实现接收端和发送端,双方可以像聊天一样随意地互相发送消息。
先看Android端接收消息的的代码。
当点击UDP的Listen按钮时,启动接收线程。在线程中先以端口号为参数创建DatagramSocket对象,再创建一个空的DatagramPacket对象用于存放收到的数据,接着再循环中调用DatagramSocket对象的receive方法。一旦接收到数据,receive方法就会返回,并且把数据保存在packet对象中。通过packet对象的getData方法,取出数据,然后构造出一个消息的字符串。最后用sendMessage方法把这个消息字符串传递给主线程显示到界面上。这是处理接收消息的代码。
Android端发送消息的代码是这样的。
先从Server文本框读取服务器的IP地址,从Content文本框读取消息内容。然后用他们创建一个DatagramPacket对象,也就是要发送的数据包。再创建一个DatagramSocket对象,调用它的send方法把数据包发出去。这些代码涉及网络操作,要在AsyncTask中异步执行。
再看JavaSE端的代码。JavaSE端的接收消息的代码都在接收线程中,主要流程是把接收到的消息输出。
接收数据的代码和Android端是一样的,都是创建一个DatagramSocket对象和一个空的DatagramPacket对象,调用DatagramSocket对象的receive方法,接收到的消息就会保存在DatagramPacket对象中。
JavaSE端的发送消息的代码在主线程中,主要流程是把键盘接收到的消息发送到Android端。
发送数据的代码和Android端是一样的,也是创建一个DatagramSocket对象,把要发送的数据保存到一个DatagramPacket对象中,然后调用DatagramSocket对象的send方法完成发送。
使用Socket接口时,经常需要进行字符转换。程序内部保存文本是字符串(String)形式,而网络传输过程中是字节流(byte[])形式,所以在发送和接收处需要进行转换。发送方需要将字符串或字符流转换为字节流,也就是String 到 byte[] 或 字符输出流到字节输出流( Writer => OutputStream)。接收方需要将字节流转换为字符串或字符流,也就是byte[] 到 String 或 字节输入流到字符输入流(InputStream => Reader)。因为通信是双向的,所以一般有四处需要转换。这四处都需要指出字符编码方式,如最常用的”utf-8”,否则容易出现中文乱码问题。
用模拟器测试UDP通信需要进行端口映射,因为模拟器和PC的IP地址相同,所有网络数据默认都会发送到PC的端口。通过端口映射,才能将发送到PC端口的数据转到模拟器端口,具体方法是:
1、用telnet连接模拟器控制台,命令是:telnet localhost 5554。(5554是模拟器的端口号,一般缺省是5554)。如果windows的telnet命令没打开,需要在控制面板-程序-启用或关闭Windows功能中打开。
2、授权,需要先执行auth <auth_token>命令,才能执行后面的端口映射命令,防止别人远程控制模拟器。<auth_token>是在C:\Users\<当前用户>\目录下的.emulator_console_auth_token文件中的内容,可以用windows记事本打开,复制到命令行窗口。
3、执行端口映射,命令是:redir add udp:8888:8888。意思是把PC8888端口接收到的UDP数据转到模拟器8888端口。
端口映射完成后,就可以测试了。如果测试过程中模拟器仍然无法收到UDP包,那么就只能用真机测试了。
Android端的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="Server:"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintEnd_toStartOf="@+id/editTextServer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/editTextServer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
app:layout_constraintBaseline_toBaselineOf="@+id/textView1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView1">
<requestFocus
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</EditText>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="TCP: "
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/buttonTCPConnect" />
<Button
android:id="@+id/buttonTCPConnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonTCPSend"
app:layout_constraintStart_toEndOf="@+id/textView2" />
<Button
android:id="@+id/buttonTCPSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:text="Send"
android:textAllCaps="false"
app:layout_constraintStart_toStartOf="@+id/buttonUDPSend"
app:layout_constraintTop_toBottomOf="@+id/editTextServer" />
<Button
android:id="@+id/buttonTCPClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Close"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonTCPSend"
app:layout_constraintStart_toEndOf="@+id/buttonTCPSend" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="UDP:"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/buttonUDPListen" />
<Button
android:id="@+id/buttonUDPListen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Listen"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonUDPSend"
app:layout_constraintStart_toEndOf="@+id/textView3" />
<Button
android:id="@+id/buttonUDPSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
android:textAllCaps="false"
app:layout_constraintStart_toEndOf="@+id/buttonUDPListen"
app:layout_constraintTop_toBottomOf="@+id/buttonTCPConnect" />
<Button
android:id="@+id/buttonUDPStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:textAllCaps="false"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonUDPSend"
app:layout_constraintStart_toEndOf="@+id/buttonUDPSend" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Content:"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintBaseline_toBaselineOf="@+id/editTextContent"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/editTextContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView4"
app:layout_constraintTop_toBottomOf="@+id/buttonUDPListen" />
<TextView
android:id="@+id/textViewResult"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextContent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Android端的完整代码如下:
import androidx.appcompat.app.AppCompatActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class MainActivity extends AppCompatActivity {
EditText etServer;
EditText etContent;
TextView tvResult;
Handler mHandler;
UDPReceiveThread udpReceiveThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etServer = findViewById(R.id.editTextServer);
etContent = findViewById(R.id.editTextContent);
tvResult = findViewById(R.id.textViewResult);
etServer.setText("192.168.1.2");
mHandler = new Handler(){
@Override
public void handleMessage(Message msg){
switch(msg.what) {
case 1: // receive socket message
String received = (String)msg.obj;
tvResult.append(received+"\r\n");
break;
}
}
};
Button btnUDPListen = (Button) findViewById(R.id.buttonUDPListen);
btnUDPListen.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
udpListen();
}
});
Button btnUDPSend = (Button) findViewById(R.id.buttonUDPSend);
btnUDPSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
String serverIP = etServer.getText().toString();
String content = "Android: " + etContent.getText().toString();
new AsyncTask<String, Void, Void>(){
@Override
protected Void doInBackground(String... arg0) {
try {
udpSend(arg0[0], arg0[1]);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}.execute(serverIP, content);
}
});
Button btnUDPStop = (Button) findViewById(R.id.buttonUDPStop);
btnUDPStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
udpStop();
}
});
}
void udpListen() {
udpReceiveThread = new UDPReceiveThread();
udpReceiveThread.start();
}
void udpSend(String ip, String ctx) throws IOException {
//Log.i("zzk", "udpSend: " + ip + ", " + ctx);
InetAddress address = InetAddress.getByName(ip);
byte[] buf = ctx.getBytes("UTF-8");
DatagramPacket recPacket = new DatagramPacket(buf, buf.length, address, 9999);
DatagramSocket socket = new DatagramSocket();
socket.send(recPacket);
socket.close();
}
void udpStop() {
if(udpReceiveThread!=null) udpReceiveThread.setStopFlag();
}
void sendMessage(String str){
Message msg = Message.obtain(); // 从Message池中取Message对象,用new创建会用到内存分配,影响效率
msg.what = 1;
msg.obj = str;
mHandler.sendMessage(msg);
}
class UDPReceiveThread extends Thread {
private boolean flag;
public void setStopFlag(){
flag = false;
}
@Override
public void run(){
flag = true;
try {
DatagramSocket socket = new DatagramSocket(8888);
socket.setSoTimeout(1000);
byte[] buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
sendMessage("UDP listen started");
while(flag){
try {
socket.receive(packet);
String s = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
sendMessage(s);
} catch(SocketTimeoutException e){} // 为了能结束线程,每秒钟退出receive一次,检查flag
}
socket.close();
sendMessage("UDP listen stoped");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
完整代码下载: