你的位置:首页 > Java教程

[Java教程]Java NIO4:Socket通道


Socket通道

上文讲述了通道、文件通道,这篇文章来讲述一下Socket通道,Socket通道与文件通道有着不一样的特征,分三点说:

1、NIO的Socket通道类可以运行于非阻塞模式并且是可选择的,这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性,因此,再也没有为每个Socket连接使用一个线程的必要了。这一特性避免了管理大量线程所需的上下文交换总开销,借助NIO类,一个或几个线程就可以管理成百上千的活动Socket连接了并且只有很少甚至没有性能损失

2、全部Socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对应的Socket对象,就是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),这些Socket可以通过调用socket()方法从通道类获取,此外,这三个java.net类现在都有getChannel()方法

3、每个Socket通道(在java.nio.channels包中)都有一个关联的java.net.socket对象,反之却不是如此,如果使用传统方式(直接实例化)创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel()方法将总是返回null

概括地讲,这就是Socket通道所要掌握的知识点知识点,不难,记住并通过自己写代码/查看JDK源码来加深理解。

 

非阻塞模式

前面第一点说了,NIO的Socket通道可以运行于非阻塞模式,这个陈述虽然简单却有着深远的含义。传统Java Socket的阻塞性质曾经是Java程序可伸缩性的最重要制约之一,非阻塞I/O是许多复杂的、高性能的程序构建的基础。

要把一个Socket通道置于非阻塞模式,要依赖的是Socket通道类的弗雷SelectableChannel,下面看一下这个类的简单定义:

public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel{  ...  public abstract void configureBlocking(boolean block) throws IOException;  public abstract boolean isBlocking();  public abstract Object blockngLock();  ...}

因为这篇文章是讲述Socket通道的,因此省略了和选择器相关的方法,这些省略的内容将在下一篇文章中说明。

从SelectableChannel的API中可以看出,设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false则设为非阻塞模式,就这么简单。同时,我们可以通过调用isBlocking()方法来判断某个Socket通道当前处于哪种模式中。

偶尔,我们也会需要放置Socket通道的阻塞模式被更改,所以API中有一个blockingLock()方法,该方法会返回一个非透明对象引用,返回的对象是通道实现修改阻塞模式时内部使用的,只有拥有此对象的锁的线程才能更改通道的阻塞模式,对于确保在执行代码的关键部分时Socket通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说,这个方法是非常方便的。

 

Socket通道服务端程序

OK,接下来先看下Socket通道服务端程序应该如何编写:

 1 public class NonBlockingSocketServer 2 { 3   public static void main(String[] args) throws Exception 4   { 5     int port = 1234; 6     if (args != null && args.length > 0) 7     { 8       port = Integer.parseInt(args[0]); 9     }10     ServerSocketChannel ssc = ServerSocketChannel.open();11     ssc.configureBlocking(false);12     ServerSocket ss = ssc.socket();13     ss.bind(new InetSocketAddress(port));14     System.out.println("开始等待客户端的数据!时间为" + System.currentTimeMillis());15     while (true)16     {17       SocketChannel sc = ssc.accept();18       if (sc == null)19       {20         // 如果当前没有数据,等待1秒钟再次轮询是否有数据,在学习了Selector之后此处可以使用Selector21         Thread.sleep(1000);22       }23       else24       {25         System.out.println("客户端已有数据到来,客户端ip为:" + sc.socket().getRemoteSocketAddress() 26             + ", 时间为" + System.currentTimeMillis()) ;27         ByteBuffer bb = ByteBuffer.allocate(100);28         sc.read(bb);29         bb.flip();30         while (bb.hasRemaining())31         {32           System.out.print((char)bb.get());33         }34         sc.close();35         System.exit(0);36       }37     }38   }39 }

整个代码流程大致上就是这样,没什么特别值得讲的,注意一下第18行~第22行,由于这里还没有讲到Selector,因此当客户端Socket没有到来的时候选择的处理办法是每隔1秒钟轮询一次。

 

Socket通道客户端程序

服务器端经常会使用非阻塞Socket通达,因为它们使同时管理很多Socket通道变得更容易,客户端却并不强求,因为客户端发起的Socket操作往往比较少,且都是一个接着一个发起的。但是,在客户端使用一个或几个非阻塞模式的Socket通道也是有益处的,例如借助非阻塞Socket通道,GUI程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的,所以,我们看一下客户端应该如何使用Socket通道:

 1 public class NonBlockingSocketClient 2 { 3   private static final String str = "Hello World!"; 4   private static final String remoteIp = "127.0.0.1"; 5    6   public static void main(String[] args) throws Exception 7   { 8     int port = 1234; 9     if (args != null && args.length > 0)10     {11       port = Integer.parseInt(args[0]);12     }13     SocketChannel sc = SocketChannel.open();14     sc.configureBlocking(false);15     sc.connect(new InetSocketAddress(remoteIp, port));16     while (!sc.finishConnect())17     {18       System.out.println("同" + remoteIp + "的连接正在建立,请稍等!");19       Thread.sleep(10);20     }21     System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis());22     ByteBuffer bb = ByteBuffer.allocate(str.length());23     bb.put(str.getBytes());24     bb.flip(); // 写缓冲区的数据之前一定要先反转(flip)25     sc.write(bb);26     bb.clear();27     sc.close();28   }29 }

总得来说和普通的Socket操作差不多,通过通道读写数据,非常方便。不过再次提醒,通道只能操作字节缓冲区也就是ByteBuffer的数据

 

运行结果展示

上面的代码,为了展示结果的需要,在关键点上都加上了时间打印,这样会更清楚地看到运行结果。

首先运行服务端程序(注意不可以先运行客户端程序,如果先运行客户端程序,客户端程序会因为服务端未开启监听而抛出ConnectionException),看一下:

看到红色方块,此时程序是运行的,接着运行客户端程序:

看到客户端已经将"Hello World!"写入了Socket并通过通道传到了服务器端,方框变灰,说明程序运行结束了。此时看一下服务器端有什么变化:

看到服务器端打印出了字符串"Hello World!",并且方框变灰,程序运行结束,这和代码是一致的。

当然,客户端看到的时间是XXX10307,服务器端看到的时间是XXX10544,这是很正常的,因为前面说过了,服务器端程序是每隔一秒钟轮询一次是否有Socket到来的。