Android 网络通信 之 UDP

本文导读

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

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

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

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

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

需求效果

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

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

代码实现

  • 布局文件 activity_main.xml 内容如下:
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/GridLayout1"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:columnCount="1"
    android:orientation="horizontal"
    android:rowCount="4">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</GridLayout>
  • 主活动 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,如下所示:
         * <pre>
         *  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();
         *      }
         *  }</pre>
         * 将 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 结束消息需要访问网络,则必须添加网络访问权限

<uses-permission android:name="android.permission.INTERNET" />

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.administrator.helloworld">

    <!--添加外部存储的读/写权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <!--添加网络访问权限-->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

猜你喜欢

转载自blog.csdn.net/wangmx1993328/article/details/82862701