Java---net(网络编程)---TCP---模拟QQ在线群聊

    模拟QQ,在线群聊:底层就是 采用 TCP的编程思想,每个用户必须连接到服务器才能进行聊天,用户之间的群聊还是私聊都必须要用过服务器进行处理和转发。

    网络通信的关键就是在于协议,所以设计软件最麻烦的就是在定义协议这个地方,需要统一信息传递的格式。

    协议如下:

        客户端向服务器发的消息格式设计:
        命令关键字@#接收方@#发送方@#消息内容
        1)连接:userName      ----握手的线程serverSocket专门接收该消息,其它的由服务器新开的与客户进行通讯的socket来接收
        2)退出:exit@#全部@#userName@#null
        3)发送: on @# list.getSelectedValue() @# tfdUserName.getText() @# tfdMsg.getText()

        服务器向客户端发的消息格式设计:
        命令关键字@#发送方@#消息内容
        登录:
           cmdAdd@#server @# userName (给客户端维护在线用户列表用的)
        退出:
           cmdRed@#server @# userName (给客户端维护在线用户列表用的)
        发送:

           msg   @#消息发送者 @# 消息内容

效果图:

Java---net(网络编程)---TCP---模拟QQ在线群聊_第1张图片

Java---net(网络编程)---TCP---模拟QQ在线群聊_第2张图片

代码:

   客户端:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.border.TitledBorder;

/**
 * 
 * 2018年5月13日 上午8:55:32
 * @author 宋进宇
 *
 */
public class ClientForm extends JFrame implements ActionListener{
	private static final long serialVersionUID = 1L;
	
	//服务器端的 IP 和 PORT
	private static final String IP = "127.0.0.1";
	private static final int PORT = 9999;
	
	private JTextField tfdUserName; //用户标识
	
	private JList list; //在线用户列表--表现层
	private DefaultListModel lm; //在线用户列表--数据层
	
	private JTextArea allMsg = new JTextArea(); //消息显示主窗口
	private JTextField tfdMsg; //发消息输入框
	
	
	private Socket socket = null;
	
	private JButton btnConn;
	
	public ClientForm() {
		setBounds( 300, 300, 400, 300 );
		
		//1上部面板
		JPanel p = new JPanel();
		
		p.add( new JLabel( "用户标识:" ) );
		tfdUserName = new JTextField( 10 );
		p.add( tfdUserName );
		btnConn = new JButton( "连接" );
		btnConn.setActionCommand( "connection" );
		btnConn.addActionListener( this );
		p.add( btnConn );
		JButton btnExit = new JButton( "退出" );
		btnExit.setActionCommand( "exit" );
		btnExit.addActionListener( this );
		p.add( btnExit );
		
		this.add( p, BorderLayout.NORTH ); //添加到上部
		
		//2中部面板
		JPanel centerP = new JPanel();
		centerP.setLayout( new BorderLayout() );
		//2.1东 在线用户列表
		lm = new DefaultListModel();
		lm.addElement( "全部" );
		list = new JList( lm );
		list.setSelectedIndex( 0 );
		list.setVisibleRowCount( 2 );
		JScrollPane jsc = new JScrollPane( list );
		jsc.setBorder( new TitledBorder( "在线" ) );
		jsc.setPreferredSize( new Dimension( 70, centerP.getHeight() ) );
		centerP.add( jsc, BorderLayout.EAST );
		
		//2.2中  聊天信息窗口
		allMsg.setEditable( false );
		centerP.add( new JScrollPane( allMsg ) );
		
		//2.3南  消息发送面板
		JPanel sendP=new JPanel();
		sendP.add( new JLabel( "消息:" ) );
		tfdMsg = new JTextField( 20 );
		sendP.add( tfdMsg );
		JButton btnSend = new JButton( "发送" );
		btnSend.setActionCommand( "send" );
		btnSend.addActionListener( this );
		sendP.add( btnSend );
		
		centerP.add( sendP, BorderLayout.SOUTH );
		
		this.add( centerP ); //添加到中部
		
		addWindowListener( new WindowAdapter() {

			@Override
			public void windowClosing(WindowEvent e) {
				//退出前,先服务器发送退出消息
				sendExitMsg();
			}
			
		});
		
		setVisible(true);
	}
	

