Socket编程 ------ 模拟QQ聊天工具

Posted 饿狼干爹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket编程 ------ 模拟QQ聊天工具相关的知识,希望对你有一定的参考价值。

模拟QQ聊天


一、要求

1、一个服务器可以与多个用户同时通讯

2、用户可以通过服务器与用户之间通讯

3、用户可以选择和所有人发消息,也可以选择和某个用户单独发消息

4、服务器要显示当前所有在线人员

5、用户要显示当前在线的人员

6、当有新用户登录时或在线用户退出时,服务器要向所有其他在线用户发送提示信息,并且服务器也要显示相应的提示信息

7、不能有相同的用户名同时登陆

8、不能发送空消息

9、客户端可以设置连接的服务器IP和端口


二、了解B/S模式的底层socket通讯原理

QQ聊天可以利用协议方式发送消息。所以先要了解浏览器和服务器直接的协议,从而仿照。

浏览器的请求

GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: 192.168.31.169:9090
DNT: 1
Connection: Keep-Alive


请求行,包含:  请求方式(GET或POST) 空格 请求的资源路径 空格 http的协议版本
接下来是请求消息头(...)
空行
请求体(包括浏览器向服务器提交的表单数据等)

HTTP/1.1 200 OK
Date: Fri, 11 Sep 2015 12:33:43 GMT
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Set-Cookie: JSESSIONID=409C8CF8220AD78D26D47B15DCEADCD3; Path=/; HttpOnly
Vary: Accept-Encoding
Connection: close
Transfer-Encoding: chunked
Content-Language: zh-CN

应答行,包含:http协议版本 空格 应答状态码 空格 应答状态码信息码描述
应答消息头(...)
空行
应答体(页面内容)

三、QQ聊天协议

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

客户端的动作:

(1)连接(登录):发送userName    服务器的对应动作:1)界面显示,2)通知其他用户关于你登录的信息, 3)把其他在线用户的userName通知当前用户 4)开启一个线程专门为当前线程

服务

(2)退出(注销):

(3)发送消息

※※发送通讯内容之后,对方如何知道是干什么,通过消息协议来实现:


客户端向服务器发的消息格式设计:
命令关键字@#接收方@#消息内容@#发送方
连接:userName      ----握手的线程serverSocket专门接收该消息,其它的由服务器新开的与客户进行通讯的socket来接收
退出:exit@#全部@#null@#userName
发送: 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])


四、注解和实现代码

ClientForm.java类

package com.sina.chat;

import java.awt.BorderLayout;
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.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 ClientForm extends JFrame implements ActionListener
	
	private JTextField userName;//用户名
	private DefaultListModel lm;//用于维护在线用户列表
	private JList list;
	private JTextField msg;//发送消息口
	private JTextArea allMsg;
	private JButton btnConn;
	
	private String HOST="127.0.0.1";//服务器地址
	private int PORT = 9090;//服务器端口号


	public ClientForm()
		addJMnuBar();//添加并处理自定义菜单
		JPanel upP = new JPanel();
		upP.add(new JLabel("用户标识"));
		userName = new JTextField(10);
		upP.add(userName);
		
		//上部面板
		btnConn = new JButton("连接");
		btnConn.setActionCommand("conn");
		btnConn.addActionListener(this);
		upP.add(btnConn);
		JButton btnExit = new JButton("退出");
		btnExit.setActionCommand("exit");
		btnExit.addActionListener(this);
		upP.add(btnExit);
		this.getContentPane().add(upP, BorderLayout.NORTH);
		
		//中部面板
		JPanel cenP = new JPanel(new BorderLayout());
		//以下这段设置“在线用户”列表	----东	
		lm = new DefaultListModel();//如果版本不对,可以添加<String>
		list = new JList(lm);
		lm.addElement("全部");
		list.setSelectedIndex(0);
		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);//设置只能进行单项选择
		list.setVisibleRowCount(2);//设置默认显示的行数
		JScrollPane jc = new JScrollPane(list);
		jc.setBorder(new TitledBorder("在线"));
		//setSize是设定的固定大小,而setPreferredSize仅仅是设置最好的大小,
		//这个不一定与实际显示出来的控件大小一致(根据界面整体的变化而变化)
		jc.setPreferredSize(new Dimension(70,cenP.getHeight()));
		cenP.add(jc,BorderLayout.EAST);
		
		//以下这段设置消息发送面板  ----南
		JPanel downP = new JPanel();
		downP.add(new JLabel("消息"));
		msg = new JTextField(20);
		downP.add(msg);
		JButton btnSend = new JButton("发送");
		btnSend.setActionCommand("send");
		btnSend.addActionListener(this);
		downP.add(btnSend);
		cenP.add(downP,BorderLayout.SOUTH);
		
		//以下设置中间的聊天记录
		allMsg = new JTextArea();
		allMsg.setEditable(false);
		allMsg.setLineWrap(true);//设置文本域自动换行
		allMsg.setWrapStyleWord(true);
//		cenP.add(new JScrollPane(allMsg));//把消息框用滚动面板包起来,再加到center中
		cenP.add(allMsg);
		
		this.getContentPane().add(cenP,BorderLayout.CENTER);
		
		this.addWindowListener(new WindowAdapter()
			public void windowClosing(WindowEvent e)
				sendExitMsg();//发送退出信息
			

		);
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置框架上的关闭按钮
		this.setBounds(300, 300, 400, 300);
		this.setVisible(true);
	
	
	private void addJMnuBar() 
		JMenuBar  bar = new JMenuBar();
		JMenu m=new JMenu("选项");
		JMenuItem mi = new JMenuItem("设置");
		mi.addActionListener(new ActionListener() 
			
			@Override
			public void actionPerformed(ActionEvent e) 
				//因为setDlg、host等变量要放在内部类中,所以要设置成final型
				final JDialog setDlg = new JDialog(ClientForm.this,true);
				setDlg.setBounds(ClientForm.this.getX()+40, ClientForm.this.getY()+100, 350, 100);
				setDlg.setLayout(new FlowLayout());
				JButton btn = new JButton("设置");
				
				final JTextField host = new JTextField(10);
				host.setText("127.0.0.1");
				final JTextField port = new JTextField(5);
				port.setText("9090");
				
				btn.addActionListener(new ActionListener() 
					
					@Override
					public void actionPerformed(ActionEvent e) 
						ClientForm.this.HOST = host.getText();
						try 
							ClientForm.this.PORT = Integer.parseInt(port.getText());
						 catch (NumberFormatException e1) 
							JOptionPane.showMessageDialog(ClientForm.this,"请输入数字");
						
						setDlg.dispose();
					
				);
				setDlg.add(new JLabel("服务器IP:"));
				setDlg.add(host);
				setDlg.add(new JLabel("端口号:"));
				setDlg.add(port);
				setDlg.add(btn);
				setDlg.setVisible(true);
			
		);
		JMenuItem help = new JMenuItem("帮助");
		help.addActionListener(new ActionListener() 
			
			@Override
			public void actionPerformed(ActionEvent e) 
				JDialog dlg = new JDialog(ClientForm.this,true);
				dlg.setBounds(ClientForm.this.getX()+40, ClientForm.this.getY()+100, 300, 80);
				dlg.setLayout(new FlowLayout());
				JLabel label = new JLabel("版本所有@城院.2015-9-13,QQ:888888");
				dlg.add(label);
				dlg.setVisible(true);
			
		);
		
		bar.add(m);
		m.add(mi);
		m.add(help);
		this.setJMenuBar(bar);
	

	public static void main(String[] args)
		JFrame.setDefaultLookAndFeelDecorated(true);
		new ClientForm();
	
	
	public void actionPerformed(ActionEvent e) 
		//连接按钮
		if(e.getActionCommand().equals("conn"))
			System.out.println("连接。。。");
			connercting();//与服务器建立连接
		else if(e.getActionCommand().equals("send"))
			sendMsg();//发送聊天消息
		else if(e.getActionCommand().equals("exit"))
			sendExitMsg();//发送退出消息
		
	
	
	private Socket client;//声明一个客户端的套接字
	private PrintWriter pw;//声明一个打印流
	private void connercting() 
		try 
			//握手
			client = new Socket(HOST,PORT);
			String userName = this.userName.getText();
			if(userName.equals(""))//控制用户名不能为空
				JOptionPane.showMessageDialog(this, "用户名不能为空");
				return ;
			
			//连接成功后,将用户名框和连接按钮设置为不可选
			btnConn.setEnabled(false);
			this.userName.setEditable(false);
			pw=new PrintWriter(client.getOutputStream(), true);
			
			pw.println(userName);
			this.setTitle("用户【"+userName+"】在线");
			
			new ClientThread().start();//在线聊天处理
		 catch (IOException e) 
			JOptionPane.showMessageDialog(null, "服务器连接失败!!!");
		
		
	
	//发送退出消息
	private void sendExitMsg() 
		if(client==null)
			System.exit(0);
		
		//自定义消息协议
		String msg = "exit@#"+"全部"+"@#"+null+"@#"+userName.getText();
		pw.println(msg);
		pw.flush();//这里一定要记得刷新
		System.exit(0);
	
	//发送聊天消息
	private void sendMsg() 
		String strMsg=this.msg.getText();
		if(strMsg.equals(""))//保证发送的消息不能为空
			JOptionPane.showMessageDialog(this, "不能发送空消息。。。");
			return ;
		
		String msg="on@#"+list.getSelectedValue()+"@#"+strMsg+"@#"+userName.getText();
		pw.println(msg);
		pw.flush();
		this.msg.setText("");
	
	
	class ClientThread extends Thread
		public void run()
			try 
				Scanner sc = new Scanner(client.getInputStream());
				while(sc.hasNextLine())
					String str = sc.nextLine();
					String[] msgs=str.split("@#");
					//通过聊天协议,解析服务端发送来的消息
					if("msg".equals(msgs[0]))
						if("server".equals(msgs[1]))
							if("error".equals(msgs[2]))//判断连接到服务器的客户中是否有同名
								btnConn.setEnabled(true);
								userName.setEditable(true);
								JOptionPane.showMessageDialog(ClientForm.this, "用户名被占用");
								continue;
							else
								str="【通知】:"+msgs[2];
							
						else
							str="【"+msgs[1]+"】说:"+msgs[2];
						
						allMsg.append("\\r\\n"+str);
					else if("cmdAdd".equals(msgs[0]))
						System.out.println(111111111);
						lm.addElement(msgs[2]);
					else if("cmdRed".equals(msgs[0]))
						System.out.println(6666);
						lm.removeElement(msgs[2]);
					
				
			 catch (IOException e) 
				e.printStackTrace();
			
			
		
	

