服务端
public class Server
{
private static final int SERVER_PORT = 30000;
// 使用CrazyitMap对象来保存每个客户名字和对应输出流之间的对应关系。
public static CrazyitMap<String , PrintStream> clients
= new CrazyitMap<>();
public void init()
{
try(
// 建立监听的ServerSocket
ServerSocket ss = new ServerSocket(SERVER_PORT))
{
// 采用死循环来不断接受来自客户端的请求
while(true)
{
Socket socket = ss.accept();
new ServerThread(socket).start();
}
}
// 如果抛出异常
catch (IOException ex)
{
System.out.println("服务器启动失败,是否端口"
+ SERVER_PORT + "已被占用?");
}
}
public static void main(String[] args)
{
Server server = new Server();
server.init();
}
}
服务端实现
public class ServerThread extends Thread
{
private Socket socket;
BufferedReader br = null;
PrintStream ps = null;
// 定义一个构造器,用于接收一个Socket来创建ServerThread线程
public ServerThread(Socket socket)
{
this.socket = socket;
}
@Override
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);
}
// 公聊要向每个Socket发送
else
{
// 得到真实消息
String msg = getRealMsg(line);
// 遍历clients中的每个输出流
for (PrintStream clientPs : Server.clients.valueSet())
{
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);
}
}
客户端
public class Client
{
private static final int SERVER_PORT = 30000;
private Socket socket;
private PrintStream ps;
private BufferedReader brServer;
private BufferedReader keyIn;
public void init()
{
try
{
// 初始化代表键盘的输入流
keyIn = new BufferedReader(
new InputStreamReader(System.in));
// 连接到服务器
socket = new Socket("127.0.0.1", SERVER_PORT);
// 获取该Socket对应的输入流和输出流
ps = new PrintStream(socket.getOutputStream());
brServer = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String tip = "";
// 采用循环不断地弹出对话框要求输入用户名
while(true)
{
String userName = JOptionPane.showInputDialog(tip
+ "输入用户名"); //①
// 将用户输入的用户名的前后增加协议字符串后发送
ps.println(CrazyitProtocol.USER_ROUND + userName
+ CrazyitProtocol.USER_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);
}
// 以该Socket对应的输入流启动ClientThread线程
new ClientThread(brServer).start();
}
// 定义一个读取键盘输出,并向网络发送的方法
private void readAndSend()
{
try
{
// 不断读取键盘输入
String line = null;
while((line = keyIn.readLine()) != null)
{
// 如果发送的信息中有冒号,且以//开头,则认为想发送私聊信息
if (line.indexOf(":") > 0 && line.startsWith("//"))
{
line = line.substring(2);
ps.println(CrazyitProtocol.PRIVATE_ROUND +
line.split(":")[0] + CrazyitProtocol.SPLIT_SIGN
+ line.split(":")[1] + CrazyitProtocol.PRIVATE_ROUND);
}
else
{
ps.println(CrazyitProtocol.MSG_ROUND + line
+ CrazyitProtocol.MSG_ROUND);
}
}
}
// 捕捉到异常,关闭网络资源,并退出该程序
catch (IOException ex)
{
System.out.println("网络通信异常!请重新登录!");
closeRs();
System.exit(1);
}
}
// 关闭Socket、输入流、输出流的方法
private void closeRs()
{
try
{
if (keyIn != null)
{
ps.close();
}
if (brServer != null)
{
ps.close();
}
if (ps != null)
{
ps.close();
}
if (socket != null)
{
keyIn.close();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
public static void main(String[] args)
{
Client client = new Client();
client.init();
client.readAndSend();
}
}
客户端实现
public class Client
{
private static final int SERVER_PORT = 30000;
private Socket socket;
private PrintStream ps;
private BufferedReader brServer;
private BufferedReader keyIn;
public void init()
{
try
{
// 初始化代表键盘的输入流
keyIn = new BufferedReader(
new InputStreamReader(System.in));
// 连接到服务器
socket = new Socket("127.0.0.1", SERVER_PORT);
// 获取该Socket对应的输入流和输出流
ps = new PrintStream(socket.getOutputStream());
brServer = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String tip = "";
// 采用循环不断地弹出对话框要求输入用户名
while(true)
{
String userName = JOptionPane.showInputDialog(tip
+ "输入用户名"); //①
// 将用户输入的用户名的前后增加协议字符串后发送
ps.println(CrazyitProtocol.USER_ROUND + userName
+ CrazyitProtocol.USER_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);
}
// 以该Socket对应的输入流启动ClientThread线程
new ClientThread(brServer).start();
}
// 定义一个读取键盘输出,并向网络发送的方法
private void readAndSend()
{
try
{
// 不断读取键盘输入
String line = null;
while((line = keyIn.readLine()) != null)
{
// 如果发送的信息中有冒号,且以//开头,则认为想发送私聊信息
if (line.indexOf(":") > 0 && line.startsWith("//"))
{
line = line.substring(2);
ps.println(CrazyitProtocol.PRIVATE_ROUND +
line.split(":")[0] + CrazyitProtocol.SPLIT_SIGN
+ line.split(":")[1] + CrazyitProtocol.PRIVATE_ROUND);
}
else
{
ps.println(CrazyitProtocol.MSG_ROUND + line
+ CrazyitProtocol.MSG_ROUND);
}
}
}
// 捕捉到异常,关闭网络资源,并退出该程序
catch (IOException ex)
{
System.out.println("网络通信异常!请重新登录!");
closeRs();
System.exit(1);
}
}
// 关闭Socket、输入流、输出流的方法
private void closeRs()
{
try
{
if (keyIn != null)
{
ps.close();
}
if (brServer != null)
{
ps.close();
}
if (ps != null)
{
ps.close();
}
if (socket != null)
{
keyIn.close();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
public static void main(String[] args)
{
Client client = new Client();
client.init();
client.readAndSend();
}
}
服务端封装的 Map,为聊天室提供存储用户的功能
// 通过组合HashMap对象来实现CrazyitMap,CrazyitMap要求value也不可重复
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 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 = "※";
}