	@Override
	public void actionPerformed(ActionEvent e) {
		if ( "connection".equals( e.getActionCommand() ) ) {//链接
			try {
				socket = new Socket( IP, PORT );
				//连接成功就给服务器发送用户名
				PrintWriter pw = new PrintWriter( socket.getOutputStream(), true );
				pw.println( tfdUserName.getText() );
				//把 tfdUserName 设置不可编辑,同时 把 连接按钮置灰
				tfdUserName.setEditable( false );
				btnConn.setEnabled( false );
				//同时设置一下 标题
				setTitle( "Java修仙群-在线中..." );
				
				//开一个线程接收服务器发送过来的信息
				new Thread( new Receive() ).start();;
			} catch (IOException e1) {
				JOptionPane.showMessageDialog( this, "服务器未启动..." );
				return;
			}
		} else if ( "exit".equals( e.getActionCommand() ) ) {
			//退出前向服务器发送退出消息
			sendExitMsg();
			
		} else if ( "send".equals( e.getActionCommand() ) ) {
			//如果 用户没有登入 就不能发送信息
			if ( socket == null ) {
				JOptionPane.showMessageDialog( this, "请您登入后再发送信息");
				return;
			}
			PrintWriter pw = null ;
			try {
				pw = new PrintWriter( socket.getOutputStream(), true );
			} catch (IOException e2) {
				e2.printStackTrace();
				return;
			}
			int index = list.getSelectedIndex();
			if ( index == 0 ) { //如果index==0说明是发送给所有人
				String mes = "send@#all@#" + tfdUserName.getText() + "@#" + tfdMsg.getText();
				pw.println( mes );
				//更新消息栏
				allMsg.append( "您说:" + tfdMsg.getText() + "\r\n" );
			} else {//否则 就是 私发消息
				if ( lm.get(index).equals( tfdUserName.getText() ) ) {
					JOptionPane.showMessageDialog( this, "不能跟自己私聊..." );
					return;
				}
				String mes = "send@#" + lm.get(index) + "@#" + tfdUserName.getText() + "@#" + tfdMsg.getText();
				pw.println( mes );
				allMsg.append( "您悄悄对" + lm.get(index) + "说:" + tfdMsg.getText() + "\r\n" );
			}
			//发送完毕后清空tfdMsg
			tfdMsg.setText( "" );
		}
	}

	/**
	 * 发送退出消息
	 */
	private void sendExitMsg() {
		if ( socket != null ) {
			try {
				PrintWriter pw = new PrintWriter( socket.getOutputStream(), true );
				pw.println( "exit@#all@#" + tfdUserName.getText() + "@#" );
			} catch (IOException e1) {
				e1.printStackTrace();
				return;
			} finally {
				//退出程序
				System.exit( 0 );
			}
		} else {
			System.exit( 0 );
		}
	}
	/**
	 * 2018年5月12日 下午8:09:11
	 * @author 宋进宇
	 *	内部类,用来接收 别的用户发送过来的信息
	 */
	class Receive implements Runnable {

		@Override
		public void run() {
			//如果 socket 就退出
			if ( socket == null ) {
				return;
			}
			BufferedReader br = null;
			try {
				br = new BufferedReader( 
						 new InputStreamReader( socket.getInputStream() ) );
				//只有有消息就接收
				while ( true ) {
					String mes = br.readLine();
					//如果 消息无效 就跳过
					if ( mes == null || mes.trim().length() == 0) {
						continue;
					}
					dealWithMessage( mes );
				}
				
			} catch (IOException e) {
				e.printStackTrace();
				return;
			} 
		}
		/**
		 * 处理服务器发送过来的信息
		 * 信息内容格式 --命令关键字@#发送方@#消息内容
		 * 命令关键字:cmdAdd,cmdRem,msg
		 * 发送方: server/otherUserNaem
		 * 消息内容:userNaem/消息内容
		 * @param mes 服务器发送过来的信息
		 */
		private void dealWithMessage(String mes) {
			String[] strs = mes.split( "@#" );
			if ( strs.length < 3 ) {
				return;
			}
			if ( "cmdAdd".equals( strs[0] ) ) { //处理cmdAdd指令
				//更新  allMsg 
				allMsg.append( "通知:用户[" + strs[2] + "]上线了\r\n" );
				//更新 lm 
				lm.addElement( strs[2] ); //更新数据
				list.validate(); //实时更新表现层
			} else if ( "cmdRem".equals( strs[0] ) ) { //处理cmdRem指令
				//更新 allMsg
				allMsg.append( "通知:用户[" + strs[2] + "]下线了\r\n" );
				//更新 lm
				lm.removeElement( strs[2] ); //更新数据
				list.validate(); //实时更新表现层
			} else if ( "msg".equals( strs[0] ) ) { //处理msg指令
				if ( "server".equals( strs[1] ) ) { //如果是服务器发送的信息说明是用来初始化的在线用户列表的信息
					allMsg.append( "欢迎您登入!\r\n" );
					for (int i = 2; i < strs.length; i++) {
						//更新在线用户列表
						lm.addElement( strs[i] ); //更新数据
						list.validate(); //实时更新表现层
					}
				}else {//否则的话,就是用户之间的通信了
					
					allMsg.append( strs[1] + mes.substring( strs[0].length() + strs[1].length() + 4 ) + "\r\n" );
				}
			} 
			
		}
		
	}
	