ServerForm.java类

package com.sina.chat;

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 ServerForm extends JFrame 
	private int PORT = 9090;
	private DefaultListModel lm;//用于维护在线用户列表
	private JList list;
	private JTextArea area;
	
	private Map<String,Socket> usersMap = new HashMap<String,Socket>();
	
	public ServerForm()
		super("这是服务器");
		//聊天消息记录框
		area = new JTextArea();
		area.setEditable(false);
		area.setLineWrap(true);
		this.getContentPane().add(new JScrollPane(area));//默认加在center位置
		//界面右边的在线用户列表
		lm = new DefaultListModel();
		list = new JList(lm);
		JScrollPane jc = new JScrollPane(list);
		jc.setBorder(new TitledBorder("在线"));
		//setSize是设定的固定大小,而setPreferredSize仅仅是设置最好的大小,
		//这个不一定与实际显示出来的控件大小一致(根据界面整体的变化而变化)
		jc.setPreferredSize(new Dimension(100,this.getHeight()));
		this.getContentPane().add(jc,BorderLayout.EAST);
		//菜单
		JMenuBar bar =new JMenuBar();
		this.setJMenuBar(bar);
		JMenu m=new JMenu("控制");
		m.setMnemonic('C');
		bar.add(m);
		//“开启”菜单项,因为内部类要用到这个变量,所以定义为final型
		final JMenuItem run = new JMenuItem("开启");
		run.setAccelerator(KeyStroke.getKeyStroke('R',KeyEvent.CTRL_MASK));
		run.setActionCommand("run");
		m.add(run);
		m.addSeparator(); //菜单分隔线
		//“开启”菜单项
		JMenuItem exit = new JMenuItem("退出");
		exit.setAccelerator(KeyStroke.getKeyStroke('E',KeyEvent.CTRL_MASK));//设置快捷键
		exit.setActionCommand("exit");
		m.add(exit);
		
		ActionListener a = new ActionListener() 
			
			public void actionPerformed(ActionEvent e) 
				if(e.getActionCommand().equals("run"))
					startServer();
					run.setEnabled(false);//点击运行按钮后,将运行按钮设置为不可选
				else if(e.getActionCommand().equals("exit"))
					System.exit(0);
				
			
		;
		run.addActionListener(a);
		exit.addActionListener(a);
		//整个界面窗口的设置
		final int winWidth = 500;
		final int winHeight = 400;
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		int width = (int) toolkit.getScreenSize().getWidth();//获得系统分辨率
		int height = (int) toolkit.getScreenSize().getHeight();
		this.setBounds(width/2-winWidth/2, height/2-winHeight/2, winWidth, winHeight);
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setVisible(true);
		
	
	
	public static void main(String[] args) 
		JFrame.setDefaultLookAndFeelDecorated(true);
		new ServerForm();
	
	
	private void startServer() 
		try 
			ServerSocket server = new ServerSocket(PORT);
			area.append("启动服务器:\\r\\nIP:"+server.getInetAddress().getHostAddress()+"\\r\\nPORT:"+server.getLocalPort());
			
			new ServerThread(server).start();
		 catch (IOException e) 
			e.printStackTrace();
		
	
	
	class ServerThread extends Thread
		ServerSocket server = null;
		public ServerThread(ServerSocket server) 
			this.server = server;
		
		//专门处理握手消息
		public void run()
			try 
				while(true)
					Socket socketClient = server.accept();
					Scanner sc = new Scanner(socketClient.getInputStream());
					if(sc.hasNextLine())
						String userName =sc.nextLine();
						if(isError(userName))//判断用户名是否存在,若存在则给用户发送提示信息
							PrintWriter pw = new PrintWriter(socketClient.getOutputStream(),true);
							String msg = "msg@#server@#error";
							pw.println(msg);
							pw.flush();
							continue;
						
						area.append("\\r\\n用户:【"+userName+"】登录,"+socketClient);
						//把用户添加到list当中----通过该list模块的数据层控件lm来完成
						lm.addElement(userName);
						//1通知所有已经在线的人,userName这个人登录了
						msgAll(userName);
						//2告诉userName这个人,有哪些人目前在线
						msgSelf(socketClient);
						//3开一个专门用于和该userName客户端通讯的线程
						new ClientThread(socketClient);
						//4把该用户放到“在线用户池”中
						usersMap.put(userName, socketClient);
					
				
			 catch (IOException e) 
				e.printStackTrace();
			
		
		//判断用户名是否已经存在了
		private boolean isError(String userName) 
			return usersMap.containsKey(userName);
		
		//通知所有已经在线的人,userName这个人登录了
		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);
					pw.flush();//刷新、刷新、刷新
					//给用户维护JList列表(在线用户)
					msg = "cmdAdd@#server@#"+userName;
					pw.println(msg);
					pw.flush();
				
				 catch (IOException e) 
					e.printStackTrace();
				
			
		
		//告诉userName这个人,有哪些人目前在线.只要给客户端发送“维护JList列表(在线用户)的消息”
		private void msgSelf(Socket socketClient) 
			PrintWriter pw = null;
			try 
				pw = new PrintWriter(socketClient.getOutputStream(),true);
				Iterator<String> it = usersMap.keySet().iterator();
				while(it.hasNext())
					String userName = it.next();
					String msg = "cmdAdd@#server@#"+userName;
					pw.println(msg);
				
			 catch (IOException e) 
				e.printStackTrace();
			
			pw.flush();
		
	
	
	class ClientThread extends Thread
		private Socket client;
		
		public ClientThread(Socket client) 
			this.client = client;
			start();
		
		
		public void run()
			try 
				Scanner sc = new Scanner(client.getInputStream());
				while(sc.hasNext())
					String str = sc.nextLine();
					String[] msgs = str.split("@#");
					if("on".equals(msgs[0]))
						sendMsgToSb(msgs);
					else if("exit".equals(msgs[0]))
						//从“在线用户池usersMap”中删除当前用户
						usersMap.remove(msgs[3]);
						//从界面中的“在线用户列表”中删除当前用户,还要在服务器的area中显示该用户的退出消息
						lm.removeElement(msgs[3]);
						area.append("\\r\\n用户【"+msgs[3]+"】退出了");
						//发该用户退出的消息发给其他在线用户
						sendExitMsgToAll(msgs);
					
				
			 catch (IOException e) 
				e.printStackTrace();
			
		

		private void sendMsgToSb(String[] msgs) throws IOException 
			if("全部".equals(msgs[1]))
				Iterator<Socket> it = usersMap.values().iterator();
				while(it.hasNext())
					Socket s = it.next();
					String str = "msg@#"+msgs[3]+"@#"+msgs[2];
					PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
					pw.println(str);
					pw.flush();
				
			else
				Socket s = usersMap.get(msgs[1]);
				String str = "msg@#"+msgs[3]+"@#"+msgs[2];
				PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
				pw.println(str);
				pw.flush();
			
		

		private void sendExitMsgToAll(String[] msgs) throws IOException 
			Iterator<Socket> it = usersMap.values().iterator();
			while(it.hasNext())
				Socket s =it.next();
				//给用户显示在消息框中的
				String str = "msg@#server@#【"+msgs[3]+"】退出了";
				PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
				pw.println(str);
				pw.flush();
				//给用户维护JList列表(在线用户)
				str = "cmdRed@#server@#"+msgs[3];
				pw.println(str);
				pw.flush();
			
		
	


五、知识点清单

1、java图形界面基础知识(javax.swing和java.awt)

2、setPreferredSize:(1)setPreferredSize需要在使用布局管理器的时候使用,布局管理器会获取空间的preferredsize,因而可以生效。例如borderlayout在north中放入一个panel,panel的

高度可以通过这样实现:panel.setPreferredSize(new Dimension(0, 100));这样就设置了一个高度为100的panel,宽度随窗口变化。

(2)setSize,setLocation,setBounds方法需要在不使用布局管理器的时候使用,也就是setLayout(null)的时候可以使用这三个方法控制布局。

区分好这两个不同点之后,我相信你的布局会更随心所欲。

3、DefaultListModel:创建并且设置列表数据模型,和JList配合使用,使添加删除JList上的元素时变得容易

4、setLineWrap:public void setLineWrap(boolean wrap)设置文本区的换行策略。

如果设置为 true,则当行的长度大于所分配的宽度时,将换行。

如果设置为 false,则始终不换行。当策略更改时,将激发 PropertyChange 事件("lineWrap")。此属性默认为 false

public void setWrapStyleWord(boolean word)设置换行方式(如果文本区要换行)。

如果设置为 true,则当行的长度大于所分配的宽度时,将在单词边界(空白)处换行。

如果设置为 false,则将在字符边界处换行。此属性默认为 false。

5、匿名类

6、内部类

7、网络编程技术






六、运行结果截图

 

 







以上是关于Socket编程 ------ 模拟QQ聊天工具的主要内容,如果未能解决你的问题,请参考以下文章

Python Socket 编程——聊天室演示样例程序

07.网络编程-1.网络基础

java是如何实现聊天功能的?

Socket实现仿QQ聊天(可部署于广域网)附源码-简介

JavaSE入门学习47:Socket网络通信编程

Java网络编程