java火fouco评测_GitHub - awmleer/cowrite: ZJU Java course project. A very simple JavaFx application for...

Java应用技术实验报告

3150104785 郝广博 计算机科学与技术

引言

这个项目实现了一个最简单的多人同时写作的应用。支持多个用户同时编辑同一份文档。

总体设计

程序一共由三部分构成:server、client、proto。其中,server和client分别是服务端和客户端,proto中是一些数据模型,为server和client共享。

server和client通过socket进行通信,利用了Java的Serializable接口和ObjectInputStream/ObjectOutputStream,将对象序列化后进行传输,再在另一端反序列化得到对象。

app/Main.java是客户端程序的入口点,server/Main.java是服务端程序的入口点。

详细设计

模型定义

为了进行客户端和服务端的通信,这里定义了一些模型,在proto文件夹下:

public class SocketDataBase implements Serializable {

String meta;

public String getMeta(){

return meta;

}

}

public class SocketData extends SocketDataBase {

private T data;

private SocketData(){}

public SocketData(String meta, T data){

this.meta=meta;

this.data=data;

}

public T getData(){

return data;

}

}

SocketData这个泛型类是用来包裹数据进行socket通信的。具体的socket通信实现下面会介绍到,这里就不再阐述了。

public class Login implements Serializable {

private String username;

private String password;

//...

}

Login是用户进行登录时填写的表单数据。

public class User implements Serializable {

private int id;

private String username;

private String password;

//...

}

User是用户模型。

public class Document implements Serializable {

private int id;

private static int nextId=0;

private User creator;

private String title;

private String content;

public Document(User creator, String title){

this.id=nextId;

nextId++;

this.creator=creator;

this.title=title;

}

//...

}

Document是文档模型,其中的静态属性nextId用来记录下一个实例应该分配给的id,在构造函数中会对其进行加一的操作。

socket通信

客户端和服务端进行socket通信时,主要包括以下几个步骤:(以服务端向客户端发送数据为例)

服务端将要发送的数据包裹成一个SocketData

通过ObjectOutputStream.writeObject()将其序列化后发给客户端

客户端接收数据,将其反序列化后得到一个Object

将这个对象强制Down Cast为SocketDataBase,然后根据其meta属性,进行不同的处理操作

通过SocketData和SocketDataBase的继承,可以使得在不知道SocketData中T的类型的情况下,就得到其meta属性,从而可以根据meta逻辑推出T的类型,再进一步进行Down Cast。

除此以外,由于IO是阻塞的,需要把socket独立为一个线程。在服务端,每一个socket连接都是一个Handler线程,由new Handler(listener.accept()).start()创建。在客户端,进行socket通信也是一个独立于GUI的线程,并且由单例的SocketClient进行管理。

SocketClient单例模式的实现

把它的构造函数设置为private的,然后外部想要获取SocketClient实例的时候,通过getSocketClient()方法获取类内的socketClient静态属性:

private static SocketClient socketClient = null;

public static SocketClient getSocketClient(){

if(socketClient==null){

socketClient = new SocketClient();

}

return socketClient;

}

GUI实现

GUI通过JavaFX实现。通过fxml和scene builder工具可以实现页面布局的编写,通过每一个view对应的controller类可以实现对JavaFX组件的控制。

创建一个stage:

public void start(Stage primaryStage) throws Exception{

FXMLLoader loader = new FXMLLoader();

loader.setLocation(Main.class.getResource("../view/Home.fxml"));

Parent root = loader.load();

primaryStage.setTitle("Cowrite");

primaryStage.setScene(new Scene(root, 600, 400));

primaryStage.show();

this.stage =primaryStage;

loader.setLocation(getClass().getResource("../view/Home.fxml"));

HomeController controller = loader.getController();

SocketClient.getSocketClient().setHomeController(controller);

controller.setMainApp(this);

}

在controller中获得FXML中的控件:

@FXML

private Button addDocumentButton;

相应按钮的点击事件及创建对话框:

@FXML

private void addDocumentButtonClicked(){

TextInputDialog dialog;

Optional result;

dialog = new TextInputDialog("");

dialog.setTitle("Login");

dialog.setHeaderText("Username");

dialog.setContentText("Please enter your username:");

result = dialog.showAndWait();

if (result.isPresent()){

SocketClient.getSocketClient().addDocument(result.get());

}

}

