JavaSE结合Socket实现QQ聊天

前面几天刚学学了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() {
			@Override
			public void windowClosing(WindowEvent e) {
				sendExitMsg();
			}

		});

		setBounds(300, 300, 400, 300);
		setVisible(true);
	}

	@Override
	public 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@#userName
		String msg = "exit@#全部@#null@#" + tfdUserName.getText();
		pw.println(msg);
		System.exit(0);
	}

	class ClientThread extends Thread {

		@Override
		public 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,和端口号
			 */
			@Override
			public 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);// ip
				tfdHost.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() {

					@Override
					public 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() {

			@Override
			public 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() {

			@Override
			public 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;
		}

		@Override
		public void run() {
			while (true) {
				try {
					Socket socketClient = server.accept();
					// 到此,说明有一个新用户上线并握手成功,马上读取他发来用户名
					Scanner sc = new Scanner(socketClient.getInputStream());
					if (sc.hasNextLine()) { // 注意,对方只发一个用户(一行),这里直接用if,而不是while
						String 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;
			}

			@Override
			public 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,毕竟需求多变......希望各位大神指出不足之处!
 
  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值