实现了私聊和群聊功能的聊天工具


前面的博客(简单的C/S聊天室)中,我们已经提到了,采用的是多线程的方法。服务器端主线程负责不断的侦听端口,子线程负责接收和发送消息。客户端主线程需要接收键盘消息,将其发送到服务器端,子线程需要接收服务器端发过来的消息。在这个简易的C/S聊天室的实现中,仅仅实现了群聊的功能,没有实现私聊。那么,本文就讲实现私聊和群聊。


首先我们想到的是,消息发过来,我怎么知道是公聊消息还是私聊消息呢。所以,这里需要对消息进行处理,比如说在消息前后都加上一些特殊的字符,我们称为协议字符。为此,我们可以定义一个接口,专门来定义协议字符。

第二个问题就是,如果是私聊信息,客户端会将目的用户(私聊对象)发给服务器端,那么服务器端是如何将找到那个目的用户的呢。这里,很明显,我们需要建立一个用户和Socket的映射关系,所以我们采用了map,但是这里的map我们需要改进一下,因为其实我们这里不仅仅是key不能重复,而且value也不能重复,我们也需要通过value能够查找到key,所以我们进行了改进。

还有一点针对本实现需要指出的是,服务器子线程负责接收和发送消息,这里面也包括客户端首次建立连接的时候,需要判断用户名是否重复,也就是要保证key不重复,于此想对应的,客户端在首次建立连接时,其需要进行不断的尝试,直到提供的名字不重复为止。

代码如下:


public interface CrazyitProtocol {
	public static final int PROTOCOL_LEN=2; //默认的类型就是public static final,不加也是可以的
	
	public static final String MSG_ROUND="△▽";
	public static final String USR_ROUND="□☆";
	public static final String LOGIN_SUCCESS="☆▷";
	public static final String NAME_REP="-1";
	public static final String PRAVITE_ROUND="◆★";
	public static final String SPLIT_SIGN="☀";

}

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;


public class CrazyitMap<K,V> extends HashMap<K,V> {

	// 根据value来删除指定项
	public void removeByValue(Object value)
	{
		for(Object key :keySet())
		{
			if(get(key)==value||get(key).equals(value))
			{
				remove(key);
				break;
			}
		}
	}
	
	// 获取value集合
	public Set<V> valueSet()
	{
		Set<V> result=new HashSet<V>();
		for(Object key : keySet())
		{
			result.add(get(key));
		}
		return result;
	}
	
	// 重写HashMap的put方法,该方法不允许value重复
	public V put(K key,V value)
	{
		for(V val : valueSet())
		{
			if(val==value||val.equals(value))
			{
				throw new RuntimeException("MyMap实例中不允许有重复value");
			}
		}
		return super.put(key, value);	
	}
	
	// 通过value查找key
	public K getKeyByValue(Object value)
	{
		for(K key : keySet())
		{
			if(get(key)==value||get(key).equals(value))
			{
				return key;
			}
		}
		return null;
	}
}

import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;


public class Server {
	
	private static final int PORT=30000;
	public static CrazyitMap<String,PrintStream> clients=new CrazyitMap<>();
	
	void init()
	{
		try (
			ServerSocket ss=new ServerSocket(PORT);
		)
		{
			while(true)
			{
				Socket s=ss.accept();
				new Thread(new ServerThread(s)).start();
			}
			
		}catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println("服务器启动失败,是否端口被占用?");
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Server s=new Server();
		s.init();

	}

}

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;


public class ServerThread implements Runnable {
	
	private Socket s;
	private BufferedReader br=null;
	private PrintStream ps=null;
	
	public ServerThread(Socket s)
	{
		this.s=s;
		
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		try {
			br=new BufferedReader(new InputStreamReader(s.getInputStream()));
			ps=new PrintStream(s.getOutputStream());
			String content=null;
			while((content=br.readLine())!=null)
			{
				if(content.startsWith(CrazyitProtocol.USR_ROUND) //发过来的是名字信息
						&&content.startsWith(CrazyitProtocol.USR_ROUND))
				{
					String userName=getRealMsg(content);
					if(Server.clients.containsKey(userName)) // 姓名重复
					{
						System.out.println("重复");
						ps.println(CrazyitProtocol.NAME_REP);
						
					}
					else // 姓名不重复
					{
						System.out.println("成功");
						Server.clients.put(userName, ps);
						ps.println(CrazyitProtocol.LOGIN_SUCCESS);
					}
				}
				else if(content.startsWith(CrazyitProtocol.PRAVITE_ROUND)
						&&content.startsWith(CrazyitProtocol.PRAVITE_ROUND))// 发过来的是实际的消息,且为私聊消息
				{
					String userAndMsg=getRealMsg(content);
					String userName=userAndMsg.split(CrazyitProtocol.SPLIT_SIGN)[0];
					String Msg=userAndMsg.split(CrazyitProtocol.SPLIT_SIGN)[1];
					// 获取私聊用户的输出流
					Server.clients.get(userName).println(Server.clients.getKeyByValue(ps)
							+"悄悄的对你说"+Msg);
					
				}
				else // 公聊信息
				{
					String Msg=getRealMsg(content);
					for(PrintStream ps : Server.clients.valueSet())
					{
						ps.println(Server.clients.getKeyByValue(this.ps)
								+"说:"+Msg);
					}
				}
			}
		} 
		// 捕获异常,表明该Socket对应的客户端已出现问题,
		// 所以客户端将其对应的输出流从Map中删除
		catch (IOException e) {
			// TODO Auto-generated catch block
			Server.clients.removeByValue(ps);
			try{
				if(br!=null)
				{
					br.close();
				}
				if(ps!=null)
				{
					ps.close();
				}
				if(s!=null)
				{
					s.close();
				}
			}
			catch(IOException ex)
			{
				ex.printStackTrace();
			}
		}

	}
	// 讲读到的内容去掉前后的协议字符,恢复为真实数据
	private String getRealMsg(String content) {
		// TODO Auto-generated method stub
		
		return content.substring(CrazyitProtocol.PROTOCOL_LEN, 
				content.length()-CrazyitProtocol.PROTOCOL_LEN);
	}

}

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;

