用户端
1.
Client.java
package com.qfedu.a_charoom.client;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author Anonymous* @description* @date 2020/3/9 10:01** 完成内容* 1. 连接服务器* 2. 启动接收端线程和发送端线程* 需要注意:* 提供给服务器一个用户名,这里是在连接Socket获取之后,第一次发送数据* 给服务器时需要提供的,也就是第一次启动Send发送线程完成的*/
public class Client {public static void main(String[] args) {// 从键盘上获取用户输入的数据BufferedReader br = new BufferedReader(new InputStreamReader(System.in));Socket socket = null;String name = null;try {System.out.println("请输入你的用户名:");name = br.readLine();if ("".equals(name)) {return;}// 存在连接异常情况,考虑捕获异常处理socket = new Socket("192.168.31.154", 8888);} catch (IOException e) {System.err.println("连接失败!!!");try {br.close();} catch (IOException ex) {ex.printStackTrace();}System.exit(0);}// 使用线程池启动两个线程,一个是发送一个接受ExecutorService pool = Executors.newFixedThreadPool(2);pool.submit(new ClientSend(socket, name));pool.submit(new ClientReceive(socket));}
}
ClientReceive.java
package com.qfedu.a_charoom.client;import com.qfedu.a_charoom.util.CloseUtil;import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;/*** @author Anonymous* @description* @date 2020/3/9 10:01** 客户端接收数据线程** 这里需要输入流* 输入流是通过Socket对象获取的,也就是在客户端连接服务器之后才可以获取到输入流* 成员变量:* 输入流* DataInputStream* 标记是否连接* 构造方法:* 需要Socket作为当前构造的参数* 成员方法:* 从服务器接收数据,展示*/
public class ClientReceive implements Runnable {/*** 用于接收数据的输入流对象*/private DataInputStream inputStream;/*** 是否连接状态标记*/private boolean connection;/*** 根据客户端连接服务器对应的Socket对象获取输入流对象** @param socket 客户端连接服务器对应的Socket*/public ClientReceive(Socket socket) {try {inputStream = new DataInputStream(socket.getInputStream());connection = true;} catch (IOException e) {e.printStackTrace();connection = false;}}/*** 接收数据并且展示*/public void receive() {String msg = null;try {msg = inputStream.readUTF();} catch (IOException e) {e.printStackTrace();/*接收数据出现异常:连接标记修改不是null关闭资源*/connection = false;CloseUtil.closeAll(inputStream);}System.out.println(msg);}/*** 线程核心方法,只要连接状态OK,始终保存接收状态*/@Overridepublic void run() {while (connection) {receive();}}
}
ClientSend.java
package com.qfedu.a_charoom.client;import com.qfedu.a_charoom.util.CloseUtil;import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;/*** @author Anonymous* @description 发送端* @date 2020/3/9 10:01** 这里需要输出流* 输出流对象需要通过Socket获取,在当前客户端连接到服务器之后,Socket对象存在* 的情况下才可以启动的* 成员变量:* 输出流* DataOutputStream* 需要一个从键盘上输入数据使用的输入流,获取用户输入信息* BufferedReader* 标记是否连接* 构造方法:* 需要Socket作为当前构造的参数,第一次访问服务器需要带有用户名,用于注册* 成员方法:* 发送数据给服务器* 从键盘上获取用户的数据*/
public class ClientSend implements Runnable {/*** 基于Socket获取的输出流对象,用于发送数据给服务器*/private DataOutputStream outputStream;/*** 从键盘上获取用户输入的BufferedReader字符缓冲输入流*/private BufferedReader console;/*** 是否连接*/private boolean connection;/*** 使用客户端和服务器连接使用的Socket对象,和用户指定的用户名创建* ClientSend线程对象,同时初始化输出流和键盘录入输入流对象** @param socket 客户端连接服务器对应的Socket对象* @param userName 用户指定的用户名,用于服务器注册*/public ClientSend(Socket socket, String userName) {// 初始化输出流和输出流try {outputStream = new DataOutputStream(socket.getOutputStream());console = new BufferedReader(new InputStreamReader(System.in));// 发送用户名给服务器注册 需要完成一个send方法send(userName);connection = true;} catch (IOException e) {e.printStackTrace();// 连接标记关闭,同时处理对应的输入流和键盘录入流connection = false;CloseUtil.closeAll(outputStream, console);}}/*** 从键盘上获取用户输入的数据** @return 用户输入的数据字符串形式*/public String getMsgFromConsole() {String msg = null;try {// 从键盘上读取一行数据msg = console.readLine();} catch (IOException e) {e.printStackTrace();/*发生异常:1. connection连接标记改成false2. 不是null需要关闭资源outputStream, console*/connection = false;CloseUtil.closeAll(outputStream, console);}return msg;}/*** 发送数据给服务器** @param msg 需要发送给服务器的数据*/public void send(String msg) {// 如果这里数据为null,或者"" 不发送try {if (msg != null && !"".equals(msg)) {outputStream.writeUTF(msg);outputStream.flush();}} catch (IOException e) {e.printStackTrace();/*发生异常:1. connection连接标记改成false2. 不是null需要关闭资源outputStream, console*/connection = false;CloseUtil.closeAll(outputStream, console);}}/*** 线程代码,只要当前connection是连接状态,一直执行send和getMsgFromConsole*/@Overridepublic void run() {while (connection) {send(getMsgFromConsole());}}
}
服务端
Server.java
package com.qfedu.a_charoom.server;import com.qfedu.a_charoom.util.CloseUtil;import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;/*** @author Anonymous* @description* @date 2020/3/9 11:12** 服务器需要转发数据* 1. 数据转发* 群聊* 私聊* 2. 用户注册* 用户 ==> class** 私聊,群聊属于用户功能* 用户这里看做是一个类* 成员变量* 输入流* 输出流* 用户ID* 用户名* 连接状态标记* 成员方法:* 接收方法* 利用客户端连接服务器对应的Socket得到输入流接收用户发送的数据* 发送方法* 群聊* 遍历整个有效用户* 私聊* 找到对应用户* 利用客户端连接服务器对应的Socket得到输出流发送数据* 【成员内部类】* 用户做出一个成员内部类* 作为Server服务器类的一个成员变量内部类** 用户注册流程* 1. ServerSocket Accept客户端连接,获取对应Socket对象* 2. 记录在线人数,创建一个新的UserSocket* 3. Map中映射对应的UserSocket,Key 为ID, Value是UserSocket**/
public class Server {/*** 用户ID和UserSocket映射*/private HashMap<Integer, UserSocket> userMap;/*** 累加访客人数*/private static int count = 0;/*** Server构造方法,用于初始化底层保存数据的HashMap双边队列*/public Server() {userMap = new HashMap<>();}/*** 服务器启动方法** @throws IOException IO异常*/public void start() throws IOException {// 启动服务器,同时监听8888端口ServerSocket serverSocket = new ServerSocket(8888);System.out.println("服务器启动");// 服务器始终处于一个保存连接的状态while (true) {// 接收客户端请求,得到一个Socket对象Socket socket = serverSocket.accept();// 创建UserSocket用于注册,并且保存到userMap当中count += 1;UserSocket userSocket = new UserSocket(count, socket);userMap.put(count, userSocket);// 启动当前UserSocket服务new Thread(userSocket).start();}}/*** @author Anonymous* @description 用户Socket类,需要完成绑定操作,并且是一个线程类* @date 2020/3/9 11:23*/class UserSocket implements Runnable {/** 对应当前客户端连接服务器对应Socket生成输入流和输出流*/private DataInputStream inputStream;private DataOutputStream outputStream;/*** 用户ID号,是当前用户的唯一表示,不可以重复*/private int userId;/*** 用户名,用于注册,同时在发送数据时给予其他用户标记*/private String userName;/*** 是否连接状态标记*/private boolean connetion;/*** 创建UserSocket对象,需要的参数使用userID,和对应的Socket对象** @param userId 当前用户的ID号* @param socket 客户端连接服务器对应的Socket*/public UserSocket(int userId, Socket socket) {this.userId = userId;try {inputStream = new DataInputStream(socket.getInputStream());outputStream = new DataOutputStream(socket.getOutputStream());connetion = true;} catch (IOException e) {e.printStackTrace();connetion = false;}try {// 用户在创建Send线程时,首先会将用户的名字发送给服务器assert inputStream != null;this.userName = inputStream.readUTF();// 广播告知所有人,ID:XXX 姓名: XXX 上线 【群聊,系统广播】sendOther("ID:" + this.userId + " " + this.userName + "来到直播间", true);// 服务器告诉当前客户端,你已经进入聊天室send("欢迎来到聊天室");} catch (IOException e) {e.printStackTrace();}}/*** 接收客户端发送的数据,用于转发操作** @return 用户发送的数据*/public String receive() {String msg = null;try {// 接收用户发送到服务器需要服务器转发的数据msg = inputStream.readUTF();} catch (IOException e) {e.printStackTrace();/*发生异常:1. 连接状态修改2. 关闭资源3. 删除在userMap中对应的数据【留下】对号是书签 快捷键 F11查看当前项目所有的书签 ALT + 2*/connetion = false;CloseUtil.closeAll(inputStream, outputStream);}return msg;}/*** 发送数据到客户端** @param msg 需要发送的数据*/public void send(String msg) {try {outputStream.writeUTF(msg);outputStream.flush();} catch (IOException e) {e.printStackTrace();/*发生异常:1. 连接状态修改2. 关闭资源3. 删除在userMap中对应的数据*/connetion = false;CloseUtil.closeAll(inputStream, outputStream);}}/*这里需要一个方法1. 私聊判断2. 群聊这里需要根据msg进行判断1. 如果是@数字: 前缀私聊通过HashMap --> ID --> UserSocket --> 转发消息2. 非@数字:开头群聊a. 系统播报b. 私人发送这里需要做一个标记获取所有在线用户,判断除自己之外,其他人转发消息*//*** 转发数据判断方法,msg需要处理,选择对应的私聊和群发,同时要判断是否是系统发送消息** @param msg 需要转发的消息* @param sys 系统标记*/public void sendOther(String msg, boolean sys) {if (msg.startsWith("@") && msg.contains(":")) {// 私聊// @1:XXXXXXInteger id = Integer.parseInt(msg.substring(1, msg.indexOf(":")));String newMsg = msg.substring(msg.indexOf(":"));UserSocket userSocket = userMap.get(id);// 如果没有对应的UserSocket用户存在,无法发送消息if (userSocket != null) {// ID:1 小磊磊悄悄的对你说:XXXXuserSocket.send("ID:" + this.userId + " " + this.userName + "悄悄的对你说" + msg);}} else {// 群聊// 从userMap中获取对应的所有value,也就是所有UserSocket对象Collection集合Collection<UserSocket> values = userMap.values();for (UserSocket userSocket : values) {// 不需要将消息发送给自己if (userSocket != this) {// 判断是不是系统消息if (sys) {userSocket.send("系统公告:" + msg);} else {userSocket.send("ID:" + this.userId + " " + this.userName + msg);}}}}}/*** 线程代码*/@Overridepublic void run() {while (connetion) {// 使用receive收到的消息作为参数,同时标记非系统消息,调用sendOthersendOther(receive(), false);}}}public static void main(String[] args) {// 启动服务器!!!Server server = new Server();try {server.start();} catch (IOException e) {e.printStackTrace();}}
}
工具类
CloseUtil.java
package com.qfedu.a_charoom.util;import java.io.Closeable;
import java.io.IOException;/*** @author Anonymous* @description 关闭资源的工具类* @date 2020/3/9 15:02*/
public class CloseUtil {/*** 关闭代码中所需资源的close工具类方法** @param closeable 要求为符合Closeable接口的实现类对象,为不定长参数*/public static void closeAll(Closeable... closeable) {/*不定长参数可以按照数组的形式来处理*/try {for (Closeable source : closeable) {if (source != null) {source.close();}}} catch (IOException e) {e.printStackTrace();}}
}