一.功能介绍
首先,要完成多人聊天的程序,我们要先明白其运行的基本机制是什么?
该图源自网上
当两个用户进行通讯时,是采用C/S模式进行的。也就是说,当两个客户端进行通讯时,并不止是两个用户之间建立了tcp链接,而是当客户端登录服务器时,服务器获取了客户端的Socket,并存储下来。当两个客户端想要通讯时,服务器拿出这两个客户端的socket后,再进行通讯。服务器的功能有点类似于中转站
因此,不难预见,该程序也要用到多线程的调度,以及数据库简单的增删改查操作。
此外,该程序还简单实现了文件的传输功能。
二、项目结构与程序简单的展示
二.(1)登录界面
二.(2)用户好友列表界面
该图展示了三个用户的界面,方便分析。
从左到右依次展示的是:
好友列表:用户添加的所有好友。
对等方列表:展示的是已经上线的对等方。
在线的好友:用户当前在线的好友。
可以得知的是,对等方列表是根据只要有一个对等方上线即把它加入列表,并不会判断新上线的对等方是不是自己的好友。这也是对等方列表与在线的好友的主要区别。
二.(3)聊天界面
一个用户可以同时与多个用户同时在线聊天
当在聊天窗口点击选择文件发送的按钮时,此时你即为发送方,跟你聊天的那个人为接收方。接收方的窗口会弹出一个是否接收文件的请求,
发送方弹出选择文件的窗口
发送方选择完后,接收方选择保存文件的路径
点击发送
上述打印的相同的数据是用来缓冲文件的字节数组,一个数组大小1KB(该文件大小7KB),上传于下载完毕后,发送方和接收方都会在控制台打印出相关信息。
二.(4)用户个人界面(其余功能)
该界面可以实现对好友的操作,群聊、聊天记录、画板等功能,本文主要讲述核心功能的实现,在此略过。
二、部分功能的代码实现
import java.io.*;
import java.net.*;
import pers.fjl.communication_system.Thread.ConnectThread;
import pers.fjl.communication_system.Thread.adminConnectThread;
import pers.fjl.communication_system.transfer_files.Download;
import pers.fjl.communication_system.transfer_files.Jdbc_downloadpath;
import pers.fjl.communication_system.utils.name;
public class server implements java.io.Serializable {
private static int i=0;
private static String DownloadPath=null;
public void Startserver() {
try {
System.out.println("服务器启动,端口是5228");
ServerSocket ss=new ServerSocket(5228);
while(true) {
//阻塞,等待连接
Socket s = ss.accept();
ObjectInputStream ois=null;
String downloadpath=Jdbc_downloadpath.getDownloadpath();
if(!downloadpath.equals("0")){
InputStream is=s.getInputStream(); //获取发送方的流
// ObjectInputStream is=new ObjectInputStream(s.getInputStream());
System.out.println(downloadpath);
FileOutputStream fos=new FileOutputStream(downloadpath,true);
System.out.println("Download2");
byte[] b=new byte[1024];
int len;
while ((len=is.read(b))!=-1){
fos.write(b,0,len);
}
System.out.println("数据下载完成");
Jdbc_downloadpath.deleteDownloadpath(downloadpath);
}else {
ois=new ObjectInputStream(s.getInputStream());
name n=(name)ois.readObject(); //强转从客户端获取对象o
// System.out.println("服务器启2收到id"+n.getId()+n.getPassword());
ConnectThread ct=new ConnectThread(s); //启动与该客户说话的线程
Download dl=new Download(s);
// dl.start();
adminConnectThread.addConnectThread(n.getId(),ct);//获取用户名和线程加入到哈希表
ct.start();
ct.tellalluser(n.getId());
}
}
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
服务器端
import java.io.IOException;
import java.io.*;
import java.net.*;
import pers.fjl.communication_system.Thread.CSThread;
import pers.fjl.communication_system.Thread.adminCSThread;
import pers.fjl.communication_system.udp.getHostIp;
import pers.fjl.communication_system.utils.name;
public class client implements java.io.Serializable{
public Socket s;
private String name;
private String password;
public client(Object o) { //采用对象流,从user_login类获取
try {
s=new Socket("127.0.0.1",5228);
new getHostIp();
ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream());
oos.writeObject(o);
CSThread cst=new CSThread(s);//创建该用户和服务器保持通讯的线程
cst.start();
adminCSThread.addCSThread(((name)o).getId(), cst); //得到o的用户名
} catch (UnknownHostException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
客户端
需要注意的是,当服务器和客户端启动时,都要启动管理socket的线程,以便管理服务器与客户端之间的连接,在此我使用哈希表管理线程。(用户名为键)
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import javax.swing.*;
import pers.fjl.communication_system.Thread.adminCSThread;
import pers.fjl.communication_system.Thread.adminfriendlist;
import pers.fjl.communication_system.udp.peer;
import org.springframework.jdbc.core.JdbcTemplate;
import pers.fjl.communication_system.client.client;
import pers.fjl.communication_system.utils.*;
public class user_login extends JFrame implements java.io.Serializable{ //登录界面,先执行user_login()方法
private static final long serialVersionUID=1L; //实现序列化不同版本的兼容性
protected String correctname;
protected String correctpassword; //声明两个变量,用于传给子类启动数据库
protected boolean flag;
public static boolean flag2; //用来传递是否从数据库查询成功的真值
public static boolean flag3; //用来传递是否从数据库查询成功的真值
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getCorrectname() {
return correctname;
}
public void setCorrectname(String correctname) {
this.correctname = correctname;
}
public String getCorrectpassword() {
return correctpassword;
}
public void setCorrectpassword(String correctpassword) {
this.correctpassword = correctpassword;
}
public user_login() {
setTitle("登录窗体"); //窗体标题
setBounds(800,400,300,150);
Container c=getContentPane(); //容器
c.setLayout(null);
JLabel jl1=new JLabel("用户名:"); //标签
jl1.setBounds(10, 10, 200, 18);
final JTextField name = new JTextField();
name.setBounds(80, 10, 150, 18);
JLabel jl2=new JLabel("密码:");
jl2.setBounds(10, 50, 200, 18);
final JPasswordField password=new JPasswordField(); //创建密码框对象
password.setBounds(80,50 , 150, 18); //设置位置大小
c.add(jl1);
c.add(name);
c.add(jl2);
c.add(password); //全部加到容器中
JButton jb=new JButton("确定");
jb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
user_login a=new user_login(); //创建对象
a.dispose(); //关掉因实例化a而多出来的窗口
a.setCorrectname(name.getText().trim());
System.out.println(a.getCorrectname());
char data[]=password.getPassword();
a.setCorrectpassword(new String(data)); //用户名和密码
new user_login(a.getCorrectname(),a.getCorrectpassword());
System.out.println(flag2);
if(name.getText().trim().length()==0
|| new String(password.getPassword()).trim().length()==0) { //除去空格后,长度为0的话
JOptionPane.showMessageDialog(null, "用户名或者密码不准为空");
return;
}
if (flag2 && !flag3) {
JOptionPane.showMessageDialog(null, "登录成功");
JdbcTemplate template=new JdbcTemplate(druid_utils.getDataSource());
String sql="update user set onlinestatus=1 where username=?";
template.update(sql,a.getCorrectname());//下线时状态为0,用户上线则把状态改变为1
correctname=name.getText().trim();
correctpassword=new String(password.getPassword());
close_login cl=new close_login(); //创建线程类的对象
try {
Thread.sleep(800); // 登录成功后,过0.8s关掉登录页面
cl.start();
} catch (InterruptedException e1) {
// TODO 自动生成的 catch 块
e1.printStackTrace();
}
} else if(flag2==false){
JOptionPane.showMessageDialog(null, "用户名或密码错误");
}else if(flag2 && flag3) {
JOptionPane.showMessageDialog(null, "该用户已在线,请勿重新登录");
}
}
});
jb.setBounds(80, 80, 60, 18);
c.add(jb);
final JButton button = new JButton();
button.setText("重置");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
// TODO 自动生成方法存根
name.setText("");
password.setText(""); //清空文本框实现重置操作
}
});
button.setBounds(150, 80, 60, 18);
getContentPane().add(button);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setVisible(true);
}
public user_login(String username,String password) {
// TODO 自动生成的方法存根
Connection conn=null;
ResultSet rs=null;
PreparedStatement pstmt=null;
Map<String, Object>map=null;
try {
conn=jdbc_utils.getConnection(); //调用工具类,加载驱动获取连接
String sql="select * from user where username= ? and password = ?"; //?来拼接
pstmt=conn.prepareStatement(sql);
pstmt.setString(1,username);
pstmt.setString(2, password); //给问号赋值
rs =pstmt.executeQuery(); //执行查询表中的用户名和密码
flag2=rs.next(); //如果有下一行,则返回true。表示如果能从数据库找到账号密码则,返回true,否则返回false
try{
JdbcTemplate template=new JdbcTemplate(druid_utils.getDataSource());
String sql2="select * from user where username=? and onlinestatus =1";
map=template.queryForMap(sql2,username);
if(map!=null) {
flag3=true;
}
}catch(Exception e) {
flag3=false; //搜索不到在线状态为1的该用户名,则会发生异常
}
}catch (SQLException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}finally { //释放资源
jdbc_utils.close(rs, pstmt, conn); // 调用工具类,关闭资源
}
}
public static void main(String[] args) {
new user_login();
}
class close_login extends Thread{
public void run(){
user_login.this.dispose(); //登录成功后,用来自动关掉登录的页面
name n=new name();
n.setId(correctname);
n.setPassword(correctpassword);
new client(n); //开启该客户端
try {
friendlist f=new friendlist(correctname); // 打开好友界面
adminfriendlist.addfriendlist(correctname, f);
ObjectOutputStream oos=new ObjectOutputStream
(adminCSThread.getCSThread(n.getId()).getS().getOutputStream());
Message m=new Message();
m.setMesType(MesType.MesType_onlinenum); //发好友的状态
m.setSender(correctname);//要的是刚登陆的用户的好友列表
oos.writeObject(m);
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}finally{
new peer(correctname); //会阻塞程序,放在最后
}
}
}}
登录验证的窗体以及方法
该方法代码有点冗余,看看就好。
import javax.swing.*;
import pers.fjl.communication_system.Thread.adminCSThread;
import pers.fjl.communication_system.Thread.adminchat;
import pers.fjl.communication_system.transfer_files.Transfer_file;
import pers.fjl.communication_system.transfer_files.Upload;
import pers.fjl.communication_system.chat_record.record;
import pers.fjl.communication_system.utils.Message;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class chat extends JFrame implements ActionListener,java.io.Serializable { //与好友聊天的界面
JTextArea jta;
JTextField jtf;
JButton jb1,jb2; //发送按钮和选择文件夹的按钮
JPanel jp;
String myname;
String friendname;
// public static void main(String[] args) {
// // TODO 自动生成的方法存根
// chat ct=new chat("s");
// }
public chat(String myname,String friend) {
this.myname=myname;
this.friendname=friend;
jta=new JTextArea();
jtf=new JTextField(30);
jb1=new JButton("发送");
jb1.addActionListener(this);
jb2=new JButton("选择文件发送");
jb2.addActionListener(this);
jp=new JPanel();
jp.add(jtf);
jp.add(jb1);
jp.add(jb2);
this.add(jta,"Center");
this.add(jp,"South");
this.setTitle("你("+myname+")正在和"+friend+" 聊天");
this.setBounds(800, 400, 550, 460);
this.setVisible(true);
}
public void download_or_not(String friendname){//接收方弹出是否下载文件的确认框
int isDelete = JOptionPane.showConfirmDialog(null, "请问是否接收?", friendname+"给你("+myname+")发来了文件", JOptionPane.YES_NO_CANCEL_OPTION);
if(isDelete == JOptionPane.YES_OPTION){
System.out.println("download or not我的名字"+myname+friendname);
Transfer_file t=new Transfer_file();
chat c= adminchat.getchat(myname+" "+friendname); //在friendlist的时候加入的哈希表的键值取出
Transfer_file a=new Transfer_file();
a.Sender_filepath(friendname,myname);
// c.upload_or_not(); //弹出是否发送的确认框
// t.Downloader_filepath(friendname,myname); //下载人而言,他的朋友就是最初的发送者
}
}
public void upload_or_not(String UploadPath,String Uploader){//发送方弹出是否发送文件的确认框
int isDelete = JOptionPane.showConfirmDialog(null, "是", "开始发送?", JOptionPane.YES_NO_CANCEL_OPTION);
if(isDelete == JOptionPane.YES_OPTION){
System.out.println("upload_or_not"+Uploader);
Transfer_file a=new Transfer_file();
new Upload(UploadPath,Uploader);
// a.Sender_filepath(String );
// new Upload();
}
}
public void showMessage(Message m) {
String info=" "+m.getSender()+"对你("+m.getReceiver()+")说:"+m.getInfo()+"\r\n";
String time=m.getSendtime();
this.jta.append(time+"\n"); //加到自己的文本域上显示
this.jta.append(info); //加到自己的文本域上显示
new record(m.getReceiver(),m.getSender(),info);//存储聊天记录
}
public void actionPerformed(ActionEvent e) {
// TODO 自动生成的方法存根
if(e.getSource()==jb1) {//如果点了发送
Date d = new Date(); //用来获取时间
SimpleDateFormat sdf = new SimpleDateFormat("yy年MM月dd日 E HH:mm:ss");
String msg=" 【你对"+friendname+"说】:"+jtf.getText();
new record(myname,friendname,msg);//存储聊天记录
this.jta.append(sdf.format(d)+"\n");
this.jta.append(msg+"\n");
Message m=new Message();
m.setSender(this.myname);
m.setReceiver(this.friendname);
m.setInfo(jtf.getText());
m.setMesType("3");//这是信息包
jtf.setText("");
m.setSendtime(sdf.format(d));
// m.setSendtime(new java.util.Date().toString());
System.out.println("这里是chat类"+m.getSender());
try { //通过类取得线程,通过线程取得Socket
ObjectOutputStream oos=new ObjectOutputStream
(adminCSThread.getCSThread(myname).getS().getOutputStream()); //这是客户端的Socket s产生的输出流,只有服务器端的输入流才能获取
oos.writeObject(m);
} catch (IOException e1) {
// TODO 自动生成的 catch 块
e1.printStackTrace();
}
}else if(e.getSource()==jb2){//选择文件发送
try {
Transfer_file a=new Transfer_file();
ObjectOutputStream oos=new ObjectOutputStream
(adminCSThread.getCSThread(myname).getS().getOutputStream()); //这里要修改的
Message m=new Message();
m.setSender(this.myname);
m.setReceiver(this.friendname);
m.setMesType("6"); //这是传输文件的包
oos.writeObject(m); //发送给服务器
// a.Sender_filepath(); //选择要发送的文件
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
public static void main(String[] args) {
// new chat("张三","李四");
}
}
chat聊天窗体以及里面的方法
下面是两个管理通讯的线程:
import java.net.*;
import pers.fjl.communication_system.Thread.adminchat;
import pers.fjl.communication_system.Thread.adminfriendlist;
import pers.fjl.communication_system.settings.chat;
import pers.fjl.communication_system.settings.friendlist;
import pers.fjl.communication_system.utils.MesType;
import pers.fjl.communication_system.utils.Message;
import java.io.*;
public class CSThread extends Thread {//客户端连接服务端的线程
private Socket s;
public CSThread(Socket s) {
this.s=s;
}
public Socket getS() {
return s;
}
public void setS(Socket s) {
this.s = s;
}
public void run() {
System.out.println("CSThread启动了");
while(true) {
//不停读取从服务端发来的消息
try {
// String downloadpath= Jdbc_downloadpath.getDownloadpath();
//如果不是对象流则不作任何处理
ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
Message m=(Message)ois.readObject();
if(m.getMesType().equals(MesType.MesType_returnnum)) {
System.out.println("CST读取从服务器发来的消息"+m.getInfo());
String info=m.getInfo(); //返回在线人的列表
String friends[]=info.split(" ");//拆分
String receiver=m.getReceiver();//刚才的发送者要接收传回去的好友列表
friendlist fr= adminfriendlist.getfriendlist(receiver);//返回一个好友列表
if(fr!=null) {
//代表这是返回好友列表的,里面的包并不是用来传递聊天信息
}
}else if(m.getMesType().equals(MesType.MesType_transfer_file)){
System.out.println("接收方收到了发送方发送文件的请求");
chat c= adminchat.getchat(m.getReceiver()+" "+m.getSender()); //在friendlist的时候加入的哈希表的键值取出
c.download_or_not(m.getSender()); //弹出是否下载的确认框
}
else {//好友列表是空的,代表里面的包是用来传递客户端间的聊天讯息
chat c= adminchat.getchat(m.getReceiver()+" "+m.getSender()); //在friendlist的时候加入的哈希表的键值取出
c.showMessage(m);
}
} catch (Exception e) {
// TODO 自动生成的 catch 块
// e.printStackTrace();
// System.out.println("CSThread出现异常");
}
}
}
}
管理客户端连接服务器的线程
import java.net.*;
import java.util.*;
import pers.fjl.communication_system.utils.MesType;
import pers.fjl.communication_system.utils.Message;
import java.io.*;
public class ConnectThread extends Thread implements java.io.Serializable{//服务器和某个客户端的通讯线程
Socket s;
public ConnectThread(Socket s) {//给线程连接通道,获取客户端的s
this.s=s;
}
public void tellalluser(String own) {//另外一个用户登录后,刷新上线的表,告诉以前的人上线请求
HashMap hm=adminConnectThread.hm;
Iterator it=hm.keySet().iterator();
while(it.hasNext()) {//取出在线人的列表
Message m=new Message();
m.setInfo(own);
m.setMesType(MesType.MesType_returnnum);
String onlineuser=it.next().toString();
try {
ObjectOutputStream oos=new ObjectOutputStream(adminConnectThread.getConnectThread(onlineuser).s.getOutputStream());
m.setReceiver(onlineuser);//把包给已经上线的人
oos.writeObject(m);
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
public void run() {
System.out.println("线程运行了");
while(true) {
try {
ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
Message m=(Message)ois.readObject(); //chat那里,服务器拿到的
if(m.getMesType().equals(MesType.MesType_onlinenum)){//把服务器上线的人给客户返回
String res=adminConnectThread.getonlineuser();
Message m2=new Message();
m2.setMesType(MesType.MesType_returnnum);
m2.setInfo(res);
m2.setReceiver(m.getSender());//最初的发送者此时变为接受者接回服务器发回给其的好友信息
ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream());//s是接收信息的人与服务器的连接
oos.writeObject(m2);
}else if (m.getMesType().equals(MesType.MesType_transfer_file)){//传输文件的
System.out.println("ConnectThread 收到发送方"+m.getSender()+"给"+m.getReceiver()+"的消息");
Message m3=new Message();
m3.setMesType(MesType.MesType_transfer_file);
m3.setSender(m.getSender());
m3.setReceiver(m.getReceiver()); //
//应该输出把流输出给接收方
// ObjectOutputStream oos=new ObjectOutputStream
// (adminCSThread.getCSThread(m.getReceiver()).getS().getOutputStream());
ConnectThread ct=adminConnectThread.getConnectThread(m.getReceiver());//找到接收人的通讯线程,并给她
ObjectOutputStream oos=new ObjectOutputStream(ct.s.getOutputStream());//s是接收信息的人与服务器的连接
// ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream());//s是接收信息的人与服务器的连接
oos.writeObject(m3);
}
else{
ConnectThread ct=adminConnectThread.getConnectThread(m.getReceiver());//找到接收人的通讯线程,并给她
ObjectOutputStream oos=new ObjectOutputStream(ct.s.getOutputStream());//s是接收信息的人与服务器的连接
oos.writeObject(m);//给接收人的包
}
//
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
服务器连接客户端的线程