一. 前言
本文探讨多人在线文件编辑器的实现,主要借助swing来进行图形化界面实现。
基于java RMI进行RPC调用和回调控制。
服务器其实是一个消息中转站。
二. 开发环境
Windows10
jdk1.8
idea2020.3
三. 具体过程
1.接口设置
1.1定义服务端接口(用于客户端调用)
public interface Server extends Remote{
String sayHello(String name) throws RemoteException;
/**
* 客户端文本改变时上载服务端进行广播
* @param words
* @param client
* @return
* @throws RemoteException
*/
Boolean onTextChange(Words words,Client client) throws RemoteException;
/**
* 注册客户端
* @param client
* @return
* @throws RemoteException
*/
Boolean registerClient(Client client) throws RemoteException;
void unregisterClient(Client client) throws RemoteException;
}
1.2定义客户端接口(用于服务端回调,根据注册集合)
public interface Client extends Remote {
/**
* 当文本内容改变时,服务端向客户端广播时调用
* @param words
* @throws RemoteException
*/
public void OtherChanged(Words words) throws RemoteException;
public void setMainWindow(MainWindow mainWindow)throws RemoteException;
}
2.定义传输内容Words类
定义words类用于文本内容传输,注意要implements Serializable 用于序列化
public class Words implements Serializable {
public Words(){}
//操作,0代表删除,1代表插入
private int Operation;
//插入项
private String item;
//插入/删除开始位置
private int Startposition;
//插入/删除结束位置
private int Endposition;
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
public int getStartposition() {
return Startposition;
}
public void setStartposition(int startposition) {
Startposition = startposition;
}
public int getEndposition() {
return Endposition;
}
public void setEndposition(int endposition) {
Endposition = endposition;
}
public int getOperation() {
return Operation;
}
public void setOperation(int operation) {
Operation = operation;
}
}
3.定义MainWindow类显示主界面
public class MainWindow extends JFrame implements DocumentListener {
private JPanel contentPanel;
private JScrollPane jScrollPane;
public JTextArea textArea;
private Server stub;
private String id;
private Client client;
public void init(Server stub,Client client) {
this.client=client;
this.stub=stub;
textArea = new JTextArea(20, 50);
jScrollPane = new JScrollPane(textArea);
contentPanel = new JPanel();
setContentPane(contentPanel);
contentPanel.setBounds(400, 200, 500, 500);
textArea.setWrapStyleWord(true);//设置单词在一行不足容纳时换行
textArea.setLineWrap(true);//设置文本编辑区自动换行默认为true,即会"自动换行"
id = UUID.randomUUID().toString();
//关键是下面这两行代码
Document document = textArea.getDocument();
document.addDocumentListener(this);
contentPanel.add(jScrollPane);
this.setVisible(true);
this.setSize(600, 400);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
插入事件处理,构造words类向服务端传输
@Override
public void insertUpdate(DocumentEvent e) {
try {
System.out.println(e.getDocument().getText(e.getOffset(), e.getLength()));
Words words = new Words();
words.setItem(e.getDocument().getText(e.getOffset(), e.getLength()).toString());
words.setOperation(1);
words.setStartposition(e.getOffset());
words.setEndposition(e.getOffset());
stub.onTextChange(words,client);
} catch (BadLocationException | RemoteException badLocationException) {
badLocationException.printStackTrace();
}
}
删除事件处理
@Override
public void removeUpdate(DocumentEvent e) {
try {
System.out.println(e.getDocument());
Words words = new Words();
words.setOperation(0);
words.setStartposition(e.getOffset());
words.setEndposition(e.getOffset() + e.getLength());
stub.onTextChange(words, client);
} catch (RemoteException badLocationException) {
badLocationException.printStackTrace();
}
}
4.初始化客户端
public class ClientImpl extends UnicastRemoteObject implements Client, Serializable {
public static Server stub;
public static MainWindow mainWindow;
private String Clientid;
protected ClientImpl() throws RemoteException {
super();
}
static private ExecutorService service = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "output");
}
});
public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException, InterruptedException {
// if(System.getSecurityManager()==null){
// System.setSecurityManager(new SecurityManager());}
Client clientImpl =new ClientImpl();
String host = (args.length < 1) ? "localhost" : args[0];
String name = (args.length == 2) ? args[1] : "World";
String urlo = "rmi://" + host + ":3333/Server";
stub = (Server) Naming.lookup(urlo);
MainWindow mainWindow1=new MainWindow();
mainWindow1.init(stub,clientImpl);
clientImpl.setMainWindow(mainWindow1);
System.out.println("link to the server: \n" + urlo);
//Registry registry = LocateRegistry.getRegistry(host);
//rmi_test.Hello stub = (rmi_test.Hello)registry.lookup("rmi_test.Hello");
String response = stub.sayHello(name);
System.out.println("Response: " + response);
Boolean isok=stub.registerClient(clientImpl);
}
调用接口实现:
@Override
public void OtherChanged(Words words) throws RemoteException {
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("received change"+words.getItem());
mainWindow.textArea.getDocument().removeDocumentListener(mainWindow);
if(words.getOperation()==1) {
mainWindow.textArea.insert(words.getItem(), words.getStartposition());
}
else {
mainWindow.textArea.replaceRange("", words.getStartposition(), words.getEndposition());
}
mainWindow.textArea.getDocument().addDocumentListener(mainWindow);
}
});
}
5.初始化服务端
public class ServerImpl extends UnicastRemoteObject implements Server, Serializable {
public ServerImpl() throws RemoteException {
super();
client=new HashSet();
// clientToId=new HashMap();
}
private HashSet client;
public static void main(String args[]) throws RemoteException {
//System.setSecurityManager(new RMISecurityManager());
//System.setSecurityManager(new SecurityManager());
final ServerImpl obj = new ServerImpl();
try { // 0 - anonymous TCP port ↓
//Server stub = (Server)UnicastRemoteObject.exportObject(obj, 0);
//Bind the remote object's stub in the registry
Registry registry = LocateRegistry.createRegistry(3333);
registry.rebind("Server", obj);
for(int i = 0; i < registry.list().length; i++)
System.out.println(registry.list()[i]);
// Naming.bind("localhost/Server",obj);
System.err.println("Server ready....");
System.err.println("Listinging on port 3333 ....");
} catch (Exception e) {
e.printStackTrace();
}
}
服务端接口实现:
public Boolean onTextChange(Words words,Client client1) {
System.out.println("text changed");
//Words words1=new Words();
// wordsQueue.add(words1);
Iterator iterator=client.iterator();
while(iterator.hasNext())
{
Client client =(Client) iterator.next();
if(client.equals(client1))
continue;
try{
client.OtherChanged(words);
}catch (Exception e)
{
System.out.println("移出无效对象");
e.printStackTrace();
iterator.remove();
}
}
return true;
}
public Boolean registerClient(Client client) throws RemoteException {
System.out.println("new client connected");
this.client.add(client);
return true;
}
public void unregisterClient(Client client) throws RemoteException {
System.out.println("client disconnected");
this.client.remove(client);
}
最终效果展示:
最终界面
四. 总结与讨论
遇到的坑还是挺多的,主要有以下几点:
1.RMI要求客户端和服务端的远程调用接口必须一致,包括包名和方法属性等。
客户端
服务端
2.swing只支持单线程,在同步更新文本时发生了死锁的现象,故在远程接收消息更新文本内容时应再开一个线程。
3.安全机制比较麻烦,需慎重考虑
4.由于java只支持单继承,所以这里将图形界面的类放在了客户端主类里面(客户端主类继承UnicastRemoteObject,图形界面继承JFrame)
继承