文件传输通常利用的是TCP协议,采用C/S的方式进行的,优点是效率高,传输过程可控制,但如果传输过程涉及一些可能会有变动的业务时,就可能要同时改动服务端与客户端。
如果直接用RMI进行文件传输,通常的方法是把文件弄成byte[],再传输,优点是简单,同时业务的变动可以只在服务端进行,可缺点也很明显,对于一个大文件,传输时就太占用内存了。
在我的毕业设计《邮件系统》中,我想利用RMI把核心部分作为服务,而web界面就可以不用关心业务逻辑了,附件传输就遇到了上面的问题,想了一下,有了一个思路可以结合上面两者的优点,就是仍然利用TCP socket进行传输,但是利用RMI代码动态下载的能力,将客户端部分的实例化放在服务端。具体方法如下。
首先是开放给客户端的接口
public interface UpLoadService extends Remote{
public UpLoader getUpLoader(String name) throws RemoteException;
}
public interface UpLoader {
public void upLoad(InputStream output);
}
UpLoadService是RMI提供的服务,UpLoader是服务端实例化后传给客户端使用的。
接口都是在服务端实现的。
然后就是接受文件的服务端FileServer
public class FileServer {
private int port;
private volatile boolean isRunning = false;
private ExecutorService exec = Executors.newCachedThreadPool();
private Map<String, String> map;
public FileServer(int port) {
this.port = port;
map = Collections.synchronizedMap(new HashMap<String, String>());// 使map支持同步
}
public String requested(String fileName) {
String key = RandomUtils.randomString(16);
map.put(key, fileName);
//每当有一个文件上传请求,将随机生成一个key,并与文件名建立映射,如果有更复杂的业务或安全要求,可以修改
//UpLoader将凭借这个key与服务端连接,确保正确性与安全性
return key;
}
public synchronized void service() {
if (!isRunning) {
new Thread() {
public void run() {
try {
ServerSocket server = new ServerSocket(port);
System.out.println("文件传输服务已启动...");
isRunning = true;
while (isRunning) {
Socket socket = server.accept();
synchronized (this) {
exec.execute(new FileTransferSession(socket));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
} else {
System.out.println("文件传输服务已启动");
}
}
public synchronized void close() {
if (!isRunning) {
new Thread() {
public void run() {
System.out.println("正在结束文件传输服务...");
exec.shutdown();
isRunning = false;
try {
while (!exec.isTerminated()) {
sleep(100);
}
System.out.println("已结束文件传输服务");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
} else {
System.out.println("文件传输服务未启动");
}
}
public int getPort(){
return port;
}
class FileTransferSession implements Runnable {
private Socket socket;
public FileTransferSession(Socket socket) {
this.socket = socket;
}
public void run() {
InputStream is = null;
BufferedReader reader = null;
try {
is = socket.getInputStream();
reader = new BufferedReader(new InputStreamReader(is));
String key = reader.readLine();
String name = map.get(key);
if (name != null) {//验证key的正确性
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()));
writer.println("OK");//确认
writer.flush();
FileOutputStream fileOutput = new FileOutputStream(name);
byte[] buffer = new byte[1024];
int count = 0;
//开始接受文件
for (int i = 0; count != -1; i++) {
count = is.read(buffer);
if (count != -1)
fileOutput.write(buffer, 0, count);
}
fileOutput.close();
}
is.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这里的关键在那个map,它记录每个上传请求生成的key,UpLoader的实现会发送自己的key到服务端,从而实现了简单认证。下面是UpLoader的实现UpLoaderImpl。
public class UpLoaderImpl implements UpLoader, Serializable {
private static final long serialVersionUID = 1393382791756158190L;
private String ip;
private int port;
private String key;
public UpLoaderImpl(String ip, int port,String key) {
this.ip = ip;
this.port = port;
this.key = key;
}
public void upLoad(InputStream input) {
try {
Socket s = new Socket(ip, port);
OutputStream os = s.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(os));
BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
byte[] buffer = new byte[1024];
writer.println(key);//发送认证key
writer.flush();
if(reader.readLine().equals("OK")){//确认
int count = 0;
for (int i = 0; count != -1; i++) {
count = input.read(buffer);
if (count != -1)
os.write(buffer, 0, count);//发送
}
}
writer.close();
reader.close();
os.close();
s.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
UpLoadService的实现
public class UpLoadServiceImpl extends UnicastRemoteObject implements
UpLoadService {
private static final long serialVersionUID = 8108929499018468087L;
private FileServer server;
public UpLoadServiceImpl(FileServer server) throws RemoteException {
super();
this.server = server;
}
public UpLoader getUpLoader(String name) throws RemoteException {
String key = server.requested(name);
return new UpLoaderImpl("127.0.0.1",server.getPort(),key);
}
}
接口的实现全是在服务端实现的,客户端只用知道接口的定义就可使用接口提供的服务,虽然还是使用socket进行的文件传输,但遇到业务逻辑的改变时,只要接口的定义不变,客户端完全不用作任何改变。
上面的代码看起来挺麻烦的,可兼顾了RMI,和Socket传送文件的优点,不知道还有没有更好的思路。
完整下载:http://download.csdn.net/source/1229822
两个Eclipse工程,分别作为服务端与客户端,不依赖其他http服务可直接可运行。