分享

Android NIO非阻塞包

 Ebric 2011-12-01
大家都知道,对于Android的网络通讯性能的提高,我们可以使用Java上高性能的NIO (New I/O) 技术进行处理,NIO是从JDK 1.4开始引入的,NION我们可以理解为Noblocking即非阻塞的意思,相对应传统的I/O,比如Socketaccpet()、read()这些方法而言都是阻塞的。

       NIO主要使用了ChannelSelector来实现,JavaSelector类似WinsockSelect模式,是一种基于事件驱动的,整个处理方法使用了轮训的状态机,如果你过去开发过Symbian应用的话这种方式有点像活动对象,好处就是单线程更节省系统开销,NIO的好处可以很好的处理并发,对于Android网游开发来说比较关键,对于多点Socket连接而言使用NIO可以大大减少线程使用,降低了线程死锁的概率,毕竟手机游戏有UI线程,音乐线程,网络线程,管理的难度可想而知,同时I/O这种低速设备将影响游戏的体验。
  
       NIO作为一种中高负载的I/O模型,相对于传统的BIO (Blocking I/O)来说有了很大的提高,处理并发不用太多的线程,省去了创建销毁的时间,如果线程过多调度是问题,同时很多线程可能处于空闲状态,大大浪费了CPU时间,同时过多的线程可能是性能大幅下降,一般的解决方案中可能使用线程池来管理调度但这种方法治标不治本。使用NIO可以使并发的效率大大提高。当然NIOJDK 7中的AIO还存在一些区别,AIO作为一种更新的当然这是对于Java而言,如果你开发过Winsock服务器,那么IOCP这样的I/O完成端口可以解决更高级的负载。   
       NIO我们分为几个类型分别描述,作为Java的特性之一,我们需要了解一些新的概念,比如ByteBuffer类,ChannelSocketChannelServerSocketChannelSelectorSelectionKey。有关具体的使用,大家可以在Android SDK文档中看下java.niojava.nio.channels两个包了解。

       有关Android NIO我们主要分为三大类,ByteBuffer、FileChannelSocketChannelNIO和传统的I/O比较大的区别在于传输方式非阻塞,一种基于事件驱动的模式,将会使方法执行完后立即返回,传统I/O主要使用了流Stream的方式,而在New I/O中,使用了字节缓存ByteBuffer来承载数据。   ByteBuffer位于java.nio包中,目前提供了Java基本类型中除Boolean外其他类型的缓冲类型,比如ByteBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBufferShortBuffer  。同时还提供了一种更特殊的映射字节缓冲类型MappedByteBuffer。在传统IO的输入输出流中,InputStream中只提供了字节型或字节数组的访问对应NIO就是ByteBuffer,但是处理传统的DataInputStreamint等类型,就是IntBuffer,但是缓冲类型并没有提供UTF这样的类型处理,所以我们仍然需要使用ByteBuffer处理字符串,但是NIO提供了一个封装的类在java.nio.charset包中,通过字符的编码CharsetEncoder和解码CharsetDecoder类来处理字符串,同时这些类可以方便转换编码比如GBKUTF等等。

       当然了今天eoeAndroid主要给大家讲解下为什么使用NIOAndroid中有哪些用处。还有就是Android NIO我们主要分为三大类,eoeAndroid将在下一篇详细讲解Android NIO前两个类。

  我们这一片主要就是来介绍Android NIO三大类中的前两个类。大家是不是很期待呀,那么就来看看吧。

       一、ByteBuffer类
   
       1) 实例化
       直接使用ByteBuffer类的静态方法static ByteBuffer allocate(int capacity)  static ByteBuffer allocateDirect(int capacity)  这两个方法来分配内存空间,两种方法的区别主要是后者更适用于繁复分配的字节数组。而 put(ByteBuffer src) 可以从另一个ByteBuffer中构造,也可以通过wrap方法从byte[]中构造,具体参考下面的类型转化内容。

       2) 类型转化
       ByteBuffer可以很好的和字节数组byte[]转换类型,通过执行ByteBuffer类的final byte[]  array() 方法就可以将ByteBuffer转为byte[]。byte[]来构造ByteBuffer可以使用wrap方法,目前Android或者说Java提供了两种重写方法,比如为static ByteBuffer  wrap(byte[] array)  和 static ByteBuffer  wrap(byte[] array, int start, int len)  ,第二个重载方法中第二个参数为从array这个字节数组的起初位置,第三个参数为array这个字节数组的长度。

       3) 往ByteBuffer中添加元素
       目前ByteBuffer提供了多种put重写类型来添加,比如put(byte b) 、putChar(char value) 、putFloat(float value) 等等,需要注意的是,按照Java的类型长度,一个byte占1字节,一个char类型是2字节,一个floatint是4字节,一个long则为8字节,和传统的C++有些区别。所以内部的相关位置也会发生变化,同时每种方法还提供了定位的方法比如ByteBuffer  put(int index, byte b)
  
       4) 从ByteBuffer中获取元素
       同上面的添加想法,各种put被换成了get,比如byte  get()  、float  getFloat()  ,当然了还提供了一种定位的方式,比如double  getDouble(int index)
  
       5) ByteBuffer中字节顺序
       对于Java来说默认使用了BIG_ENDIAN方式存储,和C正好相反的,通过final ByteOrder  order() 返回当前的字节顺序。final ByteBuffer  order(ByteOrder byteOrder)  设置字节顺序,ByteOrder类的值有两个定义,比如LITTLE_ENDIAN、BIG_ENDIAN,如果使用当前平台则为ByteOrder.nativeOrder()Android中则为BIG_ENDIAN,当然如果设置为order(null) 则使用LITTLE_ENDIAN

       二、FileChannel类
        在NIO中除了Socket外,还提供了File设备的通道类,FileChannel位于java.nio.channels.FileChannel包中,在Android SDK文档中我们可以方便的找到,对于文件复制我们可以使用ByteBuffer方式作为缓冲,比如

