网络编程(五)--仿QQ聊天程序

来源:互联网 发布:c 输出语言 编辑:程序博客网 时间:2024/05/17 02:36
前面几天刚学学了TCP、UDP等协议,因此这里算是一个小小总结项目,仿照QQ用C/S模式来做一个项目!
首先做了一个设计:
在服务器端 用一个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   @#消息发送者( msgs[3] ) @# 消息内容 (msgs[2])
以下附客户端源代码:
package cn.hucu.sina;import java.awt.BorderLayout;import java.awt.Container;import java.awt.Dimension;import java.awt.FlowLayout;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.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;import javax.swing.ListSelectionModel;import javax.swing.border.TitledBorder;public class ClientForm extends JFrame implements ActionListener {private static String HOST = "127.0.0.1"; //单例private static int PORT = 9090;private JTextField tfdUserName;// 用户标识private DefaultListModel lm; // 在线用户列表private JList list; // 在线用户列表的表现层 JList<String>private JTextField tfdMsg; // 将要发送的消息private JTextArea allMsg = new JTextArea(); // 聊天消息窗口private JButton btnSend = null;    private JButton btnCon=null;public ClientForm() {setTitle("用户聊天");addJMenuBar();/** * 上部面板 */JPanel p = new JPanel();p.add(new JLabel("用户标识:"));tfdUserName = new JTextField(10);p.add(tfdUserName);btnCon = new JButton("连接");btnCon.setActionCommand("c");// ///p.add(btnCon);JButton btnExit = new JButton("退出");btnExit.setActionCommand("exit");p.add(btnExit);// 中部面板JPanel centerP = new JPanel(new BorderLayout());// 东lm = new DefaultListModel();list = new JList(lm);lm.addElement("全部");list.setSelectedIndex(0); // 默认选中第0项list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);// 单选模式list.setVisibleRowCount(2);JScrollPane jc = new JScrollPane(list);jc.setBorder(new TitledBorder("在线"));jc.setPreferredSize(new Dimension(70, centerP.getHeight()));centerP.add(jc, BorderLayout.EAST);// 南JPanel sendP = new JPanel();sendP.add(new JLabel("消息:"));tfdMsg = new JTextField(20);sendP.add(tfdMsg);btnSend = new JButton("发送");btnSend.setActionCommand("send");btnSend.setEnabled(false);sendP.add(btnSend);centerP.add(sendP, BorderLayout.SOUTH);// 中allMsg.setEditable(false);centerP.add(new JScrollPane(allMsg));// 把上部和中部面板 加到框架Container c = getContentPane();c.add(p, BorderLayout.NORTH);c.add(centerP);// 事件监听btnCon.addActionListener(this);   btnSend.addActionListener(this);btnExit.addActionListener(this);this.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {sendExitMsg();}});setBounds(300, 300, 400, 300);setVisible(true);}@Overridepublic void actionPerformed(ActionEvent e) {if (e.getActionCommand().equals("c")) {System.out.println("连接.....");connecting();  //连接服务器btnCon.setEnabled(false);} else if (e.getActionCommand().equals("send")) {/** *  on @# JList.getSelectedValue() @# tfdMsg.getText() @# tfdUserName.getText() */String msg = "on@#" + list.getSelectedValue() + "@#"+ tfdMsg.getText() + "@#" + tfdUserName.getText();pw.println(msg);tfdMsg.setText("");} else if (e.getActionCommand().equals("exit")) {sendExitMsg();}}private Socket client;private PrintWriter pw;private void connecting() {try {client = new Socket(HOST, PORT);// 握手// 到这里,握手一定成功,就发送用户名给服务器String userName = tfdUserName.getText(); //获取用户名pw = new PrintWriter(client.getOutputStream(), true);pw.println(userName);this.setTitle("用户[" + userName + "]在线...");btnSend.setEnabled(true); // 打开"发送"按钮的开关tfdUserName.setEditable(false); // 用户名不能再修改            // 开启一个线程专门接收服务器发来的消息new ClientThread().start();} catch (UnknownHostException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}      }/** * 退出聊天,将退出用户的退出信息发送给其他用户 */private void sendExitMsg() {if (client == null) {// 未连接就退出System.exit(0);}//exit@#全部@#null@#userNameString msg = "exit@#全部@#null@#" + tfdUserName.getText();pw.println(msg);System.exit(0);}class ClientThread extends Thread {@Overridepublic void run() {try {Scanner sc = new Scanner(client.getInputStream());while (sc.hasNextLine()) {String str = sc.nextLine();String msgs[] = str.split("@#"); //正则拆分工具// 简单防黑if (msgs == null || msgs.length != 3) {System.out.println("通讯异常....异常消息为:" + str);return;}if ("msg".equals(msgs[0])) {if ("server".equals(msgs[1])) { // 系统消息str = "[系统通知]:" + msgs[2];} else { // 聊天消息str = "[" + msgs[1] + "]说:" + msgs[2];}allMsg.append(str + "\r\n");} else if ("cmdAdd".equals(msgs[0])) {lm.addElement(msgs[2]);} else if ("cmdRed".equals(msgs[0])) {lm.removeElement(msgs[2]);}list.validate();  //这里必须要刷新,否则当前用户在线栏目为空白}} catch (IOException e) {e.printStackTrace();}}}private void addJMenuBar() {JMenuBar menubar = new JMenuBar();JMenu menu = new JMenu("选项");JMenuItem miSet = new JMenuItem("设置");JMenuItem miHelp = new JMenuItem("帮助");miSet.addActionListener(new ActionListener() {/** * 创建一块面板,用户修改IP,和端口号 */@Overridepublic void actionPerformed(ActionEvent e) {final JDialog setDlg = new JDialog(ClientForm.this); //将新面板依附到主板上setDlg.setBounds(ClientForm.this.getX() + 10,ClientForm.this.getY(), 350, 100);setDlg.setLayout(new FlowLayout());   //JPanel默认布局(流布局)final JTextField tfdHost = new JTextField(10);// iptfdHost.setText(HOST);final JTextField tfdPort = new JTextField(5); // 端口号tfdPort.setText("" + PORT);JButton btnSet = new JButton("设置");setDlg.add(new JLabel("服务器:"));setDlg.add(tfdHost);setDlg.add(new JLabel(":"));setDlg.add(tfdPort);setDlg.add(btnSet);btnSet.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {ClientForm.HOST = tfdHost.getText();ClientForm.PORT = Integer.parseInt(tfdPort.getText());btnCon.setEnabled(true);setDlg.dispose();// 关闭且销毁当前对话框}});setDlg.setVisible(true);}});miHelp.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {final JDialog helpDlg = new JDialog(ClientForm.this);helpDlg.setBounds(ClientForm.this.getX(),ClientForm.this.getY(), 300, 100);JLabel str = new JLabel("版权所有@湖南.2017-08-16.QQ:666666");helpDlg.add(str);helpDlg.setVisible(true);}});menu.add(miSet);menu.add(miHelp);menubar.add(menu);this.setJMenuBar(menubar);}public static void main(String[] args) {JFrame.setDefaultLookAndFeelDecorated(true);new ClientForm();}}

以下附服务器端代码:
package cn.hucu.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.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.JOptionPane;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.KeyStroke;import javax.swing.border.TitledBorder;public class ServerForm extends JFrame {private JTextArea area; // 在线用户信息的显示窗口private DefaultListModel lm; // //DefaultListModel<String>private static final int PORT = 9090; // 服务器的监听端口private HashMap<String, Socket> usersMap = new HashMap<String, Socket>();public ServerForm() {this.setTitle("聊天服务器");this.setDefaultCloseOperation(EXIT_ON_CLOSE);  //Toolkit toolkit = Toolkit.getDefaultToolkit();//获取系统屏幕分辨率大小int winWidth = 500;int winHeight = 400;int width = (int) toolkit.getScreenSize().getWidth();int height = (int) toolkit.getScreenSize().getHeight();setBounds(width / 2 - winWidth / 2, height / 2 - winHeight / 2,winWidth, winHeight);  //设置面板位置以及大小/** * 在线用户登录与退出信息的显示窗口 */area = new JTextArea();area.setEditable(false);getContentPane().add(new JScrollPane(area)); //添加滚动条/** * 在线用户名列表 */lm = new DefaultListModel();JList list = new JList(lm);// /////JList<String>这里可以考虑用泛型,不过需要将JDKSE-1.7才可以JScrollPane jc = new JScrollPane(list);jc.setBorder(new TitledBorder("在线"));jc.setPreferredSize(new Dimension(100, this.getHeight()));getContentPane().add(jc, BorderLayout.EAST);/** * 菜单 */JMenuBar bar = new JMenuBar();this.setJMenuBar(bar);JMenu menu = new JMenu("控制(C)");menu.setMnemonic('C'); // Alt+C 菜单的快捷键bar.add(menu);// 菜单项“开启”final JMenuItem miRun = new JMenuItem("开启");miRun.setAccelerator(KeyStroke.getKeyStroke('R', KeyEvent.CTRL_MASK));miRun.setActionCommand("run");menu.add(miRun);menu.addSeparator(); // 加分隔条// 菜单项"退出"JMenuItem miExit = new JMenuItem("退出");miExit.setAccelerator(KeyStroke.getKeyStroke('E', KeyEvent.CTRL_MASK));miExit.setActionCommand("exit");menu.add(miExit);// 事件监听ActionListener al = new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {if ("run".equals(e.getActionCommand())) {startServer();miRun.setEnabled(false); // 服务器不能反复开启,因此开启之后就要灭掉}if("exit".equals(e.getActionCommand())){ExitServer(); }}private void ExitServer() {               System.exit(0);}};miRun.addActionListener(al);miExit.addActionListener(al);this.setVisible(true);}private void startServer() {try {System.out.println("启动服务器...");ServerSocket server = new ServerSocket(PORT);area.append("启动服务:" + server);// 单开一个线程用于握手new ServerThread(server).start();} catch (IOException e) {JOptionPane.showMessageDialog(null, "端口号有误!!"); // 以后错误信息要记录到日志,return;}}/** * 专门负责握手的服务线程 */class ServerThread extends Thread {private ServerSocket server;public ServerThread(ServerSocket server) {this.server = server;}@Overridepublic void run() {while (true) {try {Socket socketClient = server.accept();// 到此,说明有一个新用户上线并握手成功,马上读取他发来用户名Scanner sc = new Scanner(socketClient.getInputStream());if (sc.hasNextLine()) { // 注意,对方只发一个用户(一行),这里直接用if,而不是whileString userName = sc.nextLine();area.append("\r\n用户:[" + userName + "]登录了:"+ socketClient);lm.addElement(userName);new ClientThread(socketClient).start();// 通知所有已经在线的用户,有新人登录了msgAll(userName);// 通知当前新登录的用户,其他已经在线用户的名字msgSelf(socketClient);usersMap.put(userName, socketClient); //加入新用户}} catch (IOException e) {JOptionPane.showMessageDialog(null, "接受用户信息失败!");return;}}}/** * 专用于负责和某一个用户通讯的线程 */class ClientThread extends Thread {private Socket socketClient = null;public ClientThread(Socket socketClient) {this.socketClient = socketClient;}@Overridepublic void run() {try {Scanner sc = new Scanner(socketClient.getInputStream());while (sc.hasNextLine()) {String msg = sc.nextLine();// 按理这里应该防黑String msgs[] = msg.split("@#");if ("on".equals(msgs[0])) { // 有聊天消息sendMsgToSb(msgs);} else if ("exit".equals(msgs[0])) { // 有用户退出// 更新服务器自己的界面和用户池usersMap.remove(msgs[3]);lm.removeElement(msgs[3]);area.append("\r\n用户[" + msgs[3] + "]退出了");// 通知其他在线用户sendExitMsgToAll(msgs);}}} catch (IOException e) {e.printStackTrace();}}}}// 通知当前新登录的用户,其他已经在线用户的名字private void msgSelf(Socket socketClient) {try {PrintWriter pw = new PrintWriter(socketClient.getOutputStream(),true);Iterator<String> it = usersMap.keySet().iterator();while (it.hasNext()) {String msg = "cmdAdd@#server@#" + it.next();pw.println(msg);}} catch (IOException e) {e.printStackTrace();}}// 通知所有已经在线的用户,有新人登录了private void msgAll(String userName) {Iterator<Socket> it = usersMap.values().iterator();while (it.hasNext()) {Socket s = it.next();try {PrintWriter pw = new PrintWriter(s.getOutputStream(), true);String msg = "msg@#server@# 用户[" + userName + "]登录了";// 给客户端显示用的pw.println(msg);msg = "cmdAdd@#server@#" + userName; // 给客户端维护在线用户列表用的pw.println(msg);} catch (IOException e) {e.printStackTrace();}}}// 通知所有在线的用户,有新人退出了private void sendExitMsgToAll(String[] msgs) throws IOException {Iterator<Socket> it = usersMap.values().iterator();while (it.hasNext()) {Socket s = it.next();PrintWriter pw = new PrintWriter(s.getOutputStream(), true);String msg = "msg@#server@# 用户[" + msgs[3] + "]退出了";// 给客户端显示用的pw.println(msg);msg = "cmdRed@#server@#" + msgs[3]; // 给客户端维护在线用户列表用的pw.println(msg);}}// 发聊天消息给某人private void sendMsgToSb(String[] msgs) throws IOException {if ("全部".equals(msgs[1])) {// 群聊Iterator<String> userNames = usersMap.keySet().iterator();while (userNames.hasNext()) {String userName = userNames.next();// if(!userName.equals(msg[3])){//在聊天板上不显示自己说的话Socket s = usersMap.get(userName);PrintWriter pw = new PrintWriter(s.getOutputStream(), true);String str = "msg@#" + msgs[3] + "@#" + msgs[2];pw.println(str);// }}} else { // 私聊// 发给接收方Socket s = usersMap.get(msgs[1]);PrintWriter pw = new PrintWriter(s.getOutputStream(), true);String str = "msg@#" + msgs[3] + "@#" + msgs[2];pw.println(str);// 发给发送方Socket s2 = usersMap.get(msgs[3]);PrintWriter pw2 = new PrintWriter(s2.getOutputStream(), true);String str2 = "msg@#" + msgs[3] + "@#" + msgs[2];pw2.println(str2);}}public static void main(String[] args) {JFrame.setDefaultLookAndFeelDecorated(true);new ServerForm();}}

结果如下:

这里还是存在许多小Bug,毕竟需求多变......希望各位大神指出不足之处!

原创粉丝点击