CSFramework(二)---会话层和最外层

承接上文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也需要外露一些启动服务器,宕机等等这些方法就可以供使用者使用。当连接上之后内部执行的就是上述的“交流方式”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值