Android 网络通信 之 UDP

目录

本文导读

需求效果

代码实现


本文导读

  • 本文将介绍基于UDP 协议的 Socket 通信,一些注意事项在这里提前说明。

1)Android 为了确保用户流畅的操作体验,一些耗时的任务不能够在 UI 线程中运行,像网络访问/通信就属于这类任务,因此必须新开线程执行这些操作。

2)Android 规定除了 UI 线程外,其它线程都不可以对 UI 控件访问和操控。

3)当后台线程获取到数据(如 UDP 监听到的消息)之后,需要将这些数据显示到 UI 界面上时,这就又涉及到了 Android 的线程间数据传递问题。

4)Android 的许多操作(如网络访问、手机存储访问等),都需要在 主配置文件 AndroidManifest.xml 中先声明权限。

需求效果

Android 网络通信 之 UDP_第1张图片

  • 手机上简单的播放效果如上所示,播放的内容通过 UDP 发送过去,即发送什么,它就播放什么。
  • 播放的视频地址是网络上的在线视频地址,直接使用 UDP 工具进行发送消息,当然也可以自己写代码进行 UDP 消息发送,而且写法与 Java 完全一样。

Android 网络通信 之 UDP_第2张图片

  • 就使用如下简单的两个类达到要求,一个主线程 UI 类,一个后台监听 UDP 消息的 类。

Android 网络通信 之 UDP_第3张图片

代码实现

  • 布局文件 activity_main.xml 内容如下:


    

  • 主活动 MainActivity.java 内容如下:
package com.example.administrator.helloworld;

import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;

import com.example.administrator.helloworld.thread.UdpThread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 一个应用程序可以有1个或多个活动,而没有任何限制。
 * 每个为应用程序所定义的活动都需要在 AndroidManifest.xml 中声明,应用的主活动的意图过滤器标签中需要包含 MAIN 动作和 LAUNCHER 类别
 * 如果 MAIN 动作还是 LAUNCHER 类别没有在活动中声明,那么应用程序的图标将不会出现在主屏幕的应用列表中。
 */
public class MainActivity extends AppCompatActivity {

    /**
     * android.widget.VideoView:视频播放器控件
     * myHandler:用于线程间通信的内部类 Handler
     */
    private VideoView videoView;
    public MyHandler myHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bindViews();

        /** 创建 handler 并与 looper 绑定
         * 主线程 MainActivity extends AppCompatActivity,AppCompatActivity 的祖上有 ContextWrapper
         * android.content.ContextWrapper#getMainLooper() :获取 Looper
         * */
        myHandler = new MyHandler(MainActivity.this.getMainLooper());

        /**
         * 创建新线程用于循环监听 UDP 消息
         * 虽然如果将 监听UDP的线程 直接放在本类中,操作会简单一些,但是为了更加清晰,推荐新建线程类,
         * 所以要将创建好的 Handler 传递到后台的 UDP 线程中去
         */
        UdpThread udpThread = new UdpThread(myHandler);
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(udpThread);
    }

    /**
     * 绑定视屏控件
     * 绑定之后,后台 UDP 线程监听发来的在线视频地址,然后传回给主线程进行播放
     */
    private void bindViews() {
        videoView = findViewById(R.id.videoView);

        /**
         * 为 VideoView 视图设置媒体控制器,设置了之后就会自动由进度条、前进、后退等操作
         */
        videoView.setMediaController(new MediaController(this));

        /**视频准备完成时回调
         * */
        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.i("Wmx Logs::", "--------------视频准备完毕,可以进行播放.......");
            }
        });
        /**
         * 视频播放完成时回调
         */
        videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                /**播放完成时,再次循环播放*/
                videoView.start();
                Log.i("Wmx Logs::", "--------------视频播放完成,再次进行播放.......");
            }
        });

        /**
         * 视频播放发送错误时回调
         */
        videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                Log.i("Wmx Logs::", "--------------视频播放发送错误.......");
                return false;
            }
        });
    }

    /**
     * 自定义 android.os.Handler,用于接收后台线程传递过来给主线程的数据
     * Handler 是 Android 中专门用来处理线程间传递数据的工具
     */
    public class MyHandler extends Handler {

        /**
         * android.os.Looper 主要功能是为特定单一线程运行一个消息环,一个线程对应一个 Looper,同样一个 looper 对应一个线程。
         * 一个线程创建时本身是没有自己的 looper (只有主线程有),因此需要手动创建 Looper ,然后将 Looper 与线程相关联,
         * 操作方式:在需要关联的 looper 的线程中调用 Looper.prepare(预备),之后再调用 Looper.loop(循环) 启动 looper,如下所示:
         * 
         *  class LooperThread extends Thread {
         *      public Handler mHandler;
         *
         *      public void run() {
         *          Looper.prepare();
         *
         *          mHandler = new Handler() {
         *              public void handleMessage(Message msg) {
         *                  // process incoming messages here
         *              }
         *          };
         *
         *          Looper.loop();
         *      }
         *  }
* 将 looper 与线程关联的时候,looper 会同时生产一个 messageQueue(消息队列),looper 会不停的从 messageQueue 中取出消息(Message), * 然后线程就可以根据 Message 中的内容进行相应的操作。 * 在创建 Handler 的时候,需要与特定的 looper 绑定,这样通过 handler 就可以把 message 传递给特定的 looper,继而传递给特定的线程。 * 线程---Looper---Handler:一个 looper 可以对应多个 handler,而一个 handler 只能对应一个 looper * * @param L */ public MyHandler(Looper L) { super(L); } /** * 必须重写这个方法,用于处理 android.os.Message, * 当 后台线程调用 android.os.Handler#sendMessage(android.os.Message) 方法发送消息后 * 下面的 handleMessage(Message msg) 就会自动触发,然后处理消息 */ @Override public void handleMessage(Message message) { /** * android.os.Bundle 就是消息中的数据 * android.os.BaseBundle#getString(java.lang.String):取值的 key 不存在时,返回 null */ Bundle bundle = message.getData(); String messageText = bundle.getString("messageText"); Log.i("Wmx Logs::", "handleMessage 接收到消息>>>" + messageText); /**android.widget.VideoView#stopPlayback():停止视频,释放资源 * android.widget.VideoView#setVideoURI(android.net.Uri):重新绑定视频资源 * android.widget.VideoView#start():再次开始播放 * */ if (messageText != null && !"".equals(messageText)) { videoView.stopPlayback(); videoView.setVideoURI(Uri.parse(messageText)); videoView.start(); } } } }
  • 后台 UDP 监听的子线程 UdpThread.java 内容如下:
