Android | UDP的C(Java|Android)/S(Java)通信实战案例(简易聊天室)

简介: Android | UDP的C(Java|Android)/S(Java)通信实战案例(简易聊天室)

案例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、portInetAddress对象
然后通过 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,
所以将客户端 安装在可以联网的真机
也是可以跑的;
只是这里为了录图方便就跑在虚拟机调试而已。**





参考自,慕课网。就业班

相关文章
|
11月前
|
安全 Java API
Java 集合高级应用与实战技巧之高效运用方法及实战案例解析
本课程深入讲解Java集合的高级应用与实战技巧,涵盖Stream API、并行处理、Optional类、现代化Map操作、不可变集合、异步处理及高级排序等核心内容,结合丰富示例,助你掌握Java集合的高效运用,提升代码质量与开发效率。
410 0
|
11月前
|
安全 JavaScript Java
java Web 项目完整案例实操指南包含从搭建到部署的详细步骤及热门长尾关键词解析的实操指南
本项目为一个完整的JavaWeb应用案例,采用Spring Boot 3、Vue 3、MySQL、Redis等最新技术栈,涵盖前后端分离架构设计、RESTful API开发、JWT安全认证、Docker容器化部署等内容,适合掌握企业级Web项目全流程开发与部署。
954 0
|
12月前
|
缓存 算法 NoSQL
校招 Java 面试高频常见知识点深度解析与实战案例详细分享
《2025校招Java面试核心指南》总结了Java技术栈的最新考点,涵盖基础语法、并发编程和云原生技术三大维度: 现代Java特性:重点解析Java 17密封类、Record类型及响应式Stream API,通过电商案例演示函数式数据处理 并发革命:对比传统线程池与Java 21虚拟线程,详解Reactor模式在秒杀系统中的应用及背压机制 云原生实践:提供Spring Boot容器化部署方案,分析Spring WebFlux响应式编程和Redis Cluster缓存策略。
356 0
|
12月前
|
人工智能 Java API
Java 生态大模型应用开发全流程实战案例与技术路径终极对决
在Java生态中开发大模型应用,Spring AI、LangChain4j和JBoltAI是三大主流框架。本文从架构设计、核心功能、开发体验、性能扩展性、生态社区等维度对比三者特点,并结合实例分析选型建议。Spring AI适合已有Spring技术栈团队,LangChain4j灵活性强适用于学术研究,JBoltAI提供开箱即用的企业级解决方案,助力传统系统快速AI化改造。开发者可根据业务场景和技术背景选择最适合的框架。
2665 2
|
12月前
|
自然语言处理 前端开发 Java
JBoltAI 框架完整实操案例 在 Java 生态中快速构建大模型应用全流程实战指南
本案例基于JBoltAI框架,展示如何快速构建Java生态中的大模型应用——智能客服系统。系统面向电商平台,具备自动回答常见问题、意图识别、多轮对话理解及复杂问题转接人工等功能。采用Spring Boot+JBoltAI架构,集成向量数据库与大模型(如文心一言或通义千问)。内容涵盖需求分析、环境搭建、代码实现(知识库管理、核心服务、REST API)、前端界面开发及部署测试全流程,助你高效掌握大模型应用开发。
1071 5
|
12月前
|
缓存 Java API
Java 集合容器实操技巧与案例详解
本教程基于Java 8+新特性和现代开发实践,深入讲解Java集合容器的实操技巧。通过具体场景演示Stream API数据处理、ConcurrentHashMap并发控制、LinkedHashMap实现LRU缓存、TreeSet自定义排序等高级特性。同时涵盖computeIfAbsent优化操作、EnumMap专用集合使用、集合统计与运算(交集、并集、差集)等内容。代码示例丰富,助力掌握高效编程方法。[点击获取完整代码](https://pan.quark.cn/s/14fcf913bae6)。
294 0
|
前端开发 JavaScript Java
Java 学习路线规划及项目案例中的技术栈应用解析
内容包括:**Java 17核心特性**(如sealed class、record)与模块化开发;Spring Boot 3 + Spring Cloud微服务架构,涉及响应式编程(WebFlux)、多数据库持久化(JPA、R2DBC、MongoDB);云原生技术**如Docker、Kubernetes及CI/CD流程;性能优化(GraalVM Native Image、JVM调优);以及前后端分离开发(Vue 3、Spring Boot集成)。通过全栈电商平台项目实战,掌握从后端服务(用户、商品、订单)到前端应用(Vue 3、React Native)的全流程开发。
553 9
|
存储 Java 数据安全/隐私保护
Java技术栈揭秘:Base64加密和解密文件的实战案例
以上就是我们今天关于Java实现Base64编码和解码的实战案例介绍。希望能对你有所帮助。还有更多知识等待你去探索和学习,让我们一同努力,继续前行!
691 5
|
缓存 NoSQL Java
校招 Java 面试常见知识点及实战案例全解析
本文全面解析了Java校招面试中的常见知识点,涵盖Java新特性(如Lambda表达式、、Optional类)、集合框架高级应用(线程安全集合、Map性能优化)、多线程与并发编程(线程池配置)、JVM性能调优(内存溢出排查、垃圾回收器选择)、Spring与微服务实战(Spring Boot自动配置)、数据库与ORM框架(MyBatis高级用法、索引优化)、分布式系统(分布式事务、缓存应用)、性能优化(接口优化、高并发限流)、单元测试与代码质量(JUnit 5、Mockito、JaCoCo)以及项目实战案例(电商秒杀系统、社交消息推送)。资源地址: [https://pan.quark.cn/s
320 4
|
人工智能 Java 开发者
【Java实例-简易计算机】使用Java实现简单的计算机案例
一个简单的Java案例——“简易计算器”,帮助编程新手快速上手。通过实现用户输入、基本逻辑运算和结果输出,学习者可以掌握变量声明、Scanner对象使用、控制流语句等关键知识点。文章分为设计思路、关键知识点、完整代码和测试运行四个部分。
332 9
【Java实例-简易计算机】使用Java实现简单的计算机案例