Java实现的聊天工具(部分功能完成)
准备换工作了,下一份工作也已经搞定。从学校毕业,浑浑噩噩的做了一年测试,终于是要转向自己喜欢的开发了。浪费了一年时间终于再次回到轨道上,希望没有落后太多。
打发业余时间,想要一个聊天工具,于是便开始做了。这是初步的成果,采用客户端和服务器的模式。服务器端比较简单,主要保存有一个在线用户列表,每个客户端登录,则会向服务器登记,同时服务器会返回当前所有的在线用户,由客户端显示在界面当中。
主要界面如下:

?
文件传输:

?
当前实现的功能主要是文本聊天和文件传输功能,接着主要想实现类型QQ的图片发送及语音、视频聊天功能。图片发送功能其实已经完成,但在公司电脑上无法拷贝回家,因此此处上传的代码中没有图片传输功能。
没有在界面上花太多功夫,因此界面很粗糙,准备是相关的功能完成之后再对界面进行优化。
?
下图是Client在Eclipse下面的结构:

?
主要包括有三个包:
client: 存储客户端相关的类
common: 存储客户端和服务器端共用的类
common_ui: 存储我自己所做的公用类,其中主要是界面相关的类。主要是自己为了方便在不同的工程下复用相关代码而建的类。
?
引用的Jar包主要有三个,实际上在该聊天软件中仅使用了miglayout这个包来进行相关布局,另外两个包是在common_ui中有使用Jfreechart而引用的,在该工程中未使用。
?
附件中有Server及Client的源代码,如果Server启动失败请修改com/liuqi/chart/common/uitl/Constants中的服务器IP地址及端口。相关的Jar包请自行下载。
?
以下对几个主要使用的类进行说明:
一 服务器端:

1. UserCache: 在线用户缓存器,存储服务器上的在线用户列表,采用单例模式,同时保证线程安全。

?2. Server:在Constants中所规定的IP上启动监听,接收服务器的消息并进行处理。当前主要处理两种消息,登录和离线。登录时通知所有在线用户该用户登录,离线时通知所有用户该用户离线。

?
二 客户端:
客户端也在相应的IP和端口上进行监听,收取来自其它用户和服务器的消息。
1. 消息分发:
ClientServer类:使用SwingWorker来不断的在后台监听相关消息进行处理:
?
package com.liuqi.chart.client;import java.io.IOException;import java.net.InetAddress;import java.net.ServerSocket;import java.net.Socket;import java.util.List;import javax.swing.SwingWorker;import com.liuqi.chart.client.ui.FileTransfferDialog;import com.liuqi.chart.client.ui.MainFrame;import com.liuqi.chart.client.util.MessageCache;import com.liuqi.chart.common.bean.Message;import com.liuqi.chart.common.bean.User;import com.liuqi.chart.common.tran.TransfferImpl;import com.liuqi.chart.common.tran.bean.TranObject;import com.liuqi.chart.common.tran.bean.TranObjectType;import com.liuqi.chart.common.util.Constants;/** * 用户机器上的消息接收服务器,应当使用单独的线程来进行处理 * * @author 67 * */public class ClientServer extends SwingWorker<Object, TranObject> {private ServerSocket serverSocket;private MainFrame frame;private TransfferImpl transffer = new TransfferImpl();public ClientServer(MainFrame frame, String ip) throws IOException {this.frame = frame;InetAddress address = InetAddress.getByName(ip);serverSocket = new ServerSocket(Constants.CLIENT_SERVER_PORT, 0,address);}/** * 对传输的对象进行处理 一般用户传输过来的消息类型有: 文本消息 传输文件请求 * * @param object */public void process(List<TranObject> list) {for (TranObject object : list) {switch (object.getType()) {case MESSAGE: {// 发送的是来自其它用户的消息,则将其添加到消息缓存中去等待处理Object o = object.getObject();if (o != null && o instanceof Message) {Message message = (Message) o;MessageCache.getInstance().add(message);}break;}case LOGIN: {// 表明接收到的是来自服务器的消息,有某个用户登录,需要在用户列表中表现出来Object o = object.getObject();if (o != null && o instanceof User) {User user = (User)o;frame.getCenterPanel().addUser(user);}break;}case LOGOUT: {//表明是接收到的来自服务器某个用户退出登录的消息 Object o = object.getObject();if(o!=null&&o instanceof User){User user = (User)o;frame.getCenterPanel().deleteUser(user);}break;}}}}@Overrideprotected Object doInBackground() throws Exception {while (!isCancelled()) {try {Socket socket = serverSocket.accept();// 从Socket中取得所传输的对象TranObject object;try {object = transffer.get(socket);if (object != null) {publish(object);//如果是文件传输请求,首先弹出是否接收的对话框,如果是,则在相应端口启动文件接收线程,然后再回应准备OK的消息,否则返回否if(object.getType().equals(TranObjectType.FILE)){FileTransfferDialog dialog = new FileTransfferDialog(object,socket);dialog.setVisible(true);}}} catch (ClassNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}return null;}}??
?
?
2. 文本消息处理:
??MessageCache:接收到的消息队列,来自不同用户的消息被保存在这个队列中等待处理。
? MessageProcessor:系统启动后不断的从MessageCahce中取得消息来进行处理,如果队列中无消息则等待,队列中有消息进行则被唤醒进行处理。
package com.liuqi.chart.client.util;
import java.util.List;import javax.swing.SwingWorker;import com.liuqi.chart.client.ui.ChartDialog;import com.liuqi.chart.common.bean.Message;import com.liuqi.chart.common.bean.TextMessage;import com.liuqi.common.log.LLogger;/** * 消息处理器 * @author 67 * */public class MessageProcessor extends SwingWorker<Object,Message>{private boolean isStopped = false;public MessageProcessor(){LLogger.info("消息处理器启动完毕!");}/** * 不断从消息缓存中取得消息然后进行处理 */@Overridepublic Object doInBackground(){while(!isStopped()){//没有被停止的时候不断的处理消息 Message message = MessageCache.getInstance().get();if(message!=null){publish(message);}}return null;}/** * 处理消息 * @param message */@Overridepublic void process(List<Message> list){for(Message message: list){//将消息显示在对应的聊天窗口中ChartDialog dialog = ChartDialogCache.getInstance().get(message.getFromUser());if(dialog!=null){dialog.appendMessage(dialog.getUser(),((TextMessage)message).getMessage());dialog.setVisible(true);}}}public boolean isStopped() {return isStopped;}public void setStopped(boolean isStopped) {this.isStopped = isStopped;}}??
3. 文件传输:
TranFileCache:文件传输队列,当有新的文件发送请求时,需要被发送的文件及进度面板被存入该队列。
SendFileWorker:不断的从文件传输队列中取得文件来进行传输,没有文件传输请求则等待;有新请求则被唤醒:
?
package com.liuqi.chart.client.tran;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.OutputStream;import java.net.InetSocketAddress;import java.net.Socket;import java.net.SocketAddress;import java.util.Calendar;import java.util.List;import javax.swing.SwingWorker;import com.liuqi.chart.client.ui.ChartDialog;import com.liuqi.chart.client.ui.tran.TranOneFilePanel;import com.liuqi.chart.client.util.Cache;import com.liuqi.chart.common.bean.User;import com.liuqi.chart.common.tran.bean.TranObject;import com.liuqi.chart.common.tran.bean.TranObjectType;/** * 文件传输工具 * 先与目的取得连接,取得一个socket,对方有回应后再发送数据 * * @author 67 * */public class SendFileWorker extends SwingWorker<Object,String>{private ChartDialog dialog;private TranFileCache cache = TranFileCache.getInstance();private static final String START = "start";private static final String REJECT = "reject";private static final String END = "end";public SendFileWorker(ChartDialog dialog){this.dialog = dialog;}@Overrideprotected Object doInBackground() throws Exception {while(!isCancelled()){TranOneFilePanel panel = cache.get();File file = panel.getFile();InetSocketAddress address = new InetSocketAddress(dialog.getUser().getIp(),dialog.getUser().getPort());Socket socket = new Socket();try{socket.connect(address);OutputStream output = socket.getOutputStream();InputStream input = socket.getInputStream();//第一步,先向对方发送文件传输请求ObjectOutputStream oo = new ObjectOutputStream(output);TranObject<File> to = new TranObject<File>(TranObjectType.FILE);to.setFromUser(Cache.getInstance().getNowUser());to.setToUser(dialog.getUser());to.setObject(file);oo.writeObject(to);oo.flush();ObjectInputStream oi = new ObjectInputStream(input);TranObject<User> object = (TranObject<User>)oi.readObject();if(object.getObject()==null){publish(REJECT);}else{publish(START);InetSocketAddress address2 = new InetSocketAddress(object.getObject().getIp(),object.getObject().getPort());Socket ss = new Socket();ss.connect(address2);OutputStream so = ss.getOutputStream();InputStream fi = new FileInputStream(file);long fileLength = file.length();long nowLength = 0;long oneofhundred = fileLength/100;byte[] b = new byte[1024];int oldrake = 0;int newrake = 0;int race = 0;//速度long starttime = Calendar.getInstance().getTimeInMillis()/1000;//开始时间,按秒计算 long time = 0;while(fi.read(b)!=-1){so.write(b);nowLength += b.length;newrake = (int)(nowLength / oneofhundred);if(newrake!=oldrake){time = Calendar.getInstance().getTimeInMillis()/1000 - starttime;if(time==0){time = 1;}race = (int)((nowLength/1024) / time);publish(newrake + "");oldrake = newrake;}}publish(END);so.flush();so.close();fi.close();}}catch(Exception ex){//连接失败}}return null;}protected void process(List<String> list){for(String str: list){//dialog.appendMessage(str);if(str.equals(START)){//开始传输dialog.appendMessage("对方已经接收!");}else if(str.equals(REJECT)){dialog.appendMessage("对方拒绝接收!");dialog.getRightPanel().getTranFilePanel().del(cache.getNowPanel());}else if(str.equals(END)){dialog.appendMessage("传输完成!");dialog.getRightPanel().getTranFilePanel().del(cache.getNowPanel());}else{int rake = Integer.valueOf(str);cache.getNowPanel().getPb().setValue(rake);}}}}?
?
?
FileTransfferDialog: 接收消息处理对话框,选择文件的保存方式。在ClientServer中有文件传输请求时被调用。

?
选择完处理方式后,给发送者发送接收或者拒绝回应,同时启动文件接收器来进行接收:
?
TransfferImpl transffer = new TransfferImpl();TranObject<User> retO = new TranObject<User>(TranObjectType.FILE);User user = new User();user.setIp(Cache.getInstance().getNowUser().getIp());user.setPort(Constants.CLIENT_FILE_TRANSPORT_PORT);retO.setObject(user);if(e.getSource().equals(cancelButton)){//拒绝接收,返回的对象中Object为nullretO.setObject(null);}String path = "D:\" + object.getObject().getName();if(e.getSource().equals(saveAsButton)){//保存到指定目录 chooser.showSaveDialog(null);File file = chooser.getSelectedFile();if(file.isDirectory()){path = file.getPath() + object.getObject().getName();}else{path = file.getPath();}}try {transffer.tran(socket, retO);} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}if(!e.getSource().equals(cancelButton)){//在不是取消的时候执行worker.setPath(path);worker.execute();}?
?ReceiveFileWorker: 文件接收器
?
package com.liuqi.chart.client.tran;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.net.Socket;import java.util.List;import javax.swing.SwingWorker;import com.liuqi.chart.client.util.Cache;import com.liuqi.chart.common.bean.User;import com.liuqi.chart.common.tran.TransfferImpl;import com.liuqi.chart.common.tran.bean.TranObject;import com.liuqi.chart.common.tran.bean.TranObjectType;import com.liuqi.chart.common.util.Constants;public class ReceiveFileWorker extends SwingWorker<Object,String>{private TranObject<File> object;private String path;//保存目录,默认保存到D盘 public ReceiveFileWorker(TranObject<File> object){this.object = object;path = "D:\" + object.getObject().getName();}public void setPath(String path){this.path = path;}@Overrideprotected Object doInBackground() throws Exception {//启动文件接收线程,并返回给用户,其中FromUser的端口使用文件接收端口try{File file = (File)object.getObject();ServerSocket serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(Cache.getInstance().getNowUser().getIp(),Constants.CLIENT_FILE_TRANSPORT_PORT));//启动接收Socket retSocket = serverSocket.accept();InputStream input = retSocket.getInputStream();OutputStream output = new FileOutputStream(new File(path));//保存到默认目录 publish("文件传输开始,文件大小:" + file.length());byte[] b = new byte[1024];double nowSize = 0;while(input.read(b)!=-1){output.write(b);nowSize ++;//publish("已传输:" + nowSize + "KB");}publish("文件传输结束");output.flush();output.close();//关闭文件接收retSocket.close();serverSocket.close();}catch(Exception ex){ex.printStackTrace();}return null;}protected void process(List<String> list){for(String str: list){System.out.println(str);}}}?
?
?
?
?
?
?
?