JAVA TCP多线程类多客户端QQ通信

JAVA TCP多线程类QQ通信

这是基于李刚老师疯狂JAVA的17.3.5 TCP通讯, 做的图形化界面
首先我们看下效果图

这是客户端
输入你的名字, 数据库还没弄…
在这里插入图片描述

界面的右边会显示在线用户
客户端
正常输入是公聊, 如果在后面加//client name:则是对名称为name的用户私聊
如下图:
在这里插入图片描述
还可以点击侧边栏的按键得到私聊框
在这里插入图片描述

点开前记得打开服务器

下面进入代码环节:
这里只展示关键代码

我们大致完成QQ的功能, 大致需要解决两个问题:

  1. 为了使服务器辨认客户端发送来的消息, 是私聊还是公聊, 我们需要标识信息( 用类, 还是字符串之类的传送, 一次还是分几次? )
  2. 如果是私聊消息, 客户端会发送该消息的目的用户( 私聊对象 )给服务器端, 服务器端将如何将给信息会发送给对应客户端

[1]书上为了解决信息的标识, 王刚老师做了一个接口CrazyitProtocol, 用于定义特殊标识字符, 不妨叫做协议字符, 协议字符用于在各方( 客户端, 服务器端 )接收时判断信息种类, 这可以使得我们能够解决第一个问题( 同过字符串解决的, 其他的应该是比较困难, 或者不是多线程? ).
像这样的

public interface CrazyitProtocol
{
	// 定义协议字符串的长度
	int PROTOCOL_LEN = 2;
	// 下面是一些协议字符串,服务器和客户端交换的信息
	// 都应该在前、后添加这种特殊字符串。
	String MSG_ROUND = "§γ";
	String USER_ROUND = "∏∑";
	String LOGIN_SUCCESS = "1";
	String NAME_REP = "-1";
	String PRIVATE_ROUND = "★【";
	String SPLIT_SIGN = "※";
	String SEND_LIST = "@1";
	String SENTENCE_END = ". ";
}

[2]为了解决第二个问题, 使用了一个类Map的东西, 书上的意思好像是不需要直接用Map因为用不到那么多的方法( 其实直接用Map的子类就行了看, 可能也是方法不全吧… ),这个类的目的只是保存了对应用户名和对应输出流映射的关系, 在服务器端提供了一个HashMap的子类 ,该类不允许value重复, 并提供了跟据value获取Key( 跟据PrinStream获取userName ), 删除等操作.
像这种的

public class CrazyitMap<K,V>
{
	// 创建一个线程安全的HashMap
	public Map<K ,V> map = Collections.synchronizedMap(new HashMap<K,V>());
	// 根据value来删除指定项
	public synchronized void removeByValue(Object value)
	{
		for (Object key : map.keySet())
		{
			if (map.get(key) == value)
			{
				map.remove(key);
				break;
			}
		}
	}
	// 获取所有value组成的Set集合
	public synchronized Set<V> valueSet()
	{
		Set<V> result = new HashSet<V>();
		// 将map中所有value添加到result集合中
		map.forEach((key , value) -> result.add(value));
		return result;
	}
	// 根据value查找key。
	public synchronized K getKeyByValue(V val)
	{
		// 遍历所有key组成的集合
		for (K key : map.keySet())
		{
			// 如果指定key对应的value与被搜索的value相同,则返回对应的key
			if (map.get(key) == val || map.get(key).equals(val))
			{
				return key;
			}
		}
		return null;
	}
	// 实现put()方法,该方法不允许value重复
	public synchronized V put(K key,V value)
	{
		// 遍历所有value组成的集合
		for (V val : valueSet() )
		{
			// 如果某个value与试图放入集合的value相同
			// 则抛出一个RuntimeException异常
			if (val.equals(value)
				&& val.hashCode()== value.hashCode())
			{
				throw new RuntimeException("MyMap实例中不允许有重复value!");
			}
		}
		return map.put(key , value);
	}
	
	public synchronized Set<K> keySet( )
	{
		return map.keySet( );
	}
	
	public boolean isEmpty( )
	{
		return map.isEmpty( );
	}
}

下面是程序的建立

服务器的主类建立ServerSocket来监听客户端的服务请求
在这里插入图片描述

public class Server
{
	private static final int SERVER_PORT = 30000;
	// 使用CrazyitMap对象来保存每个客户名字和对应输出流之间的对应关系。
	public static CrazyitMap<String , PrintStream> clients
		= new CrazyitMap<>();
	public void init()
	{
		int Only = 0;
		try(
			// 建立监听的ServerSocket
			ServerSocket ss = new ServerSocket(SERVER_PORT))
		{
			// 采用死循环来不断接受来自客户端的请求
			while(true)
			{
				Socket socket = ss.accept();
				new ServerThread(socket).start();
				if( Only == 0 )
				{timingSend( );Only++;}
			}
			
		}
		// 如果抛出异常
		catch (IOException ex)
		{
			System.out.println("服务器启动失败,是否端口"
				+ SERVER_PORT + "已被占用?");
		}
	}
	
	// Timing to pass online List message
	public void timingSend( )
	{
		final Timer timer = new Timer( 2000, new ActionListener( )
		{
			public void actionPerformed( ActionEvent e )
			{
				System.out.println( clients.isEmpty( ) );
				if( !clients.isEmpty( ) )		// For stability
				{
					String result = CrazyitProtocol.SEND_LIST;
						
					for( String temp_Str : clients.keySet( ) )
					{
						result = result.concat( ":" );
						result = result.concat( temp_Str );
					}
					result = result.concat( CrazyitProtocol.SEND_LIST );
						
					for( PrintStream clientPs : clients.valueSet( ) )
					{
						clientPs.println( result );
					}
				}
			}
		});
		timer.start( );
	}

