学完了socket通讯后,在老师的要求下,写了一个仿qq的聊天程序:
最终调试程序结果如下图: 有bug希望提出来,我们一起解决。
设计思路:
在服务器端 用一个HashMap<userName,socket> 维护所有用户相关的信息,从而能够保证和所有的用户进行通讯。
客户端的动作:(1)连接(登录):发送userName 服务器的对应动作:1)界面显示,2)通知其他用户关于你登录的信息, 3)把其他在线用户的userName通知当前用户 4)开启一个线程专门为当前线程服务
(2)退出(注销):
(3)发送消息
客户端向服务器发的消息格式设计:
命令关键字 @# 接收方 @# 消息内容 @# 发送方
1)连接:userName ----握手的线程serverSocket专门接收该消息,其它的由服务器新开的与客户进行通讯的socket来接收
2)退出:exit @# 全部 @# null@# userName
3)发送: on @# JList.getSelectedValue() @# tfdMsg.getText() @# tfdUserName.getText()
服务器向客户端发的消息格式设计:
命令关键字 @# 发送方 @# 消息内容
登录:
1) msg @# server @# 用户[userName]登录了 (给客户端显示用的)
2) cmdAdd @# server @# userName (给客户端维护在线用户列表用的)
退出:
1) msg @# server @# 用户[userName]退出了 (给客户端显示用的)
2) cmdRed@#server @# userName (给客户端维护在线用户列表用的)
发送:
msg @# 消息发送者 @# 消息内容
代码实现:
客户端:
package cn.hncu.net.sina;import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
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.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.border.TitledBorder;public class ClientFrom extends JFrame implements ActionListener{private static String ip="127.0.0.1";private static int port=8080;private JTextField tfdUserName=new JTextField(10); //用户标识private JTextArea allMsg=new JTextArea(); //聊天信息显示private JTextField tfdMsg=new JTextField(10);//发送消息消息框private JButton btnSend; //发送消息按钮private JButton btnCon;//在线用户列表private DefaultListModel<String> dataModel=new DefaultListModel<String>();private JList<String> list=new JList<String>(dataModel);public ClientFrom() {setBounds(300,300,400,300);addMenuBar(); //添加菜单上方面板/JPanel northPanel=new JPanel();northPanel.add(new JLabel("用户名称"));tfdUserName.setText("");northPanel.add(tfdUserName);btnCon=new JButton("连接");btnCon.setActionCommand("c");JButton btnExit=new JButton("退出");btnExit.setActionCommand("exit");northPanel.add(btnCon);northPanel.add(btnExit);getContentPane().add(northPanel,BorderLayout.NORTH); //放在上方//中间面板JPanel centerPanel=new JPanel(new BorderLayout());//中allMsg=new JTextArea();allMsg.setEditable(false);allMsg.setForeground(Color.blue);allMsg.setFont(new Font("幼圆", Font.BOLD, 14));centerPanel.add(new JScrollPane(allMsg));//东dataModel.addElement("全部");list.setSelectedIndex(0); //设置默认选择位置list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); //设置只能单选list.setVisibleRowCount(5); //设置显示的行数list.setFont(new Font("幼圆", Font.BOLD, 12));JScrollPane scroll=new JScrollPane(list); //为list添加滚动条scroll.setBorder(new TitledBorder("在线")); //Border的实现类TitileBorderscroll.setPreferredSize(new Dimension(70, allMsg.getHeight())); //设置滚动条的首选大小centerPanel.add(scroll,BorderLayout.EAST);//南JPanel southPanel=new JPanel();southPanel.add(new JLabel("消息"));southPanel.add(tfdMsg);btnSend=new JButton("发送");btnSend.setActionCommand("send");btnSend.setEnabled(false);southPanel.add(btnSend);centerPanel.add(southPanel,BorderLayout.SOUTH);//把中间面板加到框架中getContentPane().add(centerPanel);//事件监听btnCon.addActionListener(this);btnExit.addActionListener(this);btnSend.addActionListener(this);addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){int result = JOptionPane.showConfirmDialog(ClientFrom.this, "你还没登录,是否退出");if(result==JOptionPane.YES_OPTION){System.exit(0);}else{return;}}System.out.println(tfdUserName.getText()+"退出");sendExitMsg();System.exit(0);}});setVisible(true);}private void addMenuBar() {JMenuBar menuBar=new JMenuBar();setJMenuBar(menuBar);JMenu menu=new JMenu("选项");menuBar.add(menu);JMenuItem itemSet=new JMenuItem("设置");JMenuItem itemHelp=new JMenuItem("帮助");itemSet.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {final JDialog setDlg=new JDialog(ClientFrom.this);setDlg.setBounds(ClientFrom.this.getX(), ClientFrom.this.getY(), 250, 100);setDlg.setLayout(new FlowLayout());setDlg.add(new JLabel("服务器:"));final JTextField tfdIP=new JTextField(10);tfdIP.setText(ip);setDlg.add(tfdIP);setDlg.add(new JLabel("端口:"));final JTextField tfdPort=new JTextField(10);tfdPort.setText(port+"");setDlg.add(tfdPort);JButton btnSet=new JButton("设置");btnSet.setActionCommand("set");JButton btnCanel=new JButton("取消");btnCanel.setActionCommand("canel");setDlg.add(btnSet);setDlg.add(btnCanel);btnSet.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {if("set".equals(e.getActionCommand())){if(tfdIP.getText()!=null && tfdIP.getText().trim().length()>0){ClientFrom.this.ip=tfdIP.getText();}if(tfdPort.getText()!=null && tfdPort.getText().trim().length()>0){try {ClientFrom.this.port=Integer.parseInt(tfdPort.getText());} catch (NumberFormatException e1) {JOptionPane.showMessageDialog(setDlg, "端口号格式输入错误,请输入数字");}}btnCon.setEnabled(true);tfdUserName.setEditable(true);if(client!=null){//如果前面已经登录着用户,就把用户退出String msg="exit@#全部@#null@#"+tfdUserName.getText();pw.println(msg);dataModel.removeElement(tfdUserName.getText());list.validate();tfdUserName.setText("");}setDlg.dispose();}else if("canel".equals(e.getActionCommand())){return;}}});setDlg.setVisible(true);}});itemHelp.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {JDialog helpDlg = new JDialog(ClientFrom.this);helpDlg.setBounds(ClientFrom.this.getX()+10, ClientFrom.this.getY(), 300, 100);JLabel str = new JLabel("版权所有@dragon_Dai.QQ:794530831");helpDlg.add(str);helpDlg.setVisible(true);}});menu.add(itemSet);menu.add(itemHelp);}@Overridepublic void actionPerformed(ActionEvent e) {if("c".equals(e.getActionCommand())){System.out.println(tfdUserName.getText());if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){JOptionPane.showMessageDialog(this, "用户名不能为空");return;}System.out.println(tfdUserName.getText()+":连接ing...");connecting();}else if("exit".equals(e.getActionCommand())){if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){int result = JOptionPane.showConfirmDialog(this, "你还没登录,是否退出");if(result==JOptionPane.YES_OPTION){System.exit(0);}else{return;}}System.out.println(tfdUserName.getText()+"退出");sendExitMsg();}else if("send".equals(e.getActionCommand())){if(tfdMsg.getText()==null){JOptionPane.showMessageDialog(this, "发送消息不能为空");return;}String msg="on@#"+list.getSelectedValue()+"@#"+tfdMsg.getText()+"@#"+tfdUserName.getText();pw.println(msg);}}private Socket client;private PrintWriter pw;private void connecting() {//与服务器建立连接,把userName传给服务器try {client=new Socket(ip,port);//发送用户名给服务器btnCon.setEnabled(false); //连接成功后关掉连接按钮String userName=tfdUserName.getText().trim();pw=new PrintWriter(client.getOutputStream(),true);pw.println(userName);//连接之后,设置标题为userName在线setTitle(userName+"在线");btnSend.setEnabled(true); //打开发送按钮tfdUserName.setEditable(false); //用户名不能再修改//开一个线程单独用于跟服务器通信new ClientThread(client).start();} catch (UnknownHostException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}private void sendExitMsg() {//与服务器建立连接,把userName传给服务器try {client=new Socket(ip, port);String msg="exit@#全部@#null@#"+tfdUserName.getText();pw.println(msg);System.exit(0);} catch (UnknownHostException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}class ClientThread extends Thread{private Socket client;public ClientThread(Socket client) {this.client=client;}@Overridepublic void run() {//接收服务器返回的信息try {Scanner sc=new Scanner(client.getInputStream());while(sc.hasNext()){String msg=sc.nextLine();String msgs[]=msg.split("@#");if(msgs==null || msgs.length!=3){System.out.println("通讯异常");return;}if("msg".equals(msgs[0])){//表示该信息是用来显示用的if("server".equals(msgs[1])){//表示该信息是系统信息msg="系统信息:"+msgs[2];allMsg.append(msg+"\r\n");}else{//表示该信息聊天信息msg=msgs[1]+msgs[2];allMsg.append(msg+"\r\n");}}else if("cmdAdd".equals(msgs[0])){//表示该消息是用来更新用户在线列表的,添加用户dataModel.addElement(msgs[2]);}else if("cmdRed".equals(msgs[0])){//表示该消息是用来更新用户在线列表的,移除用户dataModel.removeElement(msgs[2]);}list.validate(); //需要刷新list,不然可能出现list更新失败的bug}} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {JFrame.setDefaultLookAndFeelDecorated(true);new ClientFrom();}
}
服务器:
package cn.hncu.net.sina;import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.TitledBorder;public class ServerFrom extends JFrame{private JTextArea area;//在线的用户信息显示private DefaultListModel<String> dataModel; //在线的用户列表显示//注册的用户名不能相同//用于存储所有的用户,这里采用注册的"用户名"做key值,通信的socket做value值private Map<String, Socket> userMap=new HashMap<String, Socket>();public ServerFrom() {setTitle("聊天服务器");setDefaultCloseOperation(EXIT_ON_CLOSE);Toolkit toolkit=Toolkit.getDefaultToolkit();Dimension dim=toolkit.getScreenSize();int runWidth=500;int runHeight=400;int width=(int) dim.getWidth();int height=(int) dim.getHeight();//设置界面居中显示setBounds(width/2-runWidth/2, height/2-runHeight/2, runWidth, runHeight);area=new JTextArea();area.setEditable(false);getContentPane().add(new JScrollPane(area),BorderLayout.CENTER);//列表显示dataModel=new DefaultListModel<String>();JList<String> list=new JList<String>(dataModel);JScrollPane scroll=new JScrollPane(list);scroll.setBorder(new TitledBorder("在线"));scroll.setPreferredSize(new Dimension(100, this.getHeight()));getContentPane().add(scroll,BorderLayout.EAST);//菜单JMenuBar menuBar=new JMenuBar();setJMenuBar(menuBar);JMenu menu=new JMenu("控制(C)");menu.setMnemonic('C'); //设置快捷键为 Alt+CmenuBar.add(menu);//开启final JMenuItem itemRun=new JMenuItem("开启");//快捷键 Ctrl+RitemRun.setAccelerator(KeyStroke.getKeyStroke('R', KeyEvent.CTRL_MASK));itemRun.setActionCommand("run");menu.add(itemRun);//退出JMenuItem itemExit=new JMenuItem("退出");itemExit.setAccelerator(KeyStroke.getKeyStroke('E', KeyEvent.CTRL_MASK));itemExit.setActionCommand("exit");menu.add(itemExit);itemRun.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {if("run".equals(e.getActionCommand())){startServer();itemRun.setEnabled(false);}}});setVisible(true);}private void startServer() {try {System.out.println("服务器启动");ServerSocket server=new ServerSocket(8080);area.append("启动服务器:"+server);//单独开启一个线程用于与客户端握手new ServerThread(server).start();} catch (IOException e) {e.printStackTrace();}}class ServerThread extends Thread{private ServerSocket server;public ServerThread(ServerSocket server) {this.server=server;}@Overridepublic void run() {try {while(true){Socket s=server.accept();//读取客户端第一次向服务器请求的信息
// BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream()));
// if(br.readLine()!=null){
// String userName=br.readLine();
// }Scanner sc=new Scanner(s.getInputStream());if(sc.hasNext()){String userName=sc.next();area.append("\r\n"+userName+"上线了。"+s);dataModel.addElement(userName);
// userMap.put(userName, s); //在后面在把这个用户加入到集合中好一点,那样发送上线信息给所有用户时,就不用判断不发发给自己了。//登录成功//在专门开一个线程用于跟针对某一个客户端通讯//根据接收客户端发来的协议判断,客户端进行的是什么样的请求new ClientThread(s).start();//告诉其他用户有人上线了sendMsgToAll(userName);//把消息其他在线的用户的信息传给登录的这个客户端sendMsgToSelf(s);userMap.put(userName, s);}}} catch (IOException e) {e.printStackTrace();}}}public void sendMsgToAll(String userName) throws IOException{ //这里的异常可以抛,因为调用这个方法的位置抓了IOException//遍历map中所有除了该用户之外的客户--此时登录的用户还没有加入到容器中,所有可以直接遍历所有用户Iterator<Socket> it = userMap.values().iterator();while(it.hasNext()){Socket s=it.next();PrintWriter pw=new PrintWriter(s.getOutputStream(),true);//服务器向客户端发的消息格式设计://命令关键字@#发送方@#消息内容String msg="msg@#server@#"+userName+"登录了"; //用于显示用的.pw.println(msg);msg="cmdAdd@#server@#"+userName; //用于给客户端维护在线用户列表用的pw.println(msg);// pw.close();
// s.close();}}public void sendMsgToSelf(Socket s) throws IOException{PrintWriter pw = new PrintWriter(s.getOutputStream(),true);Iterator<String> it = userMap.keySet().iterator();while(it.hasNext()){String userName=it.next();System.out.println("map:"+userMap);//告诉用户当前在线用户信息,不需要发送显示信息,只需要发送给客户端更新在线列表的信息String msg="cmdAdd@#server@#"+userName;pw.println(msg);}// pw.close();}//专门用于跟某一个用户通讯的线程class ClientThread extends Thread{private Socket s;public ClientThread(Socket s) {this.s=s;}@Overridepublic void run() {try {//根据接收客户端发来的协议判断,客户端进行的是什么样的请求Scanner sc=new Scanner(s.getInputStream());while(sc.hasNextLine()){String msg=sc.nextLine();String msgs[]=msg.split("@#");//简单防黑。if(msgs==null || msgs.length!=4){System.out.println("通讯异常:"+msg);return;}if("on".equals(msgs[0])){//表示客户端的请求是:向别人发送信息sendMsgToSb(msgs);}else if("exit".equals(msgs[0])){//表示客户端发送的请求是:退出(下线)area.append("\r\n"+msgs[3]+"下线了"+s);dataModel.removeElement(msgs[3]);userMap.remove(msgs[3]);//通知其他所有在线的用户,***退出了sendSbExitMsgToAll(msgs);}}} catch (IOException e) {e.printStackTrace();}}}//命令关键字@#接收方@#消息内容@#发送方public void sendMsgToSb(String[] msgs) throws IOException {//可能是发给所有人,也可能是发给某一个人if("全部".equals(msgs[1])){//发给所有人(群聊)Iterator<String> it = userMap.keySet().iterator();while(it.hasNext()){String userName=it.next();String msg=null;if(userName.equals(msgs[3])){msg="msg@#"+"我"+"@#说:"+msgs[2];}else{msg="msg@#"+msgs[3]+"@#说:"+msgs[2];}Socket s=userMap.get(userName);//msg@#消息发送者@#消息内容PrintWriter pw=new PrintWriter(s.getOutputStream(), true);pw.println(msg);}}else{//发送给某一个人String userName=msgs[1];Socket s=userMap.get(userName);//msg@#消息发送者@#消息内容String msg="msg@#"+msgs[3]+"@#悄悄对你说:"+msgs[2];PrintWriter pw=new PrintWriter(s.getOutputStream(), true);pw.println(msg);//在发给自己Socket s2 = userMap.get(msgs[3]);PrintWriter pw2 = new PrintWriter(s2.getOutputStream(), true);String str2 = "msg@#"+"我"+"@#对 "+userName+"说:"+msgs[2];pw2.println(str2);}}//通知其他所有在线的用户,***退出了//1) msg @# server @# 用户[userName]退出了 (给客户端显示用的)//2) cmdRed@#server @# userName (给客户端维护在线用户列表用的)public void sendSbExitMsgToAll(String[] msgs) throws IOException {Iterator<String> it=userMap.keySet().iterator();while(it.hasNext()){String userName=it.next();Socket s=userMap.get(userName);PrintWriter pw=new PrintWriter(s.getOutputStream(), true);String msg="msg@#server@#用户["+msgs[3]+"]退出了";pw.println(msg);msg="cmdRed@#server@#"+msgs[3];pw.println(msg);}}public static void main(String[] args) {JFrame.setDefaultLookAndFeelDecorated(true);new ServerFrom();}}