一、文件系统简介
本章的目的是通过使用ICE来实现一个简单的文件系统应用,文件系统应用将实现一个简单的层次结构的文件系统,文件系统由目录和文件组成,目录是可以容纳目录或文件的容器。
二、文件系统的Slice 定义
文件和目录有共同之处:它们都有名字,而且文件和目录都可以包含在目录中。可以使用基类型来提供共有的功能,用派生类型来提供目录和文件专有的功能。如下图所示:
文件系统的完整Slice 定义如下:
module Filesystem {//映射成为java中的package
interface Node {//文件和目录的基接口,
string name();//因为目录和文件都有名字
};
exception GenericError {//异常
string reason;//reason数据成员中会提供对失败原因的解释。
};
sequence<string> Lines;//sequence映射为一个java数组结构,Lines是一个数组,数组中存放的是string类型的数据。
interface File extends Node {//文件接口,继承Node
Lines read();//提供对文件内容的读操作,假定read 操作永远不会失败。Read操作返回的是Lines字符串数组。
void write(Lines text) throws GenericError;//写操作,假定写操作会遇到出错的情况。
};
sequence<Node*> NodeSeq;//NodeSeq序列包含的是Node* 类型的元素,sequence被slice2java编译成一个数组,Node*编译为一个Node的代理NodePrx。最终编译成一个数组,数组中存放的是节点的代理NodePrx对象。
interface Directory extends Node {
NodeSeq list();//返回一个数组,数组中存放的是一些节点代理,list函数会被客户端调用,每个代理都指向在服务器上的一个远地节点。
};
interface Filesys {
Directory* getRoot();//客户端调用此函数,获得根目录,返回的是一个Directory的一个代理对象到客户端。以便客户端对Directory进行其他的远程操作。
};
};
三、开发文件系统客户
客户会从根目录开始,递归地列出文件系统的内容。对于文件系统中的每一个节点,客户都会显示节点名,以及该节点是文件还是目录。如果节点是文件,客户就获取文件的内容,并打印它们。
下面是客户代码的主体:
package Filesystem.client;
import Filesystem.DirectoryPrx;
import Filesystem.DirectoryPrxHelper;
import Filesystem.FilePrx;
import Filesystem.FilePrxHelper;
import Filesystem.NodePrx;
public class Client {
/**
* 递归打印出目录中内容
* @param dir 服务器目录在本地的代理对象
* @param depth 记录目录的深度,方便打印缩进。
*/
static void listRecursive(DirectoryPrx dir, int depth) {
//每递归调用一次,depth深度增加1
char[] indentCh = new char[++depth];
java.util.Arrays.fill(indentCh, '\t');
String indent = new String(indentCh);
NodePrx[] contents = dir.list();
for (int i = 0; i < contents.length; ++i) {
DirectoryPrx subdir = DirectoryPrxHelper.checkedCast(contents[i]);
FilePrx file = FilePrxHelper.uncheckedCast(contents[i]);
System.out.println(indent + contents[i].name()
+ (subdir != null ? " (directory):" : " (file):"));
if (subdir != null) {
listRecursive(subdir, depth);
} else {
String[] text = file.read();
for (int j = 0; j < text.length; ++j)
System.out.println(indent + "\t" + text[j]);
}
}
}
public static void main(String[] args) {
int status = 0;
Ice.Communicator ic = null;
try {
ic = Ice.Util.initialize(args);
Ice.ObjectPrx base = ic.stringToProxy("RootDir:default -p 10000");
if (base == null)
throw new RuntimeException("Cannot create proxy");
DirectoryPrx rootDir = DirectoryPrxHelper.checkedCast(base);
if (rootDir == null)
throw new RuntimeException("Invalid proxy");
System.out.println("Contents of root directory:");
listRecursive(rootDir, 0);
} catch (Ice.LocalException e) {
e.printStackTrace();
status = 1;
} catch (Exception e) {
System.err.println(e.getMessage());
status = 1;
} finally {
if (ic != null)
ic.destroy();
}
System.exit(status);
}
}
1. Main函数中,在初始化run time 之后,客户创建一个代理,指向文件系统的根目录。在这个例子中,我们假定服务器运行在本地主机上,并且使用缺省协议(TCP/IP)在10000 端口处侦听。根目录的对象标识叫作RootDir。
2. 客户把代理向下转换成DirectoryPrx,并把这个代理传给listRecursive,由它打印出文件系统的内容。
大多数工作都是在listRecursive 中完成的。这个函数收到的参数是一个代理,指向要列出的目录;另外还有一个缩进层次参数(缩进层次随着每一次递归调用增加,这样,打印每个节点名时的缩进层次就会与该节点的树深度对应)。listRecursive 调用目录的list 操作,并且遍历所返回的节点序列:
1. 代码调用checkedCast,把Node 代理窄化成Directory 代理;并且调用uncheckedCast,把Node 代理窄化成File 代理。在这两个转换中只有,而且肯定会有一个成功,所以不需要两次调用checkedCast:如果节点是一个Directory,代理就使用checkedCast 返回的DirectoryPrx ;如果checkedCast 失败,我们知道了这个节点是一个File,因此,要获得FilePrx,使用uncheckedCast 就足够了。一般而言,如果你知道向下转换到特定类型能成功,就最好使用uncheckedCast,而不是checkedCast,因为uncheckedCast 不需要进行任何网络通信。
2. 代码打印文件或目录的名字,然后,取决于成功的转换是哪一个,在名字的后面打印"(directory)" 或"(file)"。
3. 代码检查节点的类型:
l 如果是目录,代码就会递归,同时增加缩进层次。
l 如果是文件,代码就调用文件的read 操作,取回文件内容,然后遍历返回的内容行序列,打印每一行。
假定我们有一个很小的文件系统,由两个文件和两个目录组成:
这个文件的客户产生的输出是:
Contents of root directory:
README (file):
This file system contains a collection of poetry.
Coleridge (directory):
Kubla_Khan (file):
In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.
四、开发文件系统服务器
服务器由三个源文件组成:
l Server.java,这个文件含有服务器主程序。
l DirectoryI.java,这个文件含有Directory servant 的实现。
l FileI.java,这个文件含有File servant 的实现。
1.服务器的main程序
Server类派生自Ice.Application,在其run 方法中含有主应用逻辑。run方法创建对象适配器、为文件系统里的目录和文件创建一些servants,然后激活适配器。下面是main程序完成代码:
package Filesystem.server;
import Filesystem.File;
import Filesystem.GenericError;
import Filesystem.servant.DirectoryI;
import Filesystem.servant.FileI;
public class Server extends Ice.Application {
public int run(String[] args) {
Ice.ObjectAdapter adapter = communicator()
.createObjectAdapterWithEndpoints("SimpleFilesystem",
"default -p 10000");
DirectoryI._adapter = adapter;
FileI._adapter = adapter;
//创建根目录,名字是"/" ,没有父目录。
//对于没有父目录的根目录,传递null作为父引用
DirectoryI root = new DirectoryI("/", null);
//在根目录中创建README 文件
File file = new FileI("README", root);
String[] text;
text = new String[] { "This file system contains a collection of poetry." };
try {
//用文本填充文件
file.write(text, null);
} catch (GenericError e) {
System.err.println(e.reason);
}
//创建Coleridge 子目录
DirectoryI coleridge = new DirectoryI("Coleridge", root);
//在Coleridge目录中创建Kubla_Khan文件
file = new FileI("Kubla_Khan", coleridge);
//Kubla_Khan文件的内容
text = new String[] { "In Xanadu did Kubla Khan",
"A stately pleasure-dome decree:",
"Where Alph, the sacred river, ran",
"Through caverns measureless to man", "Down to a sunless sea." };
try {
file.write(text, null);
} catch (GenericError e) {
System.err.println(e.reason);
}
adapter.activate();
communicator().waitForShutdown();
return 0;
}
public static void main(String[] args) {
Server app = new Server();
System.exit(app.main("Server", args));
}
}
服务器实例化文件系统的几个节点,创建了下图所示的结构:
2.FileI Servant 类
FileI servant 类具有这样的基本结构:
public class FileI extends _FileDisp
{
public static Ice.ObjectAdapter _adapter;
private String _name;
private DirectoryI _parent;
private String[] _lines;
}
这个类有一些数据成员:
• _adapter
这个静态成员存储的是一个引用,指向我们在服务器中使用的唯一一个对象适配器。
• _name
这个成员存储的是servant 所体现的文件的名字。
• _parent
这个成员存储的是一个引用,指向文件的父目录的servant。
• _lines
这个成员存放文件的内容。
_name 和_parent 数据成员由构造器初始化。
FileI servant类完成代码如下:
package Filesystem.servant;
import Filesystem.GenericError;
import Filesystem.NodePrx;
import Filesystem.NodePrxHelper;
import Filesystem._FileDisp;
public class FileI extends _FileDisp {
//这个静态成员存储的是一个引用,指向我们在服务器中使用的唯一一个对象适配器。
public static Ice.ObjectAdapter _adapter;
//这个成员存储的是servant 所体现的文件的名字。
private String _name;
//这个成员存储的是一个引用,指向文件的父目录的servant。
private DirectoryI _parent;
//这个成员存放文件的内容。
private String[] _lines;
/**
* _name 和_parent 数据成员由构造器初始化:
* @param name
* @param parent
*/
public FileI(String name, DirectoryI parent) {
_name = name;
_parent = parent;
//核实指向父目录的引用不是null,因为每个文件都必须有父目录。
assert (_parent != null);
// 为这个文件生成一个标识,
Ice.Identity myID = Ice.Util.stringToIdentity(Ice.Util.generateUUID());
//把自己增加到适配器的servant映射表中。
_adapter.add(this, myID);
// 为这个文件创建代理
NodePrx thisNode = NodePrxHelper.uncheckedCast(_adapter
.createProxy(myID));
//调用父目录的addChild 方法,把自己增加到父目录的后代节点列表中。
_parent.addChild(thisNode);
}
/**
* name 方法继承自生成的Node 接口
* 它简单地返回_name 成员的值。
*/
public String name(Ice.Current current) {
return _name;
}
//非线程安全
public String[] read(Ice.Current current) {
return _lines;
}
//非线程安全
public void write(String[] text, Ice.Current current) throws GenericError {
_lines = text;
}
}
3.DirectoryI Servant 类
DirectoryI 类具有这样的基本结构:
package Filesystem;
public final class DirectoryI extends _DirectoryDisp
{
public static Ice.ObjectAdapter _adapter;
private String _name;
private DirectoryI _parent;
private java.util.ArrayList _contents
= new java.util.ArrayList();
}
和FileI 类的情况一样,我们拥有一些数据成员,用于存储对象适配器、名字,以及父目录(对于根目录, _parent 成员存放的是null 引用)。此外,我们还有一个_contents 数据成员,存储的是子目录列表。这些数据成员都由构造器初始化。
DirectoryI servant类完成代码如下:
package Filesystem.servant;
import Filesystem.NodePrx;
import Filesystem.NodePrxHelper;
import Filesystem._DirectoryDisp;
public final class DirectoryI extends _DirectoryDisp {
//指向服务器中唯一一个对象适配器
public static Ice.ObjectAdapter _adapter;
//存储目录的名称
private String _name;
//指向父目录
private DirectoryI _parent;
//存储子目录列表
private java.util.ArrayList _contents = new java.util.ArrayList();
public DirectoryI(String name, DirectoryI parent) {
_name = name;
_parent = parent;
//为新目录创建标识,
//对于根目录,使用固定的"RootDir" 标识
Ice.Identity myID = Ice.Util
.stringToIdentity(_parent != null ? Ice.Util.generateUUID()
: "RootDir");
//把自己增加到servant 映射表中
_adapter.add(this, myID);
//创建一个指向自身的代理
NodePrx thisNode = NodePrxHelper.uncheckedCast(_adapter
.createProxy(myID));
//把自己增加到父目录的后代节点列表中。
if (_parent != null)
_parent.addChild(thisNode);
}
/**
* 助手函数,子目录或文件可以调用它,把自己增加到父目录的后代节点列表中。
* @param child
*/
void addChild(NodePrx child) {
_contents.add(child);
}
public String name(Ice.Current current) {
return _name;
}
public NodePrx[] list(Ice.Current current) {
NodePrx[] result = new NodePrx[_contents.size()];
_contents.toArray(result);
return result;
}
}
注意,按照现在的情况,这里给出的服务器代码并不是线程安全的,因为如果有两个客户分别通过不同的线程,同时访问同一个文件,一个线程可能在读取_lines 数据成员,而另一个线程在更新它。显然,如果发生这样的情况,我们可能会写入或返回垃圾数据。 要让read 和write 操作成为线程安全,需要使用java提供的同步关键字,或者是使用java提供的同步API中的类。
参考《Ice 分布式程序设计》 马维达 译.