【Betty】应用层 —— API的设计
在之前博文中,本人逐步实现了 网络传输层通信协议
、会话层逻辑设计
、action分发处理器
、发布订阅事件监听器
那么,在本篇博文中,本人就来实现下 供使用者调用的应用层API:
首先是 客户端的API:
客户端API:
那么,由于在网络通信的过程中,每个客户端都需要一些标识来区分,
因此,本人先来给出一个 客户端网络节点 —— NetNode类:
客户端网络节点 —— NetNode类:
实现思路:
由于每一个客户端,都是一个网络节点,
因此,NetNode类 就需要包含 客户端 的 ip 和 port 这两个基本信息
实现代码:
package edu.youzg.betty.core;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class NetNode {
private String ip;
private int port;
public NetNode() throws UnknownHostException {
InetAddress address = InetAddress.getLocalHost();
this.ip = address.getHostAddress();
}
public int getPort() {
return port;
}
public NetNode setPort(int port) {
this.port = port;
return this;
}
public String getIp() {
return ip;
}
}
对于客户端,有几个 必须实现的功能
因此,本人来给出一个 客户端基本功能接口,以实现客户端几个必备的功能:
客户端基本功能接口 —— IClientAction接口:
package edu.youzg.betty.core;
public interface IClientAction {
void serverOutOfRoom();
void afterConnectToServer();
boolean confirmOffline();
void beforeOffline();
void afterOffline();
void serverAbnormalDrop();
void serverForcedown();
void dealToOne(String sourceId, String message);
void dealToOther(String sourceId, String message);
}
在实现上述接口之前,本人先来给出一个 自定义异常 —— ClientActionNotSetException类:
客户端基本功能未实现异常 —— ClientActionNotSetException类:
package edu.youzg.betty.exception;
/**
* 客户端基本功能未实现异常
*/
public class ClientActionNotSetException extends Exception {
private static final long serialVersionUID = 5945489040239619377L;
public ClientActionNotSetException() {
}
public ClientActionNotSetException(String message) {
super(message);
}
public ClientActionNotSetException(Throwable cause) {
super(cause);
}
public ClientActionNotSetException(String message, Throwable cause) {
super(message, cause);
}
public ClientActionNotSetException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
接下来,本人来给出一个 客户端基本功能适配器,以便我们之后功能的扩展:
客户端基本功能 适配器 —— ClientActionAdapter类:
package edu.youzg.betty.core;
public class ClientActionAdapter implements IClientAction {
public ClientActionAdapter() {
}
@Override
public void afterConnectToServer() {
}
@Override
public void serverOutOfRoom() {
}
@Override
public boolean confirmOffline() {
return true;
}
@Override
public void afterOffline() {
}
@Override
public void serverAbnormalDrop() {
}
@Override
public void dealToOne(String sourceId, String message) {
}
@Override
public void dealToOther(String sourceId, String message) {
}
@Override
public void serverForcedown() {
}
@Override
public void beforeOffline() {
}
}
那么,有了上面的基础,本人现在就来实现下 客户端连接对象:
客户端连接 —— Client类:
实现思路:
作为一个 用于客户端连接的类,至少要实现如下几个功能:
- 初始化 连接参数 及 客户端配置
- 连接服务器
- 向服务器发送请求
- 向服务器发送文本消息
- 与其他客户端 私聊/公聊
- 下线
实现代码:
package edu.youzg.betty.core;
import java.net.Socket;
import java.net.UnknownHostException;
import edu.youzg.betty.action.DefaultActionProcessor;
import edu.youzg.betty.action.IActionProcessor;
import edu.youzg.betty.communication.ClientConversation;
import edu.youzg.betty.constant.INetBaseConfigConst;
import edu.youzg.betty.exception.ClientActionNotSetException;
import edu.youzg.util.PropertiesParser;
public class Client {
private String ip; // 客户端ip
private Socket socket; // 客户端网络通信的 通信信道
private ClientConversation clientConversation; // 客户端会话对象
private IClientAction clientAction; // 客户端基本功能 提供对象
private NetNode me; // 当前客户端节点信息
private IActionProcessor actionProcessor;
public Client() throws UnknownHostException {
this.ip = INetBaseConfigConst.DEFAULT_IP;
this.me = new NetNode().setPort(INetBaseConfigConst.DEFAULT_PORT);
this.actionProcessor = new DefaultActionProcessor();
}
public IClientAction getClientAction() {
return clientAction;
}
public NetNode getMe() {
return me;
}
public IActionProcessor getActionProcesser() {
return this.actionProcessor;
}
public void setClientAction(IClientAction clientAction) {
this.clientAction = clientAction;
}
public void setIp(String ip) {
this.ip = ip;
}
public void setPort(int port) {
this.me.setPort(port);
}
public String getId() {
return this.clientConversation.getId();
}
/**
* 根据"默认路径"下的 配置文件的内容,初始化客户端连接对象
* @param configPath 指定配置文件的路径
*/
public void initClient() {
initClient("/net.cfg.properties");
}
/**
* 根据指定配置文件的内容,初始化客户端连接对象
* @param configPath 指定配置文件的路径
*/
public void initClient(String configPath) {
PropertiesParser pp = new PropertiesParser();
pp.loadProperties(configPath);
String ip = pp.value("ip");
if (ip != null && ip.length() > 0) {
this.ip = ip;
}
String strPort = pp.value("port");
if (strPort != null && strPort.length() > 0) {
this.me.setPort(Integer.valueOf(strPort));
}
}
/**
* 执行 连接服务器逻辑
* @return 连接是否成功
* @throws ClientActionNotSetException
*/
public boolean connectToServer() throws ClientActionNotSetException {
if (clientAction == null) {
throw new ClientActionNotSetException("未设置Client Action!");
}
try {
this.socket = new Socket(ip, me.getPort());
this.clientConversation = new ClientConversation(this, socket);
} catch (Exception e) {
return false;
}
return true;
}
/**
* 向服务器发送消息
* @param message 要发送的文本消息
*/
public void sendMessageToServer(String message) {
this.clientConversation.sendMessageToServer(message);
}
/**
* 向服务器发送请求
* @param action 请求描述符
* @param parameter 请求参数
*/
public void sendRequest(String action, String parameter) {
this.clientConversation.sendRequest(action, action, parameter);
}
/**
* 向服务器发送请求
* @param request 请求类型
* @param response 具体请求内容
* @param parameter 请求参数
*/
public void sendRequest(String request, String response, String parameter) {
this.clientConversation.sendRequest(request, response, parameter);
}
/**
* 执行 私聊逻辑
* @param id
* @param message
*/
public void toOne(String id, String message) {
clientConversation.toOne(id, message);
}
/**
* 执行 公聊逻辑
* @param message
*/
public void toOther(String message) {
clientConversation.toOther(message);
}
/**
* 执行 下线逻辑
*/
public void offline() {
if (clientAction.confirmOffline()) {
clientAction.beforeOffline();
clientConversation.offline();
clientAction.afterOffline();
}
}
}
接下来,是 服务端的API:
服务端API:
首先,本人来给出两个 存储客户端会话的pool:
第一个是 用于 存储临时会话 的 TemporaryConversationPool:
临时会话池 —— TemporaryConversationPool类:
实现思路:
可能有同学有疑问了:
什么是 临时会话 呢?
答曰:
向服务端 发送了连接请求,但是 身份校验流程还未结束 的客户端会话
实现代码:
package edu.youzg.betty.core;
import java.util.LinkedList;
import java.util.List;
import edu.youzg.betty.communication.ServerConversation;
public class TemporaryConversationPool {
private List<ServerConversation> tempConversationList;
TemporaryConversationPool() {
tempConversationList = new LinkedList<>();
}
int getTempClientcount() {
return tempConversationList.size();
}
void addTempConversation(ServerConversation conversation) {
tempConversationList.add(conversation);
}
public ServerConversation removeTempConversation(ServerConversation conversation) {
if (tempConversationList.remove(conversation) == true) {
return conversation;
}
return null;
}
}
第二个,是用于存储身份校验成功的客户端会话 的pool:
登录成功会话池 —— ClientConversationPool类:
实现思路:
登录成功会话池 中,存储的是 登陆验证通过 的 客户端
实现代码:
package edu.youzg.betty.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import edu.youzg.betty.communication.ServerConversation;
public class ClientConversationPool {
private Map<String, ServerConversation> clientPool; // 以客户端id为键,会话对象为值,存储的map
ClientConversationPool() {
clientPool = new HashMap<>();
}
boolean isEmpty() {
return clientPool.isEmpty();
}
int getClientCount() {
return clientPool.size();
}
public void addClient(ServerConversation client) {
String clientId = client.getId();
clientPool.put(clientId, client);
}
public void removeClient(ServerConversation client) {
String clientId = client.getId();
clientPool.remove(clientId);
}
ServerConversation getClientById(String id) {
return clientPool.get(id);
}
/**
* 获取 排除指定id的其他客户端会话对象
* @param id 指定客户端id
* @return 排除指定id的其他客户端会话对象
*/
List<ServerConversation> getClientExcept(String id) {
List<ServerConversation> clientList = new ArrayList<>();
for (String orgId : clientPool.keySet()) {
if (orgId.equals(id)) {
continue;
}
clientList.add(clientPool.get(orgId));
}
return clientList;
}
/**
* 获取 全部客户端会话对象
* @return 全部客户端会话对象
*/
List<ServerConversation> getClientList() {
return getClientExcept(null);
}
}
那么,有了上面的基础,本人现在就来实现下 服务端连接对象:
服务端连接 —— Server类:
实现思路:
作为一个 用于服务端连接的类,至少要实现如下几个功能:
- 初始化 连接参数 及 客户端配置
- 连接服务器
- 向服务器发送请求
- 向服务器发送文本消息
- 与其他客户端 私聊/公聊
- 下线
实现代码:
package edu.youzg.betty.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import edu.youzg.betty.action.DefaultActionProcessor;
import edu.youzg.betty.action.IActionProcessor;
import edu.youzg.betty.communication.InteractiveInfo;
import edu.youzg.betty.communication.ServerConversation;
import edu.youzg.betty.constant.INetBaseConfigConst;
import edu.youzg.betty.event.IListener;
import edu.youzg.betty.event.ISpeaker;
import edu.youzg.util.PropertiesParser;
public class Server implements Runnable, ISpeaker {
private ServerSocket server; // 服务端连接通道
private int port; // 服务端端口号
private volatile boolean goon; // 服务器是否继续运行 标志
private ClientConversationPool clientPool; // 登录成功的客户端会话池
private List<IListener> listenerList; // 监听列表,监听每一个客户端消息,便于之后客户端状态触发事件,服务端这里做处理
private TemporaryConversationPool temporaryConversationPool; // 未登录完成 的 临时客户端池
private int maxClientCount; // 最大客户端连接数
private IActionProcessor actionProcessor; // 客户端请求处理器
public Server() {
this.listenerList = new ArrayList<>();
this.actionProcessor = new DefaultActionProcessor();
this.port = INetBaseConfigConst.DEFAULT_PORT; // TODO 此处改为解析获取
this.maxClientCount = INetBaseConfigConst.DEFAULT_MAX_CLIENT_COUNT;
}
public void setPort(int port) {
this.port = port;
}
public ClientConversationPool getClientPool() {
return clientPool;
}
public TemporaryConversationPool getTemporaryConversationPool() {
return temporaryConversationPool;
}
/**
* 根据"默认路径"的配置文件的内容,初始化 服务端
*/
public void initServer() {
initServer("/net.cfg.properties");
}
/**
* 根据指定路径的配置文件的内容,初始化 服务端
* @param configPath 配置文件的路径
*/
public void initServer(String configPath) {
PropertiesParser pp = new PropertiesParser();
pp.loadProperties(configPath);
String strPort = pp.value("port");
if (strPort != null && strPort.length() > 0) {
this.port = Integer.valueOf(strPort);
}
String strMaxClientCount = pp.value("maxClientCount");
if (strMaxClientCount != null && strMaxClientCount.length() > 0) {
this.maxClientCount = Integer.valueOf(strMaxClientCount);
}
}
/**
* 判断 服务器 是否已启动
* @return 服务器 是否已启动
*/
public boolean isStartup() {
return goon;
}
@Override
public void run() {
publishMessage("开始侦听客户端连接……");
while (goon) {
try {
Socket client = server.accept();
ServerConversation clientConversation = new ServerConversation(client, this);
clientConversation.setActionProcessor(this.actionProcessor);
if (temporaryConversationPool.getTempClientcount() + clientPool.getClientCount()
>= this.maxClientCount) {
clientConversation.outOfRoom();
continue;
}
temporaryConversationPool.addTempConversation(clientConversation);
clientConversation.whoAreYou();
} catch (IOException e) {
goon = false;
}
}
}
/**
* 启动服务器
* @throws IOException
*/
public void startup() throws IOException {
if (goon) {
publishMessage("服务器已启动!");
return;
}
publishMessage("开始启动服务器……");
server = new ServerSocket(port);
temporaryConversationPool = new TemporaryConversationPool();
clientPool = new ClientConversationPool();
publishMessage("服务器已启动!");
goon = true;
new Thread(this, "服务器").start();
}
/**
* 转发 客户端的 公聊消息
* @param interactiveInfo 客户端私聊消息的 会话层协议对象
*/
public void toOther(InteractiveInfo interactiveInfo) {
List<ServerConversation> otherList = clientPool.getClientExcept(interactiveInfo.getSourceId());
for (ServerConversation client : otherList) {
client.toOther(interactiveInfo);
}
}
/**
* 转发 客户端的 私聊消息
* @param interactiveInfo 客户端私聊消息的 会话层协议对象
*/
public void toOne(InteractiveInfo interactiveInfo) {
String targetId = interactiveInfo.getTargetId();
ServerConversation targetClient = clientPool.getClientById(targetId);
if (targetClient != null) {
targetClient.toOne(interactiveInfo);
}
}
/**
* 正常关闭服务器
*/
public void shutdown() {
if (!goon) {
publishMessage("服务器已宕机!");
return;
}
if (!clientPool.isEmpty()) {
publishMessage("尚有客户端在线,不能宕机!");
return;
}
close();
clientPool = null;
publishMessage("服务器已正常关闭!");
}
/**
* 强制关闭服务器
*/
public void forcedown() {
if (!goon) {
publishMessage("服务器已宕机!");
return;
}
List<ServerConversation> clientList = clientPool.getClientList();
for (ServerConversation conversation : clientList) {
// 通知客户端,服务器强制宕机,并强行终止所有在线客户端的会话!
conversation.forcedown();
}
close();
clientPool = null;
publishMessage("服务器已强制关闭!");
}
private void close() {
goon = false;
try {
if (server != null && !server.isClosed()) {
server.close();
}
} catch (IOException e) {
} finally {
server = null;
}
}
@Override
public void addListener(IListener listener) {
if (this.listenerList.contains(listener)) {
return;
}
this.listenerList.add(listener);
}
@Override
public void removeListener(IListener listener) {
if (!this.listenerList.contains(listener)) {
return;
}
this.listenerList.remove(listener);
}
@Override
public void publishMessage(String message) {
if (this.listenerList.isEmpty()) {
return;
}
for (IListener listener : listenerList) {
listener.processMessage(message);
}
}
}
至此,应用层API设计完毕!整个 Betty 的代码也就全部讲解完毕了!
本专栏博文,是本人第三次重构 讲解逻辑 和 内容,今后也不会再改动本专栏博文
其实讲句实话,本专栏所设计的 网络通信框架
,真的非常巧妙
其中所运用到的 接口的使用和对未来使用者的扩展预留
、网络通信API使用
、反射机制巧妙调用
、Java小工具的编写和使用
、适配器的使用
、请求处理分发器
、发布订阅模式监听器
等技术性的设计,
站在本人如今拿到一家很布戳的公司的offer后的角度上来看,依然觉得很骄傲
希望有幸看完本专栏的同学能够吃透这些技巧,在我们今后的框架学习中,会经常看到这些设计技巧
那么,本框架的介绍,就到此为止了,
在之后的博文中,本人来展示下通过 使用本框架 实现一个功能豪华的聊天室有多轻松!😏