承接上文CSFramework(一)---通信层的建立,接下来需要定义会话层和最外层的逻辑。这个需要从每一个消息类型说起,上文中NetMessage对象中用一个枚举类表示消息类型。下面是一步一步经过完善后的消息类型的定义:
public enum ENetCommand {
Who_Are_You,
I_am,
Ensure_Online,
Server_Force_Down,
To_One,
To_Other,
Offline,
Out_Of_Room,
Request,
Response
}
针对上面的一些类型一个一个说起:
服务器端主动询问Who_Are_You:
最初的想法是服务器端利用ServerSocket对象从连接请求队列里面拿出来一个Socket对象,这就可以与某一个客户端建立了通信信道,即可以相互发送消息了,我将这种情况称之为CS的一个会话。服务器端与客户端的会话用ServerConversion类表示。下面是这个类的大体内容:
public class ServerConversion extends Communication{
private Server server; //服务器端最外层Server对象
private String clientIp;
/*
*每一个与客户端的会话都用clientId标识。在外层Server类中将其存储进一个容器中。
* 以clientId为键,ServerConversion对象为值。而且有了这个会话Id可以方便精准的找到这个会话
*/
private String clientId;
public ServerConversion(Socket socket, Server server) throws IOException {
super(socket);
this.server = server;
}
public String getClientIp() {
return clientIp;
}
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
@Override
public void dealMessage(NetMessage netMessage) {
ENetCommand command = netMessage.getCommand();
String message = netMessage.getMessage();
String action = netMessage.getAction();
switch (command) {
case I_am: dealIam(message);break;
case To_One : dealToOne(message); break;
case To_Other: dealToOther(message); break;
case Offline : dealOffline(); break;
case Request : dealRequest(action, message);break;
default : break;
}
}
//…………
}
由于BIO的阻塞式限制,accept()方法会阻塞,所以最外层服务器端Server类中也是利用线程来获取连接的。当获取到Socket对像之后就可以相互发送消息。每当服务器拿到一个连接,就先向对端发送Who_Are_You消息,用于判断对端的IP是否合法,当然一些细节的实现还可以做出修改,具体逻辑如下:
//利用线程处理客户端连接请求
@Override
public void run() {
Socket socket = null;
while(goon) {
try {
//获取客户端的套接字对象
socket = serverSocket.accept();
//与该客户端建立会话
ServerConversion serverConversion = new ServerConversion(socket, this);
//如果当前维护的客户端连接数量已经达到阈值
if(this.serverConversionPool.getSize() + 1 >= clientCount) {
serverConversion.outOfRoom();
serverConversion.close();
continue;
}
//获取对端IP地址
String clientIp = socket.getInetAddress().getHostAddress();
serverConversion.setClientIp(clientIp);
//连接就绪之后主动向客户端发送信息---Who_Are_You命令
serverConversion.sendMessage(new NetMessage().setCommand(ENetCommand.Who_Are_You));
speakOut("客户端 [" + clientIp + "]请求上线");
} catch (IOException e) {
//这里如果serverSocket因为某些原因关闭则会掉入异常
goon = false;
}
}
}
上述代码中除过发送Who_Are_You命令还有一些细节处理。即判断当前在线的客户端数量是否达到某一个阈值。由于每次建立一个会话之后都会将会话对象serverConversion存入容器中,如果达到某一个阈值(clientCount),则告知客户端此时连接已满即发送Out_Of_Room消息。
先说客户端连接已满的情况,即告知客户端Out_Of_Room.
服务器端保持连接数量已满:Out_Of_Room
由上述可知服务器端若满只需要发送该命令告知客户端就行,具体是利用与客户端的会话对象(ServerConversion)发送消息。如下:
public class ServerConversion extends Communication{
public void outOfRoom() {
sendMessage(new NetMessage().setCommand(ENetCommand.Out_Of_Room));
}
}
消息发送完毕之后便可以关闭与客户端的通信。(这里不会立刻释放底层的TCP连接,会等到发送积压数据完毕之后才释放连接)
来自客户端对Who_Are_You的回应:I_am
这里的处理是收到服务器消息之后客户端只需要根据自己的IP地址生成一个字符串发送给服务器由服务器效验即可。
public class ClientConversion extends Communication{
private String clientId; //来自由服务器分配的clientId
private Client client; //客户端最外层Client对象
public ClientConversion(Socket socket, Client client) throws IOException {
super(socket);
this.client = client;
}
public String getClientId() {
return clientId;
}
public void setId(String clientId) {
this.clientId = clientId;
}
@Override
public void dealMessage(NetMessage netMessage) {
ENetCommand command = netMessage.getCommand();
String action = netMessage.getAction();
String message = netMessage.getMessage();
switch (command) {
case Who_Are_You: dealWhoAreYou();break;
case Ensure_Online: dealOnline(message); break;
case Server_Force_Down: dealServerForcedown(); break;
case To_One : dealToOne(message); break;
case To_Other : dealToOther(message); break;
case Out_Of_Room: dealOutOfRoom(); break;
case Response : dealResponse(action, message); break;
default : break;
}
}
//客户端将自己的IP地址发送出去,由服务器端效验
private void dealWhoAreYou() {
try {
//此处是获取本地机的ip地址
String ip = InetAddress.getLocalHost().getHostAddress();
ip = ip.hashCode() + "";
NetMessage netMessage = new NetMessage();
netMessage.setCommand(ENetCommand.I_am).setMessage(ip);
sendMessage(netMessage);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
服务器端对I_am消息的回应:Ensure_Online
服务器端收到客户端IP之后,就可以对其进行效验,若合格则为此客户端分配ID,并告知其ID。当然客户端自然受到Ensure_Online消息之后保存自己的clientId即可。至此一个客户端便成功“上线”。
private void dealIam(String message) {
//检查客户端ip是否合法
if(checkClientIp(message)) {
//生成客户端id
createClientId();
//将此会话添加进容器中保存
this.server.getConversionPool().put(this.clientId, this);
//向对端发送同意上线的消息
sendMessage(new NetMessage()
.setCommand(ENetCommand.Ensure_Online)
.setMessage(this.clientId));
this.server.speakOut("客户端[" + this.clientId + "]上线");
}
}
单聊和群聊:To_One和To_Other
单聊和群聊其实在此处的实现本质上是服务器对消息的转发,比如clientA向clientB发消息,那么clinetA告知服务器端clientB的clientId,由于服务器端保存了所有客户端的会话,便可以找到clientB,并将消息转发。群聊则略有不同,clientA的目标是所有在线客户端,则服务器将消息发送给所有在线客户端即可。所以群聊和单聊的message便可以封装成一个对象。由发送者Id,接受者Id,和消息内容组成。
public class InteractiveInfo {
private String resourceId; //发送者Id
private String targetId; //接受者Id
private String data; //消息内容
//省略一堆get和set……
}
一个InteractiveInfo对象可以转换成json字符串的形式存储进NetMessage对象中。
/**
* 单聊(主动与某一客户端聊天):将单聊的目的客户端id和聊天数据告知服务器端,由服务器端负责转发消息
* @param targetId :聊天的目标客户端id
* @param data : 聊天数据
*/
public void toOne(String targetId, String data) {
InteractiveInfo interactiveInfo = new InteractiveInfo();
interactiveInfo.setResourceId(this.clientId);
interactiveInfo.setTargetId(targetId);
interactiveInfo.setData(data);
//利用Gson将该对象发送出去
sendMessage(new NetMessage()
.setCommand(ENetCommand.To_One)
.setMessage(new Gson().toJson(interactiveInfo)));
}
客户端下线:Offline
public void offline() {
IClientAction clientAction = this.client.getAction();
if(clientAction.confirmOffLine()) {
sendMessage(new NetMessage().setCommand(ENetCommand.Offline));
close();
clientAction.afterOffline();
}
}
客户端下线首先判断自己是否要确定下线,如果确实要下线只需要向服务器端发送Offline消息即可,然后服务器端便移除该客户端。而且下线之后便关闭Socket即可。区别与异常掉线。如果客户端关闭Socket,服务器端的就不能读取消息,会掉入IO异常,但是不一样的是主动下线会先发送Offline消息,这是并不会立刻释放连接,服务器端收到之后便关闭自己的Socket,所以不会判断为对端异常掉线。但是如果客户端直接结束进程,那么服务器端的控制通信线程的goon变量还是为true,所以会判定为异常掉线。上述IClientAction是一个接口,由于本次是框架的设计,所以具体的确认下线的逻辑可以由使用者自己实现,即confirmOffLine(),还有下线之后仍然需要处理的事也是由使用者在afterOffline()中定义。
Request和Response的实现(分发器)
有时候客户端需要向服务器端发送请求去完成某个功能,比如客户端登录功能,由于客户的账号密码在服务器后端存储,所以需要让服务器去效验。这就相当于一个“UserLogin”类型的请求。而且服务器端也需要向客户端做出响应,一个请求便对应一个响应。之前所有的消息都不会用到NetMessage类中的action成员,而这个成员是专门为请求/响应准备的,比如“UserLogin”就是一个action,那么服务器端收到“UserLogin”的action之后便可以执行其所对应的请求方法,执行完毕之后,就可以向客户端发送一个“UserLogin”标识的响应。这就是一个分发器的设计,根据某个action去执行相应的请求和响应。
这里的做法是,客户端发送Request命令(即command),并将action设置进去,然后message中存储请求参数即可。发送给服务器之后服务器必须要知道自己对于不同的action应该做什么事。由于这些具体的请求底层是不可能知道的,所以应该利用配置的方式来告诉服务器需要执行的方法。而服务器端执行完毕之后也需要将响应结果“告知客户端”,发送给客户端之后。客户端已经提前解析过配置,自己便可以利用反射的方式执行自己对该action的响应方法。
//客户端发送请求
public void request(String action, String parameter) {
sendMessage(new NetMessage()
.setCommand(ENetCommand.Request)
.setAction(action)
.setMessage(parameter));
}
//服务器端处理请求
private void dealRequest(String action, String parameter) {
try {
Object result = this.server.getActionProcessor().dealRequest(action, parameter);
//将响应结果发送给客户端,客户端执行相应的方法
sendMessage(new NetMessage()
.setCommand(ENetCommand.Response)
.setAction(action)
.setMessage(new Gson().toJson(result, result.getClass())));
} catch (ActionIsNull e) {
e.printStackTrace();
}
}
//客户端处理响应
private void dealResponse(String action, String message) {
//处理响应 此处的message是服务器端回传响应的gson化的字符串
try {
this.client.getActionProcessor().dealResponse(action, message);
} catch (ActionIsNull e) {
e.printStackTrace();
}
}
配置的方式可以类似于下面:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 服务器端执行该配置方法处理action对应请求 -->
<mappings>
<!--UserLogin请求对应的执行方法-->
<mapping action = "UserLogin" class = "com.mec.csFramework.app.service.UserService" method = "checkUser">
<!-- checkUser方法的参数名,和参数类型 -->
<parameter name = "id" type = "String"></parameter>
<parameter name = "password" type = "String"></parameter>
</mapping>
</mappings>
服务器端和客户端在发送请求/响应之前都需要预先解析这些配置,将这些数据存储起来。,如上述每个mapping都对应一个请求/响应。可以先将每一个mapping数据存储起来形成一个对象ActionBeanDefinition对象,再将这些对象按照action:ActionBeanDefinition的方式存储进ActionFactory即可。具体如下:
public class ActionBeanDefinition {
private Class<?> klass; //方法所在类的字节码对象
private Object object; //反射调用方法的对象实例
private Method method; //方法对象
private List<String> parameterNames; //方法的参数名集合
private Class<?>[] parameterTypeValues; //方法的参数类型数组
//……
}
//解析配置文件生成ActionDefinition容器
public class ActionFactory {
private static Map<String, ActionBeanDefinition> actionPool = new HashMap<String, ActionBeanDefinition>();
public static ActionBeanDefinition getDefinition(String action) {
return actionPool.get(action);
}
public static void parse(String path) throws XmlPathNull {
//解析配置文件填充容器
}
//该方法用于获取配置文件中配置的type与Java类型之间的映射
private static Class<?> getClassByName(String parameterName){
if("byte".equalsIgnoreCase(parameterName)) {
return byte.class;
}else if("float".equalsIgnoreCase(parameterName)){
return float.class;
}else if("double".equalsIgnoreCase(parameterName)) {
return double.class;
}else if("char".equalsIgnoreCase(parameterName)) {
return char.class;
}else if("boolean".equalsIgnoreCase(parameterName)) {
return boolean.class;
}else if("int".equalsIgnoreCase(parameterName)) {
return int.class;
}else if("long".equalsIgnoreCase(parameterName)) {
return long.class;
}else if("short".equalsIgnoreCase(parameterName)) {
return short.class;
}else if("String".equalsIgnoreCase(parameterName)){
return String.class;
}else {
try {
return Class.forName(parameterName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
//通过反射方式生成的对象实例都是新的,可能用户需要利用自己的对象进行方法的调用
public static void setObject(String action, Object obj) {
ActionBeanDefinition abd = actionPool.get(action);
if(abd != null) {
abd.setObject(obj);
}
}
}
对于参数值的传递,一种思路是客户端可以按照参数名和参数值的方式将其存入Map中,然后利用gson工具发送即可。服务器端已经解析出方法的参数名和类型自然可以还原出来。我将这两个功能封装进ArgumentMaker类中,形成一个工具类。
这里可以定义一个接口IActionProcessor并进行默认实现,方便以后扩展。客户端和服务器端都需要保存此接口变量,执行其中方法。具体如下:
public interface IActionProcessor {
//服务器端处理请求的方法
public Object dealRequest(String action, String parameter) throws ActionIsNull;
//客户端处理响应结果的方法
public void dealResponse(String action, String parameter) throws ActionIsNull;
}
public class DefaultActionProcessor implements IActionProcessor {
@Override
public Object dealRequest(String action, String parameter) throws ActionIsNull {
//所有的action已经通过解析配置文件保存在ActionFactory中
ActionBeanDefinition abd ;
if(action == null || (abd = ActionFactory.getDefinition(action)) == null) {
throw new ActionIsNull("[" + action + "未进行配置");
}
Method method = abd.getMethod();
Class<?> klass = abd.getKlass();
Object obj = abd.getObject();
if(obj == null) {
try {
obj = klass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
Class<?>[] parameterTypeValues = abd.getParameterTypeValues();
List<String> parameterNames = abd.getParameterNames();
ArguementMaker am = new ArguementMaker(parameter);
String paraName = null;
Object[] args = new Object[parameterTypeValues.length];
for(int index = 0; index < parameterNames.size(); index++) {
paraName = parameterNames.get(index);
args[index] = am.getVal(paraName, parameterTypeValues[index]);
}
Object result = null;
try {
result = method.invoke(obj, args);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
return result;
}
@Override
public void dealResponse(String action, String parameter) throws ActionIsNull {
ActionBeanDefinition abd ;
if(action == null || (abd = ActionFactory.getDefinition(action)) == null) {
throw new ActionIsNull("[" + action + "未进行配置");
}
Method method = abd.getMethod();
Object obj = abd.getObject();
Class<?> parameterType = abd.getParameterTypeValues()[0];
try {
method.invoke(obj, new Gson().fromJson(parameter, parameterType));
} catch (Exception e) {
e.printStackTrace();
}
}
}
至于最外层,其实很多核心的方法都是在会话层实现的。Client需要向外部暴露连接服务器,关闭连接,群聊,单聊等等方法。Server也需要外露一些启动服务器,宕机等等这些方法就可以供使用者使用。当连接上之后内部执行的就是上述的“交流方式”。