CSFramework之会话层,服务器与客户端(1)

在上一篇文章中我们完成了Communication(通信层)的编写,但是Communication毕竟只是一个抽象类,这篇文章中我们一起来看看它的两个实现类以及我们的服务器和客户端是如何组织起来的。
小声bb一下,这篇文章没有办法将这些全部写完,因为工程量实在太庞大了,怕大家看不下去,所以我会分成几篇文章来详细说明这些到底该如何编写。那么话不多说,我就开搞了!

1.发现问题:

会话层应该分为服务器会话层和客户端会话层,但这两个会话层都需要由Communication类派生出来,分别是serverConversation类和clienConversation类,但是这两个类暂时我们只能给一个空壳子,至于为什么呢——一个最重要的原因是我我们无法验证之前编写代码的有效性,所以必须建立一个最简单的网络系统,于是就给出了两个存在但没有完全存在的类(好像是说了句废话,哈哈哈)。

我们先来建立最外层的两个类,就是未来程序猿使用的两个类:Server类和Client类。

先来看看Server类,也就是服务器部分。我们先来思考一下服务器应该实现的最基本的功能:

  1. 开启服务器
  2. 关闭服务器(宕机)
  3. 强行关闭服务器(强制宕机)

我们先来看看这个Server类的简单实现:

public class Server implements Runnable{
	private static final int DEFAULT_PORT = 54188;
	private static final int MAX_CLIENT_COUNT = 10;
	
	private ServerSocket serverSocket;
	private int port;
	private volatile boolean goon;
	private int maxClientCount;
	
	public Server() {
		this.port = DEFAULT_PORT;
		this.goon = false;
		this.maxClientCount = MAX_CLIENT_COUNT;
	}
	
	
	public void setPort(int port) {
		this.port = port;
	}


	public void setMaxClientCount(int maxClientCount) {
		this.maxClientCount = maxClientCount;
	}

	public void startup() throws IOException {
		if (this.goon) {
			// 此时服务器已经启动过了,可是APP怎么知道呢?
			return;
		}
		
		this.goon = true;
		this.serverSocket = new ServerSocket(this.port);
		// 此时服务器成功启动,APP怎么知道呢?
		new Thread(this, "MEC SERVER").start();
		
	}

