基于BIO模式下的即时通信,我们需要解决客户端到客户端的通信,也就是需要实现客户端与客户端的端口消息转发逻辑。
技术选型分析
本项目案例涉及到Java基础加强的案例,具体涉及到的技术点如下:
-
- Java 面向对象设计,语法设计。
-
- 多线程技术。
-
- IO流技术。
-
- 网络通信相关技术。
-
- 集合框架。
-
- 项目开发思维。
-
- Java 常用 api 使用。
功能清单简单说明:
1.客户端登陆功能。
可以启动客户端进行登录,客户端登陆只需要输入用户名和服务端ip地址即可。
2.在线人数实时更新。
客户端用户户登陆以后,需要同步更新所有客户端的联系人信息栏。
3.离线人数更新
检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。
4.群聊
*任意一个客户端的消息,可以推送给当前所有客户端接收。
5.私聊
可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收。
6.@消息
可以选择某个员工,然后发出的消息可以@该用户,但是其他所有人都能
7.消息用户和消息时间点
服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。
项目代码结构演示
项目启动步骤:
-
1.首先需要启动服务端,点击ServerChat类直接右键启动,显示服务端启动成功!
-
2.其次,点击客户端类ClientChat类,在弹出的方框中输入服务端的ip和当前客户端的昵称
-
3.登陆进入后的聊天界面如下,即可进行相关操作。
- 如果直接点击发送,默认发送群聊消息
-
如果选中右侧在线列表某个用户,默认发送@消息
- 如果选中右侧在线列表某个用户,然后选择右下侧私聊按钮默,认发送私聊消息
服务端设计
服务端接收多个客户端逻辑
目标
服务端需要接收多个客户端的接入。
实现步骤
- 1.服务端需要接收多个客户端,目前我们采取的策略是一个客户端对应一个服务端线程。
- 2.服务端除了要注册端口以外,还需要为每个客户端分配一个独立线程处理与之通信。
代码实现
- 服务端主体代码,主要进行端口注册,和接收客户端,分配线程处理该客户端请求
public class ServerChat {
/** 定义一个集合存放所有在线的socket */
public static Map<Socket, String> onLineSockets = new HashMap<>();
public static void main(String[] args) {
try {
/** 1.注册端口 */
ServerSocket serverSocket = new ServerSocket(Constants.PORT);
/** 2.循环一直等待所有可能的客户端连接 */
while(true){
Socket socket = serverSocket.accept();
/**3. 把客户端的socket管道单独配置一个线程来处理 */
new ServerReader(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端分配的独立线程类负责处理该客户端Socket的管道请求。
class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
} catch (Exception e) {
e.printStackTrace();
}
}
}
常量包负责做端口配置
public class Constants {
/** 常量 */
public static final int PORT = 7778 ;
}
小结
本节实现了服务端可以接收多个客户端请求。
服务端接收登陆消息以及监测离线
实现步骤
- 需要在服务端处理客户端的线程的登陆消息。
- 需要注意的是,服务端需要接收客户端的消息可能有很多种。
- 分别是登陆消息,群聊消息,私聊消息 和@消息。
- 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
- 1代表接收的是登陆消息
- 2代表群发| @消息
- 3代表了私聊消息
- 服务端的线程中有异常校验机制,一旦发现客户端下线会在异常机制中处理,然后移除当前客户端用户,把最新的用户列表发回给全部客户端进行在线人数更新。
代码实现
public class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataInputStream dis = null;
try {
dis = new DataInputStream(socket.getInputStream());
/** 1.循环一直等待客户端的消息 */
while(true){
/** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
int flag = dis.readInt();
if(flag == 1){
/** 先将当前登录的客户端socket存到在线人数的socket集合中 */
String name = dis.readUTF() ;
System.out.println(name+"---->"+socket.getRemoteSocketAddress());
ServerChat.onLineSockets.put(socket, name);
}
writeMsg(flag,dis);
}
} catch (Exception e) {
System.out.println("--有人下线了--");
// 从在线人数中将当前socket移出去
ServerChat.onLineSockets.remove(socket);
try {
// 从新更新在线人数并发给所有客户端
writeMsg(1,dis);
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
private void writeMsg(int flag, DataInputStream dis) throws Exception {
// DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
// 定义一个变量存放最终的消息形式
String msg = null ;
if(flag == 1){
/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
/** onlineNames = [波仔,zhangsan,波妞]*/
StringBuilder rs = new StringBuilder();
Collection<String> onlineNames = ServerChat.onLineSockets.values();
// 判断是否存在在线人数
if(onlineNames != null && onlineNames.size() > 0){
for(String name : onlineNames){
rs.append(name+ Constants.SPILIT);
}
// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
// 去掉最后的一个分隔符
msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));
/** 将消息发送给所有的客户端 */
sendMsgToAll(flag,msg);
}
}else if(flag == 2 || flag == 3){
}
}
}
private void sendMsgToAll(int flag, String msg) throws Exception {
// 拿到所有的在线socket管道 给这些管道写出消息
Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
for(Socket sk : allOnLineSockets){
DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
dos.writeInt(flag); // 消息类型
dos.writeUTF(msg);
dos.flush();
}
}
}
小结
- 此处实现了接收客户端的登陆消息,然后提取当前在线的全部的用户名称和当前登陆的用户名称发送给全部在线用户更新自己的在线人数列表。
服务端接收群聊消息
实现步骤
- 接下来要接收客户端发来的群聊消息。
- 需要注意的是,服务端需要接收客户端的消息可能有很多种。
- 分别是登陆消息,群聊消息,私聊消息 和@消息。
- 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
- 1代表接收的是登陆消息
- 2代表群发| @消息
- 3代表了私聊消息
代码实现
public class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataInputStream dis = null;
try {
dis = new DataInputStream(socket.getInputStream());
/** 1.循环一直等待客户端的消息 */
while(true){
/** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
int flag = dis.readInt();
if(flag == 1){
/** 先将当前登录的客户端socket存到在线人数的socket集合中 */
String name = dis.readUTF() ;
System.out.println(name+"---->"+socket.getRemoteSocketAddress());
ServerChat.onLineSockets.put(socket, name);
}
writeMsg(flag,dis);
}
} catch (Exception e) {
System.out.println("--有人下线了--");
// 从在线人数中将当前socket移出去
ServerChat.onLineSockets.remove(socket);
try {
// 从新更新在线人数并发给所有客户端
writeMsg(1,dis);
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
private void writeMsg(int flag, DataInputStream dis) throws Exception {
// DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
// 定义一个变量存放最终的消息形式
String msg = null ;
if(flag == 1){
/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
/** onlineNames = [波仔,zhangsan,波妞]*/
StringBuilder rs = new StringBuilder();
Collection<String> onlineNames = ServerChat.onLineSockets.values();
// 判断是否存在在线人数
if(onlineNames != null && onlineNames.size() > 0){
for(String name : onlineNames){
rs.append(name+ Constants.SPILIT);
}
// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
// 去掉最后的一个分隔符
msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));
/** 将消息发送给所有的客户端 */
sendMsgToAll(flag,msg);
}
}else if(flag == 2 || flag == 3){
// 读到消息 群发的 或者 @消息
String newMsg = dis.readUTF() ; // 消息
// 得到发件人
String sendName = ServerChat.onLineSockets.get(socket);
// 内容
StringBuilder msgFinal = new StringBuilder();
// 时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
if(flag == 2){
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
sendMsgToAll(flag,msgFinal.toString());
}else if(flag == 3){
}
}
}
private void sendMsgToAll(int flag, String msg) throws Exception {
// 拿到所有的在线socket管道 给这些管道写出消息
Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
for(Socket sk : allOnLineSockets){
DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
dos.writeInt(flag); // 消息类型
dos.writeUTF(msg);
dos.flush();
}
}
}
小结
- 此处根据消息的类型判断为群聊消息,然后把群聊消息推送给当前在线的所有客户端。
服务端接收私聊消息
实现步骤
- 解决私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端
- 我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。
- 需要注意的是,服务端需要接收客户端的消息可能有很多种。
- 分别是登陆消息,群聊消息,私聊消息 和@消息。
- 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
- 1代表接收的是登陆消息
- 2代表群发| @消息
- 3代表了私聊消息
代码实现
public class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataInputStream dis = null;
try {
dis = new DataInputStream(socket.getInputStream());
/** 1.循环一直等待客户端的消息 */
while(true){
/** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
int flag = dis.readInt();
if(flag == 1){
/** 先将当前登录的客户端socket存到在线人数的socket集合中 */
String name = dis.readUTF() ;
System.out.println(name+"---->"+socket.getRemoteSocketAddress());
ServerChat.onLineSockets.put(socket, name);
}
writeMsg(flag,dis);
}
} catch (Exception e) {
System.out.println("--有人下线了--");
// 从在线人数中将当前socket移出去
ServerChat.onLineSockets.remove(socket);
try {
// 从新更新在线人数并发给所有客户端
writeMsg(1,dis);
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
private void writeMsg(int flag, DataInputStream dis) throws Exception {
// DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
// 定义一个变量存放最终的消息形式
String msg = null ;
if(flag == 1){
/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
/** onlineNames = [波仔,zhangsan,波妞]*/
StringBuilder rs = new StringBuilder();
Collection<String> onlineNames = ServerChat.onLineSockets.values();
// 判断是否存在在线人数
if(onlineNames != null && onlineNames.size() > 0){
for(String name : onlineNames){
rs.append(name+ Constants.SPILIT);
}
// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
// 去掉最后的一个分隔符
msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));
/** 将消息发送给所有的客户端 */
sendMsgToAll(flag,msg);
}
}else if(flag == 2 || flag == 3){
// 读到消息 群发的 或者 @消息
String newMsg = dis.readUTF() ; // 消息
// 得到发件人
String sendName = ServerChat.onLineSockets.get(socket);
// 内容
StringBuilder msgFinal = new StringBuilder();
// 时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
if(flag == 2){
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
sendMsgToAll(flag,msgFinal.toString());
}else if(flag == 3){
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis())).append("对您私发\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
// 私发
// 得到给谁私发
String destName = dis.readUTF();
sendMsgToOne(destName,msgFinal.toString());
}
}
}
/**
* @param destName 对谁私发
* @param msg 发的消息内容
* @throws Exception
*/
private void sendMsgToOne(String destName, String msg) throws Exception {
// 拿到所有的在线socket管道 给这些管道写出消息
Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
for(Socket sk : allOnLineSockets){
// 得到当前需要私发的socket
// 只对这个名字对应的socket私发消息
if(ServerChat.onLineSockets.get(sk).trim().equals(destName)){
DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
dos.writeInt(2); // 消息类型
dos.writeUTF(msg);
dos.flush();
}
}
}
private void sendMsgToAll(int flag, String msg) throws Exception {
// 拿到所有的在线socket管道 给这些管道写出消息
Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
for(Socket sk : allOnLineSockets){
DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
dos.writeInt(flag); // 消息类型
dos.writeUTF(msg);
dos.flush();
}
}
}
小结
- 本节我们解决了私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端Socket管道
- 我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。