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

学完了socket通讯后,在老师的要求下,写了一个仿qq的聊天程序:

最终调试程序结果如下图:   有bug希望提出来,我们一起解决。

java 网络编程五 (仿QQ聊天程序)_第1张图片

设计思路:

在服务器端 用一个HashMap 维护所有用户相关的信息,从而能够保证和所有的用户进行通讯。

客户端的动作:
(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 dataModel=new DefaultListModel();
	private JList list=new JList(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的实现类TitileBorder
		scroll.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() {
			@Override
			public 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() {
			@Override
			public 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() {
					@Override
					public 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() {
			@Override
			public 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);
		
	}
	
	@Override
	public 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;
		}

		@Override
		public 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 dataModel;	//在线的用户列表显示
	
	//注册的用户名不能相同
	//用于存储所有的用户,这里采用注册的"用户名"做key值,通信的socket做value值
	private Map userMap=new HashMap();
	
	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();
		JList list=new JList(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+C
		menuBar.add(menu);
		//开启
		final JMenuItem itemRun=new JMenuItem("开启");
		//快捷键 Ctrl+R
		itemRun.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() {
			@Override
			public 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;
		}

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

}



你可能感兴趣的:(----java,net,网络编程-----)