Java代码:
  1. String infile = "/sdcard/cwj.dat";
  2. String outfile = "/sdcard/android123-test.dat";
  3. FileInputStream fin = new FileInputStream( infile );
  4. FileOutputStream fout = new FileOutputStream( outfile );
  5. FileChannel fcin = fin.getChannel();
  6. FileChannel fcout = fout.getChannel();
  7. ByteBuffer buffer = ByteBuffer.allocate( 1024 ); //分配1KB作为缓冲区


  8. while (true) {
  9. buffer.clear(); //每次使用必须置空缓冲区
  10. int r = fcin.read( buffer );
  11. if (r==-1) {
  12. break;
  13. }
  14. buffer.flip(); //写入前使用flip这个方法
  15. fcout.write( buffer );
  16. }
复制代码

       flipclear这两个方法是java.nio.Buffer包中,ByteBuffer的父类是从Buffer类继承而来的,这点eoeandroid要提醒大家看Android SDK文档时注意Inherited Methods,而JDK的文档就比较直接了,同时复制文件使用FileChanneltransferTo(long position, long count, WritableByteChannel target) 这个方法可以快速的复制文件,无需自己管理ByteBuffer缓冲区。
 
 
  有关Android NIO的精髓主要用于高负载的Socket网络传输,相对于传统I/O模型的Socket传输方式的优势,我们已经在 Android开发进阶之NIO非阻塞包(一)中讲到了,这里不再赘述,一起来看看Android NIO有关Socket操作提供的类吧: 

        一、ServerSocketChannel 服务器套接字通道在Android SDK中查找package名为  java.nio.channels.ServerSocketChannel

        在JavaNIO中,ServerSocketChannel对应的是传统IO中的ServerSocket,通过ServerSocketChannel类的socket() 方法可以获得一个传统的ServerSocket对象,同时从ServerSocket对象的getChannel() 方法,可以获得一个ServerSocketChannel()对象,这点说明NIOServerSocketChannel和传统IO的ServerSocket是有关联的,实例化ServerSocketChannel 只需要直接调用ServerSocketChannel 类的静态方法open()即可。

        二、 SocketChannel 套接字通道 java.nio.channels.SocketChannel    
        在JavaNew I/O中,处理Socket类对应的东西,我们可以看做是SocketChannel,套接字通道关联了一个Socket类,这一点使用SocketChannel类的socket() 方法可以返回一个传统IOSocket类。SocketChannel()对象在Server中一般通过Socket类的getChannel()方法获得。

       三、SelectionKey 选择键 java.nio.channels.SelectionKey
        在NIOSelectionKeySelector是最关键的地方,SelectionKey类中描述了NIO中比较重要的事件,比如OP_ACCEPT(用于服务器端)、OP_CONNECT(用于客户端)、OP_READ和OP_WRITE。 

        四、Selector 选择器 java.nio.channels.Selector 
        在NIO中注册各种事件的方法主要使用Selector来实现的,构造一个Selector对象,使用Selector类的静态方法open()来实例化。
  
        对于Android平台上我们实现一个非阻塞的服务器,过程如下:
  
       1. 通过Selector类的open()静态方法实例化一个Selector对象。
       2. 通过ServerSocketChannel类的open()静态方法实例化一个ServerSocketChannel对象。
       3. 显示的调用ServerSocketChannel对象的configureBlocking(false);方法,设置为非阻塞模式,eoeAndroid提示大家这一步十分重要。
       4. 使用ServerSocketChannel对象的socket()方法返回一个ServerSocket对象,使用ServerSocket对象的bind()方法绑定一个IP地址和端口号
       5. 调用ServerSocketChannel对象的register方法注册感兴趣的网络事件,很多开发者可能发现Android SDK文档中没有看到register方法,这里eoeandroid给大家一个ServerSocketChannel类的继承关系。