import javax.swing.JOptionPane;


public class Client {
	
	private static final int PORT=30000;
	private Socket s=null;
	private PrintStream ps=null;
	private BufferedReader brServer=null; //服务器发送过来的内容
	private BufferedReader keyIn=null; // 键盘输入内容
	public void init()
	{
		try
		{
		s=new Socket("127.0.0.1",PORT);
		ps=new PrintStream(s.getOutputStream());
		keyIn=new BufferedReader(new InputStreamReader(System.in));
		brServer=new BufferedReader(new InputStreamReader(s.getInputStream()));
		
		// 用于在服务器端登录,因为名字有可能重复
		String tip="";
		while(true)
		{
			String userName=JOptionPane.showInputDialog(tip
					+"输入用户名");
			
			// 在用户输入的用户名前后增加协议字符串后发送
			ps.println(CrazyitProtocol.USR_ROUND+userName
					+CrazyitProtocol.USR_ROUND);
			String result=brServer.readLine();
			if(result.equals(CrazyitProtocol.NAME_REP))
			{
				tip="用户名重复,请重新";
				continue;
			}
			// 登录成功
			if(result.equals(CrazyitProtocol.LOGIN_SUCCESS))
			{
				break;
			}
		}
		}
		catch(UnknownHostException ex)
		{
			System.out.println("找不到远程服务器,请确定服务器已启动!");
			closeRs();
			System.exit(1);
		}
		catch(IOException ex)
		{
			System.out.println("网络异常!请重新登录!");
			closeRs();
			System.exit(1);
		}
		
		new Thread(new ClientThread(brServer)); // 子线程负责接收服务器端传过来的消息
		
	}
	

	private void closeRs() {
		// TODO Auto-generated method stub
		try{
		if(keyIn!=null)
		{
			keyIn.close();
		}
		if(brServer!=null)
		{
			brServer.close();
		}
		
		if(ps!=null)
		{
			ps.close();
		}
		
		if(s!=null)
		{
			s.close();
		}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		
	}
	
	// 主线程的接收键盘消息函数
	public void readAndSend()
	{
		String content=null;
		try
		{
		while((content=keyIn.readLine())!=null)
		{
			// 所发消息中以/开头,且有:则认为是是私聊信息
			if(content.startsWith("/")&&content.indexOf(":")>0) 
			{
				content=content.substring(1); //消息中不需要带开头的/
				content=CrazyitProtocol.PRAVITE_ROUND+content.split(":")[0]+CrazyitProtocol.SPLIT_SIGN
						+content.split(":")[1]+CrazyitProtocol.PRAVITE_ROUND;
				ps.println(content);
				
			}
			else // 群聊信息
			{
				content=CrazyitProtocol.MSG_ROUND+content+CrazyitProtocol.MSG_ROUND;
				ps.println(content);
			}
		}
			
		}
		catch(IOException e)
		{
			System.out.println("网络通信异常!请重新登录!");
			closeRs();
			System.exit(1);
		}
	}
	


	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Client client=new Client();
		client.init();
		client.readAndSend();

	}

}

import java.io.BufferedReader;
import java.io.IOException;


public class ClientThread implements Runnable {
	
	private BufferedReader brServer=null;
	public ClientThread(BufferedReader brServer)
	{
		this.brServer=brServer;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		String content=null;
		
		try
		{
		while((content=brServer.readLine())!=null)
		{
			System.out.println(content);
		}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		finally
		{
			try{
			if(brServer!=null)
			{
				brServer.close();
			}
			}
			catch(IOException e)
			{
				e.printStackTrace();
			}
		}

	}

}

参考资料:JAVA疯狂讲义


  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值