关山难越,谁悲失路之人;萍水相逢,尽是他乡之客。
百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程教程 > 技术文章 > 正文

Android App开发基础篇—实现非阻塞Socket通信

guanshanw 2023-09-12 10:31 30 浏览 0 评论

Android App开发基础篇—实现非阻塞Socket通信

前言:Android开发中可以使用Java API提供的Socket和ServerSocket类来实现Socket通信。但是,通过这两个类实现的Socket通信是阻塞式的,当程序执行输入/输出操作后,在这些操作返回之前会一直阻塞线程。当有大量任务需要处理时,这种方式会降低性能。在Java中提供了另一种NIO API,可以实现非阻塞的Socket通信,该NIO API主要提供了以下两种特殊类:Selector和SelectableChannel。

一、Selector类:

Selector是SelectableChannel对象的多路复用器,可以同时监控多个SelectableChannel的IO状况,是非阻塞IO的核心。所有希望采用非阻塞方式进行通信的Channel都应该注册到Selector对象。可通过调用Selector类的静态open()方法来创建Selector实例。一个Selector实例包含3个SelectionKey集合:

Android App开发基础篇—实现非阻塞Socket通信

(1)所有SelectionKey集合:通过keys()方法返回,表示注册在该Selector上的所有Channel。

(2)被选择的SelectionKey集合:通过selectedKeys()返回,表示所有可通过select()方法监测到、需要进行IO处理的Channel。

(3)被取消的SelectionKey集合:表示所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除。程序通常无需直接访问该集合。

除了3个SelectionKey集合,Selector还提供了和select()相关的方法:

(1)int select():监控所有注册的Channel,当有Channel需要处理IO操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中。该方法返回这些Channel的数量。

(2)int select(long timeout):可以设置超时时长的select()操作。

(3)int selectNow():执行一个立即返回的select()操作,相对于无参数的select()方法,该方法不会阻塞线程。

(4)Selector wakeup():使一个还未返回的select()方法立即返回。

二、SelectableChannel类:

SelectableChannel代表了可以支持非阻塞IO操作的Channel对象,可以将其注册到Selector上,这种注册的关系由SelectionKey实例表示。Java中可以调用SelectableChannel实例的register()方法将其注册到指定的Selector上。当注册到Selector中的Channel当中有需要处理IO操作时,可以调用Selector的select()方法获取它们的数量,并通过selectedKeys()方法返回它们对应的SelectionKey集合。通过这个集合,可以获取所需要处理IO操作的SelectableChannel集。

SelectableChannel支持阻塞和非阻塞两种模式,默认是阻塞的,必须通过非阻塞模式才能实现非阻塞IO操作。可以通过以下两个方法来设置和返回Channel的模式:

(1)SelectionKey configureBlocking(boolean block):设置是否采用阻塞模式

(2)boolean isBlocking():返回该Channel是否阻塞模式。

不同的SelectableChannel所支持的操作不一样。在SelectableChannel中提供了如下方法来返回不同SelectableSocketChannel所支持的操作:

int validOps():返回一个bit mask,表示这个Channel上支持的IO操作。

此外,SelectableChannel还提供了如下方法获取它的注册状态:

boolean isRegistered():返回该Channel是否已注册在一个或多个Selector上。

selectionKey keyFor(Selector sel):返回该Channel和Selector之间的注册关系,如不存在注册关系,则返回null。

下面介绍两种SelectableSocketChannel,分别是对应于java.net.ServerSocket的ServerSocketChannel和对应于java.net.Socket的SocketChannel。

2.1 ServerSocketChannel类:

ServerSocketChannel代表一个ServerSocket,提供了一个TCP协议的IO接口,对应于java.net.ServerSocket,支持非阻塞模式。它只支持OP_ACCEPT操作。同时,该类也提供了accept()方法,功能相当于ServerSocket的accept()方法。

2.2 SocketChannel类:

