Java实现的聊天工具(部分功能完成)

准备换工作了,下一份工作也已经搞定。从学校毕业,浑浑噩噩的做了一年测试,终于是要转向自己喜欢的开发了。浪费了一年时间终于再次回到轨道上,希望没有落后太多。

打发业余时间,想要一个聊天工具,于是便开始做了。这是初步的成果,采用客户端和服务器的模式。服务器端比较简单,主要保存有一个在线用户列表,每个客户端登录,则会向服务器登记,同时服务器会返回当前所有的在线用户,由客户端显示在界面当中。

主要界面如下:


Java实现的聊天工具(部分功能完成)

 

文件传输:


Java实现的聊天工具(部分功能完成)

 

当前实现的功能主要是文本聊天和文件传输功能,接着主要想实现类型QQ的图片发送及语音、视频聊天功能。图片发送功能其实已经完成,但在公司电脑上无法拷贝回家,因此此处上传的代码中没有图片传输功能。

没有在界面上花太多功夫,因此界面很粗糙,准备是相关的功能完成之后再对界面进行优化。

 

下图是Client在Eclipse下面的结构:


Java实现的聊天工具(部分功能完成)

 

主要包括有三个包:

client: 存储客户端相关的类

common: 存储客户端和服务器端共用的类

common_ui: 存储我自己所做的公用类,其中主要是界面相关的类。主要是自己为了方便在不同的工程下复用相关代码而建的类。

 

引用的Jar包主要有三个,实际上在该聊天软件中仅使用了miglayout这个包来进行相关布局,另外两个包是在common_ui中有使用Jfreechart而引用的,在该工程中未使用。

 

附件中有Server及Client的源代码,如果Server启动失败请修改com/liuqi/chart/common/uitl/Constants中的服务器IP地址及端口。相关的Jar包请自行下载。

 

以下对几个主要使用的类进行说明:

一 服务器端:


Java实现的聊天工具(部分功能完成)

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

Java实现的聊天工具(部分功能完成)

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


Java实现的聊天工具(部分功能完成)

 

二 客户端:

客户端也在相应的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中有文件传输请求时被调用。


Java实现的聊天工具(部分功能完成)

 

选择完处理方式后,给发送者发送接收或者拒绝回应,同时启动文件接收器来进行接收:

 

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);
		}
	}
}