
4.4 客户端编程范例
本节介绍如何用java.nio包中的类来创建客户程序EchoClient,本节提供了两种实现方式。
·4.4.1节的例程4-5:采用阻塞模式,单线程。
·4.4.2节的例程4-6:采用非阻塞模式,单线程。
4.4.1 创建阻塞的EchoClient
客户程序一般不需要同时建立与服务器的多个连接,因此用一个线程,按照阻塞模式运行就能满足需求。本书1.5.2节的例程1-3的EchoClient类是通过创建Socket来建立与服务器的连接的,例程4-5的EchoClient类通过创建SocketChannel来与服务器连接,这个SocketChannel采用默认的阻塞模式。
例程4-5 EchoClient.java(阻塞模式)


在EchoClient类中,调用socketChannel.connect(isa)方法连接远程服务器,该方法在阻塞模式下运行时,将等到与远程服务器的连接建立成功才返回。在EchoClient类的talk()方法中,通过socketChannel.socket()方法获得与SocketChannel关联的Socket对象,然后从这个Socket中获得输出流与输入流,再一行行地发送和接收数据,这种处理方式与1.5.2节的例程1-3的EchoClient类相同。
4.4.2 创建非阻塞的EchoClient
对于客户与服务器之间的通信,按照它们收发数据的协调程度来区分,可分为同步通信和异步通信。同步通信指甲方向乙方发送了一批数据后,必须等接收到了乙方的响应数据后,再发送下一批数据。异步通信指发送数据和接收数据的操作互不干扰,各自独立进行。值得注意的是,通信的两端并不要求都采用同样的通信方式,当一方采用同步通信方式时,另一方可以采用异步通信方式。
同步通信要求一个I/O操作完成之后,才能完成下一个I/O操作,用阻塞模式更容易实现它。异步通信允许发送数据和接收数据的操作各自独立进行,用非阻塞模式更容易实现它。本书第1章和第3章介绍的EchoServer都采用同步通信,每当接收到客户的一行数据,都发回一行响应数据,然后接收下一行数据。4.3.2节的例程4-3,以及4.3.3节的例程4-4介绍的EchoServer都采用异步通信,每次接收数据时,能读到多少数据,就读多少数据。
1.5.2节的例程1-3和4.4.1节的例程4-5的EchoClient类都采用同步通信方式,每次向EchoServer发送了一行数据后,都必须等接收到了EchoServer发回的响应数据,再发送下一行数据。例程4-6的EchoClient类利用非阻塞模式来实现异步通信。在EchoClient类中,定义了sendBuffer和receiveBuffer两个ByteBuffer。EchoClient把用户向控制台输入的数据存放到sendBuffer中,并且把sendBuffer中的数据发送给远程服务器;EchoClient把从远程服务器接收到的数据存放在receiveBuffer中,并且把receiveBuffer中的数据打印到控制台。图4-16展示了这两个Buffer的作用。

图4-16 sendBuffer和receiveBuffer的作用
例程4-6 EchoClient.java(非阻塞模式)




在EchoClient类的构造方法中,创建了SocketChannel对象后,该SocketChannel对象采用默认的阻塞模式,调用socketChannel.connect(isa)方法,该方法将按照阻塞模式来与远程服务器EchoServer连接,只有当连接建立成功,该connect()方法才会返回。接下来程序调用socketChannel.configureBlocking(false)方法把SocketChannel设为非阻塞模式,这使得接下来通过SocketChannel来接收和发送数据都会采用非阻塞模式。

EchoClient类共使用了主线程和Receiver线程两个线程。主线程主要负责接收和发送数据,这些操作由talk()方法实现。Receiver线程负责读取用户向控制台输入的数据,该操作由receiveFromUser()方法实现。


receiveFromUser()方法读取用户输入的字符串,把它存放到sendBuffer中。如果用户输入字符串“bye”,就退出receiveFromUser()方法,这使得执行该方法的Receiver线程结束运行。由于主线程在执行send()方法时,也会操纵sendBuffer,为了避免两个线程对共享资源sendBuffer的竞争,receiveFromUser()方法对操纵sendBuffer的代码进行了同步。

talk()方法向Selector注册读就绪和写就绪事件,然后轮询已经发生的事件,并做出相应的处理。如果发生读就绪事件,就执行receive()方法,如果发生写就绪事件,就执行send()方法。
receive()方法接收EchoServer发回的响应数据,把它们存放在receiveBuffer中。如果receiveBuffer中已经存满一行数据,就向控制台打印这一行数据,并且把这行数据从receiveBuffer中删除。如果打印的字符串为“echo:bye\r\n”,就关闭SocketChannel,并且结束程序。
send()方法把sendBuffer中的数据发送给EchoServer,然后删除已经发送的数据。由于Receiver线程以及执行send()方法的主线程都会操纵共享资源sendBuffer,为了避免对共享资源的竞争,对send()方法中操纵sendBuffer的代码进行了同步。