案例GitHub地址
创建UDP服务端
- new一个Module:
模块名为:sample
- 创建一个package,名为udp:
InetAddress.InetAddressHolder源码:
InetAddressHolder(String hostName, int address, int family) {
this.originalHostName = hostName;
this.hostName = hostName;
this.address = address;
this.family = family;
}
- 在包下创建UdpServer.class,编写:
package com.lwp.sample.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* <pre>
* author : 李蔚蓬(简书_凌川江雪)
* time : 2019/10/27 17:08
* desc :
* </pre>
*/
public class UdpServer {
private InetAddress mInetAddress;
private int mPort = 7777;//尽可能用5000以后的
private DatagramSocket mSocket;
private Scanner mScanner;
//构造方法中初始化
public UdpServer() {
try {
mInetAddress = InetAddress.getLocalHost();
//传入,设置好本服务器ip 和 本服务程序指定的端口,虚拟“链接”的服务器一端
mSocket = new DatagramSocket(mPort, mInetAddress);
//用于控制面板的输入
mScanner = new Scanner(System.in);
mScanner.useDelimiter("\n");//指定控制面板的输入以换行来结束
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
}
}
public void start() {
//让Server端持续运行
while (true) {
try {
//类似于缓存区的一个字节数组
//UDP每次通信的数据大小受限制
//限制就来自于服务端传给DatagramPacket的字节数组
//因为UDP是通过DatagramPacket封装数据的,
// 而DatagramPacket的创建必须传入一个字节数组,这个数组便是通信数据包的大小限制
//
//这里指定的是1024,也就是客户端发送过来的数据包,
// 每次不能超过1024个字节,1byte = 8bit
byte[] buf = new byte[1024];
//接收客户端数据
DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
//如果没有数据包到来的话,程序会一直阻塞在receive()这里,receive()会阻塞,
// 如果有一个客户端发送一个数据包到这个程序中,
// 程序就会去执行receive()方法,将接收到的数据传输到receivedPacket中进而传输给receive()
mSocket.receive(receivedPacket);
//所以如果程序能往下走,就证明接收到数据了
//拿到客户端地址、端口号、发送过来的数据
InetAddress address = receivedPacket.getAddress();
int port = receivedPacket.getPort();
byte[] data = receivedPacket.getData();
String clientMsg = new String(data, 0, data.length);//把接收到的字节数据转换成String
//打印客户端信息和发送过来的数据
System.out.println("address = " + address +
", port = " + port + ",(Client's) msg = " + clientMsg);
/*
读取Terminal的输入
next()也是阻塞的,监听Terminal输入(消息+回车)
给客户端返回数据,
返回的数据我们希望可以在控制面板Terminal上写,
写完按Enter键完成
*/
String returnedMsg = mScanner.next();
byte[] returnedMsgBytes = returnedMsg.getBytes();//将String转换成byte数组
//getSocketAddress中包含getAddress(), getPort(),即包含地址跟数组
//下面把需要返回给客户端的数据封装成一个DatagramPacket
DatagramPacket sendPacket = new DatagramPacket(returnedMsgBytes,
returnedMsgBytes.length, receivedPacket.getSocketAddress());
mSocket.send(sendPacket);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new UdpServer().start();
}
}
**如此便完成了UDP Server的代码的编写,
相对比较简单,
涉及到的API就是以上所说的DatagramSocket
以及DatagramPacket
,
接收、发送数据时候,
都要提前封装一个DatagramPacket
对象,
接收时的封装传入的参数:缓存字节数组引用
及其长度
;
发送时的封装传入的参数:缓存字节数组引用
及其长度
、封装了客户端(发送目的地)ip、port
的InetAddress对象
;
然后通过receive()
和send()
操作即可;**
创建UDP客户端
- 先创建java文件,调试完毕之后,再移植到Android上来;
- udp包下,创建一个
UdpClient
:
package com.lwp.sample.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* <pre>
* author : 李蔚蓬(简书_凌川江雪)
* time : 2019/10/28 15:20
* desc :
* </pre>
*/
public class UdpClient {
/**
* 指定Server的 ip 和 port
*/
private String mServerIp = "***";
private int mServerPort = 7777;
private InetAddress mServerAddress;
/**
* 通信用的Socket
*/
private DatagramSocket mSocket;
private Scanner mScanner;
//构造方法中初始化
public UdpClient() {
try {
/*
直接实例化一个默认的Socket对象即可,
因为我们不需要像服务端那样把别的Client接入过来,
不必特别明确指定 自己的ip和port(服务程序),!!!!!!!!!!
因为这里是Client,是数据请求获取方,不是数据提供方,!!!!
所以只需要一个默认的Socket对象
来进行send 和 receive 即可
*/
mSocket = new DatagramSocket();
mServerAddress = InetAddress.getByName(mServerIp);
mScanner = new Scanner(System.in);
mScanner.useDelimiter("\n");
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public void start() {
while (true) {
try {
/*
完成向服务端发送数据
*/
String clientMsg = mScanner.next();
byte[] clientMsgBytes = clientMsg.getBytes();
/*
封装数据包,传入数据数组以及服务端地址、端口号
*/
DatagramPacket clientPacket = new DatagramPacket(clientMsgBytes,
clientMsgBytes.length, mServerAddress, mServerPort);
mSocket.send(clientPacket);
/*
接收服务端数据
*/
byte[] buf = new byte[1024];
DatagramPacket serverMsgPacket = new DatagramPacket(buf, buf.length);
mSocket.receive(serverMsgPacket);
//拿到服务端地址、端口号、发送过来的数据
InetAddress address = serverMsgPacket.getAddress();
int port = serverMsgPacket.getPort();
byte[] data = serverMsgPacket.getData();
String serverMsg = new String(data, 0, data.length);//把接收到的字节数据转换成String
//打印服务端信息和发送过来的数据
System.out.println("(Server's) msg = " + serverMsg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new UdpClient().start();
}
}
**private String mServerIp = "***";
这里,我把代码的内容略去了,
这部分使用的是本机的网络IPV4的ip,
查看本机ip方法传送门,
为何用的是本机ip
呢,因为UdpServer
中:
mInetAddress = InetAddress.getLocalHost();
处,
设置的服务端ip
正是本机ip
**
- 开始测试:
**注意,
程序运行第二次的时候,
如果第一次运行没有对链接进行关闭,
则第一次运行的端口号
会被占用
,
导致第二次相关程序运行时Socket对象
无法实例化
,
以致于Socket对象
为空(NULL)
,
程序报空指针
的错误!****为了避免这种情况,
可以在不需要Server
的时候,将Server
程序暂停;
也可以在更改程序之后,使PC睡眠再重新打开,亦可刷新port
占用;
或者直接为更改后的程序指定新的port
,当然这种方法不推荐;**
进入UdpServer.class,右键启动main程序:
同理启动UdpClient.class的main程序,启动完毕弹出底部界面如下:
Client端敲一个"nihao I am Client",然后回车:
切换到UdpServer终端,可以看到接收到信息:
反复测试:
移植客户端
- 将UDP客户端程序移植到Android中;
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/id_btn_send"
android:text="Send"
android:layout_width="wrap_content"
android:layout_alignParentRight="true"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/id_et_msg"
android:layout_toLeftOf="@+id/id_btn_send"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_below="@+id/id_et_msg"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/id_tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</RelativeLayout>
建一个Package名为biz,用于打包业务代码:
- 直接copy那UdpClient.class,粘贴在biz包下,
改名为UdpClientBiz:
编写之:
public class UdpClientBiz {
/**
* 指定Server的 ip 和 port
*/
private String mServerIp = "172.18.1.59";
private int mServerPort = 7778;
private InetAddress mServerAddress;
private Handler mUIHandler = new Handler(Looper.getMainLooper());
/**
* 通信用的Socket
*/
private DatagramSocket mSocket;
//构造方法中初始化
public UdpClientBiz() {
try {
/*
直接实例化一个默认的Socket对象即可,
因为我们不需要像服务端那样把别的Client接入过来,
不必特别明确指定 自己的ip和port(服务程序),!!!!!!!!!!
因为这里是Client,是数据请求获取方,不是数据提供方,!!!!
所以只需要一个默认的Socket对象
来进行send 和 receive 即可
*/
mSocket = new DatagramSocket();
mServerAddress = InetAddress.getByName(mServerIp);
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
/*
需求:客户端接收Server端返回的数据,并展示在控件上
实现:send 方法绑定一个接口
ps:这里的回调机制实现其实还有一种写法,
就是另外单独再起一个setListener方法来绑定Listener ,
但是这样做不太符合这里具体的场景——每个 服务端 return 回来的数据
都是跟每个 客户端 send 出去的数据相关联对应的;
单独使用setListener 的方式,看不到这个关联的逻辑,
所以这里直接把Listener 作为sendMsg 的必要形参,形成关联逻辑
以及绑定关系——必须先 sendMsg 之后才能 returnMsg(receiveMsg)
*/
public interface onMsgReturnedListener{
void onMsgReturned(String msg);
/*
Handle Exception
如果是异步的方法调用:可以把Exception 通过 Listener 给回调出去
如果是同步的方法调用:尽可能不要在方法中进行try catch,
最好是将其throw 出去,
或者catch 之后 封装下错误类型再将其throw 出去,
即一定要让调用者能知道这个异常;
这里是异步调用
*/
void onError(Exception ex);
}
public void sendMsg(final String msg, final onMsgReturnedListener listener) {
new Thread() {
@Override
public void run() {
try {
//信息转型
byte[] clientMsgBytes = msg.getBytes();
/*
封装数据包,传入数据数组以及服务端地址、端口号
*/
DatagramPacket clientPacket = new DatagramPacket(clientMsgBytes,
clientMsgBytes.length, mServerAddress, mServerPort);
mSocket.send(clientPacket);
/*
接收服务端数据
*/
byte[] buf = new byte[1024];
DatagramPacket serverMsgPacket = new DatagramPacket(buf, buf.length);
mSocket.receive(serverMsgPacket);
//拿到服务端地址、端口号、发送过来的数据
InetAddress address = serverMsgPacket.getAddress();
int port = serverMsgPacket.getPort();
byte[] data = serverMsgPacket.getData();
final String serverMsg = new String(data, 0, data.length);//把接收到的字节数据转换成String
/*
以上是信息的发送和接收,写在sendMsg 方法体中,名副其实
以下是对接收数据的处理,通过回调处理
*/
//这里是子线程,
// 但是 Handler 已同 MainLooper 进行绑定,
// 则利用这个handle 去更新UI,等同于切回主线程更新UI
mUIHandler.post(new Runnable() {
@Override
public void run() {
//数据借助回调外传
//“切回了”主线程,在调用的时候,接收数据之后才能更新UI
listener.onMsgReturned(serverMsg);
}
});
} catch (final Exception e) {
mUIHandler.post(new Runnable() {
@Override
public void run() {
//异常回调
listener.onError(e);
}
});
}
}
}.start();
}
public void onDestroy() {
if(mSocket != null){
mSocket.close();
}
}
}
MainActivity.java:
public class MainActivity extends AppCompatActivity {
private EditText mEtMsg;
private Button mBtnSend;
private TextView mTvContent;
private UdpClientBiz mUdpClientBiz = new UdpClientBiz();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews() {
mEtMsg = findViewById(R.id.id_et_msg);
mBtnSend = findViewById(R.id.id_btn_send);
mTvContent = findViewById(R.id.id_tv_content);
mBtnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String msg = mEtMsg.getText().toString();
if (TextUtils.isEmpty(msg)) {
return;
}
appendMsgToContent("client:" + msg);
//发送后清除编辑框文本
mEtMsg.setText("");
mUdpClientBiz.sendMsg(msg, new UdpClientBiz.onMsgReturnedListener() {
@Override
public void onMsgReturned(String msg) {
//更新UI
appendMsgToContent("server:" + msg);
}
@Override
public void onError(Exception ex) {
ex.printStackTrace();
}
});
}
});
}
private void appendMsgToContent(String msg) {
mTvContent.append(msg + "\n");
}
/*
回收资源
*/
@Override
protected void onDestroy() {
super.onDestroy();
mUdpClientBiz.onDestroy();
}
}
然后记得添加网络权限:
<uses-permission android:name="android.permission.INTERNET"/>
- 测试:
启动UdpServer:
启动sample模块;
反复测试:
**本例用的服务器ip是真实的ipv4的网络本机ip,
所以将客户端安装在可以联网的真机
上
也是可以跑
的;
只是这里为了录图方便就跑在虚拟机调试而已。**
参考自,慕课网。就业班