package com.example.administrator.helloworld.thread;

import android.os.Bundle;
import android.os.Message;
import android.util.Log;

import com.example.administrator.helloworld.MainActivity;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.charset.Charset;

/**
 * 后台监听 UDP 消息的子线程
 */
public class UdpThread implements Runnable {

    /**
     * TAG:日志标签
     * messageText:UDP 监听到的消息文本,每次不能超过 1024 字节
     * myHandler:用于给主线程发送消息的 Handler
     */
    private final String TAG = "Wmx Log::";
    private String messageText;
    public MainActivity.MyHandler myHandler;

    public UdpThread(MainActivity.MyHandler myHandler) {
        this.myHandler = myHandler;
    }

    @Override
    public void run() {
        Log.i("Wmx Logs:: ", "Udp 新线程开启..........." + Thread.currentThread().getName());
        DatagramSocket datagramSocket = null;
        /** 数据接收大小设置为 1024 字节,超出部分是接收不到的
         */
        byte[] buffer = new byte[1024];
        DatagramPacket datagramPacket;
        try {
            /**
             * InetSocketAddress(String hostname, int port):网络套接字地址,同时指定监听的 ip 与 端口
             *      A valid port value is between 0 and 65535.
             * InetSocketAddress(int port):网络套接字地址,指定监听的 端口,ip 默认为移动设备本机 ip
             * */
            InetSocketAddress socketAddress = new InetSocketAddress(9090);

            /**DatagramSocket(SocketAddress bindaddr):根据绑定好的 SocketAddress 创建 UDP 数据包套接字
             * DatagramSocket(int port):只指定 监听的端口时,IP 默认为移动设备本机 ip
             */
            datagramSocket = new DatagramSocket(socketAddress);
            datagramPacket = new DatagramPacket(buffer, buffer.length);

            /**循环监听*/
            while (true) {
                datagramSocket.receive(datagramPacket);
                /**读取数据
                 * 指定使用 UTF-8 编码,对于中文乱码问题,遵循对方发送时使用什么编码,则接收时也使用同样的编码的原则*/
                messageText = new String(datagramPacket.getData(), 0, datagramPacket.getLength(), Charset.forName("UTF-8"));

                /**
                 * 可以创建一个新的 Message,但是推荐调用 handler 的 obtainMessage 方法获取 Message,
                 * 这个方法的作用是从系统的消息池中取出一个 Message,这样就可以避免 Message 创建和销毁带来的资源浪费。
                 *
                 */
                Message obtainMessage = myHandler.obtainMessage();
                Bundle bundle = new Bundle();
                bundle.putString("messageText", messageText);
                /**为发送的消息设置数据*/
                obtainMessage.setData(bundle);
                /**发送消息*/
                myHandler.sendMessage(obtainMessage);
                Log.i("WMx Logs::", " UDP 监听到消息>>>>>" + messageText + " >>> 线程:" + Thread.currentThread().getName() + ">>为主线程传输完毕...");
            }
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (datagramSocket != null) {
                if (!datagramSocket.isConnected()) {
                    datagramSocket.disconnect();
                }
                if (!datagramSocket.isClosed()) {
                    datagramSocket.close();
                }
                /**即使抛异常了,也要再次监听*/
                new Thread(new UdpThread(myHandler)).start();
            }
        }
    }
}
  • 主配置文件 AndroidManifest.xml 内容如下,主要就是因为 UDP 结束消息需要访问网络,则必须添加网络访问权限




    
    
    

    
    

    
        
            
                

                
            
        
    

 

 

你可能感兴趣的:(Android)