SocketChannel对应于java.net.Socket,同样提供一个TCP协议的IO接口,支持非阻塞模式。它支持OP_CONNECT、OP_READ和OP_WRITE操作。此外,该类还实现了ByteChannel接口、ScatteringByteChannel接口和GatheringByteChannel接口,可以直接通过SocketChannel来读写ByteBuffer对象。

三、使用

服务器上所有的Channel都需要向Selector注册,包括ServerSocketChannel和SocketChannel。该Selector负责监控这些Channel的IO状态,当有Channel需要IO操作时,Selector的select()方法将会返回具有IO操作的Channel的数量。并且,可通过selectedKeys()方法获取这些Channel对应的SelectionKey集合,进而获取到具有IO操作的SelectableChannel集。下面以一个实例来说明Java中NIO Socket的使用。直接上代码加注释

3.1 Server端:(在Eclipse上)


public class NIOSocketServer implements ActionListener { private String ip = "192.168.1.132";  private Window mWindow;  private JButton mBtnSend;  private JButton mBtnSendAll;  private JTextField mTextFiled;  private JTextArea mTextArea;  private JButton mBtnClear;  // 用于检测所有Channel状态的Selector  private Selector mSelector = null;  // 定义实现编码、解码的字符集对象  private Charset mCharset = Charset.forName("UTF-8");  private SocketChannel mSocketChannel = null;  // private static ExecutorService mThreadPool;  // private static SocketRun mSocketRun;   public NIOSocketServer() { mWindow = new Window("服务端");  mBtnSend = mWindow.getSendButton();  mBtnSend.setName("发送");  mBtnSendAll = mWindow.getSendAllButton();  mBtnSendAll.setName("群发");  mBtnClear = mWindow.getClearButton();  mBtnClear.setName("clear");  mTextFiled = mWindow.getTextField();  mTextArea = mWindow.getJTextArea();  mBtnSend.addActionListener(this);  mBtnSendAll.addActionListener(this);  mBtnClear.addActionListener(this);  // mSocketRun = new SocketRun();  // mThreadPool = Executors.newCachedThreadPool();  } public static void main(String[] args) throws IOException { System.out.println("服务端已启动...");  new NIOSocketServer().init();  } @Override public void actionPerformed(ActionEvent event) { JButton source = (JButton) event.getSource();  String name = source.getName();  if (mBtnSend.getName().equals(name)) { // 向单个客户端发送消息  String content = mTextFiled.getText().toString();  if (mSocketChannel != null) { try { mSocketChannel.write(mCharset.encode(content));  } catch (IOException e) { e.printStackTrace();  } } } else if (mBtnSendAll.getName().equals(name)) { // 向所有客户端发送消息  String content = mTextFiled.getText().toString();  sendToAll(content);  } else if (mBtnClear.getName().equals(name)) { mTextArea.setText("");  } } private void sendToAll(String message) { for (SelectionKey sk : mSelector.keys()) { Channel channel = sk.channel();  if (channel instanceof SocketChannel) { SocketChannel dest = (SocketChannel) channel;  if (dest.isOpen()) { try { dest.write(mCharset.encode(message));  } catch (IOException e) { e.printStackTrace();  } } } } } public void init() throws IOException { mSelector = Selector.open();  // 通过open方法打开一个未绑定的ServerSocketChannel实例  ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  InetSocketAddress inetSocketAddress = new InetSocketAddress(ip, 30000);  // 将该ServerSocketChannel绑定到指定的IP地址  serverSocketChannel.socket().bind(inetSocketAddress);  // 设置ServerSocket以非阻塞方式工作  serverSocketChannel.configureBlocking(false);  // 将serverSocketChannel注册到指定Selector对象  serverSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT);  while (mSelector.select() > 0) { // 依次处理selector上的每个已选择的SelectionKey  Set<SelectionKey> selectedKeys = mSelector.selectedKeys();  //这里必须用iterator,如果用for遍历Set程序会报错  Iterator<SelectionKey> iterator = selectedKeys.iterator();  while (iterator.hasNext()) { SelectionKey sk = iterator.next();  // 从selector上的已选择的SelectionKey集合中删除正在处理的SelectionKey  iterator.remove();  // 如果sk对应的通道包含客户端的连接请求  if (sk.isAcceptable()) { // 调用accept方法接受连接,产生服务端对应的SocketChannel  SocketChannel socketChannel = serverSocketChannel.accept();  mTextArea.append("客户端接入,IP:"+socketChannel.getLocalAddress()+"\n");  // 设置采用非阻塞模式  socketChannel.configureBlocking(false);  mSocketChannel = socketChannel;  // 将该SocketChannel也注册到selector  socketChannel.register(mSelector, SelectionKey.OP_READ);  // 将sk对应的Channel设置成准备接受其他请求  sk.interestOps(SelectionKey.OP_ACCEPT);   } // 如果sk对应的通道有数据需要读取  if (sk.isReadable()) { // 获取该SelectionKey对应的Channel,该Channel中有可读的数据  SocketChannel socketChannel = (SocketChannel) sk.channel();  mSocketChannel = socketChannel;  // 定义准备执行读取数据的ByteBuffer  ByteBuffer buffer = ByteBuffer.allocate(1024);  String content = "";  // 开始读取数据  try { while (socketChannel.read(buffer) > 0) { buffer.flip();  content += mCharset.decode(buffer);  } if ("shutdown".equals(content)) { sk.cancel();  if (sk.channel() != null) { sk.channel().close();  } sendToAll("reconnect");  } else { // 打印从该sk对应的Channel里读取到的数据  mTextArea.append("来自客户端的消息:" + content + "\n");  // 将sk对应的Channel设置成准备下一次读取  sk.interestOps(SelectionKey.OP_READ);  } } // 如果捕捉到该sk对应的Channel出现了异常,即表明该Channel  // 对应的Client出现了问题,所以从Selector中取消sk的注册  catch (IOException ex) { // 从Selector中删除指定的SelectionKey  sk.cancel();  if (sk.channel() != null) { sk.channel().close();  } sendToAll("reconnect");  } } } } } } 


Server端创建GUI界面辅助测试的Window类

public class Window extends JFrame {

