高性能聊天系统
作者:板桥banq
1.4 Socket接口设计和实现
完成了Socket底层设计后,需要设计接口层以便和应用层对接。在一般情况下,大多数程序员会直接将应用核心相关代码写入Socket底层代码中,这样就会造成应用层和底层紧密耦合在一起,如果开发其他应用系统,还需要修改Socket底层代码。
在前面测试调试中已经发现,Socket底层的非堵塞I/O代码相对传统的堵塞I/O来说,在调试和运行上要困难和麻烦一些。因此,一旦调试运行正常,一般程序员都不希望再次修改Socket底层代码,因此,需要将这些底层代码与外界调用分离开来,形成一个“黑匣子 ”,只提供一个惟一通道和外界发生关系,这种关系也必须是松散的,而不是紧密的。
这种通道以及松散的关系就是设计接口层的目的,接口层是实现Socket底层和应用层的接口部分,因此接口层主要是实现两者数据之间联系以及转换的“琐碎”事情,包含很多小的“转换器”。
1.4.1 队列和对象类型
其实这样的接口设计已经在前面章节提到,使用了队列Queue模式,由于非堵塞I/O的自我触发、独立运行的模式,外界无法直接控制非堵塞I/O的数据读写。因此,设计一个队列Queue,将需要发送和接受的数据放在这个队列中,这样,应用系统层和Socket底层就可以实现数据的交换。队列Queue是底层与应用层之间的连接通道,这种通道连接是一种松散的联系,如图1-8所示。
图1-8 请求信号发生触发流程图
图1-8表示一个请求信号由客户端应用层产生后,将被放入队列Queue中,然后如图1-6所示的队列Queue触发机制,在非堵塞I/O的Selector发现网络可写时,从Queue中读取这个请求信号,发送到服务器端;在服务器端,非堵塞I/O的Selector发现有数据后,触发可读事件,接受客户端的数据请求,然后再放入服务器端的Queue中,等待服务器应用层从Queue中读取处理。图1-8是一个请求信号的路线图,服务器响应信号类似。
但是,现在有一个比较琐碎、好像不起眼的难题需要解决,也就是Java的数据类型问题。客户端应用层产生请求Request时,这些Request可能是字符串,也可能是图片等普通对象Object,放入Queue中后,当再从Queue中取出时,已经很难分辨它们的类型。如何解决这个问题?
在已经确定这些对象的类型后,还要实现数据类型转换工作,将这些请求对象转换为非堵塞I/O可读写的ByteBuffer流,也就是通常讲的序列化。同样的工作也发生在服务器,在服务器端需要将ByteBuffer流反序列化变为原始的请求对象类型,然后放入服务器端的Queue中,当服务器的应用层从Queue中取出这些对象时,又需要进行一次“身份”鉴别。
以上只是请求Request的一个发送处理过程,还有另外一个响应Response的处理发送过程。纵观这两种信号的处理过程,有相似点,也有非相似点。如果不进行巧妙设计,单就每个环节都用比较死板的代码实现,代码就会变得琐碎而复杂,这容易导致问题发生,影响系统的稳定性和拓展性。
如果仔细整理一下这些琐碎、似乎毫不起眼的事情时,会有惊人的发现。
首先,将这个接口层涉及到的几个基本对象和事件整理如下:
· 信号类型:请求信号和响应信号。
· 数据类型:字符串和普通对象,以后可能扩展增加。
· Queue操作:加入Queue和从Queue中取出。
这里一共有6种对象或动作,这6种对象和动作搭配组合的数量是巨大的,例如:一个请求信号可能是字符串,也可能是普通对象,无论是这两种数据类型中的哪一种,都需要加入Queue和从Queue中取出。如果仔细考虑Queue的放入和读取动作,服务器端和客户端各有一个Queue,那么相应动作将有8种实现,每种数据类型都对应有这8种Queue操作实现。但如果以后再拓展新的数据类型,这显然是很琐碎可怕的事情。如果试图“穷尽”这些组合数量,那是一种非常“愚笨”的办法,也永远无法从编程中获得乐趣。而且,这样“穷尽”以后的代码会变得琐碎而复杂,如果有新的数据类型添加,将导致系统愈加复杂。
当事情第一次发生时尽管如实描述它,第二次同样发生时就要引起警觉,第3次同样发生时,就该重新设计或重整(Refactorying)了。
重新仔细研究整个流程,会发现有一些可以简化的环节。其中一个最容易想到的是:将数据类型简化,都以普通对象Object来替代。这只是通过变更需求来达到简化设计的目的,还是没有找到一种可重用或通用的解决方案。何况将字符串以普通对象替代,在对象序列化时,将难以指定变换字符集。
另外可以接受的简化环节是:当Socket的非堵塞I/O从Queue中提取数据对象时,无法知道这些对象的具体数据类型,但是,可以肯定的是,这些对象是供非堵塞I/O使用的,而非堵塞I/O只会使用到可序列化的数据类型。因此,只要这些提取的对象是一种可序列化的数据类型就可以。InputStream和OutputStream是流类型的类,是典型的序列化特征数据类型。那么,如果将Queue中对象都明确为InputStream或OutputStream类,这样将简化图1-8中的A和B两个数据类型转换部分。
下面就如何精简实现A和B部分做进一步讨论,以客户端的UDPClient为例,修改其doKey方法如下,其中messageQueue就是前面介绍的MessageList实例:
private void doKey(SelectionKey key) throws Exception {
DatagramChannel keyChannel = null;
try {
keyChannel = (DatagramChannel) key.channel();
if (key.isReadable()) { //如果可以从服务器读取response数据
Debug.logVerbose("get response from the Server", module);
//从服务器读取Response后放入Queue
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
keyChannel.receive(buffer);
InputStream bin = new ByteArrayInputStream(array);
//将ByteArrayInputStream实例放入Queue
messageQueue.pushResponse(bin);
//处理结束
key.interestOps(SelectionKey.OP_WRITE);
selector.wakeup();
} else if (key.isWritable()) { //如果可以向服务器发送request数据
Debug.logVerbose("-->begin to send request", module);
//从Queue中取出ByteArrayOutputStream
ByteArrayOutputStream outByte = (ByteArrayOutputStream) messageQueue
removeReqFirst();
ByteBuffer buffer = ByteBuffer.wrap(outByte.toByteArray());
keyChannel.write(buffer);
key.interestOps(SelectionKey.OP_READ);
selector.wakeup();
}
} catch (Exception e) {
Debug.logError("run error:" + e, module);
close(keyChannel);
throw new Exception(e);
}
}
从上面代码看出,客户端Queue中统一放置对象类型都是ByteArrayInputStream和ByteArrayOutputStream,请求信号的Queue中放置的是ByteArrayOutputStream;而响应信号Queue中放置的是ByteArrayInputStream。在服务器端,Queue中也统一为这两种对象类型,以UDPHanlder的读写方法为例,如下:
private void read() throws Exception {
try {
Debug.logVerbose("-- > read request from client", module);
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
address = datagramChannel.receive(buffer);
InputStream bin = new ByteArrayInputStream(array);
messageQueue.pushRequest(bin); //放入ByteArrayInputStream
state = SENDING;
key.interestOps(SelectionKey.OP_WRITE);
} catch (Exception ex) {
Debug.logError("readRequest .. error:" + ex, module);
}
}
private void send() throws Exception {
try {
Debug.logVerbose("-- > send response to server", module);
//从Reponse Queue中取出ByteArrayOutputStream
ByteArrayOutputStream bout =
(ByteArrayOutputStream)messageQueue.removeResFirst();
ByteBuffer buffer1 = ByteBuffer.wrap(bout.toByteArray());
datagramChannel.send(buffer1, address);
state = READING;
key.interestOps(SelectionKey.OP_READ);
} catch (Exception ex) {
Debug.logError("readRequest .. error:" + ex, module);
}
}
在Socket发出和接受时,解决了图1-8中A和B的数据转换问题,下一步需要解决图1-8中放入Queue的C处和从Queue中取出的D处的数据转换问题,这两点是应用层操作点。
因为现在已经规定Queue中放置的是ByteArrayInputStream和ByteArrayOutputStream两种数据类型,那么,应用层将数据对象放入Queue中时,必须同时实现将该对象转换成ByteArrayOutputStream类型。同样,取出时,需要将Queue中ByteArrayOutputStream转换成原来的对象类型。
这就需要在C和D处完成一个转换接口,它们对应的所需要转换的对象类型可能是多种的。如String类型或普通Object类型。因为应用层可能是各种应用,包括可能需要传送的视频或声频对象。如何针对这样一个集合中各种对象类型分别实行转换,然后放入Queue中?当然,这里希望找到一种优雅、可以通用的解决方案,而不是使用IF语句穷尽所有对象类型实行繁琐的判断处理。