	public static void main(String[] args)
	{
		Server server = new Server();
		server.init();
	}
}

关键部分仍是, 这三行,如程序中粗体表示.他们完成建立ServerSocket, 监听客户端Socket的连接请求, 并为已连接的客户端启动单独的线程
ServerSocekt ss = new ServerSocket( SERVER_PORT );
}
while( true )
{
Socket socket = ss.accept( );
new ServerThread( socket ).start( );

}

为每个客户端服务的Socket要实现注册用户名是否重复, 公聊, 私聊, 定时向客户端发送列表信息等功能

import java.net.*;
import java.io.*;
/**
 * Description:
 * <br/>网站: <a href="http://www.crazyit.org">疯狂Java联盟</a>
 * <br/>Copyright (C), 2001-2016, Yeeku.H.Lee
 * <br/>This program is protected by copyright laws.
 * <br/>Program Name:
 * <br/>Date:
 * @author Yeeku.H.Lee kongyeeku@163.com
 * @version 1.0
 */
public class ServerThread extends Thread
{
	private Socket socket;
	BufferedReader br = null;
	PrintStream ps = null;
	// 定义一个构造器,用于接收一个Socket来创建ServerThread线程
	public ServerThread(Socket socket)
	{
		this.socket = socket;
	}
	public void run()
	{
		try
		{
			// 获取该Socket对应的输入流
			br = new BufferedReader(new InputStreamReader(socket
				.getInputStream()));
			// 获取该Socket对应的输出流
			ps = new PrintStream(socket.getOutputStream());
			String line = null;
			while((line = br.readLine())!= null)
			{
				// 如果读到的行以CrazyitProtocol.USER_ROUND开始,并以其结束,
				// 可以确定读到的是用户登录的用户名
				if (line.startsWith(CrazyitProtocol.USER_ROUND)
					&& line.endsWith(CrazyitProtocol.USER_ROUND))
				{
					// 得到真实消息
					String userName = getRealMsg(line);
					// 如果用户名重复
					if (Server.clients.map.containsKey(userName))
					{
						System.out.println("重复");
						ps.println(CrazyitProtocol.NAME_REP);
					}
					else
					{
						System.out.println("成功");
						ps.println(CrazyitProtocol.LOGIN_SUCCESS);
						Server.clients.put(userName , ps);
					}
				}
				// 如果读到的行以CrazyitProtocol.PRIVATE_ROUND开始,并以其结束,
				// 可以确定是私聊信息,私聊信息只向特定的输出流发送
				else if (line.startsWith(CrazyitProtocol.PRIVATE_ROUND)
					&& line.endsWith(CrazyitProtocol.PRIVATE_ROUND))
				{
					// 得到真实消息
					String userAndMsg = getRealMsg(line);
					// 以SPLIT_SIGN分割字符串,前半是私聊用户,后半是聊天信息
					String user = userAndMsg.split(CrazyitProtocol.SPLIT_SIGN)[0];
					String msg = userAndMsg.split(CrazyitProtocol.SPLIT_SIGN)[1];
					// 获取私聊用户对应的输出流,并发送私聊信息
					Server.clients.map.get(user).println( " " + Server.clients
						.getKeyByValue(ps) + "对你说: " + msg );
					// private chat for their Sender, Send back to Sender
					ps.println( " 对" + user + "说: " + msg );
				}
				// 公聊要向每个Socket发送
				else
				{
					// 得到真实消息
					String msg = getRealMsg(line);
					// 遍历clients中的每个输出流
					for (PrintStream clientPs : Server.clients.valueSet())
					{
					// judge what's client said,and each handle
						if( Server.clients.getKeyByValue(ps).equals( Server.clients.getKeyByValue( clientPs ) ) )
						{
							clientPs.println( " 我说:" + msg);
						}
						else
						{
							clientPs.println( " " + Server.clients.getKeyByValue(ps)
								+ "说:" + msg);
						}
					}
				}

			}
		}
		// 捕捉到异常后,表明该Socket对应的客户端已经出现了问题
		// 所以程序将其对应的输出流从Map中删除
		catch (IOException e)
		{
			Server.clients.removeByValue(ps);
			System.out.println(Server.clients.map.size());
			// 关闭网络、IO资源
			try
			{
				if (br != null)
				{
					br.close();
				}
				if (ps != null)
				{
					ps.close();
				}
				if (socket != null)
				{
					socket.close();
				}
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
		}
	}
	// 将读到的内容去掉前后的协议字符,恢复成真实数据
	private String getRealMsg(String line)
	{
		return line.substring(CrazyitProtocol.PROTOCOL_LEN
			, line.length() - CrazyitProtocol.PROTOCOL_LEN);
	}
}

王刚老师的程序具有相当强的健壮性, 对关闭的客户端移除Map中, 避免之后的程序崩溃
对协议的处理可以看到有
if (line.startsWith(CrazyitProtocol.USER_ROUND)
&& line.endsWith(CrazyitProtocol.USER_ROUND))
如果发过来的信息前后带有CrazyitProtocol.USER_ROUND则认为他是客户端发送的用户信息下面就是对它的处理

客户端没什么太特别的于是全部的代码我也上传上去
试的话只用点这几个.bat文件就行了
在这里插入图片描述
下面是链接
https://download.csdn.net/download/jsp13270124/10883017

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值