   /**
    * 窗口类 定义客户端和服务器端的窗口
    */
   private static final long serialVersionUID = 2L;
   private String windowName;
   private JFrame myWindow;
   private JTextArea area;
   private JTextField field;
   private JButton btnSend;
   private JButton btnSendAll;
   private JButton btnClear;

   public Window(String windowName) {
      this.windowName = windowName;
      myWindow = new JFrame(windowName);
      myWindow.setLayout(new FlowLayout());
      myWindow.setSize(new Dimension(600, 300));
      // 不能改变窗口大小
      myWindow.setResizable(false);

      area = new JTextArea();
      field = new JTextField();
      btnSend = new JButton("发送");
      btnSendAll = new JButton("群发");
      btnClear = new JButton("clear");

      // 设置field的大小
      field.setPreferredSize(new Dimension(300, 30));
      myWindow.add(field);
      myWindow.add(btnSend);
      myWindow.add(btnSendAll);
      myWindow.add(btnClear);
      myWindow.add(area);
      // 改变area的大小
      area.setPreferredSize(new Dimension(470, 210));
      area.setBackground(Color.PINK);
      area.setEditable(false);
      // 设置窗口显示在电脑屏幕的某区域
      myWindow.setLocation(400, 200);

      myWindow.setVisible(true);
      // 点击关闭按钮时触发该方法
      closeMyWindow();
   }

   /**
    * 方法名:closeMyWindow()
    * 
    * @param
    * @return 功能:当用户点击关闭按钮时,退出并且关闭该窗口
    */
   private void closeMyWindow() {
      myWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   }

   /**
    * 方法名:getFieldText()
    * 
    * @param
    * @return string 功能:获取窗口的TextField中的字符串
    */
   public String getFieldText() {
      return field.getText().toString();
   }

   /**
    * 方法名:getSendButton()
    * 
    * @param
    * @return JButton 功能:获得该窗口中的按钮
    */
   public JButton getSendButton() {
      return btnSend;
   }

   /**
    * 方法名:getSendAllButton()
    * 
    * @param
    * @return JButton 功能:获得该窗口中的按钮
    */
   public JButton getSendAllButton() {
      return btnSendAll;
   }

   /**
    * 方法名:getClearButton()
    * 
    * @param
    * @return JButton 功能:获得该窗口中的按钮
    */
   public JButton getClearButton() {
      return btnClear;
   }

   /**
    * 方法名:getJTextArea()
    * 
    * @param
    * @return JTextArea 功能:返回窗口中的JTextArea
    */
   public JTextArea getJTextArea() {
      return area;
   }

   /**
    * 方法名:getTextField()
    * 
    * @param
    * @return JTextField 功能:获得窗口中的textfield
    */
   public JTextField getTextField() {
      return field;
   }
}

3.2 客户端:(Android Studio上)


MainActivity代码:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";
    private String ip = "192.168.1.132";
    private TextView contentTv;
    // 定义检测SocketChannel的Selector对象
    private Selector mSelector;
    // 客户端SocketChannel
    private SocketChannel mSocketChannel;
    // 定义处理编码、解码的字符集
    private Charset mCharset = Charset.forName("UTF-8");
    private String mData = "";
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            switch (message.what) {
                case 0:
                    contentTv.append("连接成功...\n");
                    break;
                case 1:
                    contentTv.append("来自服务端端:" + mData + "\n");
                    break;
            }
            return false;
        }
    });
    private EditText mContentEt;

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

        contentTv = (TextView) findViewById(R.id.content_tv);
        mContentEt = (EditText) findViewById(R.id.content_et);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_conn:
                //Android里面网络操作不能放在UI线程,
                // 所以开启一个线程来测试
                new connectThread().start();
                break;
            case R.id.btn_send:
                new sendMsgThread().start();
                break;
        }
    }

    private class sendMsgThread extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                mSocketChannel.write(mCharset.encode(mContentEt.getText().toString()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private class connectThread extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                mSelector = Selector.open();
                InetSocketAddress inetSocketAddress = new InetSocketAddress(ip, 30000);
                // 调用open方法创建连接到指定主机的SocketChannel
                mSocketChannel = SocketChannel.open(inetSocketAddress);
                mHandler.sendEmptyMessage(0);
                // 设置该SocketChannel以非阻塞方式工作
                mSocketChannel.configureBlocking(false);
                // 将该SocketChannle对象注册到指定的Selector
                mSocketChannel.register(mSelector, SelectionKey.OP_READ);
                // 读取服务端消息
                while (mSelector != null && mSelector.select() > 0) {
                    Set<SelectionKey> selectionKeys = mSelector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey sk = iterator.next();
                        iterator.remove();
                        if (sk != null && sk.isReadable()) {
                            mSocketChannel = (SocketChannel) sk.channel();
                            ByteBuffer buff = ByteBuffer.allocate(1024);
                            String data = "";
                            while (mSocketChannel.read(buff) > 0) {
                                mSocketChannel.read(buff);
                                buff.flip();
                                data += mCharset.decode(buff);
                                buff.clear();
                            }
                            Log.e(TAG, "run: data:" + data);
                            mData = data;
                            mHandler.sendEmptyMessage(1);
                            sk.interestOps(SelectionKey.OP_READ);
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "run: e:" + e.getMessage());
            }
        }
    }
}
activity_main.xml代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.niosocketdemo.MainActivity">


    <Button
        android:id="@+id/btn_conn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="连接" />

    <Button
        android:id="@+id/btn_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="发送消息" />

    <EditText
        android:id="@+id/content_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入要发送的消息" />

    <TextView
        android:id="@+id/content_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="消息:\n" />
</LinearLayout>




相关推荐

七条简单命令让您玩转Git
七条简单命令让您玩转Git

凭借着出色的协作能力、快速部署效果与代码构建辅助作用,Git已经得到越来越多企业用户的青睐。除了用于开发商业及消费级应用之外,众多科学及政府机构也开始尝试使用这...