1.png 

       这里我们使用的register方法其实来自ServerSocketChannel的父类java.nio.channels.SelectableChannel,该方法原型为 final SelectionKey  register(Selector selector, int operations)  ,参数为我们执行第1步时的selector对象,参数二为需要注册的事件,作为服务器,我们当然是接受客户端发来的请求,所以这里使用SelectionKey.OP_ACCEPT了
       6. 通过Selector对象的select() 方法判断是否有我们感兴趣的事件发生,这里就是OP_ACCEPT事件了。我们通过一个死循环获取Selector对象执行select()方法的值,SDK中的原始描述为the number of channels that are ready for operation.,就是到底有多少个通道返回。
       7. 如果 Selector对象的select()方法返回的结果数大于0,则通过selector对象的selectedKeys()方法获取一个SelectionKey类型的Set集合,我们使用Java的迭代器Iterator类来遍历这个Set集合,注意判断SelectionKey对象,
       8. 为了表示我们处理了SelectionKey对象,需要先移除这个SelectionKey对象从Set集合中。这句很关键eoeandroid提醒大家注意这个地方。
       9. 接下来判断SelectionKey对象的事件,因为我们注册的感兴趣的是SelectionKey.OP_ACCEPT事件,我们使用SelectionKey对象的isAcceptable()方法判断,如果是我们创建一个临时SocketChannel对象类似上面的方法继续处理,不过这时这个SocketChannel对象主要处理读写操作,我们注册SelectionKey.OP_READSelectionKey.OP_WRITE分配ByteBuffer缓冲区,进行网络数据传输。

我们通过一个实例详细讲解下AndroidNIO非阻塞服务器的开发,对于客户端而言不推荐使用NIO,毕竟NIO相对于传统IO较为复杂,最重要的NIO是为了解决多线程并发问题而解决的技术,可能会因为管理和复杂性降低最终的结果,毕竟NIOJava的,相关的类型比较难控制,对于客户端而言我们可以使用C++、Java、C#甚至Flash Action Script来编写。    下面我们以一个简单的Echo Server为例子来分析

