NIOCSFramework

NIOServerAndBIOClientFrameWork

Ⅰ、前言

​ 本文旨在实现一个服务器端采用NIO通信模式,而客户端采用BIO通信模式的工具型框架,且此框架可以二次开发,添加其他更完善的功能与操作。

该框架将完成如下几个功能:

  • 服务器与客户端一对多,实现长连接,服务器端将连接上服务器的客户端会话放入一个特殊的客户端会话池中统一。
  • 服务器定期对客户端连接池中的所有用户发送信息,判断用户是否掉线
  • 服务器端采用轮询的方式处理缓存区中接收到的字节流
  • 客户端可以向服务器发起请求,请求完毕后等待服务器的响应信息
  • 提供APP接口,供进一步开发
  • 客户端可以进行一对一,一对多的发送消息
  • 实现分发器
  • 提供通讯日志(可配置)

​ NIO是一种非阻塞的IO(网络通信)模式,它与BIO相对应,BIO采用的是阻塞式的网络通信,BIO模式最大的缺点是不能开启过多的侦听线程,开启过多的侦听线程会使得服务器的负载增大,效率急剧降低。当然BIO也有它的优势,它的灵敏度高,实时性好,尤其的客户端一旦异常下线,服务器立刻就能感知到,此外服务器的主动性非常强,可以主动给客户端发送信息(推送)。

​ 而与之对应的NIO模式对于网络信息的处理方式不同,NIO模式采用一种轮询的方式,如果侦听到存在消息,不需要阻塞线程,这种非阻塞的机制使得NIO模式的服务器端可以开启很多个侦听线程,大大减轻了服务器端的负载,同样,此模式也存在劣势,它的劣势在于服务器灵敏度不强,无法感知到客户端的异常掉线,需要我们采取一些处理方式来解决此问题。

​ 既然NIO与BIO模式均有他们的优势与劣势,那么我们便采用二者结合的方式搭建起一个框架,为了减轻服务器端的负载与加快效率,在服务器端采用NIO模式,而在客户端则采用灵敏度高实时性好的BIO模式。这样框架的网络通信的选择就完成了。

Ⅱ、准备工作

​ 在正式开始框架的实现之前,我先来介绍一下我的工具包。为了便于我的开发工作,我实现了一个常用的工具包,里面包含了 Java 和 MySQL 的 ORM工具,包扫描,XML解析,Properties文件解析,观察者模式模板等等在开发中我需要经常用到的东西,我的CsFramework中实现的分发器,就是基于XML文件解析器,和通过包扫描器扫描注解完成的,所以 我在此先给出这两个工具的代码,关于这些工具是如何实现的,有时间我会再写文章发出。

如果对这里觉得云里雾里的读者,可以先跳过这一部分,再看到相应功能的实现时再跳转回这里。

A. 包扫描器

大家知道,Java中的命名空间是通过包来手动实现的,往往一个工程下我们会建立许多的包,在包中建立许多的类,包扫描器就是实现了一个这样的功能,它会扫描用户给出的包名,在这个包中遍历所有的类文件,通常包扫描器会配合注解一起使用,这样我们就能找到被我们要求的注解过的类,从而进行反射或者其他操作。

package util;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 包扫描类
 */
public abstract class PackageScanner {

	public PackageScanner() {
	}
	
	public abstract void dealClass(Class<?> klass);

	/**
	 * 对包内的文件进行分类解析
	 * @param packageName
	 * @throws URISyntaxException
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	public void scanPackage(String packageName) 
			throws URISyntaxException, ClassNotFoundException, IOException {
		String pathName = packageName.replace('.', '/');
		URL url = Thread.currentThread().getContextClassLoader().getResource(pathName);
		
		String protocol = url.getProtocol();
		if (protocol.equals("jar")) {
			dealJar(packageName, url);
		} else {
			File root = new File(url.toURI());
			dealDir(packageName, root);
		}
	}

	/**
	 * 循环递归处理jar包
	 * @param packageName
	 * @param url
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private void dealJar(String packageName, URL url) throws IOException, ClassNotFoundException {
		JarURLConnection connection = (JarURLConnection) url.openConnection();
		JarFile jarFile = connection.getJarFile();
		Enumeration<JarEntry> entries = jarFile.entries();
		while (entries.hasMoreElements()) {
			JarEntry entry = entries.nextElement();
			String entryName = entry.getName();
			if (!entryName.endsWith(".class")) {
				continue;
			}
			entryName = entryName.replace(".class", "");
			String className = entryName.replace("/", ".");
			if (!className.startsWith(packageName)) {
				continue;
			}
			Class<?> klass = Class.forName(className);
			dealClass(klass);
		}
	}

	/**
	 * 循环处理package内的所有类
	 * @param packageName
	 * @param curDir
	 * @throws ClassNotFoundException
	 */
	private void dealDir(String packageName, File curDir) throws ClassNotFoundException {
		File[] files = curDir.listFiles();
		for (File file : files) {
			if (file.isDirectory()) {
				String dirName = file.getName();
				dealDir(packageName + "." + dirName, file);
			} else {
				String fileName = file.getName();
				if (fileName.endsWith(".class")) {
					String className = fileName.replace(".class", "");
					className = packageName + "." + className;
					Class<?> klass = Class.forName(className);
					dealClass(klass);
				}
			}
		}
	}
	
}

B、Properties文件解析器

然后是Properties文件解析器,Java的Properties解析流程还是比较麻烦,这个工具旨在避免重复写类似的代码,所以我将固定的代码做成了一个模板。要解析Properties文件直接用这个工具就好,不需要再去写制式的代码。

package util;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * 包扫描
 */
public class PropertiesParser {
	private final static Map<String, String> propertyPool;
	static {
		propertyPool = new HashMap<String, String>();
	}

	/**
	 * 加载properties文件,并存储其中的键与值为一个Map
	 * @param propertiesPath
	 */
	public static void load(String propertiesPath) {
		InputStream is = PropertiesParser.class.getResourceAsStream(propertiesPath);
		if (is == null) {
			throw new RuntimeException("Properties文件" + propertiesPath + "不存在");
		}
		
		try {
			Properties properties = new Properties();
			properties.load(is);
			for (Object objKey : properties.keySet()) {
				String key = (String) objKey;
				String value = properties.getProperty(key);
				
				propertyPool.put(key, value);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 取出一个键所对应的值,并通过TypePaser解析器得到它的真实类型
	 * @param key
	 * @param klass
	 * @param <T>
	 * @return
	 * @throws Exception
	 */
	public static <T> T get(String key, Class<?> klass) throws Exception {
		String strValue = PropertiesParser.get(key);
		if (strValue == null || strValue.length() <= 0) {
			throw new Exception("键[" + key + "]不存在或未赋值!");
		}

		return (T) TypeParser.strToValue(klass, strValue);
	}
	
	public static String get(String key) {
		return propertyPool.get(key);
	}

	/**
	 * 得到所有存储的键
	 * @return
	 */
	public static List<String> getKeyList() {
		if (propertyPool.isEmpty()) {
			return null;
		}
		
		List<String> keyList = new ArrayList<String>();
		for (String key : propertyPool.keySet()) {
			keyList.add(key);
		}
		
		return keyList;
	}
	
}

C、TypePaser类型解析器

可以通过我们传入一个字符串类型的参数,以及这个参数原本的类型,就可以转换得到原本的参数。本质是使用了类型的ValueOf函数进行转化

public class TypeParser {
	private static final Map<String, Class<?>> typePool;
	static {
		typePool = new HashMap<String, Class<?>>();
		typePool.put("byte", byte.class);
		typePool.put("char", char.class);
		typePool.put("boolean", boolean.class);
		typePool.put("short", short.class);
		typePool.put("int", int.class);
		typePool.put("long", long.class);
		typePool.put("float", float.class);
		typePool.put("double", double.class);
		typePool.put("string", String.class);
	}
	
	public TypeParser() {
	}
	
	public static Class<?> strToClass(String str) throws ClassNotFoundException {
		Class<?> type = typePool.get(str);
		if (type == null) {
			type = Class.forName(str);
		}
		
		return type;
	}
	
	public static Object strToValue(Class<?> type, String strValue) {
		if (type.equals(byte.class)) {
			return Byte.valueOf(strValue);
		}
		if (type.equals(char.class)) {
			return strValue.charAt(0);
		}
		if (type.equals(boolean.class)) {
			return Boolean.valueOf(strValue);
		}
		if (type.equals(short.class)) {
			return Short.valueOf(strValue);
		}
		if (type.equals(int.class)) {
			return Integer.valueOf(strValue);
		}
		if (type.equals(long.class)) {
			return Long.valueOf(strValue);
		}
		if (type.equals(float.class)) {
			return Float.valueOf(strValue);
		}
		if (type.equals(double.class)) {
			return Double.valueOf(strValue);
		}
		if (type.equals(String.class)) {
			return strValue;
		}
		
		return null;
	}

Ⅲ、框架的搭建

A、通信的建立
a、底层通信类

​ 在对于通信层的初步构想时,起初是分开编写NIOCommunication与BIOCommunication两个类的,但是在中途发现这两者见存在很多重复代码,为了体现代码复用,不产生过多重复代码,将二者的相同部分提取出来,作为一个基类。供二者继承使用,体现了从外到内的编程思想。

/**
 * @author leiWei
 *  底层通信类的实现,在对NioCommunication与BioCommunication两个通信类的实现过程中
 *  发现可以提取二者的共同点作为一个基类Communication类
 */
public class Communication {
    //输入输出通信信道
    protected Socket socket;
    protected DataOutputStream dos;
    protected DataInputStream dis;

    //信息的发送与接收
    protected MessageTransfer messageTransfer;

    //外部接口
    protected ICommunication communication;

    public Communication(Socket socket) throws IOException {
        this.socket = socket;
        this.messageTransfer = new MessageTransfer();
        this.dis = new DataInputStream(this.socket.getInputStream());
        this.dos = new DataOutputStream(this.socket.getOutputStream());
    }

    //设置进来自定义处理方案
    void setCommunication(ICommunication communication) {
        this.communication = communication;
    }

    //封装底层的MessageTransfer类
    void setBufferSize(int bufferSize) {
        this.messageTransfer.setBufferSize(bufferSize);
    }

    /**
     * 发送信息
     * @param netMessage
     * @return
     */
    public boolean send(NetMessage netMessage) {
        try {
            this.messageTransfer.send(dos, netMessage);
            return true;//表示发送成功
        } catch (IOException e) {
            e.printStackTrace();
            //表示发送失败
            return false;
        }
    }

    /**
     * 接收信息
     * @return
     */
    public NetMessage receive() {
        NetMessage netMessage = null;
        try {
            netMessage = this.messageTransfer.receive(this.dis);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return netMessage;
    }
}

​ 这个Communication类实现了根据提供的Socket客户端建立了通信信道,实现了消息传输的媒介,并且将底层的消息传输类封装起来,其他通信层只能通过此类间接对内部的MessageTransfer类进行参数设置与调用。并且此类为外部提供了一个接口,即ICommunication类,在进行此框架的使用以及二次开发时可以设置接口中对端异常宕机,以及对接收到消息的处理的定义。

​ 关于MessageTransfer,它是采取了分两次发送的机制,第一次发送包含信息的消息头,第二次则发送消息体,且发送消息体时不采用直接发送字符串,为了节省空间,提高效率,采用分片传输的方式,直到消息体完全发送完毕。具体实现如下:

/**
 * @author leiWei
 * 信息的发送与接收类
 */
public class MessageTransfer {
    public static final int DEFAULT_BUFFER_SIZE = 1 << 15;
    private int bufferSize;

    public MessageTransfer() {
        this.bufferSize = DEFAULT_BUFFER_SIZE;
    }

    //设置默认长度
    void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    void send(DataOutputStream dos, NetMessage netMessage) throws IOException{
        //发送消息头
        dos.writeUTF(netMessage.toString());

        byte[] message = netMessage.getMessage();
        if (message == null) {
            message = new byte[]{};
        }
        /**
         * 分片传输,需要提供bufferSize,片段长度
         * 如果剩余信息长度为大于片段长度,则采用片段长度为发送的片段长度
         * 否则为剩余信息长度
         */
        int length = message.length;
        int offset = 0;
        int len = 0;
        while (length > 0) {
            len = length > this.bufferSize ? this.bufferSize : length;
            dos.write(message, offset, len);
            length -= len;
            offset += len;
        }
    }

    NetMessage receive(DataInputStream dis) throws IOException {
        //接收信息头
        String mess = dis.readUTF();
        NetMessage netMessage = new NetMessage(mess);

        int length = netMessage.getLength();
        //信息体
        byte[] message = new byte[length];
        int offset = 0;
        int len = 0;

        //接收信息体
        while (length > 0) {
            len = length > this.bufferSize ? this.bufferSize : length;
            //这样第二次给len赋值是防止网络传输过程中漏传,防止出现空的片段
            len = dis.read(message, offset, len);
            length -= len;
            offset += len;
        }
        netMessage.setMessage(message);

        return netMessage;
    }
}

b、通信层的实现
NIOCommunication:

​ 对于NIO通信模式在前言已经介绍过,在这里就不做过多阐述,基于Communication创建NIOCommunication类,作为服务器端的底层通信信道。此类中定义了三个状态变量且均被volatile类所修饰:

  • busy :当服务器端在轮询客户端信息时,同一时间只可以接收一个信息,这样保证了对接收到的信息的完整性与正确性

  • alive :通信对端是否正常在线

  • peerAbnormalDrop : 由于NIO是非阻塞式通信,不能像BIO模式一样灵敏的感知对端的异常,通过此标志为标记对端是否异常掉线

    接收客户端发送的信息:使用available()来判断是否存在输入流未处理,且使用busy设置每次只能接收一个信息,通过线程来接收此信息。保证了对接收到的信息的完整性与正确性。代码如下所示:

 /**
     * 轮询缓冲区中的信息
     */
    void checkMessage() {
        if (!this.alive) {
            return;
        }

        try {
            int len = this.dis.available();
            if (!isBusy() && len >= 0) {
                //处理一个信息的同时不可以别的线程进行receive操作,不然会导致消息接收错乱
                this.busy = true;
                //提供两种线程的执行方式 未设置进来线程池时采用创建新线程来执行
                if (threadPool == null) {
                    new Thread(new ClientPollinger()).start();
                } else {
                    this.threadPool.execute(new ClientPollinger());
                }
            }
        } catch (IOException e) {
            //客户端方出现问题,无法确定故障出现位置
            e.printStackTrace();
        }
    }

    /**
     * 接收处理客户端发送的信息
     */
    class ClientPollinger implements Runnable{
        public ClientPollinger() {
        }

        @Override
        public void run() {
            try {
                NetMessage message = receive();
                //处理完毕后设置busy为false,可以继续接收
                busy = false;
                //接口处理接收到的信息
                communication.dealMessage(message);
            } catch (IOException e) {
                //接收出现异常,大概率是对端发生异常掉线
                clientPeerAbnormalDrop();
            }
        }
    }

向客户端发送信息:由于NIO无法判断对端状态,我设置在客户端需要下线时向服务器发送一个OFF_LINE信息,表示自己正常下线。而客户端接收到此信息之后,再次发送一个OFF_LINE信息给客户端,当客户端接收到服务器发送的OFF_LINE信息后才真正下线。而服务器端在发送信息给客户端的过程中设置客户端下线的状态与存活状态。具体实现如下:

/**
     * 第二次检查信息发送是否成功(对端是否存活)
     * @param netMessage
     * @return
     */
    @Override
    public boolean send(NetMessage netMessage) {
        //加锁的意义在于不可同时发送两个消息,会使得字节流混乱
        synchronized(this.dos) {
            //存活时才发送信息
            if (isAlive()) {
                //判断是否成功发送信息
                if (super.send(netMessage)) {
                    ENetCommond commond = netMessage.getCommond();
                    //当服务器端接收到客户端的下线信息时会同样发送一个信令为OFF_LINE的信息,供服务器内部进行对此下线客户端的操作
                    if (commond.equals(ENetCommond.OFF_LINE)) {
                        this.alive = false;
                        this.peerAbnormalDrop = false;
                        close();
                    }
                } else {
                    //这里发现死点,标记死点且执行发现死点后的操作,即客户端异常宕机时服务端的操作(由后续会话层的实现ICommunication的操作)clientAbnormalDrop();
                    clientPeerAbnormalDrop();
                }
            }
        }
        return true;
    }

异常下线处理机制:由于我们在发送信息与接收信息均判断客户端是否异常宕机,如果简单调用宕机操作,有可能导多次调用宕机操作,造成不必要异常出现,这里将异常宕机处理操作进行整合且保证执行一次。使用线程安全的单例模式实现:

/**
     * 对于客户端异常掉线的处理,且加锁只执行一次,设置状态以及关闭通信信道
     * 这里不可以简单的调用,因为如果简单调用的话,很有可能在同一时间收发都出现问题,导致多次调用异常宕机操作
     */
    private void clientPeerAbnormalDrop() {
        if (this.peerAbnormalDrop == false) {
            synchronized(NIOCommunication.class) {
                if (this.peerAbnormalDrop == false) {
                    setPeerAbnormalDrop(true);
                    setAlive(false);
                    close();
                    this.communication.peerAbnormalDrop();
                }
            }
        }
    }
BIOCommunication:

​ 相比于NIO模式,BIO的操作更为简单,我们只需要定义一个控制一个侦听线程不断侦听客户端所发送的信息,即定义了一个volatile修饰的boolean类型的goon变量,当goon为true时,线程不断侦听,当出现异常或者下线时设置goon为false,然后线程自动结束。

@Override
    public void run() {
        synchronized(lock) {
            this.lock.notify();
        }

        while (this.goon) {
            try {
                //正常接收信息
                NetMessage message = super.receive();
                ENetCommond commond = message.getCommond();
                if (commond.equals(ENetCommond.ARE_YOU_OK)) {
                    continue;
                }
                //由提供的接口处理接收到的消息,对于ARE_YOU_OK,此信息是作为检查客户端是否正常在线的,并不需要处理
                this.communication.dealMessage(message);
            } catch (IOException e) {
                if (this.goon) {
                    //服务器异常掉线,接收失败
                    this.goon = false;
                    this.communication.peerAbnormalDrop();
                }
            }
        }
    }

​ 在BIO启动侦听线程中使用了锁的小技巧,通过 lock锁的阻塞与唤醒,保证了在侦听线程开启之前不允许其他操作影响线程,保证了侦听线程的成功启动。

public void startListen() {
        synchronized(lock) {
            this.goon = true;
            new Thread(this).start();

            try {
                //阻塞主线程
                this.lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        synchronized(lock) {
            //唤醒主线程
            this.lock.notify();
        }
c、协议的实现

​ 作为在网络中传输的消息,肯定不可以杂乱无章,如果这样的话这样无论是发送端编码还是接收端解码都是一件头大的事情。所以在我们的框架里,需要定义一个简单的协议,在我们的框架中能传递的必须是规范的信息,这样服务器和客户端才能对其进行识别和解码。于是我模拟了的tcp头部,对于所传输的信息所进行协议的制定。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5sEucdO-1656910996657)(E:\TCP头部.png)]

​ 协议的三要素为:

  • 语法,即用来规定信息的格式;

  • 语义,即用来说明通信双方应当怎么做;

  • 时序,即详细说明事件的先后顺序。

    对于协议的定义在NetMessage中,分为信息头和信息体,且分别以不同的方式进行传输

    信息头:

  1. (枚举类)NetCommond:传输的命令,即标识此次消息的信令
  2. String action 分发器命令
  3. int type 发送消息的内容是什么类型的
  4. int length 发送消息的长度

​ 信息体:

  1. byte[] message传输的内容

通过自定义的协议在网络中传输,对于消息头部编码成一个字符串,中间以":"分割,使用toString方法来编码,这样编码对于解码的时候也很便利,只需要提供一个参数为String类型的构造方法就可进行解码。

/**
     * 对消息头的封装
     * 使用toString来进行消息头的封装
     * @return
     */
    @Override
    public String toString() {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(this.commond).append(":")
                .append(this.action).append((this.action == null || this.action.length() <= 0) ? "" : ":")
                .append(this.length).append(":")
                .append(this.type);
        return stringBuffer.toString();
    }
//对消息的头部的解析
    public NetMessage(String mess) {
        String[] strs = mess.split(":");
        this.commond = ENetCommond.valueOf(strs[0]);
        this.action = strs[1];
        this.length = Integer.valueOf(strs[2]);
        this.type = Integer.valueOf(strs[3]);
    }

对于消息体的设置提供三种方式来进行,如下所示:

/**
     * 设置binary类型的消息
     * @param message
     * @return
     */
    NetMessage setMessage(byte[] message) {
        this.type = BINARY;
        this.length = message.length;
        this.message = message;
        return this;
    }

    /**
     * 设置String类型的消息
     * @param message
     * @return
     */
    NetMessage setMessage(String message) {
        this.type = STRING;
        this.message = message.getBytes();
        this.length = this.message.length;
        return this;
    }

    /**
     * 根据type参数来设置的消息
     * @param message
     * @param type
     * @return
     */
    NetMessage setMessage(String message, int type) {
        this.type = type;
        this.message = message.getBytes();
        this.length = this.message.length;
        return this;
    }
d、会话层的构建

​ 对于会话层的创建是因为我们对外提供的server与client两个接口所完成的任务只需要调用底层的会话层来进行实现,即进行业务的分离,使得各部分各司其职,实现一个简单的分布式。会话层的作用大致分为三个方面

  • 对底层通信留出的Communication接口的实现
  • 信息的构建,以及调用底层通信类进行传输
  • 接收到信息后对信息进行解析,以及匹配到合适的处理方案,自动调用该方案

Clientconversation

​ 当客户端连接到服务器端后创建一个与服务器通信的会话,此会话是基于BIO通信,初始化会话的时候就会开启客户端通信层的侦听线程,立刻侦听客户端的信息发送。

ServerConversation

​ 而对于服务器端的会话层,由于服务器端的会话层的个数不止一个,而我们需要对其进行管理与使用,就必须有一个唯一的标识,于是我们采用对接收到的Socket连接进行hashCode编码,且以“ip + “@”+ hashCode”为每个会话的专属id。如下所示:

public ServerConversation(Server server, Socket socket, ThreadPoolExecutor threadPool) throws IOException {
        this.server = server;
        this.communication = new NIOCommunication(socket);
        this.communication.setCommunication(new ServerCommunication());
        this.communication.setThreadPool(threadPool);
        this.socket = socket;
        String ip = this.socket.getInetAddress().getHostAddress();
        this.id = ip + "@" + String.valueOf(this.socket.hashCode());
    }
B、外部接口的实现

​ 这里实现的两个类的目的是为之后对此框架的二次开发或者使用此框架实现app应用的使用者提供使用的接口,对于Client类是之后实现的客户端的一系列操作,并且对于底层的通信的控制与处理。而Server则是针对客户端所发送的信息进行的一系列响应。这里实现了服务器与客户端的参数配置与相关配置的开启。

a、Server

​ Server作为框架提供给外部使用的接口,它的作用在于外部可以按照自定义的端口号以及相关信息来开启一个NIO通信模式的服务器,由于使用底层使用NIO非阻塞通信模式实现,可以添加很多个监听与处理线程,故采用了客户端连接缓冲池来处理客户端连接,连接成功之后创建客户端会话,并按照所得到的会话id作为键存储在客户端会话池中。

客户端连接缓冲池:客户端连接缓冲池的作用是减轻服务器的负载,当同一时间存在大量连接需要处理时,如果不做对应的处理,很可能服务器会挂掉,而如果我们设置一个连接缓冲池,由连接池取出需要处理的连接来进行与客户端的会话的创建,而当缓冲池中没有连接时,处理客户端连接的线程就处于阻塞状态,当新的连接加入时,线程被唤醒,继续处理连接。具体实现逻辑如下所示:

/**
     * NIO客户端连接缓冲池,减轻服务器压力
     */
    class ClientSocketPool implements Runnable {
        //lcok锁,控制线程的阻塞与唤醒
        private Object lock;
        //控制线程的继续与停止
        private boolean goon;
        //使用队列存储客户端连接
        private Queue<Socket> socketQueue;

        public ClientSocketPool() {
            this.lock = new Object();
            //线程安全的队列
            this.socketQueue = new LinkedBlockingQueue<>();

            synchronized(this.lock) {
                this.goon = true;
                if (threadPool == null) {
                    new Thread(this).start();
                } else {
                    threadPool.execute(this);
                }

                try {
                    //第一次阻塞,保证线程成功开启
                    this.lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void inClient(Socket socket) {
            synchronized (this.lock) {
                this.socketQueue.offer(socket);
                //唤醒因客户端连接池为空而阻塞的线程
                this.lock.notify();
            }
        }

        @Override
        public void run() {
            synchronized(this.lock) {
                //第一次唤醒线程
                this.lock.notify();
            }


            speakOut("NIO开始处理客户端连接....");
            ServerConverSationPool serverConverSationPool = ServerConverSationPool.getNewInstance();
            while (this.goon) {
                //如果此客户端连接池为空,则阻塞此线程
                if (this.socketQueue.isEmpty()) {
                    try {
                        this.lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //运行到这里时证明连接池非空
                Socket client = this.socketQueue.poll();
                if (client == null) {
                    continue;
                }
                //TODO 处理客户端连接,即建立客户端会话池,进行通信
                try {
                    ServerConversation serverConversation = new ServerConversation(Server.this, client, threadPool);

                    serverConverSationPool.addServerConversation(serverConversation);
                    //客户端连接上服务器后,服务器向客户端发送一个携带会话id的信息
                    serverConversation.sendId();
                    speakOut("客户端【" + serverConversation.getId() + "】已连接到服务器!");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            speakOut("NIO停止处理客户端连接....");
        }

        public void closeClientSocketPool() {
            //这里再次唤醒线程,防止存在线程阻塞无法结束的情况
            synchronized (this.lock) {
                this.goon = false;
                this.lock.notify();
            }
        }
    }

​ 由于NIO对于对端的状态无法判断,于是采用定时死点检测方案,定期向客户端会话池中的所有alive为true的会话发送一个监测信息, 进行对端是否异常的判断。称之为死点查询,它的定时功能是使用了在之前文章中介绍过的基于多线程的定时器。在服务器端则是初始话定时的时间,以及死点查询任务的制定与开启。死点检测的功能有三个:

  1. 对现存活的客户端进行检测,检测是否在线
  2. 如果发现不在线的客户端,立刻向服务器广播相关客户端的异常信息
  3. 对已经判断为不在线的死点进行相关信息的删除,主要是客户端会话池的删除

​ **死点检测:**这里所实现的run方法并不是常规意义上的run方法,它的含义在于定义了一个任务,计时器每隔一段时间都会调用这个方法,来进行死点检测。

/**
     * 定时器的task,即死点查询
     * 1、对存活的会话二次确认是否存活
     * 2、告知服务器端会话下线状态(解决服务器无法得知用户状态)
     * 3、对已经标记的死点进行清除
     */
    class DeathChecker implements Runnable {

        public DeathChecker() {
        }

        @Override
        public void run() {
            ServerConverSationPool converSationPool = ServerConverSationPool.getNewInstance();
            while (ServerConverSationPool.hasNext()) {
                ServerConversation serverConversation = ServerConverSationPool.next();
                if (serverConversation.isAlive()) {
                    serverConversation.areYouOk();
                }
                //由于是多线程处理,这里需要再次判断存活状态
                if (!serverConversation.isAlive()) {
                    String conversationId = serverConversation.getId();
                    if (serverConversation.isPeerAbnormalDrop()) {
                        speakOut("客户端【" + conversationId + "】异常掉线!");
                    } else {
                        speakOut("客户端【" + conversationId + "】正常下线!");
                    }
                    converSationPool.removeServerConversation(serverConversation);
                }
            }
        }
    }

消息的轮询:对于消息的轮询的话,是采取不间断的连续方式,也可以选择与死点检测一样采取定时轮询的方式。而服务器端的轮询调用只需要调用完成客户端连接池中定义的轮询方法即可,真正的轮询过程则是在底层的通信层实现。

//轮询类
    class ClientPolling implements Runnable {
        private Object lock;
        private boolean goon;

        public ClientPolling() {
            this.lock = new Object();

            synchronized (this.lock) {
                this.goon = true;
                if (threadPool == null) {
                    new Thread(this).start();
                } else {
                    threadPool.execute(this);
                }

                //保证线程的正确开启
                try {
                    this.lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void run() {
            synchronized(this.lock) {
                this.lock.notify();
            }

            while (this.goon) {
                ServerConverSationPool serverConverSationPool = ServerConverSationPool.getNewInstance();
                serverConverSationPool.polling();
            }
            speakOut("NIO停止轮询客户端!");
            close();
        }

        private void close() {
            this.goon = false;
        }
    }

观察者模式:在Server中使用了观察者模式,也就是订阅者发布者模式,因为我们所开发的是一个可供二次开发的框架,我们当然需要一些信息来告知使用者框架服务器的运行状态,但是如果使用System.out.printlf()的方式的话如果使用者并没有采用控制台的话我们所做的日志记录就毫无意义了。此时我们使用观察者模式完成一个简单的日志系统,使用speakOut()方法来进行日志的输出,输出给我们所观察的订阅者列表。

订阅者:处理发布者发布的信息

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

发布者: 对订阅者的管理与向所有订阅者发布信息

public interface ISpeaker {
    void addListener(IListener var1);

    void removeListener(IListener var1);

    void speakOut(String var1);
}
b、Client

对于客户端的操作比较简单,客户端初始化时会根据服务器port与ip进行连接,连接完毕后创建一个客户端会话,在初始化会话时会开启我们的BIO通信基类的侦听服务器端的信息线程。也就是说Client类的作用主要在于对底层实现的通信过程的控制,以及供外部调用的下线、上线以及私发群发信息的调用。

//使用适配器模式来添加客户端行为
    private IClientAction clientAction;

    public Client() {
        this.ip = DEFAULT_SERVER_IP;
        this.port = Server.DEFAULT_SERVER_PORT;
        this.clientAction = new ClientActionAdapter();
    }

    //外部初始化Client后调用connect方法进行与服务器端的连接
    public void connect() throws IOException {
        Socket socket = new Socket(this.ip, this.port);
        this.clientConverSation = new ClientConverSation(this, socket);
        this.clientAction.afterConnect();
    }

IClientAction:供外部实现的接口

​ 在Client中我创建了一个由上层实现的客户端接收到服务器的响应之后的操作的接口类,由于这些基本功能是必须实现的,故我们采用接口的方式来调用,同时使用适配器来实现此接口,使得用户可以使用到哪个功能重写哪个方法,避免了无效的空方法体的大量出现。

/**
 * @author leiWei
 * 留给App层进行具体逻辑的实现
 */
public interface IClientAction {
    void serverPeerAbnormalDrop();

    void toOne(String id, String message);
    void toOther(String id, String message);

    void afterConnect();
    boolean ensureOffline();
    void beforeOffline();
    void afterOffline();
}

​ Client的第二个作用即是提供给外部一系列通信方法的调用,以及请求响应的处理,对于通信以及分发器实现未知的功能的请求与调用在下面的功能实现中着重描述。

Ⅳ、框架功能的完善

A、死点标记、对端异常的判断
a、发送过程

​ 由于NIO非阻塞式IO模式的弊端,服务器端无法准确得知客户端的状态,在框架中我采取了两次判断,Server只需要定时调用向客户端发送一个ARE_YOU_OK的状态信息即可完成死点的检测与对端是否异常的判断,而真正对于死点检测则在两次send发送的调用中:

第一次调用send方法:底层通信类中的send方法,如果发送成功则返回值为true,异常则返回false,这里的返回值将在第二次调用send方法中起到大作用!

 /**
     * 发送信息
     * 且第一次判断发送是否成功(对端是否存活)
     * @param netMessage
     * @return
     */
    public boolean send(NetMessage netMessage) {
        try {
            this.messageTransfer.send(dos, netMessage);
            return true;//表示发送成功
        } catch (IOException e) {
            e.printStackTrace();
            //表示发送失败
            return false;
        }
    }

第二次调用send方法:如果第一次发送信息失败,则证明此时对端已经下线了,此时调用客户端异常掉线的处理办法。

​ 这里可以看到如果客户端发送了下线信息之后,服务器端所返回的响应信息会在send过程中被拦截且设置存活状态与异常下线状态为false。

/**
     * 第二次检查信息发送是否成功(对端是否存活)
     * @param netMessage
     * @return
     */
    @Override
    public boolean send(NetMessage netMessage) {
        //加锁的意义在于不可同时发送两个消息,会使得字节流混乱
        synchronized(this.dos) {
            //存活时才发送信息
            if (isAlive()) {
                if (super.send(netMessage)) {
                    ENetCommond commond = netMessage.getCommond();
                    //当服务器端接收到客户端的下线信息时会同样发送一个信令为OFF_LINE的信息,供服务器内部进行对此下线客户端的操作
                    if (commond.equals(ENetCommond.OFFLINE)) {
                        this.alive = false;
                        this.peerAbnormalDrop = false;
                        close();
                    }
                } else {
                    //这里发现死点,标记死点且执行发现死点后的操作,即客户端异常宕机时服务端的操作(由后续会话层的实现ICommunication的操作)clientAbnormalDrop();
                    clientPeerAbnormalDrop();
                }
            }
        }
        return true;
    }
b、接收过程

​ 接收过程中也存在着对死点的标记以及对客户端异常宕机状态的设置,体现在服务器会话层的接收线程中:

/**
     * 接收处理客户端发送的信息
     */
    class ClientPollinger implements Runnable{
        public ClientPollinger() {
        }

        @Override
        public void run() {
            try {
                NetMessage message = receive();
                //处理完毕后设置busy为false,可以继续接收
                busy = false;
                //接口处理接收到的信息
                communication.dealMessage(message);
            } catch (IOException e) {
                //接收出现异常,大概率是对端发生异常掉线
                clientPeerAbnormalDrop();
            }
        }
    }

​ 如上所示,当我们无故接收客户端信息异常时,极大概率为客户端异常掉线,此时采用客户端异常的处理方案。

c、异常处理

​ 由于在发送端与接收端都存在者死点的检测与异常宕机的处理,且均采用多线程的方式处理,这样很容易造成二次调用异常处理行为,为了避免此问题,我采用线程锁加单例的方式,保证此行为只会被调用一次。

/**
     * 对于客户端异常掉线的处理,且加锁只执行一次,设置状态以及关闭通信信道
     * 这里不可以简单的调用,因为如果简单调用的话,很有可能在同一时间收发都出现问题,导致多次调用异常宕机操作
     */
    private void clientPeerAbnormalDrop() {
        if (this.peerAbnormalDrop == false) {
            synchronized(NIOCommunication.class) {
                if (this.peerAbnormalDrop == false) {
                    setPeerAbnormalDrop(true);
                    setAlive(false);
                    close();
                    this.communication.peerAbnormalDrop();
                }
            }
        }
    }
B、客户端会话池管理

​ 对于经过客户端缓冲池的客户端连接后,将生成一个客户端会话,而如此多的客户端会话则需要服务端集中管理,那么采用散列表来管理映射关系最为合适,于是基于socket创建一个特殊的id,以此id为键,会话为值存入散列表中。存储完毕后如果获取会话呢?肯定不能直接对散列表进行操作,起初的使用方案为采用一个复制的散列表,但是在后面对框架的测试中发现总会会出现数据不同步,等等的问题。

​ 于是我采取了一个自己实现的列表工具,里面提供了需要对每一个列表中的元素的操作,以及采用焦点链与非焦点链的动态转化实现了会话的安全添加与删除。

IPollingAction: 对所有T类型元素的操作,由使用者实现

public interface IPollingAction <T> {
	void pollingAction(T e);
}

PollingActionAdapter :IPollingAction的适配器,使用哪个方法覆盖哪个

public class PollingActionAdapter <T> implements IPollingAction<T> {

	public PollingActionAdapter() {
	}

	@Override
	public void pollingAction(T e) {}
}

PollingElement:存储T类型的数据,以及此数据是否被访问过,作为焦点链与非焦点链的存储类型,且visited作为两者身份是否转换的标志。

/*此类存储了一个数据类型,包括了存储的数据以及数据是否被访问过
默认未访问*/
public class PollingElement <T> {
	private T element;
	//标明是否访问过,供hasNext与Next进行是否访问的比较,为了多线程安全问题
	private int visited;
	
	PollingElement() {
		this.visited = 0;
	}

	PollingElement(T element) {
		this();
		this.element = element;
	}

	T getElement() {
		return element;
	}

	/*不允许set操作,因为每一个element都需要保证存在一个他的专属visited状态*/

	//返回这个element元素(是否被访问与传入的状态参数的相等比较,如果相同则返回真
	boolean isVisited(int status) {
		return this.visited == status;
	}

	void revVisited() {
		this.visited = 1 - this.visited;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this.element) return true;
		if (obj == null) 	return false;
		System.out.println("obj.getClass" + obj.getClass());
		if (!obj.getClass().equals(this.element.getClass())) return false;
		
		return this.element.equals(obj);
	}
	@Override
	public String toString() {
		return "PollingElement [element=" + element + ", visited=" + visited + "]";
	}
	
	
	
}

PollingList:轮询列表工具的真正实现,底层使用一个存储两个链表的链表来实现,两个链表为焦点链与非焦点链。对于遍历完毕后的添加

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;


/**
 * 在PollingList中存在两个链表,其中一个称为“焦点”链表,另一个是非焦点链表。
 * @author LW
 *
 * @param <T>
 */
public class PollingList <T> {
	private List<List<PollingElement<T>>> list;
	//表明焦点链的下标
	private volatile int focus;
	private Object lock;
	//对于链表中元素的操作
	private IPollingAction<T> pollingAction;
	//初始状态为0,即用来判断元素是否被访问,如果状态一致则未访问过
	private int status = 0;


	public PollingList() {
		this.focus = 0;
		this.lock = new Object();
		this.pollingAction = new PollingActionAdapter<T>();

		this.list = new ArrayList<List<PollingElement<T>>>();
		this.list.add(new LinkedList<PollingElement<T>>());
		this.list.add(new LinkedList<PollingElement<T>>());
	}

	//这里是在判断是否还有元素之后取得元素的操作
	public T next() {
		synchronized (this.lock) {
			List<PollingElement<T>> list = this.list.get(0);
			for (PollingElement<T> element : list) {
				//这里再次判断是否被访问过
				if (element.isVisited(this.status)) {
					//这里实际上执行的是访问元素操作,访问完毕后转换访问状态
					element.revVisited();
					//返回取得的元素
					return element.getElement();
				}
			}

			list = this.list.get(1);
			for (PollingElement<T> element : list) {
				if (element.isVisited(this.status)) {
					element.revVisited();
					return element.getElement();
				}
			}

			return null;
		}
	}
	/*判断两个链中是否还存在元素*/
	public boolean hasNext() {
		synchronized (this.lock) {
			List<PollingElement<T>> list = this.list.get(0);
			for (PollingElement<T> element : list) {
				//这里的比较条件判断元素的状态是否与List的状态一致,一致则证明未访问过
				if (element.isVisited(this.status)) {
					return true;
				}
			}

			list = this.list.get(1);
			for (PollingElement<T> element : list) {
				if (element.isVisited(this.status)) {
					return true;
				}
			}
		}
		//全部发现无未访问的,即均访问完毕后,使得list的状态转化,
		//	转换后对于已经经过next方法访问到的元素,又可以重新访问一次
		this.status = 1 - this.status;
		return false;
	}

	//这里是轮询操作,遍历焦点链中的元素,访问操作焦点链首个元素,访问完毕后删除元素,添加到非焦点链的末端
	//且由于锁的存在,使得删除首位元素不会出现逻辑错误,当先轮询再删除时,实际上删除的是非焦点链中的最后一个
	//先删除再轮询则所轮询到的是第二位元素
	public void polling() {
		synchronized (this.lock) {

			List<PollingElement<T>> focusList = this.list.get(this.focus);
			List<PollingElement<T>> auxList = this.list.get(1 - this.focus);

			while (!focusList.isEmpty()) {
				PollingElement<T> element = focusList.remove(0);
				this.pollingAction.pollingAction(element.getElement());
				auxList.add(element);
			}
			//轮询完毕后转换焦点状态
			this.focus = 1 - this.focus;
		}
	}

	/**
	 * 要增加的元素一定追加到“非焦点”链表的末尾!这样做可以避免多线程操作时的不一致性。
	 * @param e 要加入的元素
	 */
	//Appends the specified element to the end of this list 添加一个元素到非焦点链的列表
	public void add(T e) {
		synchronized (this.lock) {
			this.list.get(1 - this.focus).add(new PollingElement<T>(e));
		}
	}

	/**
	 * 若e存在于非焦点链表中,则,直接删除即可;若e存在于焦点链表中,需要先判断e是否是
	 * 焦点链表首元素,若不是首元素,则,直接删除即可;若是首元素,它肯定会成为“轮询”
	 * 的下一个元素,在删除时,可能会造成很多逻辑困境。
	 * @param e 要删除的元素
	 */
	public void remove(T e) {
		synchronized (this.lock) {
			//这里错误的原因是使用了e,即String类型的equals()方法
			if (this.list.get(this.focus).contains(e)) {
				this.list.get(this.focus).remove(e);
				return;
			}

			if (this.list.get(1 - this.focus).contains(e)) {
				this.list.get((1 - this.focus)).remove(e);
				return;
			}
		}
	}

	public void setPollingListAction(IPollingAction<T> pollingAction) {
		this.pollingAction = pollingAction;
	}

	public boolean isEmpty() {
		synchronized (this.lock) {
			return this.list.get(0).isEmpty() && this.list.get(1).isEmpty();
		}
	}

	public int size() {
		synchronized (this.lock) {
			return this.list.get(0).size() + this.list.get(1).size();
		}
	}

	//清空所有链表中的内容
	public void clear() {
		synchronized (this.lock) {
			this.list.get(0).clear();
			this.list.get(1).clear();
			this.focus = 0;
		}
	}

	public boolean contains(T object) {
		synchronized (this.lock) {
			return this.list.get(0).contains(object) || this.list.get(1).contains(object);
		}
	}

	//取得所有的元素
	public List<T> getElementList() {
		synchronized (this.lock) {
			List<T> elementList = new ArrayList<>();
			List<PollingElement<T>> focusList = this.list.get(this.focus);
			List<PollingElement<T>> auxList = this.list.get(1 - this.focus);

			for (PollingElement<T> ele : focusList) {
				elementList.add(ele.getElement());
			}
			for (PollingElement<T> ele : auxList) {
				elementList.add(ele.getElement());
			}

			return elementList;
		}
	}

}
C、通信信息的处理

​ 对于通信信息的处理,采用的是传输的信令对应存在一个方法来处理接收到这个信令的信息的操作。接收到信息之后首先得到信令,然后将信令单词取出按照驼峰式组成一个单词,且首字母大写,再加上deal即可得到方法名,然后按照反射机制得到对应的方法,反射执行此方法,使得接收到信息后的处理更加自动。进行名称转化以及反射调用的类为NetMessageDealer类。

package com.lw.nio.core;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author leiWei
 * 接收到信息后的自动匹配对应信令的处理方法,别反射自动执行该方法,对于服务器与客户端均适用
 * */
public class NetMessageDealer {

    public NetMessageDealer() {
    }

    /**
     * @param object 处理消息的对象
     * @param netMessage 接收到的信息
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    static void dealMessage(Object object, NetMessage netMessage) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ENetCommond commond = netMessage.getCommond();
        //通过Command匹配对应的deal处理方法
        String methodName = commandToMethodName(commond);

        Class<?> klass = object.getClass();
        Method method = klass.getDeclaredMethod(methodName, NetMessage.class);
        //反射调用处理方法
        method.invoke(object, netMessage);
    }

    /**
     * 转换信令为方法名
     * @param commond 信令
     * @return 规定处理消息的方法名为deal + 信令的驼峰格式
     */
    private static String commandToMethodName(ENetCommond commond) {
        //TO_ONE
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("deal");
        String str = commond.name();
        String[] coms = str.split("_");
        for(String com : coms) {
            com = com.substring(0,1) + com.substring(1).toLowerCase();
            stringBuffer.append(com);
        }
        return stringBuffer.toString();
    }

}

​ 上面介绍了信息的处理,下面介绍一些真正通信的实现。

​ 由于我们提供了对客户端会话的管理,即客户端连接池,我们可以根据客户会话的id来取得此客户端的会话,从而实现通信

一对一通信

​ 由发送客户端向服务器发送私聊信息,且对于action存储接收客户端的会话id,服务器会话层接收到信息后,将接收的会话id即发送客户端会话id,传输的信息,以及接收客户端的id传输给服务器的私聊方法(toOne),由服务器根据接收端会话id得到接收客户端会话,调用接收客户会话的s私聊方法,由服务器转发发送者id加信息message。

1)发送客户端调用发送信息:

 public void toOne(String id, String message) {
        this.clientConverSation.toOne(id, message);
    }

2)发送客户端会话向服务器发送信息:

void toOne(String id, String message) {
        NetMessage netMessage = new NetMessage().setCommond(ENetCommond.TO_ONE)
                .setAction(id)
                .setMessage(message);
        this.bioCommunication.send(netMessage);
    }

3)发送端的服务器端会话层接收到发送端信息:

void dealToOne(NetMessage netMessage) {
        String tarId = netMessage.getAction();
        String message = netMessage.getString();
        this.server.toOne(this.id, tarId, message);
    }

4)服务器根据接收端id得到接收端的服务器会话层:

void toOne(String id, String tarId, String message) {
        ServerConverSationPool serverConverSationPool = ServerConverSationPool.getNewInstance();
        ServerConversation serverConversation = serverConverSationPool.getServerConversation(tarId);
        serverConversation.toOne(id, message);
    }

5)接收端的服务器会话层向接收端客户端会话层发送信息:

/**
     * 私发信息
     * @param id
     * @param message
     */
    public void toOne(String id, String message) {
        NetMessage netMessage = new NetMessage().setAction(id)
                .setCommond(ENetCommond.TO_ONE)
                .setMessage(message);
        this.communication.send(netMessage);
    }

这里的id是发送端会话层id,告知接收端发送方是谁。

6)接收端会话层接收到信息后,调用客户端留给app层进行实现的接口方法:

void dealToOne(NetMessage netMessage) {
       String ownId = netMessage.getAction();
       String mess = netMessage.getString();
       this.clientAction.toOne(ownId, mess);
    }

​ 因为我们实现的是一个框架,而对于这个操作的后续实现我们是不知道的,故作为接口留给后续开发人员进行开发。

一对多通信

​ 对于一对多通信,其实它的本质还是在于使用一对一的操作,不过是在服务器端对于传输过来的发送者id,根据客户端会话池提供的除了此id之外的所有会话的方法得到所要发送的会话,在for循环中对此会话列表进行遍历,进行对于的服务器会话进行方法的调用,具体实现如下所示:

void toOther(String id, String noId, String message) {
        ServerConverSationPool serverConverSationPool = ServerConverSationPool.getNewInstance();
        List<ServerConversation> otherServerConversations = serverConverSationPool.getOtherServerConversation(noId);
        for (ServerConversation serverConversation : otherServerConversations) {
            serverConversation.toOther(id, message);
        }
    }

客户端下线通信

​ 对于客户端下线操作,是由客户端向服务器发送下线信息,当服务器会话层接收到下线信息后,再次向要下线的客户端发送下线通知,而在send发送信息的过程中对与客户端的通信信道的关闭操作,且对此客户端会话作正常下线标记,等待下次死点查询在客户端会话池中真正删除此会话。而当客户端再次接收到服务器端的下线信息时才会关闭通道,且执行app层制定的下线后的操作。

​ 1)客户端发起下线请求

/**
     * 下线操作
     */
    public void offline() {
        if (this.clientAction.ensureOffline()) {
            this.clientAction.beforeOffline();
            this.clientConverSation.offline();
            this.clientAction.afterOffline();
        }
    }

​ 2)客户端会话层向服务器端会话层发送下线信息

void offline() {
        this.bioCommunication.send(new NetMessage().setCommond(ENetCommond.OFFLINE)
                .setMessage(new byte[]{}));
    }

​ 3)服务器会话层接收到下线信息,并返回确认下线信息

 void dealOffline(NetMessage netMessage) {
        offline();
    }

void offline() {
        this.communication.send(new NetMessage().setCommond(ENetCommond.OFFLINE)
        .setMessage(new byte[]{}));
    }

​ 4)在返回信息的时期,进行状态的标记

/**
     * 第二次检查信息发送是否成功(对端是否存活)
     * @param netMessage
     * @return
     */
    @Override
    public boolean send(NetMessage netMessage) {
        //加锁的意义在于不可同时发送两个消息,会使得字节流混乱
        synchronized(this.dos) {
            //存活时才发送信息
            if (isAlive()) {
                if (super.send(netMessage)) {
                    ENetCommond commond = netMessage.getCommond();
                    //当服务器端接收到客户端的下线信息时会同样发送一个信令为OFF_LINE的信息,供服务器内部进行对此下线客户端的操作
                    if (commond.equals(ENetCommond.OFFLINE)) {
                        this.alive = false;
                        this.peerAbnormalDrop = false;
                        close();
                    }
                } else {
                    //这里发现死点,标记死点且执行发现死点后的操作,即客户端异常宕机时服务端的操作(由后续会话层的实现ICommunication的操作)clientAbnormalDrop();
                    clientPeerAbnormalDrop();
                }
            }
        }
        return true;
    }

​ 5)客户端接收到下线信息后,真正下线

void dealOffline(NetMessage netMessage) {
        this.bioCommunication.close();
    }
D、分发器的实现

​ 对于分发器这个名词的解释为,我们所做的仅仅是一个框架,并不知道之后的二次开发人员或者使用此框架的人具体会实现什么操作,那么此时我们就不能仅仅使用接口的方式来实现,于是采用分发器的方式来实现,即使用容器的思想,将需要在框架中使用的方法名(即操作名)作为键,存储操作的全部信息作为值。

​ 至于操作的全部信息,即action,arguments(经过转换后的含有参数名,类型,值的对象列表),class,object,method。

​ 对于需要管理的操作,类似于spring的配置,提供了两种方式,注解配置和XML文件配置,针对不同的方式存在不同的处理方式:

​ 对于注解配置采用包扫描的方式:

public static void scanMappingByAnnotation(String packageName) throws ClassNotFoundException, URISyntaxException, IOException {
		new PackageScanner() {
			@Override
			public void dealClass(Class<?> klass) {
				if (!klass.isAnnotationPresent(ActionClass.class)) {
					return;
				}
				try {
					Object object = klass.newInstance();
					Method[] methods = klass.getDeclaredMethods();
					for (Method method : methods) {
						if (!method.isAnnotationPresent(Action.class)) {
							continue;
						}
						//取得行为的名字
						Action action = method.getAnnotation(Action.class);
						String actionName = action.action();
						
						ActionBeanDefinition abd = new ActionBeanDefinition();
						abd.setAction(actionName);
						abd.setKlass(klass);
						abd.setObject(object);
						abd.setMethod(method);
						dealParameter(abd, method);
						
						actionBeanPool.put(actionName, abd);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}.scanPackage(packageName);
	}

​ 使用XML文件配置时的解析:

public static void scanMappingByXml(String mappingPath) throws Exception {
		new XMLParser() {
			@Override
			public void dealElement(Element element, int index) throws Exception {
				String action = element.getAttribute("name");
				String className = element.getAttribute("class");
				Class<?> klass = Class.forName(className);
				Object object = klass.newInstance();
				
				ActionBeanDefinition definition = new ActionBeanDefinition();
				definition.setAction(action);
				definition.setKlass(klass);
				definition.setObject(object);
				
				new XMLParser() {
					@Override
					public void dealElement(Element element, int index) throws Exception {
						String methodName = element.getAttribute("name");
						new XMLParser() {
							@Override
							public void dealElement(Element element, int index) throws Exception {
								String strType = element.getAttribute("type");
								String paraName = element.getAttribute("name");
								
								Argument argument = new Argument();
								argument.setName(paraName);
								argument.setParameterType(TypeParser.strToClass(strType));
								
								definition.addParameter(argument);
							}
						}.parse(element, "parameter");
						
						Class<?>[] parameterTypes = definition.getParameterTypes();
						Method method = klass.getDeclaredMethod(methodName, parameterTypes);
						definition.setMethod(method);
					}
				}.parse(element, "method");
				
				actionBeanPool.put(action, definition);
			}
		}.parse(XMLParser.getDocument(mappingPath), "action");
	}

​ 这里着重讲述注解配置:使用@ActionClass标志此类存在需要管理的行为,@Action则在方法上使用,标识行为,且其中存在一个必须赋值的action,即行为的名字。最后一个注解为@Para,它应用在参数上,且拥有一个属性value来记录参数名。

​ 在开始启动服务器前对需要管理的行为包进行包扫描或者对XML进行文件解析,将所需要的行为方法放置在容器中。具体的实现过程如下所示:

​ 包扫描之后可以得到除了参数列表之后的所有参数,故将method与 ActionBeanDefinition传入dealArgument方法中,对参数列表进行赋值,可以解决反射机制执行时会将形参名擦除的问题。

private static void dealParameter(ActionBeanDefinition abd, Method method) throws Exception {
		Parameter[] parameters = method.getParameters();
		int index = 0;
		for (Parameter parameter : parameters) {
			if (!parameter.isAnnotationPresent(Para.class)) {
				throw new Exception("方法[" + method.getName() + "]的第" + (index + 1) + "个参数没有Para注解!");
			}

			//这里解决了反射机制执行时会将形参名擦除的问题,提供一个注解@Para(value=name)
			Para para = parameter.getAnnotation(Para.class);
			String paraName = para.value();
			
			Argument arg = new Argument();
			arg.setName(paraName);
			arg.setParameter(parameter);
			arg.setParameterType(parameter.getType());
			abd.addParameter(arg);
			index++;
		}
	}
E、请求响应的具体实现

​ 分发器实现了之后,如何进行行为的调用则体现到了请求响应的实现,我们在协议的制定中的action即可提供给用户来进行行为的请求,而此请求action将请求action与响应action,客户端进行指定,而服务器端接收到信息之后对action进行解析,执行完毕请求action后,返回响应action给客户端。

请求action的执行

@Override
	public String dealRequest(String action, String argument) throws Exception {
		ActionBeanDefinition definition = ActionBeanFactory.getActionBean(action);

		if (definition == null) {
			throw new Exception("action" + action + "不存在!");
		}
		
		Object object = definition.getObject();
		Method method = definition.getMethod();
		
		Object[] args = getArgs(definition, argument);
		Object result = method.invoke(object, args);
		
		if(method.getReturnType().equals(void.class)) {
			return null;
		}
		return ArgumentMaker.gson.toJson(result);
	}

​ 从容器中取得对应的方法的全部信息,针对参数需要进行一些转化,如下所示。对于无返回值的方法执行完毕后直接返回null,存在返回值则返回执行后的结果。

private Object[] getArgs(ActionBeanDefinition definition, String string) {
		ArgumentMaker maker = new ArgumentMaker(string);
		int argCount = maker.getArgumentCount();
		
		if (argCount <= 0) {
			return new Object[] {};
		}
		
		Object[] args = new Object[argCount];
		int index = 0;
		List<Argument> argList = definition.getParameterList();
		for (Argument arg : argList) {
			String argName = arg.getName();
			Type type = arg.getParameter().getParameterizedType();
			args[index++] = maker.getArg(argName, type);
		}
		
		return args;
	}

响应action的执行

​ 这里的参数只有一个,因为请求执行完毕后,返回值为0或1个,故无需进行参数列表的转化,只需要对一个参数进行转化。

@Override
	public void dealResponse(String action, String argument) throws Exception {
		ActionBeanDefinition definition = ActionBeanFactory.getActionBean(action);
		
		if (definition == null) {
			throw new Exception("action[" + action + "] 不存在!");
		}
		Object object = definition.getObject();
		Method method = definition.getMethod();
		
		Parameter parameter = method.getParameters()[0];
		Type type = parameter.getParameterizedType();
		Object arg = ArgumentMaker.gson.fromJson(argument, type);
		
		method.invoke(object, arg);
	}

请求响应执行流程

​ 1)客户端发起请求

 /**
     * 分发器的请求操作
     * @param action 请求行为,且不需要返回行为
     * @param argument 请求行为的参数,由ArgumentMaker得到的json字符串
     */
    public void request(String action, String argument) {
        this.clientConverSation.request(action, argument);
    }

    /**
     * 存在返回值的分发器处理程序
     * @param requestAction   请求行为
     * @param responseAction  响应行为
     * @param argument
     */
    public void requestString(String requestAction, String responseAction, String argument) {
        this.clientConverSation.request(requestAction, responseAction,argument);
    }

​ 2)客户端会话层进行信息的封包

​ 如果只有一个action,无需返回action,将action也作用响应action,这样可以使用一套逻辑实现请求响应。

void request(String action, String argument) {
        request(action, action, argument);
    }

    void request(String requestAction, String responseAction, String argument) {
        String action = requestAction + "*" + responseAction;
        this.bioCommunication.send(new NetMessage().setCommond(ENetCommond.REQUEST).setAction(action)
            .setMessage(argument));
    }

​ 3)服务器会话层接收并处理请求后,返回响应

void dealRequest(NetMessage netMessage) {
        String action = netMessage.getAction();
        String argument = netMessage.getString();
        int index = action.indexOf("*");
        String requestAction = action.substring(0, index);
        String responseAction = action.substring(index + 1);

        try {
            String strResult = this.requestResponseDealer.dealRequest(requestAction, argument);
            this.communication.send(new NetMessage().setCommond(ENetCommond.RESPONSE)
                .setAction(responseAction)
                .setMessage(strResult));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4)客户端会话层接收到响应信息

void dealResponse(NetMessage netMessage) {
        String responseAction = netMessage.getAction();
        String argument = netMessage.getString();
        try {
            this.requestResponseDealer.dealResponse(responseAction, argument);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Ⅴ、总结

​ 至此,我们的NIOCSFramework框架就已经基本完成,且对于后续二次开发流出了接口,且实现了分发器作为框架对以后未知行为的处理。此框架同时也是多文件自平衡云传输的资源拥有者的底层通信实现,对于多文件云传输框架在后续的文章中具体讲述。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值