2023-10-07 12:14 guanshanw

基本完整的关于Git分支branch的操作
基本完整的关于Git分支branch的操作

Git使用背景项目中要用到dev或者其他分支开发完代码,需要将该分支合并到master的需求操作步骤下面以dev名称为lex为分支名为例来操作一遍客户端操作:...

2023-10-07 12:14 guanshanw

Git 进阶(合并与变基)
Git 进阶(合并与变基)

在Git中整合来自不同分支的修改主要有两种方法:合并(merge)以及变基(rebase)合并(merge)merge流程图merge的原理是找到这两个分...

2023-10-07 12:13 guanshanw

Git学习笔记 003 Git进阶功能 part5 合并(第一部分)

合并(merge)是很常用的操作。尤其是一个庞大的很多人参与开发的企业级应用。一般会设定一个主分支,和多个副分支。在副分支开发完成后,合并到主分支中。始终保持主分支是一个完整的,稳定的最新状态的分支。...

非标题党,三张图帮你理解git merge和git rebase的区别
非标题党,三张图帮你理解git merge和git rebase的区别

初始场景:基于正常的开发分支修改几个小bug,然后在合并到开发分支上。gitmergegitcheckoutfeaturegitmergeho...

2023-10-07 12:13 guanshanw

git 初次使用(01)
git 初次使用(01)

先从github上克隆代码下来:使用vscode克隆代码如下图,填写上github仓库地址:vscode有时候克隆代码速度比较慢,可以用命令行方式克隆gitc...

2023-10-07 12:12 guanshanw

Git 远程操作

4.Git远程操作命令说明gitremote远程版本库操作gitfetch从远程获取版本库gitpull下载远程代码并合并gitpush上传远程代码并合并4.1远程版本库操作gitre...

Git常用命令-总结
Git常用命令-总结

创建git用户$gitconfig--globaluser.name"YourName"$gitconfig--globaluser.em...

2023-10-07 12:12 guanshanw

git中删除从别人clone下来项目的git信息,并修改为自己的分支

如果你从别人的Git存储库中克隆了一个项目,并想要删除与该存储库相关的Git信息,并将其修改为你自己的分支,则可以执行以下步骤:使用gitclone命令克隆存储库:gitclone<u...

git系列-回滚和放弃本地修改

回滚历史提交就是reset的功能。这种情况是已经提交远程仓库,需要回滚到之前的提交。gitreset--hardcommitId//注:强制提交后,当前版本后面的提交版本将会删掉!gi...

GIT使用小技巧大全
GIT使用小技巧大全

在大型软件工程的开发过程中,版本控制是无法绕过去的;目前来说,最火的版本控制软件就是GIT了。早两年SVN比较火,不过被大神linus喷了几次后,就日落西山了,...

2023-10-07 12:11 guanshanw

git相关命令-上
git相关命令-上

这些命令都是看了文档后,个人觉得比较有用的一些,展示给大家。回到远程仓库的状态抛弃本地所有的修改,回到远程仓库的状态。gitfetch--all&...

2023-10-07 12:10 guanshanw

Git命令行接口:掌握Git的必备技能
Git命令行接口:掌握Git的必备技能

Git是一款强大的分布式版本控制工具,它支持命令行界面操作。熟练掌握Git命令行接口,是开发者使用Git的必备技能之一。在这篇文章中,我们将介绍Git命令行接口...

2023-10-07 12:10 guanshanw

Git命令详解
Git命令详解

相信各位小伙伴们应该都对git有一些了解,毕竟作为代码管理的神器,就算不是IT行业的小伙伴肯定也或多或少的听说过一些。今天就来和小伙伴们分享一下自己总结的常用命...

2023-10-07 12:10 guanshanw

工作7年收集到的git命令
工作7年收集到的git命令

概念git中的术语解释:仓库也叫版本库(repository)stage:暂存区,add后会存到暂存区,commit后提交到版本库git安装linux...

2023-10-07 12:10 guanshanw

取消回复欢迎 发表评论: