赞
踩
期末的课程设计,做的并不够好,写在这里记录一下自己的成长,也欢迎大家给出宝贵意见
下面程序只能在局域网内通信,要想跨网通信需要有公有IP,文末有介绍。
先放效果图:
# 主要知识点
网络编程 GUI(图形用户界面) 多线程
#实现功能
客户端:注册 登录 群聊 私聊
服务器端:显示用户上线/下线 登录日志及聊天信息的保存及查看
主要思路:
下面附上源码(代码写的很冗杂,变量名很迷,本来想重构一下,可是我懒/捂脸)
客户端 主类
package chat;
public class Client {
public static void main(String[] args) {
LogIn l = new LogIn();
l.log();
}
}
客户端 LogIn类(实现注册登录)
package chat; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.awt.*; import javax.swing.*; public class LogIn extends JFrame implements ActionListener{ Socket client; ServerSocket server; InputStream in; OutputStream out; //为了成为全局变量 String si = new String(); //id String sp = new String(); //password String name = new String(); //昵称 JTextField [] t = { new JTextField(15), new JTextField(15) }; //账号和密码的输入行 JTextField [] rt = { new JTextField(15), new JTextField(15), new JTextField(15) }; //注册界面的输入行 JButton nb = new JButton("新用户"); JButton lb = new JButton("登 录"); JButton rs = new JButton("确 定"); JButton rr = new JButton("返 回"); LogIn f; //登录窗口 LogIn rf; //注册窗口 public LogIn (String str) { super(str); } public LogIn() {}; //显示登录界面,提示用户输入账号密码 public void log() { f = new LogIn("聊天室"); //标题 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //设置为可关闭 f.setSize(260,260); //大小 f.setLocation(520,300); //窗口居中 //放置两个单行文本框,用来输入账号密码 Container c = f.getContentPane(); c.setLayout(new FlowLayout()); c.setBackground(Color.orange); //背景色 c.add(new JLabel("用户名:")); c.add(t[0]); c.add(new JLabel("密 码:")); c.add(t[1]); //添加新用户和登录按钮 c.add(nb); c.add(lb); f.setVisible(true); //设置为可见 lb.addActionListener(this); //登录按钮监听 nb.addActionListener(this); //新用户按钮监听 } //监听方法的实现 public void actionPerformed (ActionEvent e) { if(e.getSource()==lb) //登录 { si = t[0].getText(); sp = t[1].getText(); if(si.equals("") || sp.equals("")) JOptionPane.showMessageDialog(null, "用户名或密码不能为空", "提示", JOptionPane.INFORMATION_MESSAGE); else { UserIn u = new UserIn(si,sp); try { client = new Socket("127.0.0.1",6666); //与本机6666端口建立套接 in = client.getInputStream(); out = client.getOutputStream(); byte [] i = si.getBytes(); //id byte [] space = {'/'}; //分隔符 byte [] p = sp.getBytes(); //密码 byte [] buf = new byte[i.length+p.length+1]; System.arraycopy(i, 0, buf, 0, i.length); System.arraycopy(space, 0, buf, i.length, 1); System.arraycopy(p, 0, buf, i.length+1, p.length); out.write(buf); //把账号密码发给服务器判断 byte [] check = new byte[100]; in.read(check); //接受服务器返回的结构 String che = new String(check); String c1 = new String("1"); if(che.substring(0, 1).equals(c1)) //若返回1加用户昵称 { u.name = che.substring(1,che.length()); f.dispose();; //删除登录窗口 Chat c = new Chat(u,client); //开始聊天 } else //否则提示账号密码错误 JOptionPane.showMessageDialog(null, "用户名或密码错误", "提示", JOptionPane.INFORMATION_MESSAGE); }catch(IOException e1) { JOptionPane.showMessageDialog(null, "服务器故障,登录失败", "提示", JOptionPane.ERROR_MESSAGE); } } } else if(e.getSource()==nb) //新用户 { f.setVisible(false); register(); } else if(e.getSource()==rs) //注册界面的确定 { si = rt[0].getText(); sp = rt[1].getText(); name = rt[2].getText(); if(si.equals("") || sp.equals("")) JOptionPane.showMessageDialog(null, "用户名或密码不能为空", "提示", JOptionPane.INFORMATION_MESSAGE); else { try { client = new Socket("111.231.111.50",6666); //与本机6666端口建立套接 in = client.getInputStream(); out = client.getOutputStream(); byte [] tempsi = si.getBytes(); //id byte [] space1 = {'/'}; //分隔符 byte [] tempsp = sp.getBytes(); //密码 byte [] tempname = name.getBytes(); //昵称 byte [] tempbuf = new byte[tempsi.length+tempsp.length+tempname.length+3]; System.arraycopy(space1, 0, tempbuf, 0, 1); System.arraycopy(tempsi, 0, tempbuf, 1, tempsi.length); System.arraycopy(space1, 0, tempbuf, tempsi.length+1, 1); System.arraycopy(tempsp, 0, tempbuf, tempsi.length+2, tempsp.length); System.arraycopy(space1, 0, tempbuf, tempsi.length+tempsp.length+2, 1); System.arraycopy(tempname, 0, tempbuf, tempsi.length+tempsp.length+3, tempname.length); out.write(tempbuf); //把账号密码发给服务器判断 byte [] check = new byte[100]; in.read(check); //接受服务器返回的结构 String che = new String(check); String c1 = new String("1"); if(che.substring(0, 1).equals(c1)) //若返回1加用户昵称 { client.close(); rf.dispose(); f.setVisible(true); JOptionPane.showMessageDialog(null, "注册成功,请登录", "提示", JOptionPane.INFORMATION_MESSAGE); } else { client.close(); JOptionPane.showMessageDialog(null, "该账号已被占用!", "提示", JOptionPane.INFORMATION_MESSAGE); } }catch(IOException e1) { JOptionPane.showMessageDialog(null, "服务器异常,无法注册", "提示", JOptionPane.ERROR_MESSAGE); } } } else if (e.getSource()==rr) //返回按钮 { rf.setVisible(false); f.setVisible(true); } } //注册方法 public void register() { rf = new LogIn("注册"); rf.setVisible(true); rf.setSize(260, 300); rf.setLocation(520, 250); rf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Container c = rf.getContentPane(); c.setLayout(new FlowLayout()); c.setBackground(Color.pink); //背景色 c.add(new JLabel("用户名:")); c.add(rt[0]); c.add(new JLabel("密 码:")); c.add(rt[1]); c.add(new JLabel("昵 称:")); c.add(rt[2]); c.add(rs); c.add(rr); rs.addActionListener(this); //确定注册按钮监听 rr.addActionListener(this); //返回按钮监听 } } //用户信息类 class UserIn { String id; String passWord; String name; UserIn(String i, String p){ id = i; passWord = p; } UserIn(String i, String p,String n){ id = i; passWord = p; name = n; } UserIn(){} }
客户端 Chat类(实现聊天)
package chat; import java.awt.Container; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.JTextField; import server.UserInfo; public class Chat extends JFrame implements ActionListener { UserIn u = new UserIn(); //登录者 JButton sent = new JButton("发送"); //发送按钮 static JComboBox<String> selecttf = new JComboBox<String>();//选择框 JTextField tf = new JTextField(20); //输入框 static JTextArea ta = new JTextArea(); //聊天记录框 JScrollPane text = new JScrollPane(ta); //可滚动 JPanel panelh = new JPanel(); //面板对象上 JPanel paneld = new JPanel(); //面板对象下 Socket client; InputStream in; OutputStream out; public Chat() {}; public Chat(UserIn u,Socket client) { super("客户端"); this.u = u; this.client = client; setSize(350,530); //窗口大小 setLocation(520,200); //窗口位置 Container c = this.getContentPane(); JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,panelh,paneld); sp.setDividerSize(5); panelh.setLayout(null); panelh.add(text); text.setSize(340,425); paneld.setLayout(new FlowLayout()); paneld.add(tf); paneld.add(sent); paneld.add(new JLabel(" 发送给 ")); paneld.add(selecttf); selecttf.addItem("所有人"); c.add(sp); sent.addActionListener(this); //对发送按钮监听 ta.setEditable(false); //聊天记录不可编辑 setVisible(true); sp.setDividerLocation(0.8); //上下比例9:1 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //可关闭 try { ta.append("成功连接到服务器"+'\n'); in = client.getInputStream(); out = client.getOutputStream(); }catch(IOException e1) { JOptionPane.showMessageDialog(null, "连接服务器失败", "提示", JOptionPane.ERROR_MESSAGE); } //收消息线程开始 Receive receive = new Receive(client); Thread r = new Thread(receive); r.start(); } //发消息监听方法实现 public void actionPerformed (ActionEvent e) { String s = tf.getText(); tf.setText(""); try { String sele = (String)selecttf.getSelectedItem(); int he = selecttf.getSelectedIndex(); String hea = String.valueOf(he); String na = u.name; String me = s; String to = " -> "; String sp = " : "; String mess = hea+na+to+sele+sp+me; byte [] buf = mess.getBytes(); out.write(buf); if(he != 0) Chat.ta.append(mess.substring(1,mess.length())+'\n'); }catch(IOException e2) { e2.printStackTrace(); } } } class Receive extends Thread{ InputStream in; OutputStream out; public Receive(Socket client) { try { in = client.getInputStream(); out = client.getOutputStream(); } catch (IOException e) { e.printStackTrace(); } } //收消息线程方法实现 public void run (){ while(true) { try { byte [] buff = new byte[512]; //缓存数组,一次最多512个字节 in.read(buff); String str = new String(buff); str = str.trim(); if(str.charAt(0) == '5') //5是收到正常的消息 Chat.ta.append(str.substring(1, str.length())+'\n'); else if(str.charAt(0) == '9') { //9更新在线用户 //System.out.println(str); Chat.selecttf.removeAllItems(); Chat.selecttf.addItem("所有人"); str = str.substring(1, str.length()); int i = 0 ,j = 0; while(j < str.length()-1) { while(str.charAt(j) != '|') //从'|'处断开 j++; Chat.selecttf.addItem(str.substring(i, j)); i = j+1; j++; } } } catch (IOException e) { e.printStackTrace(); } } } }
服务器端 Server类(转发消息)
package server; import java.awt.Container; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextArea; public class Server extends JFrame implements ActionListener{ public static List<Socket> list = new ArrayList<Socket>(); //保存socket的集合 public static List<UserInfo> userlist = new ArrayList<UserInfo>();//保存在线用户 static JTextArea ta = new JTextArea(); //聊天记录框 JScrollPane text = new JScrollPane(ta); //可滚动 JPanel panel = new JPanel(); //面板对象 JPanel panel1 = new JPanel(); //用来放置按钮 JButton jb1 = new JButton ("登录日志"); JButton jb2 = new JButton ("聊天日志"); ServerSocket server; Socket client; InputStream in; OutputStream out; public Server() { super("服务器"); setSize(300,400); //窗口大小 setLocation(520,250); //窗口位置 Container c = getContentPane(); JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,panel,panel1); //分割界面用来放按钮 sp.setDividerSize(5); panel.setLayout(null); panel.add(text); //窗体上添加聊天记录文本框 panel.setSize(300,400); //面板大小 panel.setLocation(0, 0); //位置 text.setSize(300,360); //JButton jb1 = new JButton ("登录日志"); //JButton jb2 = new JButton ("聊天日志"); panel1.setLayout(new FlowLayout()); panel1.add(jb1); panel1.add(jb2); jb1.addActionListener(this); jb2.addActionListener(this); c.add(sp); //窗体添加面板(输入框) ta.setVisible(true);; //聊天记录可见 ta.setEditable(false); //聊天记录不可编辑 setVisible(true); sp.setDividerLocation(0.9); //上下9:1 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //可关闭 try { server = new ServerSocket(6666); ta.append("服务器已启动"+'\n'); while(true) { try { client = server.accept(); //等待连接 in = client.getInputStream(); out = client.getOutputStream(); byte [] buff = new byte[100]; //缓存数组,一次最多512个字节 in.read(buff); //接收消息 String str = new String(buff); str = str.trim(); //去掉多余的空格 if(str.substring(0,1).equals("/")) //!!用户注册!! { int i = 1,j = str.length()-1; while(str.charAt(i) != '/') //从'/'处断开 i++; //分成三部分 while(str.charAt(j) != '/') //账号、密码、昵称 j--; String id = str.substring(1, i); String pa = str.substring(i+1, j); String na = str.substring(j+1, str.length()); UserInfo u = new UserInfo(); UserInfo user = new UserInfo(id,pa,na); if(!u.checkid(user)) //注册成功 { u.save(user); //保存该用户信息 byte [] re = {'1'}; //返回给客户端1 out.write(re); } else //注册失败 { byte [] re = {'0'}; //返回给客户端0 out.write(re); } } else //!!!用户登录!!! { int item = str.indexOf("/"); String id = str.substring(0,item); String pa = str.substring(item+1,str.length()); UserInfo u = new UserInfo(); UserInfo user = new UserInfo(id,pa); if(u.check(user)) //判断合法 { Iterator it = list.iterator(); int itera = 0; while(it.hasNext()) { it.next(); itera++; } if(itera <= 10) { //限制当前在线用户人数5人 byte [] b1 = {'1'}; byte [] b2 = user.name.getBytes(); byte [] b3 = new byte[1+b2.length]; System.arraycopy(b1, 0, b3, 0, 1); System.arraycopy(b2, 0, b3, 1, b2.length); out.write(b3); //返回1加该用户昵称 Server.list.add(client); Server.userlist.add(user); u.saveLog(user, client.getInetAddress().toString()); //用户登录信息的保存 ta.append(user.name+"上线了"+'\n'); new Sent(client, user).start(); //启动对该用户的转发消息线程 } } else //判断不合法 { byte [] b4 = {'0'}; out.write(b4); //返回0(实际上客户端并不对0进行判断) } } }catch(IOException e) { } } }catch(IOException e) { } } //两个查看日志的按钮监听方法实现 public void actionPerformed (ActionEvent e) { JTextArea rzta = new JTextArea(); //聊天记录框 JScrollPane rztext = new JScrollPane(rzta); //可滚动 rzta.setVisible(true); rzta.setEditable(false); JFrame rz = new JFrame ("日志信息"); rz.setSize(300,400); rz.setLocation(520,250); JPanel rzp = new JPanel(); rzp.setSize(300,400); rzp.setVisible(true); rzp.setLayout(null); rzp.add(rztext); //窗体上添加聊天记录文本框 rzp.setLocation(0, 0); //位置 rztext.setSize(300,365); rz.add(rzp); rz.setVisible(true); try { FileReader fr; if(e.getSource() == jb1) fr = new FileReader("D:\\java-photon\\uesrlog.txt"); else fr = new FileReader("D:\\java-photon\\uesrmessage.txt"); BufferedReader br = new BufferedReader(fr); String str; while (br.ready()) { //从文件中读取已有的账户 str = br.readLine(); rzta.append(str+'\n'); } br.close(); //关闭字符流 } catch (IOException e1) { JOptionPane.showMessageDialog(null, "找不到指定文件", "错误", JOptionPane.ERROR_MESSAGE); } } //服务器程序开始运行 public static void main(String []args) { Server s = new Server(); } } //转发消息线程 class Sent extends Thread{ InputStream in; OutputStream out; Socket client; UserInfo user; public Sent(Socket client,UserInfo user) { super(); this.client = client; this.user = user; } public void run() { try { String strin = ""; for(UserInfo us:Server.userlist) strin = strin+us.name+"|"; byte [] head = {'9'}; byte [] nam = strin.getBytes(); byte [] update = new byte[nam.length+1]; System.arraycopy(head, 0, update, 0, 1); System.arraycopy(nam, 0, update, 1, nam.length); for(Socket s:Server.list) { out = s.getOutputStream(); out.write(update); } while(true) { in = client.getInputStream(); byte [] buff = new byte[512]; //缓存数组,一次最多512个字节 in.read(buff); String str = new String(buff); str = str.trim(); user.saveMessage(str.substring(1, str.length())); byte [] head1 = {'5'}; byte [] mess = str.substring(1, str.length()).getBytes(); System.arraycopy(head1, 0, buff, 0, 1); System.arraycopy(mess, 0, buff, 1, mess.length); if(str.charAt(0) == '0') for(Socket s:Server.list) { out = s.getOutputStream(); out.write(buff); } else { int i = str.charAt(0); Socket s2 = Server.list.get(i-49); out = s2.getOutputStream(); out.write(buff); } } } catch (IOException e) { Server.ta.append(user.name+"离开了\n"); Server.list.remove(client); Server.userlist.remove(user); String strin1 = ""; for(UserInfo us:Server.userlist) strin1 = strin1 + us.name+"|"; byte [] head2 = {'9'}; byte [] nam1 = strin1.getBytes(); byte [] update1 = new byte[nam1.length+1]; System.arraycopy(head2, 0, update1, 0, 1); System.arraycopy(nam1, 0, update1, 1, nam1.length); try { for(Socket s1:Server.list) { out = s1.getOutputStream(); out.write(update1); } }catch(IOException e2) { } } } }
服务器端 UserInfo类(保存用户信息及日志)
package server; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Calendar; import java.util.Date; import java.util.Vector; import javax.swing.JOptionPane; public class UserInfo { String id; String passWord; String name; Vector<UserInfo> v = new Vector<UserInfo>(); Calendar time; UserInfo(String i, String p){ id = i; passWord = p; } UserInfo(String i, String p,String n){ id = i; passWord = p; name = n; } public UserInfo(){} //判断用户名及对应密码是否存在 public boolean check(UserInfo user) { try { FileReader fr = new FileReader("D:\\java-photon\\user.txt"); BufferedReader br = new BufferedReader(fr); String str; while (br.ready()) { //从文件中读取已有的账户 str = br.readLine(); String []s = str.split(" "); //按照空格分成账号、密码、昵称三部分 UserInfo u = new UserInfo(s[0],s[1],s[2]); v.add(u); } br.close(); //关闭字符流 } catch (IOException e) { e.printStackTrace(); } for(int i=0;i<v.size();i++) { if(user.id.equals(v.get(i).id) && user.passWord.equals(v.get(i).passWord)) { user.name = v.get(i).name; return true; } } return false; } //判断用户名是否重复 public boolean checkid (UserInfo user) { try { FileReader fr = new FileReader("D:\\java-photon\\user.txt"); BufferedReader br = new BufferedReader(fr); String str; while (br.ready()) { //从文件中读取已有的账户 str = br.readLine(); String []s = str.split(" "); //分成三部分 UserInfo u = new UserInfo(s[0],s[1],s[2]); v.add(u); } br.close(); //关闭字符流 } catch (IOException e) { e.printStackTrace(); } for(int i=0;i<v.size();i++) { if(user.id.equals(v.get(i).id)) //判断用户名是否重复 { user.name = v.get(i).name; return true; } } return false; } //保存新用户信息 public void save (UserInfo user) { v.add(user); try { FileWriter fr = new FileWriter("D:\\java-photon\\user.txt"); BufferedWriter br = new BufferedWriter(fr); for(int i=0;i<v.size();i++) { br.write(v.get(i).id+' '+v.get(i).passWord+' '+v.get(i).name); br.newLine(); } br.close(); }catch(IOException e) { JOptionPane.showMessageDialog(null, "用户信息保存失败!", "提示", JOptionPane.ERROR_MESSAGE); } } //用户登录日志 public void saveLog (UserInfo user,String ip) { try { Calendar c = Calendar.getInstance(); String s = String.format("%1$ty-%1$tm-%1$td", c); FileWriter fr = new FileWriter("D:\\java-photon\\uesrlog.txt",true); //在文件末尾追加写入 BufferedWriter br = new BufferedWriter(fr); br.write("登录"+" "+ip+" "+user.name+" "+user.id+" "+s); br.newLine(); br.close(); }catch(IOException e) { JOptionPane.showMessageDialog(null, "用户登录日志保存失败!", "提示", JOptionPane.ERROR_MESSAGE); } } //保存用户发送的信息 public void saveMessage (String message ) { try { Calendar c = Calendar.getInstance(); String s = String.format("%1$ty-%1$tm-%1$td", c); FileWriter fr = new FileWriter("D:\\java-photon\\uesrmessage.txt",true); //在文件末尾追加写入 BufferedWriter br = new BufferedWriter(fr); br.write(message+" "+s); br.newLine(); br.close(); }catch(IOException e) { JOptionPane.showMessageDialog(null, "用户消息保存失败!", "提示", JOptionPane.ERROR_MESSAGE); } } }
以上为全部内容
因为ip地址的原因,这只能在同一个局域网下实现通信。但是我想让它实现全网通信,于是我以学生价租了一个月的腾讯云服务器,获得了一个公有IP,并将服务器端程序搬到云服务器上,客户端与云服务器套接(IP改成云服务器的IP就行),最后成功的实现了和室友(那时是寒假,我们在不同省)跨省通信!哈哈哈,当时心情还是有点激动的。
整个实现过程我不再赘述,感兴趣的可以自己百度,下面附上几点我在做的过程中遇到的坑。
宝塔面板
宝塔面板可以对云服务器的磁盘文件进行可视化操作,还可以可视化安装各种常用软件,很直观、方便,对新手很友好。可我在安装的时候(通过putty连接,命令行操作)一直失败,最后通过重装带有宝塔面板的系统(腾讯云官网),感觉是成功了,可是用微软edge访问面板时显示无法访问。 遂换用qq浏览器,成功了。
云服务器端口
把java源文件上传到云服务器,然后也编译成功并运行了,可客户端就是无法连接到服务器。检查了安全组也放通了,心态差点要崩,最后百度发现云服务器还有一层防火墙,遂关闭防火墙,成功连接!
中文乱码
java的一个字符(char)是两个字节,包括中文,按说不应该乱码,而且我在自己电脑上运行也没有乱码,为什么把服务器端程序放到云服务器上就乱码了呢(英文正常,中文乱码),百度后发现windows使用GBK编码,而云服务器的centos系统(linux的一种)并不是GBK,因此造成中文乱码。所以服务器程序强制使用GBK就好了。
byte[] array = str.getBytes("GBK");
Striing newstr = new String(array,"GBK");
实现跨局域网通信客户端只用改套接字的IP地址就行了,但服务器端程序做了一些改动(去除图形用户界面,把两个源文件合并成一个),如果有人想要的话我再发。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。