本教程试图解释为什么以及如何使用ProtocolCodecFilter。
为什么要使用ProtocolCodecFilter?
TCP以正确的顺序保证所有数据包的传递。但是不能保证发送方的一次写操作会导致接收方发生一次读事件。请参阅
http://en.wikipedia.org/wiki/IPv4#Fragmentation_and_reassembly
和http://en.wikipedia.org/wiki/Nagle%27s_algorithm
在MINA术语中:没有ProtocolCodecFilter一次调用IoSession.write(Object message)发送方可以在接收方上产生多个messageReceived(IoSession会话,对象消息)事件;多次调用IoSession.write(Object message)可能会导致单个messageReceived事件。当客户端和服务器在同一主机(或本地网络)上运行时,您可能不会遇到此行为,但您的应用程序应该能够应对此问题。
大多数网络应用程序需要一种方法来查找当前消息的结束位置以及下一条消息的开始位置。
您可以在IoHandler中实现所有这些逻辑,但添加ProtocolCodecFilter将使您的代码更清晰,更易于维护。
它允许您将协议逻辑与业务逻辑(IoHandler)分开。
怎么样 做?
您的应用程序基本上只是接收一堆字节,您需要将这些字节转换为消息(更高级别的对象)。
将字节流拆分为消息有三种常用技术:
1.使用固定长度的消息
2.使用固定长度的标题,指示身体的长度
3.使用分隔符;例如,许多基于文本的协议在每条消息之后附加换行符(或CR LF对)(http://www.faqs.org/rfcs/rfc977.html)
在本教程中,我们将使用第一种和第二种方法,因为它们肯定更容易实现。之后我们将看一下使用分隔符。
例子
我们将开发一个(相当无用的)图形计费服务器来说明如何实现自己的协议编解码器(ProtocolEncoder,ProtocolDecoder和ProtocolCodecFactory)。协议非常简单。这是请求消息的布局:
4 bytes | 4 bytes | 4 bytes |
width | height | numchars |
1.width:请求图像的宽度(网络字节顺序中的整数)
2.height:请求图像的高度(网络字节顺序中的整数)
3.numchars:要生成的字符数(网络字节顺序中的整数)
服务器响应所请求尺寸的两个图像,并在其上绘制所请求的字符数。这是响应消息的布局:
4 bytes | variable length body | 4 bytes | variable length body |
length1 | image1 | length2 | image2 |
我们编码和解码请求和响应所需的类概述:
1.ImageRequest:一个简单的POJO,表示对ImageServer的请求。
2.ImageRequestEncoder:将ImageRequest对象编码为特定于协议的数据(由客户端使用)
3.ImageRequestDecoder:将特定于协议的数据解码为ImageRequest对象(由服务器使用)
4.ImageResponse:一个简单的POJO,表示来自ImageServer的响应。
5.ImageResponseEncoder:服务器用于编码ImageResponse对象
6.ImageResponseDecoder:客户端用于解码ImageResponse对象 ImageCodecFactory:这个类创建了necesarry编码器和解码器
这是ImageRequest类:
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;
}
}
编码通常比解码简单,所以让我们从ImageRequestEncoder开始:
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写入队列中的所有消息调用encode函数。由于我们的客户端只会编写ImageRequest对象,因此我们可以安全地将消息转换为ImageRequest。
2.我们从堆中分配一个新的IoBuffer。最好避免使用直接缓冲区,因为通常堆缓冲区的性能更好。见http://issues.apache.org/jira/browse/DIRMINA-289)
3.您不必释放缓冲区,MINA会为您完成,请参阅http://mina.apache.org/mina-project/apidocs/org/apache/mina/core/buffer/IoBuffer.html
4.在dispose()方法中,您应释放在编码期间为指定会话获取的所有资源。如果没有任何可处理的内容,您可以让编码器继承自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. 当没有足够的数据可用于解码消息时,只返回false
响应也是一个非常简单的POJO:
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.我们将解码过程的状态存储在会话属性中。也可以将此状态存储在Decoder对象本身中,但这有几个缺点:
a)每个IoSession都需要自己的Decoder实例
b)MINA确保永远不会有多个线程同时为同一个IoSession执行decode()函数,但它并不保证它始终是相同的线程。假设第一段数据由thread-1处理,它决定它还不能解码,当下一条数据到达时,它可以由另一个线程处理。为避免可见性问题,必须正确同步对此解码器状态的访问(IoSession属性存储在ConcurrentHashMap中,因此它们对其他线程自动可见)。
c)对邮件列表的讨论得出了这样的结论:在IoSession中存储状态或在Decoder实例本身之间进行选择更多的是品味问题。为了确保没有两个线程将为同一IoSession运行解码方法,MINA需要进行某种形式的同步=>此同步还将确保您不会遇到上述可见性问题。 (感谢Adam Fisk指出这一点)请参阅http://www.nabble.com/Tutorial-on-ProtocolCodecFilter,-state-and-threads-t3965413.html
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.对于每个新会话,MINA将向ImageCodecFactory询问编码器和解码器。
2.由于我们的编码器和解码器不存储会话状态,因此让所有会话共享单个实例是安全的。
这是服务器使用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();
}
}
结论
有关编码和解码的内容还有很多。但是我希望这个教程已经让你开始了。我将尝试在不久的将来添加有关DemuxingProtocolCodecFactory的内容。然后我们还将看看如何使用分隔符而不是长度前缀。