完成聊天室私聊功能。私聊功能是指,客户端之间可以实现一对一的聊天。
服务器端程序启动后,将等待客户端连接,界面效果如图-1所示:
图-1
客户端程序运行时,需要用户先输入昵称。用户输入昵称之后,提示用户可以开始聊天。界面效果如图-2所示:
图-2
另一个客户端运行起来后,也需要输入昵称,界面效果如图-3所示:
图-3
此时,其他运行中的客户端会收到昵称为“jerry”的客户端上线的消息。比如,之前运行起来的客户端“mary”的界面效果如图-4所示:
图-4
其他客户端可以通过输入类似“\jerry:你好”这样的字样和昵称为“jerry”的客户端私聊。比如,昵称为“mary”的客户端可以输入如图-5所示的信息:
图-5
注意:如果需要进行私聊,必需使用“\昵称:信息”的格式发送消息。其中,“\昵称:”为固定格式,“昵称”表示要私聊的客户端的昵称;“信息”表示需要发送的消息。例如:"\jerry:你好",表示发送消息“你好”给昵称为“jerry”的客户端。
昵称为“jerry”的客户端将接收到客户端“mary”发来的信息,界面效果如图-6所示:
图-6
如果某客户端程序停止运行,其他客户端程序可以接收到消息并显示。例如,昵称为“jerry”的客户端停止运行,昵称为“mary”的客户端的界面效果如图-7所示:
图-7
对于服务器端而言,只要有客户端连接,就会在界面输出提示信息。界面效果如图-8所示:
图-8
参考答案
实现此案例需要按照如下步骤进行。
步骤一:创建客户端类
新建名为com.tarena.homework的包,并在包下新建名为Client的类,用于表示客户端。
在Client 类中声明全局变量 socket 表示一个客户端Socket对象,并在实例化 Client 类时使用构造方法“Socket(String ip,int port)”来创建Socket类的对象。此时,需要进行异常处理。代码如下所示:
package com.tarena.homework; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /** * 客户端应用程序 */ public class Client { //客户端Socket private Socket socket; /** * 构造方法,用于初始化 */ public Client(){ try { socket = new Socket("localhost",8088); } catch (Exception e) { e.printStackTrace(); } } }
步骤二:定义客户端线程要执行的任务
在Client类中定义成员内部类ServerHander。该内部类需要实现Runnable接口并实现该接口的run() 方法。在该方法中实现线程要执行的任务,在此,线程要执行的任务为循环接收服务端的消息并打印到控制台。代码如下所示:
public class Client { //其他代码,略 /** * 该线程用于接收服务端发送过来的信息 */ #cold_bold private class ServerHander implements Runnable{ #cold_bold @Override #cold_bold public void run() { #cold_bold try { #cold_bold InputStream in = socket.getInputStream(); #cold_bold InputStreamReader isr = new InputStreamReader(in, "UTF-8"); #cold_bold BufferedReader br = new BufferedReader(isr); #cold_bold while(true){ #cold_bold System.out.println(br.readLine()); #cold_bold } #cold_bold } catch (Exception e) { #cold_bold e.printStackTrace(); #cold_bold } #cold_bold } #cold_bold } }
步骤三:定义方法inputNickName(),用于输入昵称
为Client类定义方法inputNickName(),用于输入昵称。代码如下所示:
public class Client { //其他代码,略 /** * 输入昵称 */ #cold_bold private void inputNickName(Scanner scanner)throws Exception{ #cold_bold //定义昵称 #cold_bold String nickName = null; #cold_bold //创建输出流 #cold_bold PrintWriter pw = new PrintWriter( #cold_bold new OutputStreamWriter( #cold_bold socket.getOutputStream(),"UTF-8") #cold_bold ,true); #cold_bold //创建输入流 #cold_bold BufferedReader br = new BufferedReader( #cold_bold new InputStreamReader( #cold_bold socket.getInputStream(),"UTF-8") #cold_bold ); #cold_bold /* #cold_bold * 循环以下操作 #cold_bold * 输入用户名,并上传至服务器,等待服务器回应,若昵称可用就结束循环,否则通知用户后 #cold_bold * 重新输入昵称 #cold_bold */ #cold_bold while(true){ #cold_bold System.out.println("请输入昵称:"); #cold_bold nickName = scanner.nextLine(); #cold_bold if(nickName.trim().equals("")){ #cold_bold System.out.println("昵称不能为空"); #cold_bold }else{ #cold_bold pw.println(nickName); #cold_bold String pass = br.readLine(); #cold_bold if(pass!=null&&!pass.equals("OK")){ #cold_bold System.out.println("昵称已被占用,请更换。"); #cold_bold }else{ #cold_bold System.out.println("你好!"+nickName+",开始聊天吧!"); #cold_bold break; #cold_bold } #cold_bold } #cold_bold } #cold_bold } }
步骤四:创建客户端工作方法 start()
为 Client 类创建客户端工作方法 start()。在该方法中,首先调用方法inputNickName()得到用户昵称,然后启动接收服务端信息的线程,接收数据后打印显示。
代码如下所示:
public class Client { //其他代码,略 /** * 客户端工作方法 */ #cold_bold public void start(){ #cold_bold try { #cold_bold //创建Scanner读取用户输入内容 #cold_bold Scanner scanner = new Scanner(System.in); #cold_bold //首先输入昵称 #cold_bold inputNickName(scanner); #cold_bold #cold_bold //将接收服务端信息的线程启动 #cold_bold ServerHander handler = new ServerHander(); #cold_bold Thread t = new Thread(handler); #cold_bold t.setDaemon(true); #cold_bold t.start(); #cold_bold #cold_bold OutputStream out = socket.getOutputStream(); #cold_bold OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); #cold_bold PrintWriter pw = new PrintWriter(osw,true); #cold_bold while(true){ #cold_bold pw.println(scanner.nextLine()); #cold_bold } #cold_bold } catch (Exception e) { #cold_bold e.printStackTrace(); #cold_bold } finally{ #cold_bold if(socket != null){ #cold_bold try { #cold_bold socket.close(); #cold_bold } catch (IOException e) { #cold_bold e.printStackTrace(); #cold_bold } #cold_bold } #cold_bold } #cold_bold } }
步骤五:为客户端类定义 main() 方法
为类 Client 定义 main() 方法,并在该方法中,创建 Client 对象,调用上一步中所创建的 start() 方法。代码如下所示:
/** * 客户端应用程序 */ public class Client { //其他代码,略 #cold_bold public static void main(String[] args) { #cold_bold Client client = new Client(); #cold_bold client.start(); #cold_bold } }
步骤六:定义 Server类
定义Server类,并在Server类中添加ExecutorService类型的属性threadPool,并在构造方法中将其初始化。初始化时,使用固定大小的线程池,线程数量为40。这里使用Executors类的newFixedThreadPool(int threads)方法来创建固定大小的线程池。定义属性serverSocket,其类型为ServerSocket,并在构造方法中将其初始化,申请的服务端口为8088。再定义属性allOut,其类型为HashMap,其中key用于保存用户昵称,value用于保存该客户端的输出流,并在构造方法中初始化以便服务端可以转发信息。
代码如下所示:
package com.tarena.homework; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 服务端应用程序 */ public class Server { // 服务端Socket private ServerSocket serverSocket; // 所有客户端输出流,key为用户的昵称,value为该用户的输出流 private Map<String,PrintWriter> allOut; // 线程池 private ExecutorService threadPool; /** * 构造方法,用于初始化 */ public Server() { try { serverSocket = new ServerSocket(8088); allOut = new HashMap<String,PrintWriter>(); threadPool = Executors.newFixedThreadPool(40); } catch (Exception e) { e.printStackTrace(); } } }
步骤七:为 Server 类定义 addOut()和removeOut()方法
定义 addOut()方法,该方法向Server的属性allOut集合中添加输出流,并使用synchronized关键字修饰,使该方法变为同步方法。
再定义removeOut()方法,该方法从Server的属性allOut集合中删除输出流,并使用synchronized关键字修饰,使该方法变为同步方法。
代码如下所示:
public class Server { //其他代码,略 /** * 将输出流存入共享集合,与下面两个方法互斥,保证同步安全 * @param out */ #cold_bold private synchronized void addOut(String nickName,PrintWriter out){ #cold_bold allOut.put(nickName,out); #cold_bold } /** * 将给定输出流从共享集合删除 * @param out */ #cold_bold private synchronized void removeOut(String nickName){ #cold_bold allOut.remove(nickName); #cold_bold } }
步骤八:为 Server 类定义sendMessage()方法
定义sendMessage()方法,该方法用于遍历Server的属性allOut集合元素,将信息写入每一个输出流来完成广播消息的功能,并使用synchronized关键字修饰,使该方法变为同步方法。代码如下所示:
public class Server { //其他代码,略 /** * 将消息转发给所有客户端 * @param message */ #cold_bold private synchronized void sendMessage(String message){ #cold_bold for(PrintWriter o : allOut.values()){ #cold_bold o.println(message); #cold_bold } #cold_bold } }
步骤九:为 Server 类定义sendMessageToOne() 方法
定义sendMessageToOne()方法,该方法用于将消息发送给指定昵称的客户端来实现私聊功能。代码如下所示:
public class Server { //其他代码,略 /** * 将消息发送给指定昵称的客户端 * @param nickName * @param message */ #cold_bold private synchronized void sendMessageToOne(String nickName,String message){ #cold_bold PrintWriter out = allOut.get(nickName); #cold_bold if(out!=null){ #cold_bold out.println(message); #cold_bold } #cold_bold } }
步骤十:创建内部类
创建 Server的内部类ClientHandler,在内部类中定义run()方法。在run()方法中,读取用户昵称以发送用户上线信息,并进行消息转发,其中先判断是否为私聊信息,若是则调用发送私聊信息的方法,否则向所有客户端广播消息 。代码如下所示:
/** * 线程体,用于并发处理不同客户端的交互 */ private class ClientHandler implements Runnable { // 该线程用于处理的客户端 private Socket socket; // 开客户端的昵称 private String nickName; public ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { PrintWriter pw = null; try { //将客户端的输出流存入共享集合,以便广播消息 OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); pw = new PrintWriter(osw,true); /* * 将用户信息存入共享集合 * 需要同步 */ //先获取该用户昵称 nickName = getNickName(); addOut(nickName,pw); Thread.sleep(100); /* * 通知所有用户该用户已上线 */ sendMessage(nickName+"上线了"); InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; // 循环读取客户端发送的信息 while ((message = br.readLine())!=null) { //首先查看是不是私聊 if(message.startsWith("\\")){ /* * 私聊格式:\昵称:内容 */ //找到:的位置 int index = message.indexOf(":"); if(index>=0){ //截取昵称 String name = message.substring(1,index); //截取内容 String info = message.substring( index+1,message.length() ); //拼接内容 info = nickName+"对你说:"+info; //发送私聊信息给指定用户 sendMessageToOne(name, info); //发送完私聊后就不在广播了。 continue; } } /* * 遍历所有输出流,将该客户端发送的信息转发给所有客户端 * 需要同步 */ sendMessage(nickName+"说:"+message); } } catch (Exception e) { e.printStackTrace(); } finally { /* * 当客户端断线,要将输出流从共享集合中删除 * 需要同步 */ removeOut(nickName); /* * 通知所有用户该用户已下线 */ sendMessage(nickName+"下线了"); System.out.println("当前在线人数:"+allOut.size()); if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
步骤十一:为内部类定义方法getNickName()
为 Server的内部类ClientHandler定义方法getNickName(),用于获取用户的昵称。代码如下所示:
private class ClientHandler implements Runnable { //其他代码,略 /** * 获取该用户的昵称 * @return */ #cold_bold private String getNickName()throws Exception{ #cold_bold try { #cold_bold //获取该用户的输出流 #cold_bold OutputStream out = socket.getOutputStream(); #cold_bold OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); #cold_bold PrintWriter pw = new PrintWriter(osw,true); #cold_bold //获取该用户的输入流 #cold_bold InputStream in = socket.getInputStream(); #cold_bold InputStreamReader isr = new InputStreamReader(in, "UTF-8"); #cold_bold BufferedReader br = new BufferedReader(isr); #cold_bold //读取客户端发送过来的昵称 #cold_bold String nickName = br.readLine(); #cold_bold while(true){ #cold_bold //若昵称为空发送失败代码 #cold_bold if(nickName.trim().equals("")){ #cold_bold pw.println("FAIL"); #cold_bold } #cold_bold //若昵称已经存在发送失败代码 #cold_bold if(allOut.containsKey(nickName)){ #cold_bold pw.println("FAIL"); #cold_bold //若成功,发送成功代码,并返回昵称 #cold_bold }else{ #cold_bold pw.println("OK"); #cold_bold return nickName; #cold_bold } #cold_bold //若改昵称被占用,等待用户再次输入昵称 #cold_bold nickName = br.readLine(); #cold_bold } #cold_bold } catch (Exception e) { #cold_bold throw e; #cold_bold } #cold_bold } }
步骤十二:为 Server 类创建 start()方法
为 Server 类创建 start()方法。在该方法中,循环监听8088端口,等待客户端的连接,一旦一个客户端连接后,向线程池申请一个线程来完成针对该客户端的交互。代码如下所示:
public class Server { //其他代码,略 /** * 服务端开启方法 */ #cold_bold public void start() { #cold_bold try { #cold_bold //循环监听客户端的连接 #cold_bold while(true){ #cold_bold System.out.println("等待客户端连接..."); #cold_bold // 监听客户端的连接 #cold_bold Socket socket = serverSocket.accept(); #cold_bold System.out.println("客户端已连接!"); #cold_bold #cold_bold //启动一个线程来完成针对该客户端的交互 #cold_bold ClientHandler handler = new ClientHandler(socket); #cold_bold threadPool.execute(handler); #cold_bold } #cold_bold } catch (Exception e) { #cold_bold e.printStackTrace(); #cold_bold } #cold_bold } }
步骤十三:为 Server类定义 main() 方法
为 Server 类定义 main() 方法,并在 main() 方法中,创建 Server 对象,调用上一步中所创建的 start() 方法。代码如下所示:
public class Server { //其他代码,略 #cold_bold public static void main(String[] args) { #cold_bold Server server = new Server(); #cold_bold server.start(); #cold_bold } }
本案例中,类Server的完整代码如下所示:
package com.tarena.homework; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 服务端应用程序 */ public class Server { // 服务端Socket private ServerSocket serverSocket; // 所有客户端输出流,key为用户的昵称,value为该用户的输出流 private Map<String,PrintWriter> allOut; // 线程池 private ExecutorService threadPool; /** * 构造方法,用于初始化 */ public Server() { try { serverSocket = new ServerSocket(8088); allOut = new HashMap<String,PrintWriter>(); threadPool = Executors.newFixedThreadPool(40); } catch (Exception e) { e.printStackTrace(); } } /** * 服务端开启方法 */ public void start() { try { //循环监听客户端的连接 while(true){ System.out.println("等待客户端连接..."); // 监听客户端的连接 Socket socket = serverSocket.accept(); System.out.println("客户端已连接!"); //启动一个线程来完成针对该客户端的交互 ClientHandler handler = new ClientHandler(socket); threadPool.execute(handler); } } catch (Exception e) { e.printStackTrace(); } } /** * 将输出流存入共享集合,与下面两个方法互斥,保证同步安全 * @param out */ private synchronized void addOut(String nickName,PrintWriter out){ allOut.put(nickName,out); } /** * 将给定输出流从共享集合删除 * @param out */ private synchronized void removeOut(String nickName){ allOut.remove(nickName); } /** * 将消息转发给所有客户端 * @param message */ private synchronized void sendMessage(String message){ for(PrintWriter o : allOut.values()){ o.println(message); } } /** * 将消息发送给指定昵称的客户端 * @param nickName * @param message */ private synchronized void sendMessageToOne(String nickName,String message){ PrintWriter out = allOut.get(nickName); if(out!=null){ out.println(message); } } public static void main(String[] args) { Server server = new Server(); server.start(); } /** * 线程体,用于并发处理不同客户端的交互 */ private class ClientHandler implements Runnable { // 该线程用于处理的客户端 private Socket socket; // 开客户端的昵称 private String nickName; public ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { PrintWriter pw = null; try { //将客户端的输出流存入共享集合,以便广播消息 OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); pw = new PrintWriter(osw,true); /* * 将用户信息存入共享集合 * 需要同步 */ //先获取该用户昵称 nickName = getNickName(); addOut(nickName,pw); Thread.sleep(100); /* * 通知所有用户该用户已上线 */ sendMessage(nickName+"上线了"); InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; // 循环读取客户端发送的信息 while ((message = br.readLine())!=null) { //首先查看是不是私聊 if(message.startsWith("\\")){ /* * 私聊格式:\昵称:内容 */ //找到:的位置 int index = message.indexOf(":"); if(index>=0){ //截取昵称 String name = message.substring(1,index); //截取内容 String info = message.substring(index+1,message.length()); //拼接内容 info = nickName+"对你说:"+info; //发送私聊信息给指定用户 sendMessageToOne(name, info); //发送完私聊后就不在广播了。 continue; } } /* * 遍历所有输出流,将该客户端发送的信息转发给所有客户端 * 需要同步 */ sendMessage(nickName+"说:"+message); } } catch (Exception e) { e.printStackTrace(); } finally { /* * 当客户端断线,要将输出流从共享集合中删除 * 需要同步 */ removeOut(nickName); /* * 通知所有用户该用户已下线 */ sendMessage(nickName+"下线了"); System.out.println("当前在线人数:"+allOut.size()); if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 获取该用户的昵称 * @return */ private String getNickName()throws Exception{ try { //获取该用户的输出流 OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); PrintWriter pw = new PrintWriter(osw,true); //获取该用户的输入流 InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); //读取客户端发送过来的昵称 String nickName = br.readLine(); while(true){ //若昵称为空发送失败代码 if(nickName.trim().equals("")){ pw.println("FAIL"); } //若昵称已经存在发送失败代码 if(allOut.containsKey(nickName)){ pw.println("FAIL"); //若成功,发送成功代码,并返回昵称 }else{ pw.println("OK"); return nickName; } //若改昵称被占用,等待用户再次输入昵称 nickName = br.readLine(); } } catch (Exception e) { throw e; } } } }
本案例中,类Client的完整代码如下所示:
package com.tarena.homework; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /** * 客户端应用程序 */ public class Client { //客户端Socket private Socket socket; /** * 构造方法,用于初始化 */ public Client(){ try { socket = new Socket("localhost",8088); } catch (Exception e) { e.printStackTrace(); } } /** * 客户端工作方法 */ public void start(){ try { //创建Scanner读取用户输入内容 Scanner scanner = new Scanner(System.in); //首先输入昵称 inputNickName(scanner); //将接收服务端信息的线程启动 ServerHander handler = new ServerHander(); Thread t = new Thread(handler); t.setDaemon(true); t.start(); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); PrintWriter pw = new PrintWriter(osw,true); while(true){ pw.println(scanner.nextLine()); } } catch (Exception e) { e.printStackTrace(); } finally{ if(socket != null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Client client = new Client(); client.start(); } /** * 输入昵称 */ private void inputNickName(Scanner scanner)throws Exception{ //定义昵称 String nickName = null; //创建输出流 PrintWriter pw = new PrintWriter( new OutputStreamWriter( socket.getOutputStream(),"UTF-8") ,true); //创建输入流 BufferedReader br = new BufferedReader( new InputStreamReader( socket.getInputStream(),"UTF-8") ); /* * 循环以下操作 * 输入用户名,并上传至服务器,等待服务器回应,若昵称可用就结束循环,否则通知用户后 * 重新输入昵称 */ while(true){ System.out.println("请输入昵称:"); nickName = scanner.nextLine(); if(nickName.trim().equals("")){ System.out.println("昵称不能为空"); }else{ pw.println(nickName); String pass = br.readLine(); if(pass!=null&&!pass.equals("OK")){ System.out.println("昵称已被占用,请更换。"); }else{ System.out.println("你好!"+nickName+",开始聊天吧!"); break; } } } } /** * 该线程用于接收服务端发送过来的信息 */ private class ServerHander implements Runnable{ @Override public void run() { try { InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); while(true){ System.out.println(br.readLine()); } } catch (Exception e) { e.printStackTrace(); } } } }