准备换工作了,下一份工作也已经搞定。从学校毕业,浑浑噩噩的做了一年测试,终于是要转向自己喜欢的开发了。浪费了一年时间终于再次回到轨道上,希望没有落后太多。
打发业余时间,想要一个聊天工具,于是便开始做了。这是初步的成果,采用客户端和服务器的模式。服务器端比较简单,主要保存有一个在线用户列表,每个客户端登录,则会向服务器登记,同时服务器会返回当前所有的在线用户,由客户端显示在界面当中。
主要界面如下:
文件传输:
当前实现的功能主要是文本聊天和文件传输功能,接着主要想实现类型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;
}
}
}
}
@Override
protected 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 block
e.printStackTrace();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.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("消息处理器启动完毕!");
}
/**
* 不断从消息缓存中取得消息然后进行处理
*/
@Override
public Object doInBackground(){
while(!isStopped()){
//没有被停止的时候不断的处理消息
Message message = MessageCache.getInstance().get();
if(message!=null){
publish(message);
}
}
return null;
}
/**
* 处理消息
* @param message
*/
@Override
public 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;
}
@Override
protected 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为null
retO.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 block
e1.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;
}
@Override
protected 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);
}
}
}