`
Codestarter
  • 浏览: 11108 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类

网络通信基础总结3——一个简单的有界面的群聊服务器和客户端

阅读更多

 在我的上一篇总结中,我初步完成了一个多线程的服务器,可是还有很多问题有待解决。比如,如何使两台客户机之间可以实现通信?这就需要我们增加一个通信类和通信方法来实现服务器接收到一条消息以后将它转发给另一个线程代表的客户机。当然要实现一个功能相对健全的群聊通信工具远没有那么简单。那么这篇日记中我将一一总结。

 

 

 

 

首先让我来谈谈群聊服务器的实现。我希望实现的功能大体如下:

1.当一个客户机连结上来时,服务器要求客户机输入用户名,密码;

2.如果输入的用户名密码和服务器端所保存的帐号一致,则登陆成功;否则断开。

3.登陆成功后,即给其它客户机提示:###用户进入聊天室,当前在线N

4.登陆成功的客户机可以向服务器发送消息,其它在线的客户端也会收到这条消息;

5.当这个客户端发送bye退出时,或意外断掉时;向其它客户机通知:##离开聊天室;

在分析完需要的功能以后,我就在之前的简陋的服务器基础上,增加了4个类,第一个类为userInfo类,目的是为了模拟真是的用户信息,该类的每一个对象相当于一个用户。第二个类为DaoTools类,这个类主要是为了验证用户名和密码,判断是否可以登录成功!第三个类是ChatTool类,这个类主要是用一个队列来保存每一个客户机的线程,并提供一些处理转发消息,踢人等方法来辅助客户机之间的通信,这个类中的方法将全部被调用,用来实现群聊的功能。最后一个是界面类。当然其它的每一个类中的方法我也做了一定的修改,好了,话不多说,下面我上代码分6步具体分析!

1.我首先写一个userInfo类来模拟用户的信息。这个类中有几个属性,比如用户名,用户密码,用户IP地址等等,然后为这些属性构造一些定义和获取它的方法,一些模拟的用户就产生啦~这个类很简单,这个类的具体代码如下:

 

 

 4.ServerThread线程类:调用一些ChatTools类的方法,接收消息并转发出去,用来处理与客户机之间的通信。这个类在上一篇总结中已经详细说明了,这里的改动不是很大。重点就是要理解一个线程对象就好比打开了一个与客户机联通的话筒,就好比移动公司的客服,肯定会有多个接线员允许同时接听多个电话。这里的思想也差不多,每登录上一台客户机,我们就为它建立一个服务器线程来建立与它通信的管道!这样每一个客户机就对应一个服务器线程了~这个类的代码如下:

 

//处理与客户机通信的线程,验证信息,群发消息
public class ServerThread extends Thread{
	private java.net.Socket client;// 线程中处理的客户对象
	private java.io.OutputStream ous; // 输出流
	private UserInfo user; // 这个线程处理对象对应的用户信息

	public ServerThread(java.net.Socket client) {
		this.client = client;
	}

	// 取得这个线程代表的用户信息
	public UserInfo getUser() {
		return this.user;
	}

	// 发送一条消息的方法
	public void sendMsg(String msg) {
		try {
			msg += "\r\n";
			ous.write(msg.getBytes());
			ous.flush();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	public void run(){
		processThread(client);
	}
	
	public void processThread(java.net.Socket client){
		try {
			InputStream ins = client.getInputStream();
			ous = client.getOutputStream();
			
			String s = "欢迎你!\r\n";
			this.sendMsg(s);
			//将输入流ins封装成可以读取一行字符串的,即以\r\n结尾的一句话
			BufferedReader brd = new BufferedReader(new InputStreamReader(ins));
			//sendMsg("欢迎你来聊天!请输入你的用户名");
			String name = brd.readLine();
			System.out.println("用户名是:"+name);
			//sendMsg(name+":请输入你的密码");
			String pwd = brd.readLine();
			System.out.println("密码是:"+pwd);
			user=new UserInfo();
			user.setName(name);
			user.setPwd(pwd);
			if(!DaoTools.checkLogin(user)){//如果没有验证成功,即登录失败
				this.closeMe();
				return;
			}
			//如果验证成功,就加入这个线程
			ChatTools.addClient(this);
			
			String input = brd.readLine();
			while(!input.equals("bye")){
				System.out.println("服务器收到的是"+input);
				//读到一条消息后,就发送给其它的客户机
				ChatTools.castMsg(user, input);
				input = brd.readLine();
			}
			ChatTools.castMsg(user, "我下线了,再见!");
			ChatTools.removeClient(this);
			
			this.closeMe();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	//关闭这个线程处理对象
	public void closeMe(){
		try{
			client.close();
		}catch(Exception ef){
			ef.printStackTrace();
		}
	}
}

 

 

 5.ChatServer类:由于要实现服务器的启停,我把这个类放到一个独立的线程中去。当服务器启动以后就,这个线程就启动。当停止服务器时则关闭这个线程就好!其余大体的代码和之前的还是很类似的。就不具体解释了:

 

public class ChatServer extends Thread {
	private java.net.ServerSocket sc; // 服务器对象
	private int port; // 端口号
	private boolean isrunning = false; // 服务器是否运行的标识

	// 创建服务其对象的时候传入端口号
	public ChatServer(int port) {
		this.port = port;
	}

	public void run() {
		setupServer();
	}

	public void setupServer() {
		try {
			sc=new ServerSocket(port);
			isrunning = true;
			System.out.println("服务器创建成功:"+port);
			while(isrunning){
			java.net.Socket client = sc.accept();
			System.out.println("连进来的客户机是:"+client.getRemoteSocketAddress().toString());
			//启动线程处理这些连上来的客户机
			ServerThread th = new ServerThread(client);
			th.start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
	//查询服务器是否在运行中,在运行中则为true
	public boolean isrunning(){
		return this.isrunning;
	}
	
	//关闭服务器
	public void stopChatServer(){
		this.isrunning = false;
		try{
			sc.close();
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

 

 6.MainServerUI类:界面类。这里用到了swing中的很多方法。我这方面也不是特别熟悉,所以做得非常的简陋,这个类我主要是继承JTable来实验一个用户表。然后再把之前的各个类中的方法在这个类中进行一个汇总,并启动服务器线程。这样一个简单的有界面的服务器就做好啦!这个类的具体代码如下:

/**
 * 服务器端管理界面程序
 * 1.启/停
 * 2.发布公告消息
 * 3.显示在线用户信息
 * 4.踢人
 * 5.对某一用户发消息
 * @author Administrator
 */
public class MainServerUI extends JFrame{
	private ChatServer cserver; //服务器对象
	static JTable table_onlineUser; // 在线用户表
	private JTextField jtf_msg; //发送消息输入框
	private JTextField jtf_port; //服务器端口号输入框
	private JButton control_chat; //启动服务器的按钮
	public static void main(String[] args) {
		MainServerUI mu = new MainServerUI();
		mu.showUI();
	}
	//初始化界面
	public void showUI(){
		this.setTitle("聊天服务器");
		this.setSize(500,300);
		this.setLayout(new java.awt.FlowLayout());
		
		JLabel la_port= new JLabel("服务器端口:");
		this.add(la_port);
		jtf_port = new JTextField(4);
		this.add(jtf_port);
		control_chat=new JButton("启动服务器");
		this.add(control_chat);
		control_chat.addActionListener(new ActionListener(){

			@Override
			public void actionPerformed(ActionEvent e) {
				actionServer();
				
			}
			
		});
		JLabel la_msg = new JLabel("要发送的消息:");
		this.add(la_msg);
		//服务器要发送消息的输入框
		jtf_msg = new javax.swing.JTextField(30);
		JButton send = new JButton("send");
		ActionListener sendCastMsg = new ActionListener(){

			@Override
			public void actionPerformed(ActionEvent e) {
				// TODO Auto-generated method stub
				sendAllMsg();
			}
			
		};
		//给输入框加上时间监听器,按回车就发送
		jtf_msg.addActionListener(sendCastMsg);
		//给发送按钮加事件监听器发送广播消息
		send.addActionListener(sendCastMsg);
		this.add(jtf_msg);
		this.add(send);
		
		//界面上用以显示在线用户列表的表格
		table_onlineUser = new JTable();
		//创建我们自己的Model对象:创建时,传入处理所有的线程列表
		List<ServerThread> sts = ChatTools.getAllThread();
		UserInfoTableMode utm = new UserInfoTableMode(sts);
		table_onlineUser.setModel(utm);//将模型加给表格
		//将表格对象放到滚动面板对象上
		javax.swing.JScrollPane scrollpane = new JScrollPane(table_onlineUser);
		//设定表格在面板上的大小
		table_onlineUser.setPreferredScrollableViewportSize(new Dimension(400,100));
		//超出大小后,JScrollPane自动生成滚动条
		scrollpane.setAutoscrolls(true);
		this.add(scrollpane);//将scrollpane对象加到界面上
		//取得表格上的弹出菜单对象,加到表格上
		JPopupMenu pop=getTablePop();
		table_onlineUser.setComponentPopupMenu(pop);

		this.setDefaultCloseOperation(3);//关闭时彻底退出,关闭进程
		this.setVisible(true);
	}
	
	/**
	* 创建表格上的弹出菜单对象,实现发信,踢人功能
	* @return:弹出菜单对象,将被加到表格上
	*/
	private JPopupMenu getTablePop(){
		JPopupMenu pop = new JPopupMenu(); //弹出菜单对象
		JMenuItem mi_send = new JMenuItem("发信"); //菜单项对象
		mi_send.setActionCommand("send");
		JMenuItem mi_del = new JMenuItem("踢掉");
		mi_del.setActionCommand("del");
		//弹出菜单上的事件监听器对象
		ActionListener al = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				String s=e.getActionCommand();
				popMenuAaction(s);
			}
			
		};
		mi_send.addActionListener(al);
		mi_del.addActionListener(al);
		pop.add(mi_send);
		pop.add(mi_del);
		return pop;
	}
	
	/**
	 * 处理弹出菜单上的事件
	 * @param command:弹出菜单上的命令
	 */
	private void popMenuAaction(String command){
		//得到在表格上选中的行
		final int selectIndex = table_onlineUser.getSelectedRow();
		if(selectIndex==-1){//如果未选中
			JOptionPane.showMessageDialog(this, "请先选中一个用户");
			return;
		}
		if(command.equals("del")){
			//从队列中移除该线程
			ChatTools.removeOneClient(selectIndex);
		}
	
		else if(command.equals("send")){
			UserInfo user = ChatTools.getUser(selectIndex);
			final JDialog jd = new JDialog(this,true); //发送对话框
			jd.setLayout(new FlowLayout());
			jd.setTitle("您将对"+user.getName()+"发信息");
			jd.setSize(400,100);
			jd.setLocationRelativeTo(null);
			final JTextField jtd_m = new JTextField(20);
			JButton jb=new JButton("发送!");
			jd.add(jtd_m);
			jd.add(jb);
			
			ActionListener al = new ActionListener(){
				public void actionPerformed(ActionEvent e) {
					String msg = "系统悄悄说:"+jtd_m.getText();
					ChatTools.sendMsg2One(selectIndex, msg);
					jtd_m.setText("");//清空输入框
					jd.dispose();	
				}
			};
			
			jb.addActionListener(al);
			jtd_m.addActionListener(al);
			
			jd.setVisible(true);
		}else{
			JOptionPane.showMessageDialog(this, "未知菜单:"+command);
		}
		//刷新表格
		SwingUtilities.updateComponentTreeUI(table_onlineUser);
	}
	
	//响应启动/停止按钮!
	public void actionServer(){
		if(null==cserver){
			//如果还没有服务器线程就启动服务器的线程,
			int port = Integer.parseInt(jtf_port.getText());
			cserver = new ChatServer(port);
			cserver.start();
			this.setTitle("服务器正在运行中");
			control_chat.setText("stop!");
		}else if(cserver.isrunning()){
			cserver.stopChatServer();
			cserver = null;
			//清楚所有已在运行的程序
			ChatTools.removeAllClient();
			this.setTitle("服务器已停止!");
			control_chat.setText("start!");
		}
	}
	
	//按下发送服务器的消息,给所有的在线用户发送消息
	private void sendAllMsg(){
		String msg = jtf_msg.getText();
		UserInfo user = new UserInfo();
		user.setName("系统");
		ChatTools.castMsg(user, msg);
		jtf_msg.setText("");//清空输入框
		
	}
}

 

 

其中的那个UserInfoTableMode类我是继承了TableModel类的接口,制作了一个表格。不过还有一些问题需要改进,比如每次登录上一个客户信息,就把上一个客户信息给覆盖了,由于我swing不是很熟悉,所以这只是一个简单的表格模型。该类代码如下:

 

 

 

 

 

 

public class UserInfoTableMode implements TableModel{ private List<ServerThread> list; public UserInfoTableMode(List<ServerThread> sts) { this.list = sts; } // 多少行 public int getRowCount() { return list.size(); } // 多少列 public int getColumnCount() { return 3; } // 得到列名 public String getColumnName(int columnIndex) { if (columnIndex == 0) { return "用户名"; } if (columnIndex == 1) { return "密码"; } if (columnIndex == 2) { return "IP地址"; } else { return null; } } // 每一列的数据类型:我们这里显示的都是String类型 public Class<?> getColumnClass(int columnIndex) { return String.class; } // 指定的单元格可否从界面上编辑 public boolean isCellEditable(int rowIndex, int columnIndex) { // TODO Auto-generated method stub return true; } // 取得单元格的值 public Object getValueAt(int rowIndex, int columnIndex) { String s = null; rowIndex = list.size(); for(int i=0;i<list.size();i++){ s=list.get(i).getUser().getName(); } return s; } // 从表格界面上改变了某个单元格的值后会调用这个方法 public void setValueAt(Object aValue, int rowIndex, int columnIndex) { String s = "Change at" + rowIndex + "--" + columnIndex + "newValue:" + aValue; System.out.println(s); } @Override public void addTableModelListener(TableModelListener l) { // TODO Auto-generated method stub } @Override public void removeTableModelListener(TableModelListener l) { // TODO Auto-generated method stub } }

 


 

 

 

 

 以上就是做一个有界面的简单服务器的全部流程,当然我还有很多的地方需要改进。比如目前还不能实现踢人的功能,程序会报错。还有服务器端的客户列表也有问题,当然这些我会在以后的代码中努力改进的!

 

做完服务器之后,我们就来做一个简易的客户端。客户端的流程和服务器差不多,客户端通过写消息给服务器,再接收服务器发送过来的消息就可以啦!那么再写完服务器之后,我觉得做一个客户端不再是一件难的事情了。下面我分2步谈谈具体的实现!

1.NetClient类:负责对通信操作的方法封装,在独立的线程中运行,接收到消息后,显示到界面上。并提供写出消息给服务器以及验证用户的方法,供界面主类调用!具体代码如下:

public class NetClient extends Thread {
	private java.net.Socket client;
	private String IP; // IP地址
	private int port; // 端口
	private OutputStream ous; // 输出流对象
	private BufferedReader brd; // 输入流对象
	private JTextArea jta_input; // 显示消息的组件,从界面上传来,每读取一条消息就把它append到界面上

	public NetClient(String IP, int port, JTextArea jta_input) {
		this.IP = IP;
		this.port = port;
		this.jta_input = jta_input;
	}
	
	public java.net.Socket getClient(){
		return this.client;
	}
	
	/**
	 * 判断是否和服务器连接
	 * 
	 * @return:连接上返回true
	 */
	public boolean connect2Server() {

		try {
			client = new Socket(this.IP, this.port); // 与客户端建立连接
			InputStream ins = client.getInputStream(); // 得到输入输出流
			brd = new BufferedReader(new InputStreamReader(ins));
			ous = client.getOutputStream();
			return true;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * 判断是否成功
	 * 
	 * @param name
	 *            :要写入的用户名
	 * @param pwd
	 *            :要写入的密码
	 * @return:登录成功返回true
	 */
	public boolean isLogin(String name, String pwd) {
		try {
			// .判断用户名密码是否正确
			// String input = brd.readLine();//读取服务器发来的一条消息
			// System.out.println("服务器说:"+input);
			// 写入用户名和密码
			name += "\r\n";
			ous.write(name.getBytes());
			ous.flush();
			// input = brd.readLine();//读取服务器发来的验证密码的消息;
			pwd += "\r\n";
			ous.write(pwd.getBytes());
			ous.flush();
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

	}

	public void run() {
		while (true) {
			readFromServer();//不停的接收消息
		}
	}

	public void readFromServer() {
		try {
			String input = brd.readLine();
			//System.out.println("服务器:" + input);
			if(!input.equals(null)){
			jta_input.append(input + "\r\n");
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void sendMsg(String msg) {
		try {
			msg += "\r\n";
			ous.write(msg.getBytes());
			ous.flush();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

  

2.MainNetUI类:负责客户端登陆,显示接收消息,发送消息界面提供。这主要还是调用NetClient类的方法,然后做一个Swing界面即可,具体代码如下:

public class MainClientUI {
	private JFrame jf_login; //登录主界面
	private JFrame jf_chat; // 聊天主界面
	private JTextField userName; // 登录界面上的用户名,密码和IP地址的输入框
	private JTextField jta_pwd; 
	private JTextField jta_IP ;
	private JTextArea jta_recive = new JTextArea(10,20); //显示接收到的消息组件
	private NetClient conn; //界面所要用的连接对象
	
	public static void main(String[] args) {
		MainClientUI mu = new MainClientUI();
		mu.showLoginUI();
	}
	
	public void showLoginUI(){
		jf_login =new JFrame("聊天登录界面");
		jf_login.setSize(250,250);
		jf_login.setLayout(new java.awt.FlowLayout());
		
		jta_IP = new JTextField(15);  //IP输入框,默认为localhost
		jta_IP.setText("localhost");
		userName = new JTextField(15);   //用户名密码输入框
		jta_pwd = new JTextField(15);
		
		JLabel IP = new JLabel("IP地址:"); // IP地址,用户名,密码的标签
		JLabel name = new JLabel("用户名:");  
		JLabel pwd = new JLabel("密码:");
		
		jf_login.add(IP);
		jf_login.add(jta_IP);
		jf_login.add(name);
		jf_login.add(userName);
		jf_login.add(pwd);
		jf_login.add(jta_pwd);
		
		JButton but_login = new JButton("登录");
		JButton but_reg = new JButton("注册");
		but_login.addActionListener(new ActionListener(){

			@Override
			public void actionPerformed(ActionEvent e) {
				// TODO Auto-generated method stub
				LoginAction();
			}
			
		});
		jf_login.add(but_login);
		jf_login.add(but_reg);
		
		jf_login.setDefaultCloseOperation(3);
		jf_login.setLocationRelativeTo(null);
		jf_login.setVisible(true);
	}
	//登录事件的处理
	public void LoginAction(){
		String name = userName.getText();//读取输入框中的用户名和密码
		String pwd = jta_pwd.getText();
		String IP = jta_IP.getText();
		conn = new NetClient(IP,9090,jta_recive);
		if(conn.connect2Server()){//如果连接上服务器,就验证用户名和密码
			if(conn.isLogin(name, pwd)){//如果用户名和密码验证正确,就打来聊天界面,并启动通信线程,接收消息
				showChatUI();
				conn.start();
				jf_login.dispose(); //关闭登陆界面
			}else{
				javax.swing.JOptionPane.showMessageDialog(jf_login, "用户名或密码不正确!");
			}
		}else{
			javax.swing.JOptionPane.showMessageDialog(jf_login, "没有连上服务器,请检查网络!");
		}
	}
	
	public void showChatUI(){
		jf_chat = new JFrame("聊天界面");
		jf_chat.setSize(300,500);
		jf_chat.setLayout(new java.awt.FlowLayout());
		
		JLabel la_recive = new JLabel("接收到的消息");
		JLabel la_send = new JLabel ("要发送的消息");
		
		final JTextArea jta_send = new JTextArea(10,20);//消息发送框
		
		jf_chat.add(la_recive);
		jf_chat.add(jta_recive);
		jf_chat.add(la_send);
		jf_chat.add(jta_send);
		
		JButton bu_send = new JButton("发送!");
		
		ActionListener al = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				String msg = jta_send.getText();
				conn.sendMsg(msg);
				jta_send.setText("");//清空输出框
			}
		};
		
		bu_send.addActionListener(al);
		
		if(conn.getClient().isClosed()){
			System.out.println(">>>>>>>>>>");
			jf_chat.dispose();
		}
		
		
		jf_chat.add(la_recive);
		jf_chat.add(jta_recive);
		jf_chat.add(la_send);
		jf_chat.add(jta_send);
		jf_chat.add(bu_send);
		
		jf_chat.setDefaultCloseOperation(3);
		jf_chat.setLocationRelativeTo(null);
		jf_chat.setVisible(true);
	}
}

  

以上一个简单的客户端就建成啦~当然这个客户端也是有很多弊端的 它必须得按照服务器的流程来读写消息,对于一个大型的通信的项目,这未免太过繁琐!所以下一篇总结中,我将用XMPP协议来解决这一问题~

      总的来说,做一个通信这样的大项目,思路一定要清晰!要有全局观!最后就是要细心!拥有以上几点~攻克服务器和客户端就不是什么困难啦!

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics