基于TCP协议的网络编程学习笔记(2)
2014-04-01 23:15 fuimaz 阅读(204) 评论(0) 收藏 举报基于TCP协议的网络编程学习笔记(2)
(二)非阻塞式的Scoket通信
从JDK1.4开始,JAVA提供的NIO API来开发高性能网络服务器,前面介绍的网络通信程序是基于阻塞式API的----即当程序执行输入、输出操作后,在这些操作返回之前会一直阻塞该线程,所以服务器必须为每个客户端提供一条独立线程进行处理,当服务器需要同时处理大量客户端时,这种做法会导致性能下降.使用NIO API则可以让服务器使用一个或几个线程来同时处理连接到服务器上的所有客户端。
JAVA的NIO为非阻塞式的Socket通信提供了如下几个特殊类:
Selector:它是SelectableChannel对象的多路复用器,所有希望采用非阻塞式方式进行通信的Channel都应该注册到Selector对象。可通过此类的静态open()方法来创建Selector实例,该方法将使用系统黙认的Selector来返回新的Selector.
Selector可以同时监控多个SelectabelChannel的IO状况,是非阻塞IO的核心。
一个Selector实例有3个SelectionKey的集合.
所有SelectionKey集合:代表了所有可通过Selector上的Channel,这个集合可以通过keys方法返回
被选择的SelectionKey集合:代表了所以可通过select()方法监测到,需要进行IO处理的Channel,这个集合可以通过SelectedKeys()返回
被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除,程序通常无须直接访问该集合.
下面我主要介绍ServerSocketChannel和SocketChannel.它们都是可选择的(selectable)通道,分别可以工作在同步和异步两种方式下(注意,这里的可选择不是指可以选择两种工作方式,而是指可以有选择的注册自己感兴趣的事件)。可以用channel.configureBlocking(Boolean )来设置其工作方式。与以前版本的API相比较,ServerSocketChannel就相当于ServerSocket (ServerSocketChannel封装了ServerSocket),而SocketChannel就相当于Socket(SocketChannel封装了Socket)。当通道工作在同步方式时,编程方法与以前的基本相似,即阻塞式socket编程,这里主要介绍异步工作方式。
所谓异步输入输出机制,是指在进行输入输出处理时,不必等到输入输出处理完毕才返回。所以异步的同义语是非阻塞(None Blocking)
SelectableChannel:它代表可以支持非阻塞IO操作的Channel对象,可以将其注册到Selector上,这种注册关系由SelectionKey实例表示。Selector对象提供了一个select()方法,该方法允许应用程序同时监控多个IO Channel.
SelectableChannel对象支持阻塞和非阻塞两种模式(所有Channel默认都是阻塞模式),必须使用非阻塞式模式才可以利用非阻塞IO操作。
SelectableChannel提供了如下两个方法来设置和返回该Channel的模式状态。
SelectableChannel configureBlocking(boolean block):设置是否采用阻塞模式
boolean isBlocking():返回该Channel是否是阻塞模式。
ServerSocketChannel:支持非阻塞操作,对应于java.net.ServerSocket这个类,提供了TCP协议IO接口,只支持OP_ACCEPT操作。该类也提供了accept()方法,功能相当于ServerSocket提供的accept()方法。
SocketChannel:支持非阻塞操作,对应于java.net.Socket这个类,提供了TCP协议IO接口,支持OP_CONNECT,OP_READ和OP_WRITER操作。这个类还实现了ByteChannel接口,ScatteringByteChannel接口和GatheringByteChannel接口,所以可以直接通过SocketChannel来读写ByteBuffer 对象。
SelectionKey:该对象代表SelectableChannel和Selector之间的注册关系。
服务器上所有Channel(包括ServerSocketChannel和SocketChannel)都需要向Selector注册,而该Selector则负责监视这些Socket的IO状态,当其中任意一个或多个Channel具有可用的IO操作时,该Select()方法将会返回大于0的整数,该整数值就表示Selector上有多少个Channel具有可用的IO操作,并提供了selectedKeys()方法来返回这些 Channel对应的 SelectionKey集合。正是通过 Selector ,使得服务器只需要不断地调用 Selector实例的select()方法即可知道当前所有Channel是否有需要处理的 IO操作。
如下图所示
说明一点
当Selector上注册的所有Channel都没有需要处理的IO操作时,select()方法将被阻塞,调用该方法的线程被阻塞。
下面的示例程序使用NIO实现多人聊天室的功能,服务器使用循环不断获取Selector的select()方法返回值,当该回值大于0时就处理该Selector上被选择的SelectionKey所对应的Channel.
创建一个可用的ServerSocket需采用如下代码片断
//通过open方法来打开一个未绑定的ServerSocketChannel实例
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress(
"127.0.0.1", 30000);
//将该ServerSocketChannel绑定到指定IP地址
server.socket().bind(isa);
如果需要使用非阻塞方式来处理ServerSocketChannel,还应该设置它的非阻塞模式,并将其注册到指定的 Selector,如下代码片断
//设置ServerSocket以非阻塞方式工作
server.configureBlocking(false);
//将server注册到指定Selector对象
server.register(selector, SelectionKey.OP_ACCEPT);
经过上面步骤后,该ServerSocketChannel可以接收客户端的连接请求,但我们需要调用Selector的select()方法来监听所有Channel上的IO操作。
示例代码如下
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.channels.spi.*;
import java.net.*;
import java.util.*;
import java.nio.charset.*;
public class NServer
{
//用于检测所有Channel状态的Selector
private Selector selector = null;
//定义实现编码、解码的字符集对象
private Charset charset = Charset.forName("UTF-8");
public void init()throws IOException
{
selector = Selector.open();
//通过open方法来打开一个未绑定的ServerSocketChannel实例
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress(
"127.0.0.1", 30000);
//将该ServerSocketChannel绑定到指定IP地址
server.socket().bind(isa);
//设置ServerSocket以非阻塞方式工作
server.configureBlocking(false);
//将server注册到指定Selector对象
server.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0)
{
//依次处理selector上的每个已选择的SelectionKey
for (SelectionKey sk : selector.selectedKeys())
{
//从selector上的已选择Key集中删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
//如果sk对应的通道包含客户端的连接请求
if (sk.isAcceptable())
{
//调用accept方法接受连接,产生服务器端对应的SocketChannel
SocketChannel sc = server.accept();
//设置采用非阻塞模式
sc.configureBlocking(false);
//将该SocketChannel也注册到selector
sc.register(selector, SelectionKey.OP_READ);
//将sk对应的Channel设置成准备接受其他请求
sk.interestOps(SelectionKey.OP_ACCEPT);
}
//如果sk对应的通道有数据需要读取
if (sk.isReadable())
{
//获取该SelectionKey对应的Channel,该Channel中有可读的数据
SocketChannel sc = (SocketChannel)sk.channel();
//定义准备执行读取数据的ByteBuffer
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
//开始读取数据
try
{
while(sc.read(buff) > 0)
{
buff.flip();
content += charset.decode(buff);
}
//打印从该sk对应的Channel里读取到的数据
System.out.println("=====" + content);
//将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();
}
}
//如果content的长度大于0,即聊天信息不为空
if (content.length() > 0)
{
//遍历该selector里注册的所有SelectKey
for (SelectionKey key : selector.keys())
{
// 如下语句排除掉 客户发送的信息不发给自己
if(sk==key) continue;
//获取该key对应的Channel
Channel targetChannel = key.channel();
//如果该channel是SocketChannel对象
if (targetChannel instanceof SocketChannel)
{
//将读到的内容写入该Channel中
SocketChannel dest = (SocketChannel)targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}
}
public static void main(String[] args)
throws IOException
{
new NServer().init();
}
}
服务器端的Selector仅需要监听两种操作:连接和读数据。所以程序中分别处理了这两种操作。处理连接操作之时,系统只需要将接受连接后产生的SocketChannel注册到指定的Selector对象即可。处理数据操作之时,系统先从该Socket中读取数据,再将数据写入Selector上注册的所有的Channel
本示例程序的客户端程序需要两个线程,一个线程负责读取用户的键盘输入,并将输入的内容写入SocketChannel中,另一个线程则不断地查询Selector对象的select()方法的返回值
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;
public class NClient
{
//定义检测SocketChannel的Selector对象
private Selector selector = null;
//定义处理编码和解码的字符集
private Charset charset = Charset.forName("UTF-8");
//客户端SocketChannel
private SocketChannel sc = null;
public void init()throws IOException
{
selector = Selector.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
//调用open静态方法创建连接到指定主机的SocketChannel
sc = SocketChannel.open(isa);
//设置该sc以非阻塞方式工作
sc.configureBlocking(false);
//将SocketChannel对象注册到指定Selector
sc.register(selector, SelectionKey.OP_READ);
//启动读取服务器端数据的线程
new ClientThread().start();
//创建键盘输入流
Scanner scan = new Scanner(System.in);
while (scan.hasNextLine())
{
//读取键盘输入
String line = scan.nextLine();
//将键盘输入的内容输出到SocketChannel中
sc.write(charset.encode(line));
}
}
//定义读取服务器数据的线程
private class ClientThread extends Thread
{
public void run()
{
try
{
while (selector.select() > 0)
{
//遍历每个有可用IO操作Channel对应的SelectionKey
for (SelectionKey sk : selector.selectedKeys())
{
//删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
//如果该SelectionKey对应的Channel中有可读的数据
if (sk.isReadable())
{
//使用NIO读取Channel中的数据
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while(sc.read(buff) > 0)
{
sc.read(buff);
buff.flip();
content += charset.decode(buff);
}
//打印输出读取的内容
System.out.println("聊天信息:" + content);
//为下一次读取作准备
sk.interestOps(SelectionKey.OP_READ);
}
}
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
public static void main(String[] args)
throws IOException
{
new NClient().init();
}
}
附加说明
为了让朋友更好的理解上述程序,阿堂(网络时空),将最后运行的效果图也截图附上了,希望能对其它朋友们有所帮
助
浙公网安备 33010602011771号