为什么要使用ProtocolCodecFilter
1.TCP确保了所有的包是按正确顺序传输的.但不能保证发送端写操作的结果跟接收端读的结果是一致的.
在mina中,在没有ProtocolCodecFilter发送端调用IoSession.write(Object message)可会导致多个接收端的messageReceived(IoSession session,Object mssage)事件
多个IoSession.write(Object message)可以指向一个messageReceived 事件.你可能没有遇到客户端与服务端运行在同一台主机上.
2.大部分网络传输都需要一种方式 来判断当前消息的结束与下一个消息的开始.
3.你可能要实现你的业务逻辑在IoHandler中.但是增加ProtocolCodecFilter 将使你的代码更加整洁和易于维护.
它使你将协议逻辑与你的业务逻辑(IoHandler)独立开来.
如何使用ProtocolCodecFilter
1.你的应用程序基本上是接收一系列的字节并且将其转换这些字节为一个特定的消息对象.
2.这里有三种常用的方式来将一系列的字节流区分为一个个消息对象.
第一:使用固定长度的消息.
第二:使用固定长度的消息头并且标识消息主体的长度.
第三:使用分隔符,例如在文本协议中在每条消息后面使用换行来标识.
例子
我们编写了一个实用但没什么价值服务器画图程序.我们如何来实现自己的的编解码器(ProtocolEncoder, ProtocolDecoder, and ProtocolCodecFactory).
这个协议十分简单.下面为请求消息
4bytes | 4bytes |4bytes
width height numchars
width:请求图片的宽度(integer)
height:请求图片的高度(integer)
numchars:生成图片上显示的文字数量(integer)
服务器将返回两张请求中指定显示文字与规格大小的图片,下面为响应message
4 bytes |variable length body |4 bytes |variable length body
length1 |image1 |length2 |image2
length1: image1字节大小
image1: 一张png图片
length2: image2字节大小
image2: 一张png图片
接下来我们看一下我们要使用的类
ImageRequest: 一个简单的POJO类代表ImageServer的一个请求.
ImageRequestEncoder: 将ImageRequest编码为协议数据 (客户端使用)
ImageRequestDecoder: 将协议数据解码为ImageRequest对象(服务器端使用)
ImageResponse: 一个简单的POJO类代表ImageServer的一个响应.
ImageResponseEncoder: 服务器使用它来编码ImageResponse对像
ImageResponseDecoder: 服务器使用它来解码ImageResponse对像
ImageCodecFactory: 创建这些编解码类
public class ImageRequest { private int width; private int height; private int numberOfCharacters; public ImageRequest(int width, int height, int numberOfCharacters) { this.width = width; this.height = height; this.numberOfCharacters = numberOfCharacters; } public int getWidth() { return width; } public int getHeight() { return height; } public int getNumberOfCharacters() { return numberOfCharacters; } }
编码往往比解码简单,我们先看编码
public class ImageRequestEncoder implements ProtocolEncoder { public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception { ImageRequest request = (ImageRequest) message; IoBuffer buffer = IoBuffer.allocate(12, false); buffer.putInt(request.getWidth()); buffer.putInt(request.getHeight()); buffer.putInt(request.getNumberOfCharacters()); buffer.flip(); out.write(buffer); } public void dispose(IoSession session) throws Exception { // nothing to dispose } }
1.mina能够自动对IoSession中写queue调用编码方法.当我们的客户端只写ImageRequest对象时,我们可以放心的进行强 转.
2.我们分配了一个新的IoBuffer从heap中.最好避免直接分配.分配heap是比较好的做法.
3.你不必手动释放buffer,mina做替你做.
4.在dispose()方法中你必须释放所有在特定Session编码过程中的资源.如果你没用要释放的,你可以继承 ProtocolEncoderAdapter来编码. 下面我们来看一下解码,CumulativeProtocolDecoder 对你编码自己的解码器很有帮助;他可以缓存直到你想开始解码时所来接收到的数据.
在这个例子中,消息使用的是固定大小的.这是最简单的方式.
public class ImageRequestDecoder extends CumulativeProtocolDecoder { protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { if (in.remaining() >= 12) { int width = in.getInt(); int height = in.getInt(); int numberOfCharachters = in.getInt(); ImageRequest request = new ImageRequest(width, height, numberOfCharachters); out.write(request); return true; } else { return false; } } }
1.使用完整的消息都会被解码,你必须将它写到ProtocolDecoderOutput中,这些消息将下过滤器链中传递,最终到达
IoHandler.messageReceived方法.
2.你不必对释放IoBuffer负责.
3.当没有足够的数据到达时,直接返回就行.
下面是响应对象
public class ImageResponse { private BufferedImage image1; private BufferedImage image2; public ImageResponse(BufferedImage image1, BufferedImage image2) { this.image1 = image1; this.image2 = image2; } public BufferedImage getImage1() { return image1; } public BufferedImage getImage2() { return image2; } }
对响应对象进行编码
public class ImageResponseEncoder extends ProtocolEncoderAdapter { public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception { ImageResponse imageResponse = (ImageResponse) message; byte[] bytes1 = getBytes(imageResponse.getImage1()); byte[] bytes2 = getBytes(imageResponse.getImage2()); int capacity = bytes1.length + bytes2.length + 8; IoBuffer buffer = IoBuffer.allocate(capacity, false); buffer.setAutoExpand(true); buffer.putInt(bytes1.length); buffer.put(bytes1); buffer.putInt(bytes2.length); buffer.put(bytes2); buffer.flip(); out.write(buffer); } private byte[] getBytes(BufferedImage image) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(image, "PNG", baos); return baos.toByteArray(); } }
需要注意的是,预先计算IoBuffer的长度几乎是不可能的.你可以使用自动增长的缓存区buffer.setAutoExpand(true)
接下来是响应解码器
public class ImageResponseDecoder extends CumulativeProtocolDecoder { private static final String DECODER_STATE_KEY = ImageResponseDecoder.class.getName() + ".STATE"; public static final int MAX_IMAGE_SIZE = 5 * 1024 * 1024; private static class DecoderState { BufferedImage image1; } protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { DecoderState decoderState = (DecoderState) session.getAttribute(DECODER_STATE_KEY); if (decoderState == null) { decoderState = new DecoderState(); session.setAttribute(DECODER_STATE_KEY, decoderState); } if (decoderState.image1 == null) { // try to read first image if (in.prefixedDataAvailable(4, MAX_IMAGE_SIZE)) { decoderState.image1 = readImage(in); } else { // not enough data available to read first image return false; } } if (decoderState.image1 != null) { // try to read second image if (in.prefixedDataAvailable(4, MAX_IMAGE_SIZE)) { BufferedImage image2 = readImage(in); ImageResponse imageResponse = new ImageResponse(decoderState.image1, image2); out.write(imageResponse); decoderState.image1 = null; return true; } else { // not enough data available to read second image return false; } } return false; } private BufferedImage readImage(IoBuffer in) throws IOException { int length = in.getInt(); byte[] bytes = new byte[length]; in.get(bytes); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); return ImageIO.read(bais); } }
1.我们在Session增加了解码处理的状态属性.他可能自己会存放了这个解码对像的状态,但会有下面几个缺点.
第一:每个IoSession都必须有他们自己的解码实例.
第二:mina确信在同一个IoSession中不会有超过一个的线程会同时执行decode()方法.但这并不能确保他就是线程安全的.
假定一个数据块被线程1处理然而他又不想对他编码,当下一个数据块到达时,他可能会被其它的线程处理.为避免可见性问题
你必须同步访问解码状态.
第三:这样就得出一个结论,选择在IoSession中存放状态比在解码实例中更加有意思.确保没有两个将要运行同一IoSession解码方法
的线程.mina需要做一系列的同步操作.这个操作对你是不可见的.
2.IoBuffer.prefixedDataAvailable()在当你用一个长度做为协议前缀时将十分有用.前缀可能是1,2,4个字节.
3.不要忘记在你解码完响应后还原解码状态.
如果你只想返回一张图,你就不需要存放解码状态了.
protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { if (in.prefixedDataAvailable(4)) { int length = in.getInt(); byte[] bytes = new byte[length]; in.get(bytes); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); BufferedImage image = ImageIO.read(bais); out.write(image); return true; } else { return false; } }
现在我们把他们放在一起进行统一管理
public class ImageCodecFactory implements ProtocolCodecFactory { private ProtocolEncoder encoder; private ProtocolDecoder decoder; public ImageCodecFactory(boolean client) { if (client) { encoder = new ImageRequestEncoder(); decoder = new ImageResponseDecoder(); } else { encoder = new ImageResponseEncoder(); decoder = new ImageRequestDecoder(); } } public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception { return encoder; } public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception { return decoder; } }
1.对每一个新的Session,mina都会向ImageCodecFactory 请求一个编码,解码器.
2.如果我们的编解码器不保存会话状态,那么所有的session共享一个实现也是安全的.
接下来我们看服务器是如何使用ProtocolCodecFactory的
public class ImageServer { public static final int PORT = 33789; public static void main(String[] args) throws IOException { ImageServerIoHandler handler = new ImageServerIoHandler(); NioSocketAcceptor acceptor = new NioSocketAcceptor(); acceptor.getFilterChain().addLast("protocol", new ProtocolCodecFilter(new ImageCodecFactory(false))); acceptor.setLocalAddress(new InetSocketAddress(PORT)); acceptor.setHandler(handler); acceptor.bind(); System.out.println("server is listenig at port " + PORT); } }
客户端编码
public class ImageClient extends IoHandlerAdapter { public static final int CONNECT_TIMEOUT = 3000; private String host; private int port; private SocketConnector connector; private IoSession session; private ImageListener imageListener; public ImageClient(String host, int port, ImageListener imageListener) { this.host = host; this.port = port; this.imageListener = imageListener; connector = new NioSocketConnector(); connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new ImageCodecFactory(true))); connector.setHandler(this); } public void messageReceived(IoSession session, Object message) throws Exception { ImageResponse response = (ImageResponse) message; imageListener.onImages(response.getImage1(), response.getImage2()); } ... }
最后是服务器端的IoHandler
public class ImageServerIoHandler extends IoHandlerAdapter { private final static String characters = "mina rocks abcdefghijklmnopqrstuvwxyz0123456789"; public static final String INDEX_KEY = ImageServerIoHandler.class.getName() + ".INDEX"; private Logger logger = LoggerFactory.getLogger(this.getClass()); public void sessionOpened(IoSession session) throws Exception { session.setAttribute(INDEX_KEY, 0); } public void exceptionCaught(IoSession session, Throwable cause) throws Exception { IoSessionLogger sessionLogger = IoSessionLogger.getLogger(session, logger); sessionLogger.warn(cause.getMessage(), cause); } public void messageReceived(IoSession session, Object message) throws Exception { ImageRequest request = (ImageRequest) message; String text1 = generateString(session, request.getNumberOfCharacters()); String text2 = generateString(session, request.getNumberOfCharacters()); BufferedImage image1 = createImage(request, text1); BufferedImage image2 = createImage(request, text2); ImageResponse response = new ImageResponse(image1, image2); session.write(response); } private BufferedImage createImage(ImageRequest request, String text) { BufferedImage image = new BufferedImage(request.getWidth(), request.getHeight(), BufferedImage.TYPE_BYTE_INDEXED); Graphics graphics = image.createGraphics(); graphics.setColor(Color.YELLOW); graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); Font serif = new Font("serif", Font.PLAIN, 30); graphics.setFont(serif); graphics.setColor(Color.BLUE); graphics.drawString(text, 10, 50); return image; } private String generateString(IoSession session, int length) { Integer index = (Integer) session.getAttribute(INDEX_KEY); StringBuffer buffer = new StringBuffer(length); while (buffer.length() < length) { buffer.append(characters.charAt(index)); index++; if (index >= characters.length()) { index = 0; } } session.setAttribute(INDEX_KEY, index); return buffer.toString(); } }
整个调用过程是这样子的
1.ImageClient 调用 sendRequest(ImageRequest imageRequest)
此方法主要执行session.write(imageRequest);session写入的是高级对象
2.ImageRequestEncoder(client) 调用 encode(IoSession session, Object message, ProtocolEncoderOutput out)
此方法主要执行out.write(buffer);先将message强转为ImageRequest,读取对应属性装配为IoBuffer写入 ProtocolEncoderOutput
3.ImageRequestDecoder(server) 调用 doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
此方法主要执行 out.write(request);先从in中取值,写入ImageRequest相应属性,最后将高级对象写入 ProtocolDecoderOutput
4.ImageServerIoHandler 调用 messageReceived(IoSession session, Object message)
服务器的进行业务处理将message 强转为ImageRequest,然后生成对应的ImageResponse,将响应对像写入IoSession
5.ImageResponseEncoder(server) 调用 encode(IoSession session, Object message, ProtocolEncoderOutput out)
此方法主要执行out.write(buffer);将message强转为ImageResponse,读取对应属性装配为IoBuffer写入 ProtocolEncoderOutput
6.ImageResponseDecoder(client) 调用 doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
此方法主要执行 out.write(request);先从in中取值,写入ImageResponse相应属性,最后将高级对象写入 ProtocolDecoderOutput
7.ImageClient 调用 messageReceived(IoSession session, Object message)
客户端得到的message就是ImageResponse