Java代码:
  1. package eoe.demo;

  2. import java.io.IOException;
  3. import java.net.InetSocketAddress;
  4. import java.nio.ByteBuffer;
  5. import java.nio.CharBuffer;
  6. import java.nio.channels.SelectionKey;
  7. import java.nio.channels.Selector;
  8. import java.nio.channels.ServerSocketChannel;
  9. import java.nio.channels.SocketChannel;
  10. import java.nio.charset.Charset;
  11. import java.nio.charset.CharsetDecoder;
  12. import java.nio.charset.CharsetEncoder;
  13. import java.util.Iterator;



  14. public class Server {
  15. public static void main(String[] args) {
  16. Selector selector = null;
  17. ServerSocketChannel ssc = null;


  18. try {
  19. selector = Selector.open(); //实例化selector
  20. ssc = ServerSocketChannel.open(); //实例化ServerSocketChannel 对象
  21. ssc.socket().bind(new InetSocketAddress(1987)); //绑定端口为1987
  22. ssc.configureBlocking(false); //设置为非阻塞模式
  23. ssc.register(selector, SelectionKey.OP_ACCEPT); //注册关心的事件,对于Server来说主要是accpet了

  24. while (true) {
  25. int n= selector.select(); //获取感兴趣的selector数量
  26. if(n<1)
  27. continue; //如果没有则一直轮训检查
  28. Iterator<SelectionKey> it = selector.selectedKeys().iterator(); //有新的链接,我们返回一个SelectionKey集合
  29. while (it.hasNext()) {
  30. SelectionKey key = it.next(); //使用迭代器遍历
  31. it.remove(); //删除迭代器
  32. if (key.isAcceptable()) { //如果是我们注册的OP_ACCEPT事件
  33. ServerSocketChannel ssc2 = (ServerSocketChannel) key.channel();
  34. SocketChannel channel = ssc2.accept();
  35. channel.configureBlocking(false); //同样是非阻塞
  36. channel.register(selector, SelectionKey.OP_READ); //本次注册的是read事件,即receive接受
  37. System.out.println("CWJ Client :" + channel.socket().getInetAddress().getHostName() + ":" + channel.socket().getPort());
  38. }
  39. else if (key.isReadable()) { //如果为读事件

  40. SocketChannel channel = (SocketChannel) key.channel();
  41. ByteBuffer buffer = ByteBuffer.allocate(1024); //1KB的缓冲区
  42. channel.read(buffer); //读取到缓冲区
  43. buffer.flip(); //准备写入
  44. System.out.println("eoeandroid receive info:" + buffer.toString());
  45. channel.write(CharBuffer.wrap("it works".getBytes())); //返回给客户端
  46. }
  47. }
  48. }
  49. } catch (IOException e) {
  50. e.printStackTrace();
  51. } finally {
  52. try {
  53. selector.close();
  54. server.close();
  55. } catch (IOException e) {
  56. }
  57. }
  58. }
  59. }
复制代码

有关Android NIO的相关内容,本次整理并归类如下,为了让大家感觉NIOAndroid平台联系的紧密,这里我们结合ADT插件的重要开发工具DDMS中的源码进行分析。在android git中的sdk.git文件中,可以找到ddmlib这个文件夹。有关PC和手机的互通内核在这里使用了Java来完全实现。这里一起帮助大家了解下PC同步软件的开发原理同时学习下Java中的New I/O技术。   比较重要的代码段我们贴出,逐一分析,其他的朋友可以直接预读源码:

Java代码:
  1. public static SocketChannel open(InetSocketAddress adbSockAddr,Device device, int devicePort)
  2. //这是一个重载版本,主要是关联Device实例。
  3. throws IOException, TimeoutException, AdbCommandRejectedException {
  4. SocketChannel adbChan = SocketChannel.open(adbSockAddr); 
  5. //构造SocketChannel对象,使用常规的open方法创建
  6. try {
  7. adbChan.socket().setTcpNoDelay(true); //设置TCP非延迟
  8. adbChan.configureBlocking(false); //非阻塞
  9. setDevice(adbChan, device); 

  10. byte[] req = createAdbForwardRequest(null, devicePort); 
  11. //设置端口转发,这句很关键,否则PC和手机通过USB是无法互通的。
  12. write(adbChan, req); //发送数据
  13. AdbResponse resp = readAdbResponse(adbChan, false); //读取收到的内容
  14. if (resp.okay == false) {
  15. throw new AdbCommandRejectedException(resp.message);
  16. }
  17. adbChan.configureBlocking(true);
  18. } catch (TimeoutException e) { //一般要处理超时异常
  19. adbChan.close(); //释放channel句柄
  20. throw e;
  21. } catch (IOException e) { //处理常规的IO异常
  22. adbChan.close();
  23. throw e;
  24. }
  25. return adbChan;
  26. }
复制代码

       有关读取ADB返回的报文方法

Java代码:
  1. static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
  2. throws TimeoutException, IOException {
  3. AdbResponse resp = new AdbResponse();
  4. byte[] reply = new byte[4]; 
  5. //创建4字节数组,主要检测成功与否,adb的协议是成功返回 okay,失败fail,等等。
  6. read(chan, reply); //读取具体的返回
  7. if (isOkay(reply)) { //判断是否成功
  8. resp.okay = true;
  9. } else {
  10. readDiagString = true; // look for a reason after the FAIL
  11. resp.okay = false;
  12. }
  13. try {
  14. while (readDiagString) {

  15. byte[] lenBuf = new byte[4];
  16. read(chan, lenBuf); //读取一个字节数组,最终为了转为一个整形
  17. String lenStr = replyToString(lenBuf); //字节数组转为String
  18. int len;
  19. try {
  20. len = Integer.parseInt(lenStr, 16); 
  21. //String转为整形,这里提示,这种写法可能比较愚蠢,但是下面为Log输出提供了一点点的便利。
  22. } catch (NumberFormatException nfe) {
  23. Log.w("ddms", "Expected digits, got '" + lenStr + "': "+ lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " "+ lenBuf[3]);
  24. Log.w("ddms", "reply was " + replyToString(reply));
  25. break;
  26. }
  27. byte[] msg = new byte[len];
  28. read(chan, msg);
  29. resp.message = replyToString(msg);
  30. Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"+ resp.message + "'");
  31. break;
  32. }
  33. } catch (Exception e) {

  34. }
  35. return resp;
  36. }
复制代码

       有关PC上对Android手机屏幕截图的方法之一:

Java代码:
  1. static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
  2. throws TimeoutException, AdbCommandRejectedException, IOException {
  3. RawImage imageParams = new RawImage();
  4. byte[] request = formAdbRequest("framebuffer:"); 
  5. // 读取手机端adbd服务器的framebuffer调用返回的数组
  6. byte[] nudge = {0};
  7. byte[] reply;
  8. SocketChannel adbChan = null;
  9. try {
  10. adbChan = SocketChannel.open(adbSockAddr);
  11. adbChan.configureBlocking(false); //非阻塞
  12. setDevice(adbChan, device); //设置我们关系的设备
  13. write(adbChan, request); //发送framebuffer这个请求了
  14. AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
  15. if (resp.okay == false) { //判断返回是否ok。
  16. throw new AdbCommandRejectedException(resp.message);
  17. }
  18. reply = new byte[4];
  19. read(adbChan, reply);

  20. /*首先返回的是一个协议,目前分为两个版本,主要是兼容模式和标准的模式,兼容模式比较少见,在2.0以后几乎看不到了。部分早期的1.6或更老的T-Mobile G1会使用兼容模式,模式不同,输出的截图中的颜色编码方式略有不同。*/

  21. ByteBuffer buf = ByteBuffer.wrap(reply);
  22. buf.order(ByteOrder.LITTLE_ENDIAN); //小头字节顺序
  23. int version = buf.getInt(); 
  24. //ByteBuffer直接转int的方法,比较方便不用自己从字节数组中构造,按位计算
  25. int headerSize = RawImage.getHeaderSize(version);
  26. //根据返回的adb截图协议版本判断将收到的字节大小
  27. reply = new byte[headerSize * 4]; //分配空间,具体大小需要看协议版本
  28. read(adbChan, reply);
  29. buf = ByteBuffer.wrap(reply); //从reply数组实例化ByteBuffer
  30. buf.order(ByteOrder.LITTLE_ENDIAN); 
  31. //注意字节序列,毕竟远端的adbd是工作在linux系统的手机上。
  32. if (imageParams.readHeader(version, buf) == false) { 
  33. //判断是否有效,兼容这种截图协议。
  34. Log.e("Screenshot", "Unsupported protocol: " + version);
  35. return null;
  36. }
  37. Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="+ imageParams.size + ", width=" + imageParams.width+ ", height=" + imageParams.height); 

  38. //打印下截图的基本信息,比如bpp代表色深,size是需要分配dib图像的字节数组。比较原始,
  39. write(adbChan, nudge); //发送一个字节,代表准备接收字节数组了
  40. reply = new byte[imageParams.size]; //分配和图像大小一样的字节数组
  41. read(adbChan, reply); 
  42. /*接收图像字节数组,这里Android开发网提示大家对于Android 1.x可能为RGB565,分配大小为 wxhx2xsize ,而2.x以后基本上为32位的RGB8888,分配大小为wxhx4xsize*/
  43. imageParams.data = reply;
  44. } finally {
  45. if (adbChan != null) {
  46. adbChan.close();
  47. }
  48. }
  49. return imageParams;
  50. }
复制代码

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多