文章目录
- com.mec.csframework.core包
- Communication类(通信层)
- NetMessage类(传输的网络信息)
- ENetCommand(枚举命令)
- IDefaultNetConfig接口(默认配置)
- IListener接口(倾听者)
- ISpeaker接口(倾诉者)
- ClientPool类(客户端连接池)
- TemporaryClientList类(临时客户端连接)
- IClientAction接口(客户端可能触发的行为)
- ClientActionNotSetException类(用户行为未设置异常)
- ClientActionAdapter类(用户行为适配器)
- InteractiveMessage类(交互信息)
- Server类(服务器层)
- ServerConversation类(服务器端会话层)
- ClientConversation类(客户端会话层)
- com.mec.csframework.action包
- 关于优化
- 学习总结
我们在计算机网络课程中学过网络的OSI的七层模型,但只是理解其描述的一些概念,并没有真正的实现,只是个概念性框架。而另一种提出的TCP/IP参考模型被投入应用了。参考前两种层次模型,我们可以提出CSFramework使用的层次模型,如下图。
首先是最底层 通信层(Communication),该层只关心最底层通信的事和对端异常掉线问题,但不关心接收到的相关信息该如何处理。信息的处理不在这一层,这一层没有权利处理信息,只是将信息发给对端的作用。
下来是会话层(Conversation),该层会收到来自下层的包装好的信息(协议),根据信息的内容去处理信息。该层可以看出对等层的概念,ServerConversation和ClientConversation之间的交锋。在sever这边看ServerConversation就是客户端,在client这边看ClientConversation就是服务器。
工具的最后一层是Server层和Client层,该层是提供给外面使用的,而下面层都是工具的内部东西,不提供给外面用。所以,层是一步步建立起来的,你虽然只看到最高层用的简单,却不知道底下要有多少层在默默支持着。工具也是这样,最后用户使用的十分方便,可工具制造者才能直到建立的艰辛。“辛苦我一人,幸福千万家!”工具制造者最终的追求。话题扯远了,因为是CSFramework的最后一层,那就要包括CS一些普遍且必要的一些内容。例如,服务器这边的启动服务器和关闭服务器,客户端那边的连接服务器和下线,单发消息,群发消息等功能。
再高层称为应用层,该层的实现不管因为会因为不同的APP而不同,所以更高层的开发由CSFramework工具的使用者去完成,就不用关心我底层的实现了。
先把整体要搭建的框架一说,接下来就开始造我们的工具。友情提示:可能有些类会突兀的出现,先不要纠结于此,因为我把这个代码已经写过好几次了,一步步实现叙述太麻烦了,所以给最终代码,当然期间细节思想会讲的。
com.mec.csframework.core包
第一个肯定是我们最底层且是Server和Client都共有的通信层Communication类,前面也说了,该类只关心信息的传输,因此该层就是建立通信信道和接受发信息的过程。
Communication类(通信层)
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public abstract class Communication implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private volatile boolean goon;
public Communication(Socket socket) {
this.socket = socket;
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
goon = true;
new Thread(this, "通信层").start();
} catch (IOException e) {
e.printStackTrace();
}
}
protected void send(NetMessage message) {
try {
dos.writeUTF(message.toString());
} catch (IOException e) {
close();
}
}
void close() {
goon = false;
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
try {
if (dos != null) {
dos.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
}
protected abstract void dealPeerMessage(NetMessage message);
protected abstract void peerAbnormalDrop();
@Override
public void run() {
String message = null;
while (goon) {
try {
message = dis.readUTF();
dealPeerMessage(new NetMessage(message));
} catch (IOException e) {
if (goon == true) {
peerAbnormalDrop();
}
close();
}
}
close();
}
}
现在想这么一种情况,我一个客户端要给另一个客户端发送“byebye”消息,该消息被服务器接收到后,服务器误认为它要下线(如上篇博文规定的那样),就去做下线操作了,实际上我们只是想把信息发给另一个客户端,并无下线的意思。因此,我认为如果Communication仅仅只传字符串String类型有点不够,我们应该对网络消息规范化,将网络命令和真正的会话内容区分开了,因此有了NetMessage类。
NetMessage类(传输的网络信息)
public class NetMessage {
private ENetCommand command;
private String action;
private String parameter;
NetMessage() {
}
NetMessage(String message) {
int colonIndex = message.indexOf(':');
this.command = ENetCommand.valueOf(message.substring(0, colonIndex));
message = message.substring(colonIndex + 1);
colonIndex = message.indexOf(':');
String actionStr = message.substring(0, colonIndex).trim();
this.action = (actionStr.length() <= 0 ? null : actionStr);
this.parameter = message.substring(colonIndex + 1);
}
@Override
public String toString() {
return command + ":" + (action == null ? " " : action) + ":" + parameter;
}
ENetCommand getCommand() {
return command;
}
NetMessage setCommand(ENetCommand command) {
this.command = command;
return this;
}
String getAction() {
return action;
}
NetMessage setAction(String action) {
this.action = action;
return this;
}
String getParameter() {
return parameter;
}
NetMessage setParameter(String parameter) {
this.parameter = parameter;
return this;
}
}
传输的信息由command(命令),action(动作,这个以后会用到不必深究),parameter(真正的消息)组成。其中带参构造是解析过程,toString方法是封装一个信息过程。为了表示网络命令的简单,我们可以使用Enum(枚举)。
ENetCommand(枚举命令)
public enum ENetCommand {
ServerFoeceDown,
ServerFull,
WhoAreYou,
IAm,
EnsureOnline,
Offline,
ToOne,
ToAll,
Request,
Response,
ClientAbnormalDrop,
}
这个肯定不是未卜先知就知道所有的命令,这个是最终代码效果。
至此,一个编程手法就出现了。我通过NetMessage这个类屏蔽了底层真正传输的信息,我只接受NetMessage类的对象的结果。不允许你发任意String。这样约束了你的行为,我的解析变得有章可循。这就叫做协议。
接下来就建立下一层会话层(Conversation)
/*不完整的*/
public class ServerConversation extends Communication {
public ServerConversation(Socket socket) {
super(socket);
}
@Override
public void dealPeerMessage(NetMessage message) {
}
@Override
public void peerAbnormalDrop() {
}
}
/*不完整的*/
public class ClientConversation extends Communication {
public ClientConversation(Socket socket) {
super(socket);
}
@Override
public void dealPeerMessage(NetMessage message) {
}
@Override
public void peerAbnormalDrop() {
}
}
发现它们是继承Communication来的,然后什么都写不了。是呀,你根本没有需求啊,没有需求让他们之间进行交锋,所以当然什么都写不了。那就去建立更高层Client和Server。
/*不完整的*/
public class Server implements Runnable,ISpeaker {
private ServerSocket server;
private int port;
private volatile boolean listeningClient;
private List<IListener> listenerList;
private int maxClientCount;
public Server() {
port = IDefaultNetConfig.DEFAULT_PORT;
listenerList = new ArrayList<>();
maxClientCount = IDefaultNetConfig.DEFAULT_MAX_Client_COUNT;
}
public void initConfig(String propertiesAbsolutePath) { //方便用properties文件对server进行初始化
ProperitiesParse.loadProperities(propertiesAbsolutePath);
this.port = Integer.valueOf(ProperitiesParse.value("port"));
this.maxClientCount = Integer.valueOf(ProperitiesParse.value("maxClientCount"));
}
public void startUp() throws IOException {
if (listeningClient == true) {
publishMessage("服务器已经启动,请勿重复启动");
return;
}
publishMessage("开始建立服务器...");
server = new ServerSocket(port);
publishMessage("服务器建立成功");
listeningClient = true;
new Thread(this, "服务器");
publishMessage("开始侦听客户端连接请求");
}
public void shutDown() {
if (listeningClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
closeServer();
publishMessage("服务器正常关闭");
}
public boolean isStartUp() {
return listeningClient;
}
private void closeServer() {
listeningClient = false;
try {
if (server != null && !server.isClosed()) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server = null;
}
}
@Override
public void run() {
while (listeningClient) {
try {
Socket client = server.accept();
//TODO 做侦听到一个客户端连接要做的事
} catch (IOException e) {
closeServer(); //侦听客户端都不成功,说明server有问题直接关闭
}
}
}
@Override
public void addListener(IListener listener) {
if (listenerList.contains(listener)) {
return;
}
listenerList.add(listener);
}
@Override
public void removeListener(IListener listener) {
if (!listenerList.contains(listener)) {
return;
}
listenerList.remove(listener);
}
@Override
public void publishMessage(String message) {
for (IListener listener : listenerList) {
listener.processMessage(message);
}
}
}
这里出现了三个接口,有必要解释一下。首先是IDefaultNetConfig
IDefaultNetConfig接口(默认配置)
public interface IDefaultNetConfig {
int DEFAULT_PORT = 54188; //默认的端口号
String DEFAULT_SERVER_IP = "127.0.0.1"; //默认服务器ip地址,本地地址
int DEFAULT_MAX_Client_COUNT = 200; //默认服务器最大连接数量
}
很简单理解是默认的设置,仅仅是本地的一般测试,用户可以使用相关init方法进行修改。
而接下来就是讲一个重要的思想,观察者模式。
我们知道,server有许多信息例如服务器启动、客户端上线等信息是要处理的,很明显我们server工具对于这些信息不能插手,要交给上面APP层去处理,但是APP处理该信息我们做工具的根本不得而知,它可以System.out.println()在控制台输出下,也有可能是命令行模式,还有可能是我们以后写的界面显示一下。所以这么多不确定性该如何操作?这个时候就要使用接口了。让APP层提供一个能够处理在Server层信息的方法,我们Server调用这个方法,而APP具体如何处理信息我们不管,这和我工具Server无关。
IListener接口(倾听者)
public interface IListener {
void processMessage(String message); //处理消息
}
APP层去实现这个接口,去具体实现如何处理消息
ISpeaker接口(倾诉者)
public interface ISpeaker {
void addListener(IListener listener); //添加听众
void removeListener(IListener listener); //删除听众
void publishMessage(String message); //发布消息
}
Server实现这个接口,其中方法包括增加听众(IListener的实现类),删除听众(不再倾听就删除),发布消息,向每个听众发送消息,你如何处理是你的事,我反正都发,所有听众都发。
如果你还是不理解我再举个简单例子。ISpeaker相当于一个报社,IListener相当于一个订报纸的人。报社根本不管用户拿到报纸如何读报纸,报社只管增加订报纸的人,删除退订的人,和向每个订了报纸的人送报纸。而订报纸的用户处理得到的报纸信息,各自有各自的处理办法。
这就是观察者模式。
好,接下来继续写Server层。我们做到了侦听到一个客户端(Socket),接下来该怎么做?该client是Client层那个吗?不是的,服务器和客户端是分别开发的,Server层这边根本没有任何Client层的东西,再看最开始的结构图,Server与Client进行通信必须向下经过Conversation层,因此应该用这个client去实例化个serverConversation,正如我前面所说的Conversation是对等层,在Server这边看serverConversation就是一个客户端。同时,我们在此提个疑问,难道侦听到一个连接就真的是一个正常的客户端吗?如果这个客户端连接上服务器,半天不说话,就会占用相关有效资源。假如有很多这种“僵尸机”,服务器的性能将会大打折扣。所以我们要准备两个客户端池,一个是真正有效的用户连接池,一个是临时的用户连接池,够不够资格当我的有效用户,要经过我服务器的测试才可以。
/*不完整的*/
public class Server implements Runnable,ISpeaker {
private ServerSocket server;
private int port;
private volatile boolean listeningClient;
private List<IListener> listenerList;
private int maxClientCount;
private ClientPool clientPool;
private TemporaryClientList tempClientList;
public Server() {
port = IDefaultNetConfig.DEFAULT_PORT;
listenerList = new ArrayList<>();
maxClientCount = IDefaultNetConfig.DEFAULT_MAX_Client_COUNT;
}
public void initConfig(String propertiesAbsolutePath) { //方便用properties文件对server进行初始化
ProperitiesParse.loadProperities(propertiesAbsolutePath);
this.port = Integer.valueOf(ProperitiesParse.value("port"));
this.maxClientCount = Integer.valueOf(ProperitiesParse.value("maxClientCount"));
}
public void startUp() throws IOException {
if (listeningClient == true) {
publishMessage("服务器已经启动,请勿重复启动");
return;
}
publishMessage("开始建立服务器...");
server = new ServerSocket(port);
publishMessage("服务器建立成功");
listeningClient = true;
clientPool = new ClientPool();
tempClientList = new TemporaryClientList();
new Thread(this, "服务器");
publishMessage("开始侦听客户端连接请求");
}
public void shutDown() {
if (listeningClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
if (!clientPool.isEmpty()) { //TODO
publishMessage("尚有客户端在线,不能宕机");
return;
}
closeServer();
publishMessage("服务器正常关闭");
}
public void forceDown() {
if (listeningClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
//TODO 通知所有在线客户端要强制宕机了
}
public boolean isStartUp() {
return listeningClient;
}
private void closeServer() {
listeningClient = false;
clientPool = null;
tempClientList = null;
try {
if (server != null && !server.isClosed()) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server = null;
}
}
@Override
public void run() {
while (listeningClient) {
try {
Socket client = server.accept();
//做侦听到一个客户端连接要做的事
ServerConversation serverConversation = new ServerConversation(client);
if (clientPool.getSize() + tempClientList.getSize() >= maxClientCount) {
//TODO 通知该客户端服务器爆满
continue;
}
tempClientList.addClient(serverConversation);
//TODO 先加到tempClientList中,符合条件再说
} catch (IOException e) {
closeServer(); //侦听客户端都不成功,说明server有问题直接关闭
}
}
}
@Override
public void addListener(IListener listener) {
if (listenerList.contains(listener)) {
return;
}
listenerList.add(listener);
}
@Override
public void removeListener(IListener listener) {
if (!listenerList.contains(listener)) {
return;
}
listenerList.remove(listener);
}
@Override
public void publishMessage(String message) {
for (IListener listener : listenerList) {
listener.processMessage(message);
}
}
}
可以看到Server多了两个类ClientPool和TemporaryClientList。
ClientPool类(客户端连接池)
public class ClientPool {
private Map<String, ServerConversation> clientPool;
ClientPool() {
clientPool = new HashMap<>();
}
boolean isEmpty() {
return clientPool.isEmpty();
}
int getSize() {
return clientPool.size();
}
void addClient(ServerConversation serverConversation) {
if (clientPool.keySet().contains(serverConversation.getId())) {
return;
}
clientPool.put(serverConversation.getId(), serverConversation);
}
void removeClient(ServerConversation serverConversation) {
if (!clientPool.keySet().contains(serverConversation.getId())) {
return;
}
clientPool.remove(serverConversation.getId());
}
ServerConversation getClientById(String id) { //得到某个特定的client,单发
if (!clientPool.keySet().contains(id)) {
return null;
}
return clientPool.get(id);
}
List<ServerConversation> getClientListExceptId(String id) { //得到除了某个的所有client,群发
List<ServerConversation> result = new ArrayList<>();
for (String key : clientPool.keySet()) {
if (key.equals(id)) {
continue;
}
result.add(clientPool.get(key));
}
return result;
}
List<ServerConversation> getAllClient() { //得到所有client
return getClientListExceptId(null); //只要调用上面方法传个绝不存在的键就行了
}
}
id是区分ServerConversation的键值,这个id如何来等下讲。
TemporaryClientList类(临时客户端连接)
public class TemporaryClientList {
private List<ServerConversation> tempClientList;
TemporaryClientList() {
tempClientList = new LinkedList<>();
}
int getSize() {
return tempClientList.size();
}
void addClient(ServerConversation serverConversation) {
if (tempClientList.contains(serverConversation)) {
return;
}
tempClientList.add(serverConversation);
}
void removeClient(ServerConversation serverConversation) {
if (!tempClientList.contains(serverConversation)) {
return;
}
tempClientList.remove(serverConversation);
}
}
TemporaryClientList设置为LinkedList是因为在此徘徊进入的客户端很多所以用链表比数组好一点。
这两个类完全体现了Java面对对象思想里的分而治之,化繁为简的思想。ClientPool和TemporaryClientList的核心是里面的成员,所有方法都是围绕成员走的,相对纯粹,且逻辑不深,仅仅add和remove等操作,把它单独拿出来做一个类。
核心思想:小部件做好经过测试,组成一个大部件,该部件一定是成功的
因为有了客户端连接池,所以宕机的时候如果还有在线的用户则不能宕机。同时增加个强制宕机的方法。
好了,现在所有的TODO都是通信方面的问题了,这个我们先别着急,先去把Client层完善下,其实我在在做这个类的时候,也是许多类之间的跨越,所以自己一定要清晰该做什么,沿着主干去做。
/*不完整的*/
public class Client {
private Socket socket;
private int port;
private String serverIp;
private String ip;
private ClientConversation clientConversation;
public Client() throws UnknownHostException {
port = IDefaultNetConfig.DEFAULT_PORT;
serverIp = IDefaultNetConfig.DEFAULT_SERVER_IP;
ip = InetAddress.getLocalHost().getHostAddress();
}
public void initConfig(String propertiesAbsolutePath) {
ProperitiesParse.loadProperities(propertiesAbsolutePath);
this.port = Integer.valueOf(ProperitiesParse.value("port"));
this.serverIp = ProperitiesParse.value("serverIp");
}
String getIp() {
return ip;
}
public boolean connectToServer() {
try {
socket = new Socket(serverIp, port);
clientConversation = new ClientConversation(socket);
} catch (UnknownHostException e) {
return false;
} catch (IOException e) {
return false;
}
return true;
}
}
可以看到Client目前很简单就是一些初始化和最重要的连接服务器。现在想这样一种情况,连接服务器可能失败(服务器没开或者未找到相应服务器),失败后要提醒用户并且询问用户是否继续连接。同时还有许多用户操作例如服务器强制宕机、服务器爆满、服务器异常掉线,这些一定是会发生的事,我们可以在底层捕获到信息,但是没有权力处理他,处理权力只能在APP层。对于这种能够预知到要发生的事,但不能实现且完成,我们还是用接口。
IClientAction接口(客户端可能触发的行为)
public interface IClientAction {
void serverForcedown();
void serverFull();
void afterOnline();
void serverAbnormalDrop();
void beforeOffline();
boolean confirmOffline();
void afterOffline();
void dealToOne(String sourceId, String message);
void dealToAll(String sourceId, String message);
void otherClientDrop(String id);
}
上述还是最终代码,不必在意为什么一下能写这么多。和它相关的两个辅助类异常和适配器。
ClientActionNotSetException类(用户行为未设置异常)
public class ClientActionNotSetException extends Exception {
private static final long serialVersionUID = 5752188492855205231L;
public ClientActionNotSetException() {
super();
}
public ClientActionNotSetException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ClientActionNotSetException(String message, Throwable cause) {
super(message, cause);
}
public ClientActionNotSetException(String message) {
super(message);
}
public ClientActionNotSetException(Throwable cause) {
super(cause);
}
}
ClientActionAdapter类(用户行为适配器)
public class ClientActionAdapter implements IClientAction {
@Override
public void serverForcedown() {
}
@Override
public void serverFull() {
}
@Override
public void afterOnline() {
}
@Override
public void serverAbnormalDrop() {
}
@Override
public boolean confirmOffline() {
return true;
}
@Override
public void afterOffline() {
}
@Override
public void dealToOne(String sourceId, String message) {
}
@Override
public void dealToAll(String sourceId, String message) {
}
@Override
public void beforeOffline() {
}
@Override
public void otherClientDrop(String id) {
}
}
ClientActionNotSetException异常类是为了规定用户对ClientAction进行设置,ClientActionAdapter适配器类就和界面部分的适配器一样,不同的时候要用不同的方法。
接下来就可以根据IClientAction去完善Client。
/*不完整的*/
public class Client {
private Socket socket;
private int port;
private String serverIp;
private String ip;
private ClientConversation clientConversation;
private IClientAction clientAction;
public Client() throws UnknownHostException {
port = IDefaultNetConfig.DEFAULT_PORT;
serverIp = IDefaultNetConfig.DEFAULT_SERVER_IP;
ip = InetAddress.getLocalHost().getHostAddress();
}
public void initConfig(String propertiesAbsolutePath) {
ProperitiesParse.loadProperities(propertiesAbsolutePath);
this.port = Integer.valueOf(ProperitiesParse.value("port"));
this.serverIp = ProperitiesParse.value("serverIp");
}
String getIp() {
return ip;
}
public void setClientAction(IClientAction clientAction) {
this.clientAction = clientAction;
}
IClientAction getClientAction() {
return clientAction;
}
public boolean connectToServer() throws ClientActionNotSetException {
if (clientAction == null) {
throw new ClientActionNotSetException("未设置ClientAction");
}
try {
socket = new Socket(serverIp, port);
clientConversation = new ClientConversation(socket);
} catch (UnknownHostException e) {
return false;
} catch (IOException e) {
return false;
}
return true;
}
}
Client基本设置就完成了。现在就是完成基本通信的问题了。可以看回Server类的三个TODO。接下来就是Server、ServerConversation、Client、ClientConversation之间的来回频繁切换了。始终记得Conversation收发会话信息,最终操作到Server或者Client。
-
通知所有在线客户端强制宕机:得到所有在线用户相关的的ServerConversation,发送ENetCommand.ServerFoeceDown会话信息,ClientConversation接收到此信息,去做用户规定的服务器强制关闭的行为。
-
通知客户端服务器爆满:发送ENetCommand.ServerFull会话信息,ClientConversation接收到此信息,去做服务器满相关行为。
-
上线这个过程要好好说一下。此时情形是这个客户端(serverConversation)存在在临时连接里,还未加到真正的用户池里,目的是为了防止“僵尸主机”。到后期我们可以使用一个计时器如果不通信就踢出去。现在考虑真正的有效用户。服务器首先发送会话信息ENetCommand.WhoAreYou,clientConversation收到该信息,并返回发送信息ENetCommand.IAm且是带参的,参数是什么呢?在网络中标识各个主机的是ip地址,所以参数是ip地址。serverConversation收到消息,根据ip来创造id值,注意id是用在clientPool里的,因此要不能重复,ip可能重复呀,一个主机多次登陆怎么办,所以我们要找个一直在变的东西,现实生活中有这个东西,就是时间。所以id就是ip + :+当前时间组合而成的。clientconversation收到后设置下自己的id,以便和服务器一致。同时去做上线成功后的操作。
-
还有就是对端异常掉线问题。客户端这边服务器异常掉线具体操作不是工具应该考虑的,所以还是用clientAction。服务器这里客户端异常掉线,除了从用户池中删除。还要通知其他在线客户端有异常掉线的,会话信息是ENetCommand.ClientAbnormalDrop,参数是异常掉线客户端id的值。clientConversation这里接收到,上传到client层由clientAction去完成。
-
还有就是客户端正常下线的问题了。这个请求是由客户端提出来的,因此加下线操作。clientAction里的confirmOffline()可以询问用户是否真的要下线,同时还有下线前操作和下线后操作方便一些资源的回收。下线过程就是clientConversation发送ENetCommand.Offline信息,serverConversation接收到做相应从用户池清除。
/*不完整的*/
public class Server implements Runnable,ISpeaker {
private ServerSocket server;
private int port;
private volatile boolean listeningClient;
private List<IListener> listenerList;
private int maxClientCount;
private ClientPool clientPool;
private TemporaryClientList tempClientList;
public Server() {
port = IDefaultNetConfig.DEFAULT_PORT;
listenerList = new ArrayList<>();
maxClientCount = IDefaultNetConfig.DEFAULT_MAX_Client_COUNT;
}
public void initConfig(String propertiesAbsolutePath) { //方便用properties文件对server进行初始化
ProperitiesParse.loadProperities(propertiesAbsolutePath);
this.port = Integer.valueOf(ProperitiesParse.value("port"));
this.maxClientCount = Integer.valueOf(ProperitiesParse.value("maxClientCount"));
}
public void startUp() throws IOException {
if (listeningClient == true) {
publishMessage("服务器已经启动,请勿重复启动");
return;
}
publishMessage("开始建立服务器...");
server = new ServerSocket(port);
publishMessage("服务器建立成功");
listeningClient = true;
clientPool = new ClientPool();
tempClientList = new TemporaryClientList();
new Thread(this, "服务器").start();
publishMessage("开始侦听客户端连接请求");
}
public void shutDown() {
if (listeningClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
if (!clientPool.isEmpty()) {
publishMessage("尚有客户端在线,不能宕机");
return;
}
closeServer();
publishMessage("服务器正常关闭");
}
public void forceDown() {
if (listeningClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
//通知所有在线客户端要强制宕机了
List<ServerConversation> allOnlineClient = clientPool.getAllClient();
for (ServerConversation serverConversation : allOnlineClient) {
serverConversation.serverForceDown();
}
closeServer();
publishMessage("服务器强制宕机成功");
}
public boolean isStartUp() {
return listeningClient;
}
private void closeServer() {
listeningClient = false;
clientPool = null;
tempClientList = null;
try {
if (server != null && !server.isClosed()) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server = null;
}
}
@Override
public void run() {
while (listeningClient) {
try {
Socket client = server.accept();
//做侦听到一个客户端连接要做的事
ServerConversation serverConversation = new ServerConversation(client, this);
if (clientPool.getSize() + tempClientList.getSize() >= maxClientCount) {
//通知该客户端服务器爆满
serverConversation.serverFull();
continue;
}
tempClientList.addClient(serverConversation);
// 先加到tempClientList中,符合条件再说
serverConversation.whoAreYou();
} catch (IOException e) {
closeServer(); //侦听客户端都不成功,说明server有问题直接关闭
}
}
}
@Override
public void addListener(IListener listener) {
if (listenerList.contains(listener)) {
return;
}
listenerList.add(listener);
}
void ensureOnline(ServerConversation serverConversation) {
tempClientList.removeClient(serverConversation);
clientPool.addClient(serverConversation);
publishMessage("客户端[ "+ serverConversation.getId() +"]成功上线");
}
void clientAbnoramlDrop(ServerConversation serverConversation) {
tempClientList.removeClient(serverConversation);
clientPool.removeClient(serverConversation);
List<ServerConversation> allOnlineClient = clientPool.getAllClient();
for (ServerConversation conversation : allOnlineClient) {
conversation.noticeAbnormalDrop(serverConversation.getId());
}
publishMessage("客户端[" + serverConversation.getId() + "]异常掉线");
}
void offline(ServerConversation serverConversation) {
tempClientList.removeClient(serverConversation);
clientPool.removeClient(serverConversation);
publishMessage("客户端[" + serverConversation.getId() + "]正常下线");
}
@Override
public void removeListener(IListener listener) {
if (!listenerList.contains(listener)) {
return;
}
listenerList.remove(listener);
}
@Override
public void publishMessage(String message) {
for (IListener listener : listenerList) {
listener.processMessage(message);
}
}
}
ServerConversation
/*不完整的*/
public class ServerConversation extends Communication {
private String id;
private String ip;
private Server server;
public ServerConversation(Socket socket, Server server) {
super(socket);
this.server = server;
}
String getId() {
return id;
}
String getIp() {
return ip;
}
void serverForceDown() {
sendMessage(new NetMessage().setCommand(ENetCommand.ServerFoeceDown));
close();
}
void serverFull() {
sendMessage(new NetMessage().setCommand(ENetCommand.ServerFull));
close();
}
void whoAreYou() {
sendMessage(new NetMessage().setCommand(ENetCommand.WhoAreYou));
}
@Override
public void dealPeerMessage(NetMessage message) {
ENetCommand command = message.getCommand();
String parameter = message.getParameter();
switch (command) {
case IAm:
ensureOnline(parameter);
break;
case Offline:
server.offline(this);
close();
break;
default:
break;
}
}
@Override
public void peerAbnormalDrop() {
server.clientAbnoramlDrop(this);
close();
}
void ensureOnline(String parameter) {
this.ip = parameter;
this.id = parameter + ":" +System.currentTimeMillis();
server.ensureOnline(this);
sendMessage(new NetMessage().setCommand(ENetCommand.EnsureOnline).setParameter(id));
}
void noticeAbnormalDrop(String abnormalId) {
sendMessage(new NetMessage().setCommand(ENetCommand.ClientAbnormalDrop).setParameter(abnormalId));
}
}
/*不完整的*/
public class ClientConversation extends Communication {
private Client client;
private String id;
public ClientConversation(Socket socket, Client client) {
super(socket);
this.client = client;
}
String getId() {
return id;
}
@Override
public void dealPeerMessage(NetMessage message) {
ENetCommand command = message.getCommand();
String parameter = message.getParameter();
switch (command) {
case ServerFoeceDown:
client.serverForcedown();
close();
break;
case ServerFull:
client.serverFull();
close();
break;
case WhoAreYou:
sendMessage(new NetMessage().setCommand(ENetCommand.IAm).setParameter(client.getIp()));
break;
case EnsureOnline:
this.id = parameter;
client.ensureOnline();
break;
case ClientAbnormalDrop:
client.dealOtherClientDrop(parameter);
break;
default:
break;
}
}
@Override
public void peerAbnormalDrop() {
client.serverAbnoramlDrop();
close();
}
void offline() {
sendMessage(new NetMessage().setCommand(ENetCommand.Offline));
close();
}
}
/*不完整的*/
public class Client {
private Socket socket;
private int port;
private String serverIp;
private String ip;
private ClientConversation clientConversation;
private IClientAction clientAction;
public Client() throws UnknownHostException {
port = IDefaultNetConfig.DEFAULT_PORT;
serverIp = IDefaultNetConfig.DEFAULT_SERVER_IP;
ip = InetAddress.getLocalHost().getHostAddress();
}
public void initConfig(String propertiesAbsolutePath) {
ProperitiesParse.loadProperities(propertiesAbsolutePath);
this.port = Integer.valueOf(ProperitiesParse.value("port"));
this.serverIp = ProperitiesParse.value("serverIp");
}
String getIp() {
return ip;
}
public void setClientAction(IClientAction clientAction) {
this.clientAction = clientAction;
}
IClientAction getClientAction() {
return clientAction;
}
public boolean connectToServer() throws ClientActionNotSetException {
if (clientAction == null) {
throw new ClientActionNotSetException("未设置ClientAction");
}
try {
socket = new Socket(serverIp, port);
clientConversation = new ClientConversation(socket, this);
} catch (UnknownHostException e) {
return false;
} catch (IOException e) {
return false;
}
return true;
}
public void offline() {
if (clientAction.confirmOffline() == false) {
return;
}
clientAction.beforeOffline();
clientConversation.offline();
clientAction.afterOffline();
}
void serverForcedown() {
clientAction.serverForcedown();
}
void serverFull() {
clientAction.serverFull();
}
void ensureOnline() {
clientAction.afterOnline();
}
void serverAbnoramlDrop() {
clientAction.serverAbnormalDrop();
}
void dealOtherClientDrop(String id) {
clientAction.otherClientDrop(id);
}
}
上述代码仅仅只是完成了服务器和客户端最基础的操作,例如上线下线。现在我们就要考虑更为重要的功能了。单聊和群聊,这是以后开发出来的APP的共有特性。例如聊天室的私聊和群聊。棋牌室的向大家展示牌。
私聊(toOne)和群聊(toAll)传递的信息都有个特性,都有源id(谁发送的),目标id(谁接收的)和真正发的信息组成的。因此,根据分而治之,我们在做一个类。
InteractiveMessage类(交互信息)
public class InteractiveMessage {
private String sourceId;
private String targetId;
private String message;
InteractiveMessage() {
}
InteractiveMessage(String sourceId, String targetId, String message) {
this.sourceId = sourceId;
this.targetId = targetId;
this.message = message;
}
String getSourceId() {
return sourceId;
}
InteractiveMessage setSourceId(String sourceId) {
this.sourceId = sourceId;
return this;
}
String getTargetId() {
return targetId;
}
InteractiveMessage setTargetId(String targetId) {
this.targetId = targetId;
return this;
}
String getMessage() {
return message;
}
InteractiveMessage setMessage(String message) {
this.message = message;
return this;
}
@Override
public String toString() {
return "from " + sourceId + "to " + targetId + ":" + message;
}
}
但是我们知道我们在Communcation层传递真正的信息是String类型的,可不是现在写出的InteractiveMessage类,如何将对象变为字符串呢?以后对端得到字符串又可以转换回成相应的对象呢?这就需要大名鼎鼎的Gson了。
- 私聊过程:首先是由Client提出来的,参数是目标id和要发的信息,通过clientConversation将相应的交互信息通过Gson发送过去,服务器得到相应的字符串,再用Gson得到发送的对象的id,从用户池中得到相应的serverConversation,在发回给相应的客户端,相应的客户端有clientAction有专门的dealToOne。
- 群聊过程:和上述类似,不过改变时服务器得到除了发送者的所有其他serverConversation进行。
现在就有个问题了,Gson那么好用,为什么底层NetMessage不用Gson呢?原因是:Gson将对象变为字符串,将字符串变为对象,一定有许多操作,而我们底层希望速度快,下标定位比Gson快。
Server类(服务器层)
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import com.mec.util.ArgumentMaker;
import com.mec.util.PropertiesParse;
public class Server implements ISpeaker,Runnable {
private ServerSocket server;
private int port;
private volatile boolean listenClient;
private List<IListener> listenerList;
private int maxClientCount;
private ClientPool clientPool;
private TemporaryClientList temporaryClientList;
public Server() {
port = IDefaultNetConfig.DEFAULT_PORT;
listenerList = new ArrayList<>();
maxClientCount = IDefaultNetConfig.DEFAULT_MAX_Client_COUNT;
}
public void initConfig(String path) {
PropertiesParse.loadCfgPath(path);
this.port = Integer.valueOf(PropertiesParse.value("port"));
this.maxClientCount = Integer.valueOf(PropertiesParse.value("maxClientCount"));
}
public void initConfigAbsolutePath(String path) {
PropertiesParse.loadCfgAbsolutePath(path);
this.port = Integer.valueOf(PropertiesParse.value("port"));
this.maxClientCount = Integer.valueOf(PropertiesParse.value("maxClientCount"));
}
public void startUp() throws IOException {
if (listenClient == true) {
publishMessage("服务器已经启动,请勿重复启动");
return;
}
publishMessage("开始建立服务器...");
server = new ServerSocket(port);
publishMessage("服务器建立成功");
listenClient = true;
clientPool = new ClientPool();
temporaryClientList = new TemporaryClientList();
new Thread(this, "服务器").start();
publishMessage("开始侦听客户端连接请求");
}
@Override
public void run() {
while (listenClient) {
try {
Socket socket = server.accept();
ServerConversation serverConversation = new ServerConversation(socket, this);
if (clientPool.getSize() + temporaryClientList.getSize() >= maxClientCount) {
//通知该客户端服务器已经饱满
serverConversation.serverFull();
continue;
}
temporaryClientList.addClient(serverConversation);
//先连接到临时,经过筛选后在加到真正的用户池中去
serverConversation.whoAreYou();
} catch (IOException e) {
closeServer();
}
}
}
public void forceDown() {
if (listenClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
//通知所有在线客户端要强制宕机了
List<ServerConversation> allClient = clientPool.getAllClient();
for (ServerConversation sc : allClient) {
sc.serverForceDown();
}
closeServer();
publishMessage("服务器强制宕机成功");
}
public void shutDown() {
if (listenClient == false) {
publishMessage("服务器已经宕机,请勿重复宕机");
return;
}
if (!clientPool.isEmpty()) {
publishMessage("尚有客户端在线,不能宕机");
return;
}
closeServer();
publishMessage("服务器正常关闭");
}
private void closeServer() {
listenClient = false;
clientPool = null;
temporaryClientList = null;
try {
if (server != null && !server.isClosed()) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server = null;
}
}
public boolean isStartUp() {
return listenClient;
}
@Override
public void addListener(IListener listener) {
if (listenerList.contains(listener)) {
return;
}
listenerList.add(listener);
}
@Override
public void removeListener(IListener listener) {
if (!listenerList.contains(listener)) {
return;
}
listenerList.remove(listener);
}
@Override
public void publishMessage(String message) {
for (IListener listener : listenerList) {
listener.processMessage(message);
}
}
void ensureOnline(ServerConversation serverConversation) {
temporaryClientList.removeClient(serverConversation);
clientPool.addClient(serverConversation);
publishMessage("客户端[ "+ serverConversation.getId() +"]成功上线");
}
void offline(ServerConversation serverConversation) {
temporaryClientList.removeClient(serverConversation);
clientPool.removeClient(serverConversation);
publishMessage("客户端[" + serverConversation.getId() + "]正常下线");
}
void clientAbnormalDrop(ServerConversation serverConversation) {
temporaryClientList.removeClient(serverConversation);
clientPool.removeClient(serverConversation);
List<ServerConversation> allOnlineClient = clientPool.getClientListExceptId(serverConversation.getId());
for (ServerConversation conversation : allOnlineClient) {
conversation.noticeAbnormalDrop(serverConversation.getId());
}
publishMessage("客户端[" + serverConversation.getId() + "]异常掉线");
}
void toOne(String parameter) {
InteractiveMessage interactiveMessage = ArgumentMaker.gson.fromJson(parameter, InteractiveMessage.class);
String targetId = interactiveMessage.getTargetId();
ServerConversation serverConversation = clientPool.getClientById(targetId);
if (serverConversation != null) {
serverConversation.toOne(parameter);
}
}
void toAll(String parameter) {
InteractiveMessage interactiveMessage = ArgumentMaker.gson.fromJson(parameter, InteractiveMessage.class);
List<ServerConversation> all = clientPool.getClientListExceptId(interactiveMessage.getSourceId());
if (all == null || all.isEmpty()) {
return;
}
for (ServerConversation sc : all) {
sc.toAll(parameter);
}
}
}
ServerConversation类(服务器端会话层)
import java.net.Socket;
public class ServerConversation extends Communication {
private String id;
private String clientIp;
private Server server;
ServerConversation(Socket socket, Server server) {
super(socket);
this.server = server;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getClientIp() {
return clientIp;
}
void whoAreYou() {
send(new NetMessage().setCommand(ENetCommand.WhoAreYou));
}
void serverFull() {
send(new NetMessage().setCommand(ENetCommand.ServerFull));
close();
}
void serverForceDown() {
send(new NetMessage().setCommand(ENetCommand.ServerFoeceDown));
close();
}
void ensureOnline(String parameter) {
this.clientIp = parameter;
this.id = parameter + ":" +System.currentTimeMillis();
server.ensureOnline(this);
send(new NetMessage().setCommand(ENetCommand.EnsureOnline).setParameter(id));
}
@Override
protected void dealPeerMessage(NetMessage message) {
ENetCommand command = message.getCommand();
String parameter = message.getParameter();
switch (command) {
case IAm:
ensureOnline(parameter);
break;
case Offline:
server.offline(this);
close();
break;
case ToOne:
server.toOne(parameter);
break;
case ToAll:
server.toAll(parameter);
break;
default:
break;
}
}
@Override
protected void peerAbnormalDrop() {
server.clientAbnormalDrop(this);
close();
}
void noticeAbnormalDrop(String abnormalId) {
send(new NetMessage().setCommand(ENetCommand.ClientAbnormalDrop).setParameter(abnormalId));
}
void toOne(String parameter) {
send(new NetMessage().setCommand(ENetCommand.ToOne).setParameter(parameter));
}
void toAll(String parameter) {
send(new NetMessage().setCommand(ENetCommand.ToAll).setParameter(parameter));
}
}
ClientConversation类(客户端会话层)
import java.net.Socket;
import com.mec.util.ArgumentMaker;
public class ClientConversation extends Communication {
private Client client;
private String id;
ClientConversation(Socket socket, Client client) {
super(socket);
this.client = client;
}
String getId() {
return id;
}
@Override
protected void dealPeerMessage(NetMessage message) {
ENetCommand command = message.getCommand();
String parameter = message.getParameter();
switch (command) {
case WhoAreYou:
send(new NetMessage().setCommand(ENetCommand.IAm).setParameter(client.getIp()));
break;
case EnsureOnline:
this.id = parameter;
client.ensureOnline();
break;
case ServerFull:
client.serverFull();
close();
break;
case ServerFoeceDown:
client.serverForceDown();
close();
break;
case ClientAbnormalDrop:
client.dealOtherClientDrop(parameter);
break;
case ToOne:
client.dealToOne(parameter);
break;
case ToAll:
client.dealToAll(parameter);
break;
default:
break;
}
}
@Override
protected void peerAbnormalDrop() {
client.serverAbnormalDrop();
close();
}
void offline() {
send(new NetMessage().setCommand(ENetCommand.Offline));
close();
}
void toOne(String targetId, String message) {
InteractiveMessage interactiveMessage = new InteractiveMessage(this.getId(), targetId, message);
send(new NetMessage().setCommand(ENetCommand.ToOne)
.setParameter(ArgumentMaker.gson.toJson(interactiveMessage)));
}
void toAll(String message) {
InteractiveMessage interactiveMessage = new InteractiveMessage(this.getId(), null, message);
send(new NetMessage().setCommand(ENetCommand.ToAll)
.setParameter(ArgumentMaker.gson.toJson(interactiveMessage)));
}
}
Client类(客户端)
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import com.mec.util.ArgumentMaker;
import com.mec.util.PropertiesParse;
public class Client {
private String serverIp;
private int serverPort;
private Socket socket;
private String ip;
private ClientConversation clientConversation;
private IClientAction clientAction;
public Client() throws UnknownHostException {
serverIp = IDefaultNetConfig.DEFAULT_SERVER_IP;
serverPort = IDefaultNetConfig.DEFAULT_PORT;
ip = InetAddress.getLocalHost().getHostAddress();
}
String getIp() {
return ip;
}
public void initConfig(String path) {
PropertiesParse.loadCfgPath(path);
this.serverPort = Integer.valueOf(PropertiesParse.value("port"));
this.serverIp = PropertiesParse.value("serverIp");
}
public void initConfigAbsolutePath(String path) {
PropertiesParse.loadCfgAbsolutePath(path);
this.serverPort = Integer.valueOf(PropertiesParse.value("port"));
this.serverIp = PropertiesParse.value("serverIp");
}
public void setClientAction(IClientAction clientAction) {
this.clientAction = clientAction;
}
IClientAction getClientAction() {
return clientAction;
}
public boolean connectToServer() throws ClientActionNotSetException {
if (clientAction == null) {
throw new ClientActionNotSetException("未设置ClientAction");
}
try {
socket = new Socket(serverIp, serverPort);
clientConversation = new ClientConversation(socket, this);
} catch (UnknownHostException e) {
return false;
} catch (IOException e) {
return false;
}
return true;
}
void ensureOnline() {
clientAction.afterOnline();
}
void serverFull() {
clientAction.serverFull();
}
void serverForceDown() {
clientAction.serverForcedown();
}
public void offline() {
if (clientAction.confirmOffline() == false) {
return;
}
clientAction.beforeOffline();
clientConversation.offline();
clientAction.afterOffline();
}
void serverAbnormalDrop() {
clientAction.serverAbnormalDrop();
}
void dealOtherClientDrop(String dropId) {
clientAction.otherClientDrop(dropId);
}
public void toOne(String targetId, String message) {
clientConversation.toOne(targetId, message);
}
void dealToOne(String parameter) {
InteractiveMessage interactiveMessage = ArgumentMaker.gson.fromJson(parameter, InteractiveMessage.class);
clientAction.dealToOne(interactiveMessage.getSourceId(), interactiveMessage.getMessage());
}
public void toAll(String message) {
clientConversation.toAll(message);
}
void dealToAll(String parameter) {
InteractiveMessage interactiveMessage = ArgumentMaker.gson.fromJson(parameter, InteractiveMessage.class);
clientAction.dealToAll(interactiveMessage.getSourceId(), interactiveMessage.getMessage());
}
}
做到现在,我们已经完成了客户端基本的上线下线和最简单的私聊和群聊。但是在验证客户端登陆的时候,这个问题没有解决。所谓用户登陆,是用户将账号和密码输入后,需要传给服务器这些信息,然后服务器再在数据库中查询,并返回客户端相应的数据。
那么,该如何完成呢?可能会想在ENetCommand里再加新的命令UserLogin,这样当然可以解决问题,但是如果这样做以后客户端有许许多多的要在服务器端完成的操作,那就需要在ENetCommand加更多命令了。但是首先ENetCommand是我们工具的内部东西,你怎么让使用工具的人往里面加东西呢?其次ENetCommand可以增加的话,那里面所有的东西都需要增加东西了,再次强调我们做的是工具,工具内部不提供给外面!因此,我们只打算给APP层提供一个开发框架,不能照顾到所有APP层实现的功能,对于这些功能,我们只能提供APP层实现相关功能的途径。
答案在NetMessage里的action里。以前写的命令,都是C(客户端)给其他C发消息,但并没有返回给自己的情况,现在有了。C向S(服务端)发送请求并且希望得到回复。我们统一称为C向S发的是Request,S获取到这个请求获取数据进行一系列操作,再给C返回结果称为Response。
Client加sendRequest(String action, String parameter)方法,经过ClientConversation传到ServerConversation,然后问题来了,服务器收到这个该如何操作?仿照IClientAction写个IServerAction吗?提供给外面让其实现?里面是switch case case下去?那么要写几万个?每一个个要写几万行?每一个都是一个MVC模式,这样不行。我们再仔细考虑下,这些Request不都大同小异吗?都是S获得Request里面的数据,进行一系列操作,并返回一个东西给C。所以我可不可以让他自动执行?只要配置好相关XML文件。因此,想要自动完成要做以下三件事。
- APP层发送请求。例如:client.sendRequest(“userLogin”, 参数)。
- 在APP层的userAction写相关方法
- 配置相关XML文件
相关XML文件格式
<actions>
<action name= "userLogin" class= "com.mec.test.server.user.action.UserAction" method="userLogin">
<parameter name="id" type= "string"></parameter>
<parameter name="password" type= "string"></parameter>
</action>
</actions>
标签action里的属性name是这个action的名字,就是sendRequest里参数的名字;class是完成这个action方法的包名称,method完成这个action的方法名称。标签parameter是方法的参数,name是参数名称,type是参数类型(八大基本类型直接写,其他类型带包名称)。
当然这里只写了一个action,随着APP开发功能的丰富,会不断增加的,并且这个不是在工具内部是可以随后任意按规则增加的XML文件。
难道每遇见一次action,就去解析一次XML吗?解析是外存操作,浪费时间,能不能一次解析。再分析XML文件,我们可以让action的属性name作为key键,后面其他部分都为value值。
com.mec.csframework.action包
ActionBeanDefinition类(Action的定义)
简简单单描述value具体是由哪些组成的。
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class ActionBeanDefinition {
private Class<?> klass;
private Method method;
private List<String> parameterNameList;
private Object object;
private int index;
public ActionBeanDefinition() {
parameterNameList = new ArrayList<>();
index = 0;
}
int getParameterNameListSize() {
return parameterNameList.size();
}
boolean hasNext() {
boolean hasNext = index < parameterNameList.size();
if (!hasNext) {
index = 0;
}
return hasNext;
}
String next() {
return parameterNameList.get(index++);
}
Object getObject() {
return object;
}
void setObject(Object object) {
this.object = object;
}
Class<?> getKlass() {
return klass;
}
void setKlass(Class<?> klass) {
this.klass = klass;
}
Method getMethod() {
return method;
}
void setMethod(Method method) {
this.method = method;
}
void addParameterName(String name) {
parameterNameList.add(name);
}
List<String> getParameterNameList() {
return parameterNameList;
}
@Override
public String toString() {
return klass + "\n\t" + method;
}
}
ActionBeanFactory类(扫描存放Action工厂)
扫描XML任务,将所有扫面到的有效的action放进池子里
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;
import com.mec.util.MecType;
import com.mec.util.XMLParse;
public class ActionBeanFactory {
private static final Map<String, ActionBeanDefinition> actionPool;
static {
actionPool = new HashMap<>();
}
public ActionBeanFactory() {
}
public static void scanActionMappingXMLPath() throws Exception {
scanActionMappingXMLPath("/actionMapping.xml");
}
public static void scanActionMappingXMLPath(String xmlPath) throws Exception {
new XMLParse() {
@Override
public void dealElement(Element element, int index) {
String actionName = element.getAttribute("actionName");
String className = element.getAttribute("className");
String methodName =element.getAttribute("methodName");
try {
Class<?> klass = Class.forName(className);
ActionBeanDefinition actionBeanDefinition = new ActionBeanDefinition();
actionBeanDefinition.setKlass(klass);
setMethod(element, klass, methodName, actionBeanDefinition);
actionPool.put(actionName, actionBeanDefinition);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
}
}.parseTag(XMLParse.getDocument(xmlPath), "action");
}
private static void setMethod(Element element, Class<?> klass, String methodName, ActionBeanDefinition actionBeanDefinition) throws NoSuchMethodException, SecurityException {
List<String> parameterTypeNameList = new ArrayList<>();
new XMLParse() {
@Override
public void dealElement(Element element, int index) {
String parameterName = element.getAttribute("name");
String typeName = element.getAttribute("type");
actionBeanDefinition.addParameterName(parameterName);
parameterTypeNameList.add(typeName);
}
}.parseTag(element, "parameter");
int parameterCount = parameterTypeNameList.size();
Class<?>[] parameterTypeArray = parameterCount <=0 ? null : new Class<?>[parameterCount];
for (int i = 0; i < parameterCount; i++) {
parameterTypeArray[i] = MecType.toType(parameterTypeNameList.get(i));
}
Method method = klass.getMethod(methodName, parameterTypeArray);
actionBeanDefinition.setMethod(method);
}
static ActionBeanDefinition getActionBeanDefinition(String actionName) {
return actionPool.get(actionName);
}
public static void setObject(String action, Object object) {
ActionBeanDefinition abd = actionPool.get(action);
if (abd != null && abd.getObject() == null) {
abd.setObject(object);
}
}
}
其中XML解析工具,可以在以前写的博文查看到。还有一个类MecType。
MecType类(工具包中的)
将字符串类型的String、int、boolean变为真正的类型。
public class MecType {
public MecType() {
}
public static Class<?> toType(String typeName) {
if (typeName.equalsIgnoreCase("string")) {
return String.class;
}
if (typeName.equalsIgnoreCase("int")) {
return int.class;
}
if (typeName.equalsIgnoreCase("char")) {
return char.class;
}
if (typeName.equalsIgnoreCase("boolean")) {
return boolean.class;
}
if (typeName.equalsIgnoreCase("double")) {
return double.class;
}
if (typeName.equalsIgnoreCase("float")) {
return float.class;
}
if (typeName.equalsIgnoreCase("long")) {
return long.class;
}
if (typeName.equalsIgnoreCase("byte")) {
return byte.class;
}
if (typeName.equalsIgnoreCase("short")) {
return short.class;
}
try {
return Class.forName(typeName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
IActionProcessor接口(处理器接口)
public interface IActionProcessor {
String dealRequest(String action, String parameter) throws ActionProcessorNotFound;
void dealResponse(String action, String parameter) throws ActionProcessorNotFound;
}
ActionProcessorNotFound类(处理器未设置异常)
public class ActionProcessorNotFound extends Exception {
private static final long serialVersionUID = 108844566290027953L;
public ActionProcessorNotFound() {
}
public ActionProcessorNotFound(String message) {
super(message);
}
public ActionProcessorNotFound(Throwable cause) {
super(cause);
}
public ActionProcessorNotFound(String message, Throwable cause) {
super(message, cause);
}
public ActionProcessorNotFound(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
DefaultActionProcessor类(默认处理器)
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import com.mec.util.ArgumentMaker;
public class DefaultActionProcessor implements IActionProcessor {
@Override
public String dealRequest(String action, String parameter) throws ActionProcessorNotFound {
ActionBeanDefinition actionBeanDefinition = ActionBeanFactory.getActionBeanDefinition(action);
if (actionBeanDefinition == null) {
throw new ActionProcessorNotFound("未找到对应处理器");
}
Object object = actionBeanDefinition.getObject();
if (object == null) { //改错半天,原来是这里错了,一定注意括号问题,特别是遇到try catch的时候
try {
object = actionBeanDefinition.getKlass().getConstructor().newInstance();
actionBeanDefinition.setObject(object);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
}
Method method = actionBeanDefinition.getMethod();
ArgumentMaker argumentMaker = new ArgumentMaker(parameter);
int size = actionBeanDefinition.getParameterNameListSize();
Object[] valueList = size <= 0 ? null : new Object[size];
int index = 0;
Parameter[] parameters = method.getParameters();
while (actionBeanDefinition.hasNext()) {
valueList[index] = argumentMaker.getArgumentByName(actionBeanDefinition.next(), parameters[index].getParameterizedType());
index++;
}
try {
Object result = method.invoke(object, valueList);
return ArgumentMaker.gson.toJson(result);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
@Override
public void dealResponse(String action, String parameter) throws ActionProcessorNotFound {
ActionBeanDefinition abd = ActionBeanFactory.getActionBeanDefinition(action);
if (abd == null) {
throw new ActionProcessorNotFound("未找到对应处理器");
}
Object object = abd.getObject();
Method method = abd.getMethod();
Object[] value = new Object[1];
Parameter[] paramters = method.getParameters();
value[0] = ArgumentMaker.gson.fromJson(parameter, paramters[0].getParameterizedType());
try {
method.invoke(object, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
通过配置文件的方式,允许用户自己定义自己的类,通过反射机制来处理远端请求,分解出里面的action,根据action,将传过来的参数交给某一个类的方法,并且反射调用执行。
分发器核心:分析action,找到action相应的处理方法,这就是分发器。
dealRequest方法和dealResponse方法都是为了调用以后APP层上写相关动作的方法,且都是通过反射机制调用执行的。方法的执行需要对象和参数值数组。
对于服务器端需要处理客户的的Request,其有可能有返回值也有可能没有返回值,有返回值的话其返回值类型是千变万化的,根本不能确定。因此又需要借助Gson了,将返回值作为Gson字符串对象,到时候也可以转换回来。
对于客户端要处理来自服务器的Response,且这时候得到服务器返回回来的Gson字符串,有一也且仅有一个。
这时,问题就出现了。
反射机制调用方法,invoke(对象,参数值数组)。问题就出现在参数值数组这里,这里的参数是由用户方法中参数确定的,正因为是用户自定义的,那参数类型就会千奇百怪且数量不定。如何将多个参数揉合在一起,那就要使用容器了List或者Map了。我们知道invoke方法的参数值数组的值顺序必须与方法参数顺序保持一致,所以乍一看使用顺序结构List比较方便,但做到后期发现有问题,Parameter类得到参数名字都是arg0,arg1的。所以还是用Map,键为参数名字,值为参数值,这样对应起来找的话也好找。再把Map对象变为Gson字符串,将Gson字符串在网络间通信,解析的时候再由得到的字符串变回为Map,从Map取各自的值。
但是还是有问题,我们知道Java会有泛型擦除机制,对于Map这种带有泛型的容器发送到对端根本不能解析。
幸运的是,办法总是比困难多,在网上找到Gson的高级使用方法——Typetoken。因此下来有了重要的一个类ArgumentMaker。
ArgumentMaker类(参数构造器)
import java.util.Map;
import java.lang.reflect.Type;
import java.util.HashMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
public class ArgumentMaker {
private Map<String, String> argumentMap;
public static final Gson gson = new GsonBuilder().create();
private static final Type type = new TypeToken<Map<String, String>> () {}.getType();
public ArgumentMaker() {
argumentMap = new HashMap<>();
}
public ArgumentMaker(String parameter) {
argumentMap = gson.fromJson(parameter, type);
}
public Object getArgumentByName(String name, Type type) {
String json = argumentMap.get(name);
return gson.fromJson(json, type);
}
public Object getArgumentByName(String name, Class<?> klass) {
String json = argumentMap.get(name);
return gson.fromJson(json, klass);
}
public ArgumentMaker add(String name, Object value) {
argumentMap.put(name, gson.toJson(value));
return this;
}
@Override
public String toString() {
return gson.toJson(argumentMap);
}
}
这个类核心操作都是围绕argumentMap这个成员走的。toString是构造,而带参构造方法则是解析。
总体来说,ArgumentMaker类出现是因为dealRequest和dealResponse的parameter参数会很复杂,如何将众多参数做成一个合起来的。发现参数有K、V关系。K:参数名,V:参数值。做成一个Map,再将Map进行Gson化,但是有泛型擦除机制,要借助Gson高级用法TypeToken。就这样一个工具产生了。
至此,CSFramework工具的所有类基本都介绍完了。
关于优化
优化一:switch case的优化
我们可以在会话层看到有多个switch case出现的情况。
@Override
protected void dealPeerMessage(NetMessage message) {
ENetCommand command = message.getCommand();
String parameter = message.getParameter();
switch (command) {
case WhoAreYou:
send(new NetMessage().setCommand(ENetCommand.IAm).setParameter(client.getIp()));
break;
case EnsureOnline:
this.id = parameter;
client.ensureOnline();
break;
case ServerFull:
client.serverFull();
close();
break;
case ServerFoeceDown:
client.serverForceDown();
close();
break;
case ClientAbnormalDrop:
client.dealOtherClientDrop(parameter);
break;
case ToOne:
client.dealToOne(parameter);
break;
case ToAll:
client.dealToAll(parameter);
break;
default:
break;
}
}
这种写法没有错误,但是存在很大的问题。在switch case中要处理的问题过于庞杂,违反职责单一原则,必须在switch前取出数值,这些数值的处理是混合使用的。在某种程度上来说,应该尽量避免switch和多重if else结合的语句。
解决方法:
(对端消息处理器)
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class PeerMessageProcessor {
private Object object;
private Class<?> klass;
PeerMessageProcessor() {
}
void setObject(Object object) {
this.object = object;
this.klass = object.getClass();
}
void dealPeerMessage(NetMessage message) {
ENetCommand command = message.getCommand();
String commandStr = command.name();
String methodName = "deal" + commandStr;
try {
Method method = klass.getDeclaredMethod(methodName, NetMessage.class);
method.invoke(object, message);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
很明显通过反射机制来调用,速度肯定是比switch case慢。
但是switch case缺点是:胡子眉毛一把抓,所有的事务都在这里处理。
我们设计这个类优点:工厂设计理念。不在乎速度,而在乎工业开发,使得代码可读性更强了。
优化二:通信信道的优化
首先看我们最底层通信层Communication
的构造方法,在最底层优化,得到的效果最好。
public Communication(Socket socket) {
this.socket = socket;
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
goon = true;
new Thread(this, "通信层").start();
} catch (IOException e) {
e.printStackTrace();
}
}
这个类服务器和客户端两边都要用。主要优化是对服务器来说,在服务器端,我们侦听(accept
)到一个客户端连接请求,就来初始化Communication
。构造方法创建通信信道是十分耗时的,几个客户端连是没什么问题,但是如果在短时间内有成百上千的客户端要进行连接,服务器这边一个个建立通信信道是巨大压力难以承受的。用个图片来描述吧。
会造成这种漏斗形的瓶颈。有太多连接会导致有些客户端半天连接不上服务器,导致一直在连接服务器,用户体验不好。
解决方法:用线程去解决。
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public abstract class Communication implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private volatile boolean goon;
private Thread initThred;
public Communication(Socket socket) {
this.socket = socket;
initThred = new Thread(new InnerCommunication());
initThred.start();
// try {
// dis = new DataInputStream(socket.getInputStream());
// dos = new DataOutputStream(socket.getOutputStream());
// goon = true;
// new Thread(this, "通信层").start();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
class InnerCommunication implements Runnable {
public InnerCommunication() {
}
@Override
public void run() {
try {
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
goon = true;
new Thread(Communication.this, "通信层").start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
protected void send(NetMessage message) {
try {
if (initThred.isAlive()) {
initThred.join();
}
dos.writeUTF(message.toString());
} catch (IOException e) {
e.printStackTrace();
close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void close() {
goon = false;
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
try {
if (dos != null) {
dos.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
}
protected abstract void dealPeerMessage(NetMessage message);
protected abstract void peerAbnormalDrop();
@Override
public void run() {
String message = null;
while (goon) {
try {
message = dis.readUTF();
dealPeerMessage(new NetMessage(message));
} catch (IOException e) {
if (goon == true) {
peerAbnormalDrop();
}
close();
}
}
close();
}
}
用一个内部类去启动该线程,使得侦听客户端线程和与其他客户端建立通信信道线程并发的执行。这样可以提高效率。
因为我们是主动的发送给对端消息,被动的听对端的消息,因此需要在发送检查通信信道建立好没有,建立好才可以发送。
protected void send(NetMessage message) {
try {
if (initThred.isAlive()) {
initThred.join();
}
dos.writeUTF(message.toString());
} catch (IOException e) {
e.printStackTrace();
close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
其中join()
方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。
学习总结
网路协议三要素
- 语法。即用户数据与控制信息的结构和格式。具体可以看我们的
NetMessage
类。其信息由command(命令),action(动作),parameter(真正的消息)结合而成。其中toString()
表示了信息的封装,带参构造表示了真正的解析过程。其组成表示为command:action:parameter
。 - 语义。即需要发出的控制信息,以及完成动作所要做出的响应。具体体现在command和action,command表示这个网络命令是用来干什么的(如:ServerFoeceDown,ServerFull,Offline等),action结合分发器来实现动作的请求和响应。
- 时序。即对事件实现的顺序的详细说明。表示为你完成这个命令需要客户端做什么,服务器做什么,并且分先后顺序。例如我们的上线过程要经过WhoAreYou,IAm,EnsureOnline这三个过程,首先由S向C发起WhoAreYou,C得到该消息后返回IAm消息,S收到这个消息后发出EnsureOnline命令,各个命令分工明确,相辅相成。