	public static void main(String[] args) {
		JFrame.setDefaultLookAndFeelDecorated(true);
		new ClientForm();
	}

}
服务器端:
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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

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;

/**
 * 2018年5月13日 上午7:39:54
 * @author 宋进宇
 *
 */
public class ServerForm extends JFrame {
	private static final long serialVersionUID = 1L;

	//服务器的端口
	private static int PORT = 9999;
	
	private JList list; //在线用户列表--表现层
	private DefaultListModel lm; //在线用户列表--数据层
	
	private JTextArea allInfo = new JTextArea(); //信息显示主窗口
	
	private Map userSockets = new HashMap();

	private JMenuItem itemRun;
	
	public ServerForm() {
		setTitle( "啊哈哈-服务器" );
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		
		//1东 在线用户列表
		lm = new DefaultListModel();
		lm.addElement( "全部" );
		list = new JList( lm );
		list.setVisibleRowCount( 5 );
		JScrollPane jsc = new JScrollPane( list );
		jsc.setBorder( new TitledBorder( "在线" ) );
		jsc.setPreferredSize( new Dimension( 100, this.getHeight() ) );
		getContentPane().add( jsc, BorderLayout.EAST );
		
		//2中  信息主窗口 
		allInfo.setEditable( false );
		getContentPane().add( new JScrollPane( allInfo ) );
		
		//菜单
		JMenuBar menubar = new JMenuBar();
		setJMenuBar( menubar );
		JMenu menu = new JMenu( "控制(C)" );
		menubar.add( menu );
		menu.setMnemonic( 'C' ); //设置菜单助记符
		itemRun = new JMenuItem( "开启" );
		itemRun.setActionCommand( "run" );
		itemRun.setAccelerator(KeyStroke.getKeyStroke( 'R', KeyEvent.CTRL_MASK ) );//设置快捷键: Ctrl+R
		menu.add( itemRun );
		menu.addSeparator();
		JMenuItem itemExit = new JMenuItem( "退出" );
		itemExit.setActionCommand( "exit" );
		itemExit.setAccelerator( KeyStroke.getKeyStroke( 'E', KeyEvent.CTRL_MASK ) );//设置快捷键: Ctrl+E
		menu.add( itemExit );
		
		//监听器
		ActionListener al = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if( e.getActionCommand().equals( "run" ) ){
					new Thread() {//采用 匿名内部类,重写Thread的 run方法
						public void run() {
							serverRun();
						}
					}.start();
					
				}else if( e.getActionCommand().equals( "exit" ) ){
					System.exit( 0 );
				}
				
			}
		};
		//给2个菜单项添加监听
		itemRun.addActionListener( al );
		itemExit.addActionListener( al );
		
		
		
		int winWidth = 500;
		int winHeight = 400;
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		int width = (int) toolkit.getScreenSize().getWidth();
		int height =  (int)toolkit.getScreenSize().getHeight();
		setBounds( width/2 - winWidth/2, height/2 - winHeight/2, winWidth, winHeight );
		
		setVisible(true);
	}
	/**
	 * 启动服务器
	 */
	protected void serverRun() {
		ServerSocket server = null;
		try {
			server = new ServerSocket( PORT );
			//在 服务器信息栏 添加服务器启动信息
			allInfo.append( server + "--服务器启动了\r\n" );
			//启动服务器后,关闭启动菜单项
			itemRun.setEnabled( false );
			while ( true ) {
				Socket s = server.accept();
				new Thread( new UserRun( s ) ).start();
			}
		} catch (IOException e) {
			System.exit( -1 );
		} finally {
			if ( server != null ) {
				try {
					server.close();
				} catch (IOException e) {
				}
			}
		}
	}
	
	class UserRun implements Runnable {
		private Socket s = null;

		public UserRun(Socket s) {
			this.s = s;
		}

		@Override
		public void run() {
			//用户一连接上就获取用户IP
			String userIp = s.getInetAddress().getHostAddress();
			BufferedReader br = null;
			try {
				//能到这里说明一个用户链接成功
				//获取用户名
				//这里规定用户名为一行
				br = new BufferedReader(
						 new InputStreamReader( s.getInputStream() ) );
				
				String userName = br.readLine();
				//把用户信息放人 userSockets 中
				userSockets.put( userName, s );
				//更新allInfo
				allInfo.append( "IP:" + userIp + ",port:" + s.getPort() + ",userName:" + userName + ",上线了!\r\n");
				lm.addElement( userName );
				//把在线的用户信息反馈给当前用户
				initData();
				//同时通知其他用户
				String mes = "cmdAdd@#server@#" + userName;
				notifyOthersUser( mes, userName );
				//通知完毕后等待用户发来信息
				while ( true ) {
					//用户信息
					mes = br.readLine();
					//如果 mes 为无效数据就跳过
					if ( mes == null || mes.trim().length() == 0) {
						continue;
					}
					//解析用户信息
					//用户信息规则:命令关键字@#接收方@#发送方@#消息内容
					//命令:exit/send,接收方:all/otherUserName,发送方:userName,消息内容:...
				
					try {
						dealWithMessage( mes );
					} catch (Exception e) {
						//用户下线就退出循环
						break;
					}
				}
				
			} catch (IOException e) {
			}
		}
		/**
		 * 给用户反馈当前 在线 用户。
		 */
		private void initData() {
			try {
				PrintWriter pw = new PrintWriter( s.getOutputStream(), true );
				StringBuilder sb = new StringBuilder( "msg@#server" );
				Set names = userSockets.keySet();
				for (String name : names) {
					sb.append( "@#" + name );
				}
				pw.println( sb.toString() );
			} catch (IOException e) {
				e.printStackTrace();
				return ;
			}
			
		}

		/**
		 * 处理用户发送的信息,
		 * 用户信息规则:命令关键字@#接收方@#发送方@#消息内容,
		 * 命令:exit/send,
		 * 接收方:all/otherUserName,
		 * 发送方:userName,
		 * 消息内容:...
		 * @param mes 用户发送的信息
		 * @throws Exception 抛出该异常说明用户下线了
		 */
		private void dealWithMessage( String mes ) throws Exception {
			//如果消息无效 就不处理
			if ( mes == null || mes.trim().length() == 0) {
				return;
			}
			String[] strs = mes.split("@#");
			if ( "exit".equals( strs[0] ) ) {//进行退出处理
				String str = "cmdRem@#server@#" + strs[2] ;
				notifyOthersUser( str, strs[2] );
				//同时在 userSockets中移除该用户
				Socket s = userSockets.remove( strs[2] );
				//更新allInfo
				allInfo.append( "IP:" + s.getInetAddress().getHostAddress() + ",port:" + s.getPort() + ",userName:" + strs[2] + ",下线了!\r\n");
				lm.removeElement( strs[2] );
				//关闭s
				s.close();
				throw new Exception();
			} else if ( "send".equals( strs[0] ) ){//进行发送处理
				//组织信息
				String str = "msg@#" + strs[2] + "@#说:" + mes.substring( strs[0].length() + strs[1].length() + strs[2].length() + 6 );
				if ( "all".equals( strs[1] ) ) {//如果是发送给所有人
					System.out.println( str );
					notifyOthersUser( str, strs[2] );
				} else {
					str = "msg@#" + strs[2] + "@#悄悄对您说:" + mes.substring( strs[0].length() + strs[1].length() + strs[2].length() + 6 );
					Socket target = userSockets.get( strs[1] );
					System.out.println( strs[1] );
					PrintWriter pw = new PrintWriter( target.getOutputStream(), true );
					pw.println( str );
				}
			}
		}

		/**
		 * 通知除了  userName 的其他用户。通知内容格式 --命令关键字@#发送方@#消息内容
		 * 命令关键字:cmdAdd,cmdRem,msg
		 * 发送方: server/otherUserNaem
		 * 消息内容:userNaem/消息内容
		 * @param mes 通知内容格式 --命令关键字@#发送方@#消息内容
		 * @param userName 该用户状态
		 */
		private void notifyOthersUser(String mes, String userName) {
			Set> entrys = userSockets.entrySet();
			for (Entry en : entrys) {
				//如果是当前用户就跳过
				if ( en.getKey().equals( userName ) ){
					continue;
				}
				Socket s = en.getValue();
				try {
					PrintWriter out = new PrintWriter( s.getOutputStream(), true );
					//给 除了 userName 的其他所有用户发送消息
					out.println( mes );
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		
	}
	
	public static void main(String[] args) {
		JFrame.setDefaultLookAndFeelDecorated(true);
		new ServerForm();
	}
	
}

你可能感兴趣的:(2.Java,------①知识点,Java)