socket仿qq聊天工具(二)(扩展博客)

本文详细介绍了一个基于Java的即时通讯系统的设计与实现,包括客户端与服务器的交互流程、UI界面展示、对象序列化处理及线程管理等核心内容。

最近由于电脑出了点问题,学校那边临近毕业又一直有事耽误了不少的时间,这里就直接把所有的代码粘贴上来。附上运行结果,我要开始下面数据库的学习了。
由于这次的demo涉及到了两个项目,这里就直接把两个项目的项目结构都粘贴上来,这个阶段的项目我都是用Eclipse来完成的,等到javaweb阶段可能我就要换IDEA了。因为就这两个工具来说。现在肯定是IDEA占的市场要大,但是Eclipse也拥有除了IDEA以外最大的市场份额,而且Eclipse与IDEA相比Eclipse是最原生的,作为初学者建议用Eclipse。
在这里插入图片描述
首先是两个项目中都相同的Message.java,作为一个实现了序列化接口的类,这里涉及到了序列化和反序列化的问题,不明白的话可以去看我的这一篇博文:对象流和序列化

package model;

import java.io.Serializable;
import java.util.List;
/**
 * 消息对象  服务器和客户端的消息对象要完全一致(包名都要完全一致)才能反序列化  
 *
 */
public class Message implements Serializable{

	private int type;//消息类型
	
	//常量 常量一般是全大写  如果由多个单词构成的 就用下划线_ 分开
	public static final int LOGIN = 1;//登录消息类型
	
	public static final int FRIEND = 2;//刷新好友列表
	
	public static final int MSG = 3;//一对一聊天的消息类型
	
	public static final int GROUP = 4;//群聊
	
	public static final int EXIT = 5;//退出
	
	public static final int ERROR = 6;//错误提示
	
	private String from; //发送消息的人
	
	private String to; //接收消息的人
	
	private String content; //消息的内容
	
	private String time; //发送消息的时间
	
	public String getFrom() {
		return from;
	}

	public void setFrom(String from) {
		this.from = from;
	}

	public String getTo() {
		return to;
	}

	public void setTo(String to) {
		this.to = to;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getTime() {
		return time;
	}

	public void setTime(String time) {
		this.time = time;
	}

	private String username;
	
	private String password;
	
	private boolean success;//是否成功
	
	private List<String> friends;
	
	public List<String> getFriends() {
		return friends;
	}

	public void setFriends(List<String> friends) {
		this.friends = friends;
	}

	public boolean isSuccess() {
		return success;
	}

	public void setSuccess(boolean success) {
		this.success = success;
	}

	public int getType() {
		return type;
	}

	public void setType(int type) {
		this.type = type;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}
	
	
	
}

而后是客户端(qqClient)的线程类:MyThread.java:

package thread;


import java.io.IOException;
import java.net.Socket;

import model.Message;
import ui.Friends;
import ui.Talk;
import util.ObjectRW;

public class MyThread extends Thread{

	private Socket socket;
	
	private Friends friends;

	public MyThread(Socket socket, Friends friends) {
		super();
		this.socket = socket;
		this.friends = friends;
	}
	
	@Override
		public void run() {
			super.run();
			while(true) {
				try {
					Message message = ObjectRW.readObject(socket);
					if(message.getType() == Message.FRIEND) {
						friends.refreshFriend(message.getFriends());
					} else if(message.getType() == Message.MSG) {
						//a给b发消息的时候   相对于b  to就是消息的from
						//显示消息对象
						Talk talk = friends.getTalk(message.getFrom(), Message.MSG);
						talk.showMessage(message);
						talk.setVisible(true);
					} else if(message.getType() == Message.GROUP) {
						Talk talk = friends.getGroupTalk();
						talk.showMessage(message);
						talk.setVisible(true);
					} else if(message.getType() == Message.EXIT) {
						break;
					} else if(message.getType() == Message.ERROR) {
						Talk talk = friends.getTalk(message.getFrom(), Message.MSG);
						talk.showMessage(message);
						talk.setVisible(true);
					}
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}//关流
			System.exit(0);//退出程序
		}
}

客户端的3个ui界面,当然这里面也包含了数据交互:
好友界面,Friends.java:

package ui;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.Socket;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.EmptyBorder;

import model.Message;
import thread.MyThread;
import util.ObjectRW;

import javax.swing.JList;
import javax.swing.JButton;

public class Friends extends JFrame {

	private JPanel contentPane;

	private Socket socket;
	
	private String curUser;
	
	private JPanel panel = null;
	
	private Talk group;//群聊窗口
	
	private Map<String, Talk> talks = new Hashtable<String, Talk>();
	/**
	 * Create the frame.
	 */
	public Friends(Socket socket,String curUser) {
		this.curUser = curUser;
		this.socket = socket;
		setTitle(curUser);
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		setBounds(100, 100, 244, 520);
		contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(contentPane);
		contentPane.setLayout(new BorderLayout(0, 0));
		panel = new JPanel();
		panel.setLayout(new BorderLayout());
		contentPane.add(panel, BorderLayout.CENTER);
		
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				super.windowClosing(e);
				Message message = new Message();
				message.setFrom(curUser);//下线的人
				message.setType(Message.EXIT);//退出的消息类型
				try {
					ObjectRW.writeObject(socket, message);
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		});
		
		JButton button = new JButton("群聊");
		button.addActionListener(new ActionListener() {
			
			@Override
			public void actionPerformed(ActionEvent e) {
				group = getGroupTalk();
				group.setVisible(true);
			}
		});
		contentPane.add(button, BorderLayout.SOUTH);
		//启动接收消息的线程
		new MyThread(socket, this).start();
	}
	/**
	 * 刷新好友列表的方法
	 * @param friends
	 */
	public void refreshFriend(List<String> friends) {
		//先将panel中的元素移除
		panel.removeAll();
		//好友列表中不显示自己
		int index = 0;
		//不包含自己的好友数据
		String [] fri = new String[friends.size() - 1];
		for (String string : friends) {
			if(!string.equals(curUser)) {
				fri[index] = string;
				index++;
			}
		}
		JList list = new JList(fri);
		list.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				super.mouseClicked(e);
				if(e.getClickCount() == 2) {//实现双击
					//根据鼠标的位置确认点击的是第几个
					int index = list.locationToIndex(e.getPoint());
					//在数组中找到点击的是谁
					Talk talk = getTalk(fri[index], Message.MSG);
					//显示聊天窗口
					talk.setVisible(true);
				}
			}
		});
		//滚动条
		JScrollPane scrollPane = new JScrollPane(list);
		panel.add(scrollPane);
		//刷新一下
		panel.repaint();
		panel.updateUI();
	}
	/**
	 * 获取对话框
	 * @return
	 */
	public Talk getTalk(String to,int type) {
		Talk talk = talks.get(to);
		if(talk == null) {
			talk = new Talk(socket, curUser, to, type);
			talks.put(to, talk);
		}
		return talk;
	}

	/**
	 * 获取群聊窗口
	 * @return
	 */
	public Talk getGroupTalk() {
		if(group == null) {
			group = new Talk(socket, curUser, "群聊", Message.GROUP);
		}
		return group;
	}
}

而后登录界面:Login.java

package ui;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;

import model.Message;
import util.ObjectRW;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.JPasswordField;
import javax.swing.JButton;

public class Login extends JFrame {

	private JPanel contentPane;
	private JTextField username;
	private JPasswordField password;

	/**
	 * Launch the application.
	 */
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					Login frame = new Login();
					frame.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * Create the frame.
	 */
	public Login() {
		setTitle("登录");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(100, 100, 282, 300);
		contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(contentPane);
		contentPane.setLayout(null);
		
		JLabel label = new JLabel("账号");
		label.setBounds(35, 40, 54, 15);
		contentPane.add(label);
		
		username = new JTextField();
		username.setBounds(90, 37, 118, 21);
		contentPane.add(username);
		username.setColumns(10);
		
		JLabel label_1 = new JLabel("密码");
		label_1.setBounds(35, 104, 54, 15);
		contentPane.add(label_1);
		
		password = new JPasswordField();
		password.setBounds(90, 101, 118, 21);
		contentPane.add(password);
		
		JButton button = new JButton("登录");
		button.addActionListener(new ActionListener() {
			
			@Override
			public void actionPerformed(ActionEvent e) {
				try {
					Socket socket = new Socket("127.0.0.1", 9999);
					//执行登录
					Message message = new Message();
					message.setType(Message.LOGIN);
					message.setUsername(username.getText());
					message.setPassword(new String(password.getPassword()));
					//将对象写到服务器去
					ObjectRW.writeObject(socket, message);
					//接收服务器的响应,获取登录结果
					try {
						message = ObjectRW.readObject(socket);
						if(message.isSuccess()) {//登录成功
//							JOptionPane.showConfirmDialog(null, "登录成功", "提示", JOptionPane.YES_OPTION);
							new Friends(socket, username.getText()).setVisible(true);//显示好友列表界面
							Login.this.setVisible(false);//隐藏登录界面
						}else {//登录失败
							JOptionPane.showConfirmDialog(null, "登录失败", "提示", JOptionPane.YES_OPTION);
						}
					} catch (ClassNotFoundException e1) {
						e1.printStackTrace();
					}
				} catch (UnknownHostException e1) {
					e1.printStackTrace();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		});
		button.setBounds(85, 170, 93, 23);
		contentPane.add(button);
	}
}

最后是聊天界面 Talk.java:

package ui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.Socket;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.EmptyBorder;

import model.Message;
import util.ObjectRW;
import util.StringUtils;

import javax.swing.JTextArea;
import javax.swing.JButton;
/**
 * 聊天窗口
 * @author HASEE
 *
 */
public class Talk extends JFrame {

	private JPanel contentPane;

	private JTextArea show;
	
	private JTextArea input;
	
	private Socket socket;
	
	private String curUser;
	
	private String to;
	
	private int type;// 可以是单聊,也可以是群聊的
	

	/**
	 * Create the frame.
	 */
	public Talk(Socket socket,String curUser,String to,int type) {
		this.type = type;
		setTitle(to);
		this.curUser = curUser;
		this.socket = socket;
		this.to = to;
		//点击右上角的X不做任何事情
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		setBounds(100, 100, 322, 448);
		contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(contentPane);
		contentPane.setLayout(new BorderLayout(0, 0));
		//当点击了对话框右上角的X时,不是退出程序,只是隐藏
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				super.windowClosing(e);
				//设置不可见
				Talk.this.setVisible(false);
			}
		});
		
		JPanel panel = new JPanel();
		contentPane.add(panel);
		panel.setLayout(new BorderLayout(0, 0));
		
		show = new JTextArea();
		JScrollPane jScrollPane = new JScrollPane(show);
		jScrollPane.setPreferredSize(new Dimension(0, 300));
		panel.add(jScrollPane, BorderLayout.NORTH);
		
		input = new JTextArea();
		JScrollPane jScrollPane2 = new JScrollPane(input);
		panel.add(jScrollPane2, BorderLayout.CENTER);
		
		JButton button = new JButton("发送");
		button.addActionListener(new ActionListener() {
			
			@Override
			public void actionPerformed(ActionEvent e) {
				//构造消息对象
				Message message = new Message();
				message.setType(type);
				message.setFrom(curUser);
				message.setContent(input.getText());
				message.setTo(to);
				message.setTime(StringUtils.getCurrentTime());
				try {
					ObjectRW.writeObject(socket, message);
				} catch (IOException e1) {
					e1.printStackTrace();
				}
				
				//显示自己的消息
				showMessage(message);
				
				input.setText("");//清空输入框
			}
		});
		panel.add(button, BorderLayout.SOUTH);
	}

	/**
	 * 显示消息
	 */
	public void showMessage(Message message) {
		if(message.getType() == Message.MSG || message.getType() == Message.GROUP) {
			show.append(message.getFrom() + "\t" + message.getTime() + "\r\n");
			show.append(message.getContent() + "\n");
		} else if(message.getType() == Message.ERROR) {
			show.append("\t" + message.getContent() + "\n");
		}
	}
}

接着是客户端和服务器都相同的对象读写类:ObjectRW.java

package util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

import model.Message;

/**
 * 对象的读写操作
 * 
 *
 */
public class ObjectRW {

	/**
	 * 读取对象
	 * @param socket
	 * @return
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	public static Message readObject(Socket socket) throws ClassNotFoundException, IOException {
		return (Message) new ObjectInputStream(socket.getInputStream()).readObject();
	}
	
	/**
	 * 写对象出去
	 * @param socket
	 * @param obj
	 * @throws IOException 
	 */
	public static void writeObject(Socket socket,Object obj) throws IOException {
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
		objectOutputStream.writeObject(obj);
		objectOutputStream.flush();
	}
}

而后是客户端用来返回当前时间的类:StringUtils.java:

package util;

import java.text.SimpleDateFormat;
import java.util.Date;

public class StringUtils {

	/**
	 * 获取当前的时间
	 * 2020-02-12 14:23:30
	 * @return
	 */
	public static String getCurrentTime() {
		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
	}
}

上面客户端的类基本写完了:
而后是服务器端(qqServer):
Main类:

package main;

import qqServer.Server;

public class Main {

	public static void main(String[] args) {
		new Server();
	}
}

服务器的主类:

Server.java:
package qqServer;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import model.Message;
import thread.MyThread;
import util.ObjectRW;
/**
 * 服务器
 * 
 *
 */
public class Server {
	
	private Map<String, Socket> sockets = new Hashtable<String, Socket>();
	//线程池 假设支持10个人同时在线
	private ExecutorService executorService = Executors.newFixedThreadPool(10);

	public Server() {
		try {
			ServerSocket serverSocket = new ServerSocket(9999);
			while(true) {
				//客户端成功连接服务器
				Socket socket = serverSocket.accept();
				//服务器读取客户端发送的message对象执行登录
				try {
					Message message = ObjectRW.readObject(socket);
					if(message.getType() == Message.LOGIN) {
						//判断账号密码是否正确  
						//这里假设密码是123  就认为账号密码正确 即登录成功
						//"123"这个常量肯定不会null 可以避免出现空指针异常
						//message.getPassword()是一个变量有可能为null
						if("123".equals(message.getPassword())) {
							message.setSuccess(true);
							ObjectRW.writeObject(socket, message);
							//客户端登录成功后  服务器需要保存一下客户端
							sockets.put(message.getUsername(), socket);
							//然后开启线程为该客户端服务 如果不开启线程,则服务器不能再次接收用户的登录
							executorService.execute(new MyThread(this,sockets, socket));
						}else {//登录失败
							message.setSuccess(false);
							ObjectRW.writeObject(socket, message);
						}
					}
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * 刷新好友列表
	 * 就是将在线的人的name告诉给每一个人
	 */
	public void refreshFriend() {
		//将map中所有的key撞到list中
		List<String> friends = new ArrayList<String>();
		Set<String> sets = sockets.keySet();
		for (String string : sets) {
			friends.add(string);
		}
		
		//然后封装一个Message
		Message message = new Message();
		message.setFriends(friends);
		message.setType(Message.FRIEND);
		
		//将message响应给所有在线的人
		for (String string : sets) {
			try {
				ObjectRW.writeObject(sockets.get(string), message);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

最后是服务器端的线程类(注意这里和客户端的线程类不同):

package thread;

import java.io.IOException;
import java.net.Socket;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import model.Message;
import qqServer.Server;
import util.ObjectRW;

public class MyThread implements Runnable{

	private Map<String, Socket> sockets;
	
	private Socket socket;
	
	private Server server;
	
	public MyThread(Server server,Map<String, Socket> sockets, Socket socket) {
		super();
		this.server = server;
		this.sockets = sockets;
		this.socket = socket;
	}

	@Override
	public void run() {
		server.refreshFriend();//刷新好友列表
		//不停的接收消息
		while(true) {
			try {
				Message message = ObjectRW.readObject(socket);
				if(message.getType() == Message.MSG) {
					//找到接收者的socket
					Socket s = sockets.get(message.getTo());
					if(s != null)//如果对方在线
					ObjectRW.writeObject(s, message);
					else {//如果对方不在线 提示一下  
						//或者考虑一下离线消息  离线消息该怎么处理???
						Message msg = new Message();
						msg.setFrom(message.getTo());
						msg.setType(Message.ERROR);
						msg.setContent("对方已下线");
						ObjectRW.writeObject(socket, msg);
					}
				} else if(message.getType() == Message.GROUP) {
					Set<Entry<String,Socket>> entrys = sockets.entrySet();
					for (Entry<String, Socket> entry : entrys) {
						if(entry.getValue() != socket) {//除了自己都要把消息发送出去
							ObjectRW.writeObject(entry.getValue(), message);
						}
					}
				} else if(message.getType() == Message.EXIT) {
					// 首先从集合中移除
					sockets.remove(message.getFrom());
					//刷新好友列表
					server.refreshFriend();
					//告诉客户端你可以退出了
					ObjectRW.writeObject(socket, message);
					//需要将线程停止
					break;//循环结束,run方法结束,线程停止
				}
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			
		}
		//循环结束后
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//关流的问题
		try {
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	
}

运行结果:
首先登录操作,这里就是在一个服务器内的用户都是好友:
在这里插入图片描述
再跑一个单聊功能:
现在让bc互相发消息:
在这里插入图片描述
再跑一个群聊功能:
在这里插入图片描述
而后程序退出保证不报错(这里也需要一些代码量的不要以为很简单):
比如退出c以后ab的好友列表就没有c了
在这里插入图片描述
但是假如退出之前两个人正在聊天呢?效果如下:首先ab正常聊天,接下来将b退出,再用a给b发消息
在这里插入图片描述
提示已经下线。本文完。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值