一共有Home和Doc两个页面:Home是首页,显示用户的信息和文档列表。Doc是文档编辑页面,是一个文本输入框。

不过,由于GUI和socket是两个线程,因此如果想在socket线程中对view层进行操作,需要通过Platform.runLater(()->...)实现。

登录

用户点击登录按钮,controller中创建对话框,用户填写用户名和密码

调用SocketClient.login()方法发送登录请求

public void login(String username, String password) {

try {

outputStream.writeObject(new SocketData<>(

"login",

new Login(username, password)

));

}catch (IOException e){

e.printStackTrace();

}

}

服务端收到请求后进行验证,如果验证通过,将user保存至当前的线程的userForThisSocket属性

private void handleLogin(Login login) throws IOException{

User user = getUser(login.getUsername());

if (user==null){

return;

}else{

if(user.checkPassword(login.getPassword())){

userForThisSocket=user;

userToWriterMap.put(userForThisSocket,out);

out.writeObject(new SocketData<>(

"updateUser",

user

));

updateDocuments();

}

}

}

登录成功后,会由服务端向客户端发送一个meta为updateUser的SocketData

客户端更新view

文档列表

文档列表的展现是通过JavaFX的TableView实现的:

@FXML

private TableView documentTable;

@FXML

private TableColumn creatorColumn;

@FXML

private TableColumn titleColumn;

通过HomeController.updateDocumentList()方法可以对其进行更新:

public void updateDocumentList(ArrayList documents){

documentRowList.clear();

for (Document document: documents) {

documentRowList.add(new DocumentRow(document.getId(), document.getCreator().getUsername(), document.getTitle()));

}

}

创建文档

@FXML

private void addDocumentButtonClicked(){

TextInputDialog dialog;

Optional result;

dialog = new TextInputDialog("");

dialog.setTitle("Add document");

dialog.setHeaderText("Add document");

dialog.setContentText("Please enter the title:");

result = dialog.showAndWait();

if (result.isPresent()){

SocketClient.getSocketClient().addDocument(result.get());

}

}

在客户端,弹出对话框,由用户输入标题,然后发送请求创建文档。

在服务端,Document和User的列表都是分别存放在一个HashSet中的,当收到创建文档请求后,后端会实例化一个Document对象,并且把它插入到documents这个HashSet中:

private void handleAddDocument(String title) throws IOException{

documents.add(new Document(userForThisSocket,title));

System.out.println(documents.size());

updateDocuments();

}

修改文档

当用户要进入一个文档的编辑状态时,会首先创建出文档编辑窗口,然后调用SocketClient.startDocumentEditing()方法,发送请求让服务端记录下当前用户和这个文档对应的编辑关系:

userToDocumentMap.put(userForThisSocket,document);

接下来,服务端会自动发送updateDocument数据包,让客户端加载文档的内容:

out.writeObject(new SocketData<>(

"updateDocument",

document.getContent()

));

当一个用户修改文档的内容时,会自动发送到服务器:

textProperty.addListener((observable, oldValue, newValue) -> {

if(oldValue!=null&&newValue!=null&&!oldValue.equals(newValue)){

SocketClient.getSocketClient().editDocument(newValue);

System.out.println("TextField Text Changed (newValue: " + newValue + ")");

}

});

然后该文档对应的其他正在编辑的用户会收到updateDocument通知,并且自动更新文档内容:

private void handleUpdateDocument(SocketDataBase data){

String content=((SocketData)data).getData();

Platform.runLater(()->{

System.out.println("document update value:");

System.out.println(content);

docController.setText(content);

});

}

当用户关闭文档编辑窗口时,会自动调用SocketClient.stopDocumentEditing()方法,通知后端将其从文档编辑用户的对应关系中删除:

this.stage.setOnHiding(e->{

SocketClient.getSocketClient().stopDocumentEditing();

});

测试和运行

登录

1.png

主界面

2.png

创建文档

3.png

创建文档后会自动更新两个客户端中的文档列表

4.png

5.png

编辑文档

6.png

修改会自动同步到另一个客户端中

7.png

总结

之前在做js开发的时候,由于js的单线程和IO异步特性,数据的收发都是通过回调函数或者Promise实现的,这次在做socket通信模块的时候,习惯性的以为Java中的IO也是异步的。通过独立的线程来实现IO,让我体会到了Java的美感,也对线程有了更深的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值