	@Override
	public void run() {
		// 此时服务器已经开始侦听客户端的连接请求,但是APP怎么知道呢?
		;
		try {
				Socket clientSocket = serverSocket.accept();
				String clientIp = clientSocket.getInetAddress().getHostAddress();
				// 此时客户端【clientIp】已经连接,但是APP怎么知道?
				// 客户端在实际情况下不止一个,应该生成一个与客户端
				// 会话的ServerConversation将它们统一管理起来。
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

我们可以看到多行注释都提到一个问题,到底怎么让APP知道我们的许多操作已经执行了?

2. 解决问题:

APP的具体实现形式我们是不知道的,这时候接口这个犀利的工具就发挥它的作用了。我们可以知道APP总是要从我们的server中获取相关信息并且对相关信息进行处理,可是怎么才能获取呢,这个问题其实是很困难的原因还是我们并不知道APP的具体实现形式是怎样的!

but,我们如果换一种思路,如果APP能够提供一个处理server信息的方法,我们的server再去调用这个方法,这不就是APP获取了我们server中的信息了吗!至于APP如何去处理这些信息就不用我们关心了,所以我们可以定义一个“倾听者”的接口——IListener:

public interface IListener {
   void dealMessage(String message);
}

这样只要用户实现这个接口,我们调用就可以了!

我们在来思考一下从server中得到信息的APP相关类并不止一个,所以我们需要一个List来保存它们并给出删除,增加以及给所有倾听者发消息的方法,由此,我们在定义一个“消息发布者”接口——ISpeaker:

public interface ISpeaker {
	void addListener(IListener listener);
	void removeListener(IListener listener);
	void speakOut(String message);
}

可能大家看到上面的文字感到很疑惑,啥叫个从server中得到信息的APP相关类并不止一个,我给大家举个栗子:
比如说在APP层的客户端有两个类,一个是有关登录页面的,一个是有关聊天页面的。我们登录不管成功与否,那个页面是不是都会告诉我们结果,这个结果其实是我们服务器对于我们登录信息验证的结果,还有在聊天界面我们有时候微信聊天发出的消息会被拒收嘛,也是服务器在比较两个用户之间的好友关系后才给出的信息。这就是从server中得到信息的APP相关类并不止一个
现在再让server类实现这个ISpeaker接口,并且实现在接口中未实现的方法:

public class Server implements Runnable, ISpeaker{
	// 无关代码暂时省略
	private List<IListener> listenerList;
	
	public Server() {
		// ... ...
		listenerList = new ArrayList<IListener>();
	}
	
	
	// ... ...
	@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 speakOut(String message) {
		for (IListener listener : this.listenerList) {
			listener.dealMessage(message);
		}
	}	

上面我们提到了应该控制客户端的接入数量,我们可以在最开始定义一个final类型的MAX_CLIENT_COUNT,并且在构造方法中将这个MAX_CLIENT_COUNT赋值给maxClientCount,我们再给一个setter方法去设置maxClientCount,这样的话就算用户没有调用我们的setter方法,我们的maxClientCount也会有一个默认值。
我们当然也可以通过properties文件进行初始数据的配置(这样显得我们很专业)

	public static final int DEFAULT_PORT = 54188;
	public static final int DEFAULT_MAX_CLIENT_COUNT = 10;
	
	private int port;
	private int maxClientCount;

	// ... ...
	public void setPort(int port) {
		this.port = port;
	}

	public void setMaxClientCount(int maxClientCount) {
		this.maxClientCount = maxClientCount;
	}
	// ... ...

	public Server initServer(String configPath) {
		Pro_Parser.load(configPath);
		int temp = 0;
		try {
			temp = Pro_Parser.get("port", int.class);
			if (temp != 0) {
				this.port = temp;
			}
		} catch (Exception e) {
		}
		
		temp = 0;
		
		try {
			temp = Pro_Parser.get("max_client_count", int.class);
			if (temp != 0) {
				this.maxClientCount = temp;
			}
		} catch (Exception e) {
		}
		
		return this;
	}

那个properties文件的解析我自己写了一个还不错的工具,需要的可以留言哦。
我们在上面还提到了用ServerConversation将服务器与客户端的会话统一管理起来,具体怎么管理呢——我们可以给一个clientPool,将所有合法的 client纳入这个clientPool。这个合法的又是怎么理解的呢,这里我们可以将每个客户端的ID作为一个标志这个客户端合法的唯一值。所以,这个clientPool的类型就应该是Map,并且给出一系列的get,add,remove以及得到特定的一些客户端的方法。

public class ClientPool {
	private Map<String, ServerConversation> clientPool;
	
	ClientPool() {
		this.clientPool = new HashMap<String, ServerConversation>();
	}
	
	boolean isEmpyt() {
		return this.clientPool.isEmpty();
	}
	
	int getClientCount() {
		return this.clientPool.size();
	}
	
	void addClient(ServerConversation client) {
		String clientId = client.getId();
		this.clientPool.put(clientId, client);
	}
	
	ServerConversation removeClient(ServerConversation client) {
		String clientId = client.getId();
		
		return this.clientPool.remove(clientId);
	}
	
	ServerConversation getClient(String clientId) {
		return this.clientPool.get(clientId);
	}

	List<ServerConversation> getClientsExcept(String clientId) {
		// 得到群发列表,这群发列表应该是除了发送者自己的所有的所有客户端列表
		List<ServerConversation> clientList = new ArrayList<ServerConversation>();
		if (this.clientPool.isEmpty()) {
			return clientList;
		}
		
		for (String id : this.clientPool.keySet()) {
			ServerConversation client = this.clientPool.get(id);
			if (client.getId().equals(clientId)) {
				continue;
			}
			clientList.add(client);
		}
		
		return clientList;
	}
	
	List<ServerConversation> getClientList() {
		// 这是得到所有客户端列表
		return getClientsExcept(null);
		// 通过编写的得到群发列表,将群发列表的需要的形参改为null即可
	}
	List<ServerConversation> getSpOther(List<String> idList) {
		// 指定群发,类似于QQ每到节日的群发祝福
		List<ServerConversation> clientList = new ArrayList<ServerConversation>();
		for (String id : idList) {
			ServerConversation client = this.clientPool.get(id);
			clientList.add(client);
		}
		
		return clientList;
	}

然后将clientPool加入到我们的server中,以便于我们对client的管理。

做了这么多的准备工作了,我们就来看看这些接口如何在这个Server中发挥作用吧。

启动服务器

1. 检测控制服务器线程开关的goon是否为true,如果为true的话说明服务器已经开启了,就告诉APP一声,
	说我已经开启了,别再开服务器了,烦死了。
2. 如果没有开启的话,我们先把客户端池(clientPool)准备好
3. new一个ServerSocket,将控制线程开关的goon赋值为true,开启这个服务器的线程,
	 再给APP说一声我这边开了啊
4.如果我们catch到了异常的话也是得给APP说一声的
public void startup() {
	if (this.goon == true) {  // -> if (this.goon)
		speakOut("服务器已经开启!");
	}	
	try {
		speakOut("准备开启服务器... ...");
		this.clientPool = new ClientPool();
		
		this.serverSocket = new ServerSocket(this.port);
		this.goon = true;
		new Thread(this, "SERVER");
		speakOut("服务器开启成功!");
	} catch (IOException e) {
		speakOut("服务器开启出现异常!");
	}
}

侦听客户端的连接

1.侦听客户端的连接,如果咱的客户端池子满了,就让那个客户端执行一下他的outOfRoom方法
2. 如果还能继续纳入客户端的话就有点意思了,我们得想办法给这个客户端一个唯一的ID,
	并将这个ID注入该客户端,然后得把这个ID发给咱们的ClientConversation,等那边认证通过
	后咱们再把这个客户端纳入咱的池子里
3.如果要是出现异常的话,一定记住咱得及时把线程给关了啊
	@Override
	public void run() {
		speakOut("开始侦听客户端的连接... ...");
		
		while (this.goon) {
			try {
				Socket clientSocket = this.serverSocket.accept();
				ServerConversation client = new ServerConversation(clientSocket, this);
				speakOut("客户端【" + client.getIp() + "】请求连接... ...");
				
				if (this.clientPool.getClientCount() >= this.maxClientCount) {
					client.outOfRoom();
				} else {
					String id = client.getIp() + ":" + client.hashCode();
					client.setId(id);
					client.sendId(id);
					
					this.clientPool.addClient(client);
					speakOut("客户端【" + client.getIp() + "】连接成功!");
				}
				
			} catch (IOException e) {
				this.goon = false;
			}
		}
		speakOut("终止客户端的连接请求!");	
	}

里面好多有关client的方法咱们还没编写的,不急,咱在ServerConversation里面给空方法就是了,至于咋写,咱先不管,这边逻辑先通畅再说。

关闭服务器

1.还是得先看看我们这个goon是否为false,是false的话就给APP说一声,你都关了还关啥
2.我们还得看看我们的客户端池子中还有没有客户端,要是没有的话我们就直接关了
3.要是还有的话我们就得给APP说一声,“还有人呢,你关啥!!!” 
public void shutdown() {
	if (this.goon == false) {
		speakOut("服务器已经关闭,请勿重复关闭!");
		return;
	}
	
	if (this.clientPool.isEmpty()) {
		speakOut("即将关闭服务器!");
		close();
	} else {
		speakOut("尚有客户端在线,不能宕机!");
	}
}

啊呀,这咋多了个close()方法呢?
那是因为我们关闭服务器主要就是把ServerSocket给关掉,我们后面的强制宕机也得把ServerSocket给关掉,同一份代码写两遍这可不是我们java的风格啊,所以咱把它单独拿出来形成一个方法我们用着也方便,逻辑也清晰。这个关闭的思路跟我们之前在上一篇文章中的关闭方法几乎是一模一样的,理解起来也是蛮简单的。

private void close() {
	this.goon = false;
		
	if (this.serverSocket != null && !this.serverSocket.isClosed()) {
		try {
			this.serverSocket.close();
		} catch (IOException e) {
		} finally {
			this.serverSocket = null;
		}
	}
}

服务器强制宕机

这个的前两步和咱们正常关闭服务器是一模一样滴,但是最后一步咱们就很霸气了,咱这名字就叫服务器强制宕机,就要突出一个强制。
所以,咱就把客户端池子里面那些client拿出来让他们执行自己的在服务器强制宕机时的方法,然后再把他们从客户端池子里给踢出去。

public void forcedown() {
	if (this.goon == false) {
		speakOut("服务器已经关闭,请勿重复关闭!");
		return;
	}
		
	if (this.clientPool.isEmpty()) {
		speakOut("即将关闭服务器!");
		close();
	} else {
		List<ServerConversation> clientList = this.clientPool.getClientList();
		for (ServerConversation client : clientList) {
			client.forcedown();
			this.clientPool.removeClient(client);
		}
		close();
	}
}

那个client的forcedown方法咱们暂时还没编写,没关系,还是一样在ServerConversation里面给个空方法就行了,咱们先把大体逻辑上的关系满足了就行,至于到底咋写,往下看看呗。

ServerConversation(一部分)

咱们上面给这个ServerConversation留了那么多坑,现在慢慢填呗。
在侦听客户端连接时我们看到了,他的构造方法得给一个Server,他得有id成员,得有ip成员,id得取得存,ip得取出来,知道了这些这不就好写多了。

	private String id;
	private String ip;
	private Server server;
	

	protected ServerConversation(Socket socket, Server server) throws IOException {
		super(socket);
		this.server = server;
		this.ip = socket.getInetAddress().getHostAddress();
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getIp() {
		return ip;
	}

这个ServerConversation是继承于Communication的所以他的俩抽象方法咱还得给他实现了,
至于那个dealNetMessage方法的实现有一定的难度,就不在本篇中讲了,但是那个peerAbnormalDrop却很容易啊,既然你这个客户端异常掉线了,那我直接把你从我的客户端池子里踢出去就行啦

	@Override
	public void peerAbnormalDrop() {
		this.server.getClientPool().removeClient(this);
		this.server.speakOut("客户端【" + this.id + "】异常掉线!");

	}

然后就是剩下的三个咱们在上面留下的空方法,咱们的ServerConversation的作用就跟他爹Communication差不多也是发消息,只不过这是给客户端会话层发的,上面的三个操作咱就把消息告诉客户端会话层,至于他们对咱们消息作何处理是ClientConversation中dealNetMessage要干的事,咱只告诉他就行了。
所以,这三个方法主要就是把想给客户端会话层说的话发给他就行。

	void forcedown() {	 //服务器强制宕机,只需要告诉客户端会话层,然后将这个
						//与之对应的服务器会话层关掉即可
		
		send(new NetMessage()
				.setCommand(ENetCommand.FOREC_DOWN));
		close();
	}

	void outOfRoom() {
		send(new NetMessage()
				.setCommand(ENetCommand.OUT_OF_ROOM));
		close();  
		
	}

	void sendId(String id) {
		send(new NetMessage()
				.setCommand(ENetCommand.ID)
				.setMessage(id));
		
	}

上篇文章我说了将NetMessage中的set方法返回值设置为NetMessage类型的好处我们在sendId方法中就可以看到了。
这篇文章写到这也差不多了,再多大家也不容易理解了,这次就先不附上源码了,因为还没有写完,有些地方可能在下一篇文章中可能还要改,所以大家理解一下。
如果你真的能看完的,真的谢谢你对这篇文章的不讨厌,有啥问题还请大佬指正,有啥疑惑也可以在下面留言。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值