【Betty】应用层 —— API的设计

shadowLogo

在之前博文中,本人逐步实现了 网络传输层通信协议会话层逻辑设计action分发处理器发布订阅事件监听器
那么,在本篇博文中,本人就来实现下 供使用者调用的应用层API

首先是 客户端的API

客户端API:

那么,由于在网络通信的过程中,每个客户端都需要一些标识来区分,
因此,本人先来给出一个 客户端网络节点 —— NetNode类

客户端网络节点 —— NetNode类:

实现思路:

由于每一个客户端,都是一个网络节点,
因此,NetNode类 就需要包含 客户端ipport 这两个基本信息

实现代码:

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后的角度上来看,依然觉得很骄傲

希望有幸看完本专栏的同学能够吃透这些技巧,在我们今后的框架学习中,会经常看到这些设计技巧

那么,本框架的介绍,就到此为止了,
在之后的博文中,本人来展示下通过 使用本框架 实现一个功能豪华的聊天室有多轻松!😏

posted @ 2020-04-22 00:37  在下右转,有何贵干  阅读(50)  评论(0编辑  收藏  举报