Java—聊天室的实现
在学习了IO流,多线程以及网络编程的知识之后,我们可以利用所学到的知识做一个小项目,这里我做了一个多人聊天室,实现了群聊和私聊功能,看完分享之后也可以自己去做一个练练手。
首先是整个项目的大体架构:首先要分为服务器端和客户端两个端口。如下图所示
客户端可以向服务器发送信息,并接受服务器返回的信息。而服务器实际上是作为一个中转站:在群聊模式时,将一个客户端的发送的信息转发至其他客户端。而在私聊时,服务器将信息发送到指定的客户端处,达到私聊的效果。
我们先依次附上服务器端与客户端的代码,再讲解实现的具体过程
服务器端(这里用本地主机作为服务器):
public class Server {private List<MyChannel> all = new ArrayList<MyChannel>();public static void main(String[] args) throws IOException {new Server().start();}public void start() throws IOException {ServerSocket server = new ServerSocket(7777);while(true) {Socket client = server.accept();MyChannel channel = new MyChannel(client);all.add(channel);new Thread(channel).start(); //一条道路}}/*** 一个客户一条道路* 建立服务器与客户端之间的数据通道**/private class MyChannel implements Runnable {private DataInputStream dis;private DataOutputStream dos;private boolean flag = true;private String name;public MyChannel(Socket client) {try {dis = new DataInputStream(client.getInputStream());dos = new DataOutputStream(client.getOutputStream());} catch (IOException e) {e.printStackTrace();flag = false;try {dis.close();dos.close();} catch (IOException e1) {e1.printStackTrace();}}}//接收客户端的信息private String receive() {String msg = "";try {msg = dis.readUTF();} catch (IOException e) {flag = false;e.printStackTrace();all.remove(this);try {dis.close();} catch (IOException e1) {e1.printStackTrace();}}return msg;}//向客户端发送信息private void send(String msg) {if (null == msg || msg.equals("")) {return;}try {dos.writeUTF(time());dos.writeUTF(msg);} catch (IOException e) {flag = false;e.printStackTrace();all.remove(this); //移除自身try {dos.close();} catch (IOException e1) {e1.printStackTrace();}}}private void sendOthers(String msg) {//判断是否是私聊if (msg.contains("@") && msg.indexOf(":") > msg.indexOf("@")){String spot = null;String secreName = msg.substring(msg.indexOf("@") + 1, msg.indexOf(":"));String secretMsg = msg.substring(msg.indexOf(":") + 1);
// System.out.println(secreName);
// System.out.println(secretMsg);for (MyChannel other : all) {if (secreName.equals(other.name)) {other.send(name + "悄悄地对你说:" + secretMsg);}}}else{for (MyChannel other : all) {if (other == this) {continue;}other.send(msg);}}}private String time () {Date now = new Date(System.currentTimeMillis());String time = new SimpleDateFormat("yyyy.MM.dd hh:mm:ss").format(now);return time;}@Overridepublic void run () {send("欢迎加入群聊");name = receive();sendOthers(name + "加入了群聊");while (flag) {sendOthers(receive());}}}}
现在我们来分析这段代码
- 1> 首先,Server类中包含一个ArrayList容器,其中保存的是MyChannel类元素,实际上每一个MyChannel类的对象就是一条 连接服务器与客户端的路径(其中数据以流的形式传输)
- 2> 在主函数中实例化了Server对象之后,调用了start()方法,我们看到方法体中首先为服务器端创建了ServerSocket对象,并指定了端口。紧接着就是一个死循环,接受连接到服务器的客户端,并将其信息添加至ArrayList容器中,并为其创建一条线程(线程就绪并开始运行)
- 3> 那么MyChannel类内部又是什么呢?我们画图来分析
- 4> 构造器:只有一个以Socket类的客户端对象为参数的构造器,在该构造器中,传入了客户端对象后,建立于客户端对象的数据通道。
- 5> receive()方法:从数据通道中读取从客户端发送来的信息(字符串)
- 6> time()方法:生成当前的具体时间,并以字符串形式返回
- 7> send(String msg)方法:将time()与msg信息依次发送到客户端
- 8> sendOthers(String msg):私聊部分(私聊形式:@客户端名:私聊信息):先对传入的msg进行分析,若字符串首字符为 ‘@’ 并且字符串中含有 ‘:’时,即判断这条信息是一条私聊信息,将私聊客户端名与私聊内容从msg中分离出来,并将其单独发送给指定的客户端(遍历容器并匹配客户端名)。群聊部分(直接发送内容):遍历整个容器,除了当前客户端,向其他所有客户端发送消息。
- 9> 线程体部分(run()方法 ):在一个客户端接入服务器后,首先向其发送一条”欢迎加入群聊”的信息,再将其加入聊天室的信息发送给其他聊天室内的用户,然后执行死循环 sendOthers(receive()) 方法
客户端(包含三个类):
- 客户端类:
public class Client {public static void main(String[] args) throws IOException {System.out.println("请输入昵称");BufferedReader br = new BufferedReader(new InputStreamReader(System.in));String name = br.readLine();Socket client = new Socket("localhost",7777);//客户端发送new Thread(new Send(client,name)).start();//客户端接收new Thread(new Receive(client)).start();}}
我们对客户端代码进行分析:
- Client 类中只有一个主方法,刚开始会要求你为客户端起名,紧接着创建Socket类实例,并与本地服务器的指定端口连接。
- 多线程的问题:我们不能规定客户端是先读取再发送还是先发送再读取,所以为两个功能分别建立一条线程,即读与写可以同时实现,而接受和发送信息的类就是下面要说的Receive类和Send类
- Receive类:
public class Receive implements Runnable{//输入流private DataInputStream dis;//线程标识,判断线程运行状态private boolean flag = true;public Receive(Socket client){try {dis = new DataInputStream(client.getInputStream());} catch (IOException e) {e.printStackTrace();flag = false;try {dis.close();} catch (IOException e1) {e1.printStackTrace();}}}//接收数据public String receive(){String msg = "";try {msg = dis.readUTF();} catch (IOException e) {e.printStackTrace();flag = false;try {dis.close();} catch (IOException e1) {e1.printStackTrace();}}return msg;}@Overridepublic void run() {while(flag){System.out.println(receive());}}
}
- Receive类是负责接收从服务器发来的消息的类,主要有三个地方使用:1.刚与服务器连接后服务器发来的欢迎信息的接收。2.群聊信息的接收(时间+内容)。3.私聊信息的接收(时间+私聊内容)
- receive()方法,从输入流中读取信息。
- 线程体(run()方法):只有一个始终接收信息并打印到控制台的循环体
- Send 类:
public class Send implements Runnable{//控制台输入流private BufferedReader console;//管道输出流private DataOutputStream dos;//控制线程private boolean flag= true;//聊天昵称private String name;public Send() {console = new BufferedReader(new InputStreamReader(System.in));}public Send(Socket client,String name){this();this.name = name;try {dos = new DataOutputStream(client.getOutputStream());} catch (IOException e) {//e.printStackTrace();flag = false;}}//从控制台接收数据private String getMsgFromConsole() {try {return console.readLine();} catch (IOException e) {e.printStackTrace();}return "";}//发送数据public void send(String msg){if (null != msg && !msg.equals("")){try {dos.writeUTF(name +": " + msg);Date now = new Date(System.currentTimeMillis());String time = new SimpleDateFormat("hh:mm:ss yyyy/MM/dd ").format(now);System.out.println(time);System.out.println(name + ":" + msg);dos.flush();} catch (IOException e) {e.printStackTrace();try {flag = false;dos.close();console.close();} catch (IOException e1) {e1.printStackTrace();}}}else{try {dos.writeUTF(name);} catch (IOException e) {e.printStackTrace();}}}@Overridepublic void run() {//线程体send("");while (flag){send(getMsgFromConsole());}}
}
- Send类负责从控制台读入客户端所输入的信息,并将信息发送至服务器,由服务器判断是私聊信息还是群聊信息。
- 构造器:有一个无参构造器和一个参数为客户端对象和客户端名的构造器,建立对服务器的数据输出流,并保存客户端名
- 线程体(run()方法):先发送一个空字符串,send()方法会自动判断为发送客户端名至服务器,然后再循环体中始终执行发送从控制台读取的字符至服务器的方法
- send()方法:初次发送时,会将客户端名发送至服务器,在之后将从控制台读取信息并发送至服务器,同时在自己的控制台上打印自己发送消息的时间和消息的内容
调试结果
在IDEA 的控制台上运行服务器,将客户端类打jar包在本地Powershell上运行
第一个客户端接入:
服务器控制台信息:
多个客户端接入
服务器控制台信息:
群聊实现:
私聊实现: