1.程序说明
程序只用了一天写完,没有做很多测试,因此BUG很多
下载地址:https://github.com/EnTaroAdunZ/LAN_QQ/
1.1.怎么用?
1.1.1.打开服务端
如图所示,服务端地址表示的是本机地址(由于本程序缺少网段、网卡等相关选择设置,因此如果想要修改网段,需要到QQServer中的Main类中getServerIp进行修改),默认情况下,服务器地址是遍历本机所有网卡的网段,并且选择172开头的网段作为服务端IP地址;当前在线人数以及当前在线列表则会根据当前连接的客户端动态更新;为了让程序具有更好的通用性,本次实验中选用了hsqldb作为数据库,并且分离了dao层,更改数据源只需要修改数据源以及配置账号密码即可。要修改数据库的数据,只需要到Main类下修改initData即可。
1.1.2.打开第一个客户端,修改服务器地址
如图所示,该界面为客户端的登录界面,此界面中客户需要填写账号密码,以及服务器地址,服务器地址在服务器端有给出。点击登录后,发送一条信息到服务端,服务端验证帐号密码是否符合,如果帐号密码错误或者连接失败,都将给出提示,如果登录认证成功,将进入好友列表界面。
1.1.3.登录第二个、第三个客户端
1.1.4.进入好友列表界面
如图所示,该界面主要分为两部分,上半部分显示头像,当前登录用户的昵称、IP、端口等,由于用户头像与本实验关系不大,因此采用随机选用的方法,在数据库也没有保存头像URL等,因此三个客户端的头像出现了不一致。下图为此时服务端的状态。
1.1.5.聊天窗口
双击人物头像或者对应列,就能打开聊天窗口(同一个人物只能打开一个端口)。如图为在腾讯端与阿里的聊天。
聊天窗口分为三部分,上部表示对方的昵称,中部则是相关附加功能,如文件传输的功能入口。下部则是核心部分,文字通讯部分,分为聊天记录、待发送区域、发送按钮,以及QQ秀部分,QQ秀由于没有相关记录功能,因此是固定的。
1.1.6.发送信息
发送信息主要分为两种情况,第一种情况下,对方也打开了聊天窗口,第二种则是对方没有打开窗口,此时对方头像会闪动提醒。
第一种情况:
第二种情况:
1.1.7.传送文件
发送方:打开需要发送方的窗口,点击传送文件,同时点击需要的文件,就可以完成发送。
如果发生失败,发送方将会给出一个提示,如果成功则默认提示。同时接收方会弹出(无论此时与发送方窗口打开与否)一个窗口,提示是否接受文件,如果接受,则可以选择存放文件的位置,否则提示放弃保存。
2.核心代码
2.1.服务端
2.1.1.连接数据库
本次实验中采用JDBC进行连接,由于没有使用任何持久化工具,因此采用最原始的SQL方式,实际使用过程中(改用mysql等),只要修改TABLE_NAME以及jdbc驱动,并且添加帐号密码即可完成整个改动。
public class UserDao {
String TABLE_NAME="user";
String ID="account";
String CREATE_SQL ="(account varchar(254),password varchar(254),user_name varchar(254))";
String INSERT_SQL = "insert into "+TABLE_NAME+" values(?,?,?)" ;
String SELECT_SQL = "select * from "+TABLE_NAME ;
String SELECT_SQL_ID = "select * from "+TABLE_NAME+" where "+ID+"=?" ;
String DELETE_SQL="delete from "+TABLE_NAME+" where "+ID+"=?";
private UserDao() {}
private static UserDao single=null;
public static UserDao getInstance() {
if (single == null) {
single = new UserDao();
}
return single;
}
public void createData(){
try {
Class.forName("org.hsqldb.jdbcDriver");
} catch (Exception e) {
e.printStackTrace();
}
try(Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:aname", "sa", "");
Statement stmt = connection.createStatement();)
{
String sql1 = "create table IF NOT EXISTS "+TABLE_NAME+CREATE_SQL;
stmt.executeUpdate(sql1);
} catch (Exception e) {
e.printStackTrace();
}
}
public void insert(User user) {
try {
createData();
Class.forName("org.hsqldb.jdbcDriver");
} catch (Exception e) {
e.printStackTrace();
}
try(Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:aname", "sa", "");
PreparedStatement ps = connection.prepareStatement(INSERT_SQL))
{
ps.setString(1,user.getAccount());
ps.setString(2,user.getPassword());
ps.setString(3,user.getUserName());
ps.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
public User selectByAccount(String account) {
}
public List<User> selectAll() {
}
}
2.1.2.初始化数据
该处展示的dao的用法,同时如果继续使用jvm内存数据库的话,可以在这里添加需要增加的数据。
public static void initData(){
UserDao userDao=UserDao.getInstance();
userDao.insert(new User("ztf1","ztf1","银河"));
userDao.insert(new User("ztf2","ztf2","夏娜"));
userDao.insert(new User("ztf3","ztf3","夏库尔"));
userDao.insert(new User("ztf4","ztf4","新田"));
userDao.insert(new User("ztf5","ztf5","绿克人"));
}
2.1.3.服务器IP寻觅
获取所有的网卡,并且获取网段地址,如果是172开头,就设为服务器IP地址,从而实现校内通信,其他使用场景可以通过ipconfig或者ifconfig来查看网段来具体设置starts值。
public static void getServerIp(){
try {
Enumeration<NetworkInterface> interfaces=null;
interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
Enumeration<InetAddress> addresss = ni.getInetAddresses();
while(addresss.hasMoreElements())
{
InetAddress nextElement = addresss.nextElement();
String hostAddress = nextElement.getHostAddress();
if(hostAddress.startsWith("172")){
ServerMessage.SERVER_IP=hostAddress;
System.out.println("服务端IP地址为:" +hostAddress);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
2.1.4.客户端对应Bean
如何定义一个独一无二的客户端:IP地址+端口号+该用户的帐号
public class Link implements Serializable {
private User user;
private String port;
private String ip;
@Override
public boolean equals(Object obj) {
Link other = (Link) obj;
return this.user.equals(other.getUser());
}
@Override
public int hashCode() {
return user.hashCode();
}
}
2.1.5.数据库对应PO
对应数据库中的模型。
public class User implements Serializable {
private String account;
private String password;
private String userName;}
2.1.6.服务端客户端传输对象
其中Type定义了当前传输内容代表了什么含义,无论是客户端还是服务端都依靠type来判断msg有什么意义,CTC代表客户端到客户端,CTS代表客户端到服务端,STC代表服务端到客户端,现实场景下还应该存在STS,也就是存在多台服务器互传的情况;其中portList代表了当前服务端在线列表情况,通过这个来广播在线列表。
public class Msg implements Serializable {
public enum Type {
CTC,CTS_SENDPORT,STC_SENDPORT,CTS_LOGIN,CTS_LOGOUT,CTC_SENDFILE
}
private Type type;
private String msg;
private User user;
private Link link;
private File file;
private ArrayList<Link> portList;
public static Msg getCTC_SENDFILE(Link link,File file) {
return new Msg(Type.CTC_SENDFILE,link,file);
}
public static Msg getSTCMsg_sendPortToC(ArrayList<Link> linkList) {
return new Msg(Type.STC_SENDPORT,linkList);
}
public static Msg getCTCMsg(String p_msg,String userName) {
return new Msg(Type.CTC,p_msg,userName);
}
public static Msg getCTCMsg_CTS_LOGOUT(Link link) {
return new Msg(Type.CTS_LOGOUT,link);
}
public static Msg getCTSMsg_sendPortToS(String port) {
return new Msg(Type.CTS_SENDPORT,port);
}
public static Msg getSTC_LOGIN(String status,User user) {
return new Msg(Type.CTS_LOGIN,status,user);
}
public static Msg getCTSMsg_LOGIN(User user,Integer port) {
return new Msg(Type.CTS_LOGIN,String.valueOf(port),user);
}
public Msg(Type type,Link link, File file) {
super();
this.type = type;
this.link=link;
this.file = file;
}
public Msg(Type type, Link link) {
super();
this.type = type;
this.link = link;
}
public Msg(Type type, String msg,User user) {
super();
this.type = type;
this.msg = msg;
this.user=user;
}
public Msg(Type type, String msg) {
super();
this.type = type;
this.msg = msg;
}
public Msg(Type type, String msg, String userName) {
super();
this.type = type;
this.msg = msg;
this.user=new User(null,null,userName);
}
public Msg(Type type, ArrayList<Link> linkList) {
super();
this.type = type;
this.portList = linkList;
}
}
2.1.7.服务端监听
服务器通过监听某个特定端口,来开启线程接受信息。
public class ServerService implements Runnable{
@Override
public void run() {
try (ServerSocket serverSocket=new ServerSocket(ServerMessage.SERVER_PORT)){
while(true) {
Socket socket=serverSocket.accept();
//交由SocketHandle处理
ServerReceiveHandle handle=new ServerReceiveHandle(socket);
Thread thread=new Thread(handle);
thread.start();
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("服务器运行过程中出现了错误!");
}
}
}
2.1.8.服务端核心处理代码
在本程序的C-S架构中,服务端不承担任何C-C中任何信息交流的转发,只负责在线列表的维护,其中包括登录的校验,登录结果返回、异常退出处理、正常退出处理、这些处理当然还包括界面上的更新;值得注意的是,该线程不是javafx线程,因此界面上的更新需要转交给ui线程处理。
public class ServerReceiveHandle implements Runnable {
private Socket socket;
public ServerReceiveHandle(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
Link link = null;
try {
//连接
ObjectInputStream is = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream());
Msg msg = (Msg) is.readObject();
if (msg.getType() == Msg.Type.CTS_LOGIN) {
//客户端给服务器发来了端口
UserDao userDao = UserDao.getInstance();
User user = userDao.selectByAccount(msg.getUser().getAccount());
if (user == null || !user.getPassword().equals(msg.getUser().getPassword())) {
//当前用户不存在或者密码错误
os.writeObject(Msg.getSTC_LOGIN("101", null));
os.writeObject(null);
} else {
//开始检测是否已经登录
ArrayList<Link> linkList = ServerMessage.linkList;
if (!linkList.contains(new Link(msg.getUser()))) {
link = new Link(user, msg.getMsg(), socket.getInetAddress().getHostAddress());
linkList.add(link);
Link finalLink = link;
//更新UI
Platform.runLater(
() -> {
MainUI.online_userList.add(finalLink.toString());
MainUI.updateOnlineNum(String.valueOf(linkList.size()));
}
);
os.writeObject(Msg.getSTC_LOGIN("100", user));
os.writeObject(null);
new Thread(() -> {
for (Link link12 : linkList) {
//给所有客户端发送在线用户信息
new Thread(new STCSendPortHandle(link12.getIp(), link12.getPort(), Msg.getSTCMsg_sendPortToC(linkList))).start();
}
}).start();
} else {
//帐号已经登录
os.writeObject(Msg.getSTC_LOGIN("102", null));
os.writeObject(null);
}
}
}else if (msg.getType() == Msg.Type.CTS_LOGOUT) {
Link tempLink = msg.getLink();
Platform.runLater(
() -> {
MainUI.online_userList.remove(tempLink.toString());
}
);
ArrayList<Link> linkList = ServerMessage.linkList;
linkList.remove(tempLink);
//删除在线列表
Platform.runLater(
() -> {
MainUI.updateOnlineNum(String.valueOf(ServerMessage.linkList.size()));
}
);
new Thread(() -> {
for (Link link1 : linkList) {
//给所有客户端发送在线用户信息
new Thread(new STCSendPortHandle(link1.getIp(), link1.getPort(), Msg.getSTCMsg_sendPortToC(linkList))).start();
}
}).start();
}
} catch (SocketException e) {
e.printStackTrace();
if(link!=null){
//从视图移除
Link finalLink = link;
Platform.runLater(
() -> {
MainUI.online_userList.remove(finalLink.toString());
}
);
ArrayList<Link> linkList = ServerMessage.linkList;
linkList.remove(link);
//删除在线列表
Platform.runLater(
() -> {
MainUI.updateOnlineNum(String.valueOf(ServerMessage.linkList.size()));
}
);
new Thread(() -> {
for (Link link1 : linkList) {
//给所有客户端发送在线用户信息
new Thread(new STCSendPortHandle(link1.getIp(), link1.getPort(), Msg.getSTCMsg_sendPortToC(linkList))).start();
}
}).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.2.客户端
2.2.1.登录界面控制
在登录过程中,通过线程池中的线程去向服务器发送信息,以此来验证登录信息,服务器返回信息后,通过该信息来决定是否能够登录或者是要弹出错误信息。
public class MainController implements Initializable {
@FXML
public TextField tf_serverIP;
@FXML
public PasswordField pf_password;
@FXML
public TextField tf_account;
ExecutorService es = null;
@FXML
public void loadOn(ActionEvent event){
try {
String account=tf_account.getText();
String password=pf_password.getText();
String serverIP=tf_serverIP.getText();
Future<Status> future =es.submit(new LoadOnServer(account,password,serverIP));
Status status = future.get();
if(status==Status.Account_Password_Error){
Util.alertInformationDialog("错误",status.getMsg());
}else if(status==Status.Connection_Error){
Util.alertInformationDialog("错误",status.getMsg());
}else if(status==Status.Success){
Window window = ((Node) (event.getSource())).getScene().getWindow();
window.hide();
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/list.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
Stage stg=new Stage();
stg.setTitle(ClientMessage.USER_NAME +"的QQ");
stg.setOnCloseRequest(event1 -> {
new Thread(new LogOutServer()).start();
try {
Thread.sleep(500);
}catch (Exception e){
e.printStackTrace();
}
System.exit(0);
});
stg.setScene(scene);
stg.show();
ChatListController chatListController= (ChatListController) loader.getController();
chatListController.setValue(stg);
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
es = Executors.newSingleThreadExecutor();
new Thread(new ReceiveServer()).start();
}
}
2.2.2.全局类
ClientMessage:保存当前客户端的相关信息
ServerMessage:保存当前服务端的相关信息
Status:Server层给Controller返回的状态码信息,界面根据此来判断后续的UI更新操作
Util:一些常用的工具类
2.2.3.好友列表双向绑定数据模型
该类与UI层的listview进行绑定,通过改变这个model来动态更新好友列表的状态,包括上线更新、下线更新、头像闪烁等,还记录当前窗口的动态,防止多次打开窗口。
public class ChatListModel implements Serializable {
private final StringProperty userName;
private final StringProperty ip;
private final StringProperty port;
//头像序号
private final StringProperty headPortrait;
//是否有未读信息
private final StringProperty msgFlag;
//当前是否闪烁白色状态
private final StringProperty flicker;
//是否已经初始化
private final StringProperty init;
//窗口是否已经打开
private final StringProperty openFlag;
//当前存储的消息
private String message;
private ChatWinController chatWinController;
{
this.msgFlag = new SimpleStringProperty("false");
this.flicker = new SimpleStringProperty("false");
this.init=new SimpleStringProperty("false");
this.headPortrait=new SimpleStringProperty("null");
this.openFlag=new SimpleStringProperty("false");
message=new String();
chatWinController=null;
}
public ChatListModel(String userName, String ip, String port ) {
this.userName = new SimpleStringProperty(userName);
this.ip = new SimpleStringProperty(ip);
this.port = new SimpleStringProperty(port);
}
@Override
public String toString() {
return "ChatListModel{" +
"userName=" + userName +
", ip=" + ip +
", port=" + port +
", headPortrait=" + headPortrait +
'}';
}
}
2.2.4.好友列表控制器
该控制器主要完成聊天窗口打开、好友列表更新、文件接受功能等。也就是说只要列表窗口打开,就能接受文件。该类主要难点在于头像闪烁的实现,我的做法是当有客户端接收到信息后,如果判断该信息来自别的客户端,就寻找该信息对应的发送者,找到他对应的数据模型,更新该数据模型,然后开启一个线程,该线程间隔地修改几个变量,通过这几个变量修改头像的地址(一个是空白的图片),然后刷新UI,以此来达到闪烁的效果,最后如果打开窗口的话,则通过共享变量来终止该线程。
public class ChatListController implements Initializable {
@FXML
public ImageView iv_head;
@FXML
public Label lb_name;
@FXML
public Label lb_ip;
@FXML
public Label lb_port;
@FXML
ListView<ChatListModel> lv_chatList;
private Window window;
public void setValue(Window window){
this.window=window;
}
private final Image IMAGE1 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/2780393.jpg");
private final Image IMAGE2 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/36061669.jpg");
private final Image IMAGE3 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/35725819.jpg");
private final Image IMAGE4 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/24494195.jpg");
private final Image IMAGE5 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/51868892.jpg");
private final Image IMAGE6 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/16875648.jpg");
private final Image IMAGEWHITE = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/75659527.jpg");
private Image[] listOfImages = {IMAGE1, IMAGE2, IMAGE3,IMAGE4,IMAGE5,IMAGE6};
@Override
public void initialize(URL location, ResourceBundle resources) {
ClientMessage.chatListController=this;
int index = new Random().nextInt(listOfImages.length);
iv_head.setImage(listOfImages[index]);
iv_head.setFitHeight(100);
iv_head.setFitWidth(100);
lb_name.setText(ClientMessage.USER_NAME);
lb_ip.setText(ClientMessage.Client_IP);
lb_port.setText(String.valueOf(ClientMessage.Client_PORT));
ServerMessage.lv_chatList=lv_chatList;
lv_chatList.setItems(ServerMessage.items);
lv_chatList.setOnMouseClicked(click -> {
if (click.getClickCount() == 2) {
try {
ChatListModel selectedItem = lv_chatList.getSelectionModel()
.getSelectedItem();
if(selectedItem.getOpenFlag().equals("false")){
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/chatWin.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
Stage stg=new Stage();
stg.setTitle(ClientMessage.USER_NAME+"的聊天框");
stg.setScene(scene);
selectedItem.setOpenFlag("true");
selectedItem.setMsgFlag("false");
stg.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent event) {
selectedItem.setOpenFlag("false");
}
});
stg.show();
ChatWinController chatWinController= (ChatWinController) loader.getController();
chatWinController.setVavlue(selectedItem.getUserName(),selectedItem.getIp(),selectedItem.getPort(),stg);
}
}catch (Exception e){
e.printStackTrace();
}
}
});
lv_chatList.setCellFactory(param -> new ListCell<ChatListModel>() {
private ImageView imageView = new ImageView();
@Override
public void updateItem(ChatListModel chatListModel, boolean empty) {
super.updateItem(chatListModel, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(chatListModel.getInit().equals("false")){
chatListModel.setInit("ture");
int index = new Random().nextInt(listOfImages.length);
imageView.setImage(listOfImages[index]);
chatListModel.setHeadPortrait(String.valueOf(index));
imageView.setFitHeight(56);
imageView.setFitWidth(56);
setText(chatListModel.getUserName());
setGraphic(imageView);
}else{
if(chatListModel.getMsgFlag().equals("true")){
if(chatListModel.getFlicker().equals("true")){
imageView.setImage(IMAGEWHITE);
imageView.setFitHeight(56);
imageView.setFitWidth(56);
setText(chatListModel.getUserName());
setGraphic(imageView);
}else{
normalHeadPortrait(chatListModel);
}
}else{
normalHeadPortrait(chatListModel);
}
}
}
}
private void normalHeadPortrait(ChatListModel chatListModel) {
imageView.setImage(listOfImages[Integer.valueOf(chatListModel.getHeadPortrait())]);
imageView.setFitHeight(56);
imageView.setFitWidth(56);
setText(chatListModel.getUserName());
setGraphic(imageView);
}
});
}
public void saveFile(Link link, File file){
Platform.runLater(() -> {
boolean result = Util.alertChooseDialog("文件接收通知"
, "来自" + link.getIp() + "的" + link.getUser().getUserName() + "要给你传输文件,是否同意?");
if(result){
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("选择文件保存位置");
fileChooser.setInitialFileName(file.getName());
File saveFile = fileChooser.showSaveDialog(window);
new Thread(()->{
try (FileInputStream fileInputStream=new FileInputStream(file);
FileOutputStream fileOutputStream=new FileOutputStream(saveFile)){
byte[] b = new byte[2048];
int read;
while ((read = fileInputStream.read(b)) != -1) {
fileOutputStream.write(b,0,read);
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
}else{
Util.alertInformationDialog("成功取消文件传送","你已经成功取消来自" + link.getIp() + "的" + link.getUser().getUserName()+"的文件传输");
}
});
}
}
2.2.5.聊天窗口
聊天窗口控制器主要实现文件传输,信息发送等功能。文件传输通过开启一个新线程去发送,以此来避免UI界面的阻塞,但是没有完成信息传输成功的提示以及传输进度的显示,这是不完善的。信息发送也相对简单,完成一些UI的协调就好,当然还有发送成功失败与否的提示,比较难的地方在于聊天记录的更新与保存,在开始设计数据库时,我没有考虑到聊天记录的存储,因此在后续的设计中,选择把这些信息保存在内存,因此下一次启动就回丢失,这是不完善的。如果对方没有打开窗口,也要跟打开窗口的情况下区分开来。其中也存在一些比较麻烦的问题,例如发送信息和接受信息的顺序是否会错乱?对于服务器端宕机对方已经下线,但是本机还是显示在线这种情况如何处理?如果在离线时收到信息如何处理?
public class ChatWinController implements Initializable {
private final Image IMAGE1 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/85814151.jpg");
private final Image IMAGE2 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/86481160.jpg");
private final Image IMAGE3 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/5502061.jpg");
private final Image IMAGE4 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/92563047.jpg");
private final Image IMAGE5 = new Image("http://othgjp7hs.bkt.clouddn.com/18-6-8/81909198.jpg");
@FXML
public ImageView iv_file;
@FXML
public Label lb_name;
@FXML
public ImageView iv_sound;
@FXML
public ImageView iv_video;
@FXML
public ImageView iv_show1;
@FXML
public ImageView iv_show2;
@FXML
public TextArea ta_wait_sendMessage;
@FXML
public TextArea ta_message;
private Window window;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
private String name;
private String port;
private String ip;
ExecutorService es =null;
public void setVavlue(String name,String ip,String port,Window window){
this.name=name;
this.port=port;
this.ip=ip;
this.window=window;
lb_name.setText(name);
loadMsg();
}
@Override
public void initialize(URL location, ResourceBundle resources) {
iv_file.setImage(IMAGE1);
iv_video.setImage(IMAGE2);
iv_sound.setImage(IMAGE3);
iv_show1.setImage(IMAGE4);
iv_show2.setImage(IMAGE5);
es = Executors.newSingleThreadExecutor();
}
@FXML
public void sendFile(MouseEvent mouseEvent) {
try {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("打开需要传送的文件");
File file = fileChooser.showOpenDialog(window);
Future<Status> submit = es.submit(new SendFileServer(ClientMessage.Client_IP, Integer.valueOf(port), ClientMessage.USER_NAME, file));
submit.get();
}catch (Exception e){
e.printStackTrace();
}
}
@FXML
public void sendMessage(MouseEvent mouseEvent) {
try {
String text = ta_wait_sendMessage.getText();
StringBuilder sb=new StringBuilder();
sb.append(sdf.format(new Date()));
sb.append(" "+ClientMessage.USER_NAME+"\t:\n");
sb.append(text+"\n");
Future<Status> future =es.submit(new SendServer(sb.toString(),ip,Integer.valueOf(port),ClientMessage.USER_NAME));
Status status = future.get();
if(status==Status.Send_Success){
ta_wait_sendMessage.setText("");
StringBuilder stringBuilder=new StringBuilder(ta_message.getText());
stringBuilder.append(sdf.format(new Date()));
stringBuilder.append(" "+ClientMessage.USER_NAME+"\t:\n");
stringBuilder.append(text+"\n");
ta_message.setText(stringBuilder.toString());
storeMsg(stringBuilder.toString());
}else{
Util.alertInformationDialog("错误","你的信息发送失败,请重试!");
}
}catch (Exception e){
e.printStackTrace();
}
}
//将收到的信息保存起来
public void storeMsg(String message){
for( ChatListModel cm:ServerMessage.items){
if(cm.getUserName().equals(lb_name.getText())){
cm.setMessage(message);
break;
}
}
}
public void updataMsg(String append){
ta_message.setText(ta_message.getText()+append);
}
public void loadMsg(){
for( ChatListModel cm:ServerMessage.items){
if(cm.getUserName().equals(lb_name.getText())){
cm.setChatWinController(this);
ta_message.setText(cm.getMessage());
break;
}
}
}
}
2.2.6.通信服务
LoadOnServer:通过开启一个线程来提供登录的服务
LogOutServer:通过开启一个线程来提供退出的服务
ReceiveServer:通过开启一个线程来提供接受信息的服务
SendFileServer:通过开启一个线程来提供发送文件的服务
SendServer:通过开启一个线程来提供发送信息的服务
public class LoadOnServer implements Callable<Status> {
private String account;
private String password;
private String serverIP;
public LoadOnServer(String account, String password, String serverIP) {
this.account = account;
this.password = password;
this.serverIP = serverIP;
}
@Override
public Status call() {
try {
Socket socket=new Socket(serverIP,ServerMessage.SERVER_PORT);
ClientMessage.Client_IP=socket.getLocalAddress().toString().substring(1);
ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
os.writeObject(Msg.getCTSMsg_LOGIN(new User(account, password),ClientMessage.Client_PORT));
os.writeObject(null);
Msg msg=(Msg)ois.readObject();
if(msg.getMsg().equals("100")){
ClientMessage.USER_NAME=msg.getUser().getUserName();
ClientMessage.user=msg.getUser();
ServerMessage.SERVER_IP=serverIP;
return Status.Success;
}else if(msg.getMsg().equals("101")){
return Status.Account_Password_Error;
}else if(msg.getMsg().equals("102")){
return Status.Connection_Error;
}
}catch (Exception e){
e.printStackTrace();
}
return Status.Connection_Error;
}
}