Netty之通信协议和私有协议栈开发(二)

2021SC@SDUSC

一. 核心组件

             EventLoop 和 EventLoopGroup(已分析)
             ChannelPipeline(ChannelPipeline中已分析)
             ChannelHandler(ChannelPipeline中已分析)
             ChannelHandlerContext(已分析)
             Channel(已分析)
             ChannelFuture(已分析)

二. ChannelHandler责任链模式的过滤链 (已分析)

三. 通信协议和私有协议栈开发(本节分析)

  1. Netty HTTP协议栈的应用——HTTP文件服务器(已分析)
  2. Netty HTTP+XML 协议栈开发(本节分析)

由于HTTP协议的通用性,很多异构系统间的通信交互采用HTTP协议。

在Java领域,最常用的HTTP协议栈就是基于Servlet规范的Tomcat等 Web容器。但是,很多基于HTTP的应用都是后台应用,HTTP仅仅是承载数据交换的一个通道,是一个载体而不是Web容器,在这种场下,一般不需要类似Tomcat这样的重量型Web容器。

我们今天主要分析的就是如何利用Netty提供的基础 HTTP协议栈功能,扩展开发 HTTP+XML协议栈。我们从使用 HTTP+XML协议栈的一个具体的应用程序入手来分析。

目录

一. 开发背景

 二.HTTP+XML协议栈设计

1.分析Netty已有功能和需要拓展的功能

2.设计思路

三.高效的XML绑定框架JiBx

1. JiBX 初步了解

2. POJO对象定义

四. HTTP+XML编解码框架开发

1. HTTP+XML请求消息编码类

2.HTTP+XML请求消息解码类

3. HTTP+XML响应消息编码类

4.HTTP+XML应答消息解码

五. HTTP + XML  客户端 与 服务端 开发

六.小结


一. 开发背景

首先我们来看一下这个程序开发的背景。

我们先模拟一个简单的用户订购系统。客户端填写订单,通过HTTP客户端向服务端发送订购请求,请求消息放在HTTP消息体中,以XML承载,即采用HTTP+XML的方式进行通信。

HTTP服务端接收到订购请求后,对订单请求进行修改,然后通过 HTTP+XML的方式返回应答消息。双方采用HTTP1.1协议,连接类型为CLOSE方式,即双方交互完成,由 HTTP服务端主动关闭链路,随后客户端也关闭链路并退出。

我们再来看一下会用到的数据。

 

 二.HTTP+XML协议栈设计

这部分的分析,我们通过商品订购的流程图,来观察订购的关键步骤和主要技术点,找出当前Netty HTTP协议栈的功能不足之后,通过扩展的方式完成HTTP+XML协议栈的开发。

流程图如下:

1.分析Netty已有功能和需要拓展的功能

对订购流程图进行分析 :

先看步骤1,构造订购请求消息并将其编码为HTTP+XML形式。Netty的HTTP协议栈提供了构造HTTP请求消息的相关接口,但是无法将普通的POJO对象转换为HTTP+XML的HTTP请求消息,需要自定义HTTP+XML格式的请求消息编码器

再看步骤2,利用Netty的HTTP协议栈,可以支持HTTP链路的建立和请求消息的发送,所以不需要额外开发,直接重用Netty的能力即可。

步骤3,HTTP服务端需要将HTTP+XML格式的订购请求消息解码为订购请求POJO对象,同时获取HTTP请求消息头信息。利用Netty的HTTP协议栈服务端,可以完成HTTP请求消息的解码,但是,如果消息体为XML格式,Netty无法支持将其解码为POJO对象,需要在Netty协议栈的基础上扩展实现

步骤4,服务端对订购请求消息处理完成后,重新将其封装成XML,通过HTTP应答消息体携带给客户端,Netty的HTTP协议栈不支持直接将POJO对象的应答消息以XML方式发送,需要定制。

步骤5,HTTP客户端需要将HTTP+XML格式的应答消息解码为订购POJO对象,同时能够获取应答消息的HTTP头信息,Netty的协议栈不支持自动的消息解码。

通过上面对每一个步骤的分析,我们可以了解到哪些能力是Netty原本支持的,哪些需要扩展开发实现的。

2.设计思路

下面给出设计思路:

(1)需要一套通用、高性能的XML序列化框架,它能够灵活地实现POJO-XML的互相转换,最好能够通过工具自动生成绑定关系,或者通过XML的方式配置双方的映射关系;

(2)作为通用的HTTP+XML协议栈,XML-POJO对象的映射关系应该非常灵活,支持命名空间和自定义标签;

(3)提供HTTP+XML请求消息编码器,供HTTP客户端发送请求消息自动编码使用;

(4)提供HTTP+XML请求消息解码器,供HTTP服务端对请求消息自动解码使用;

(5)提供HTTP+XML响应消息编码器,供HTTP服务端发送响应消息自动编码使用;

(6)提供HTTP+XML响应消息编码器,供HTTP客户端对应答消息进行自动解码使用;

(7)协议栈使用者不需要关心HTTP+XML的编解码,对上层业务零侵入,业务只需要对上层的业务POJO对象进行编排。

三.高效的XML绑定框架JiBx

这部分我们主要分析XML框架的选型和开发,它是 HTTP+XML协议栈的关键技术。

1. JiBX 初步了解

JiBX是一款非常优秀的XML数据绑定框架。它提供灵活的绑定映射文件,实现数据对象与XML文件之间的转换,并不需要修改既有的Java类。另外,它的转换效率是目前很多其他开源项目都无法比拟的。

XML已经成为目前程序开发配置的重要组成部分。使用JiBX绑定XML文档与Java对象需要分两步走:

第一步是绑定XML文件,也就是映射XML文件与Java对象之间的对应关系。

第二步是在运行时,实现XML文件与Java实例之间的互相转换。这时,它已经与绑定文件无关了,可以说是完全脱耦了。

在运行程序之前,需要先配置绑定文件并进行绑定,在绑定过程中它将会动态地修改程序中相应的 class文件,主要是生成对应对象实例的方法和添加被绑定标记的属性JiBX_bindingList等。

JiBx有两个比较重要的概念 : Unmarshal(数据分解)和 Marshal(数据编排)。从字面意思也很容易理解:Unmarshal是将XML文件转换成Java对象,而Marshal则是将Java对象编排成规范的XML文件

JiBX在 Unmarshal/Marshal 上如此高效,这要归功于使用了XPP技术,而不是使用基于树型方式将整个文档写入内存,然后进行操作的 DOM (Document Object Model),也不是使用基于事件流的SAX。XPP使用的是不断增加的数据流处理方式,同时允许在解析XML文件时中断。

2. POJO对象定义

我们先定义POJO对象,再生成XML和对象的绑定文件。JiBx对POJO对象没有特殊要求,只要符合Java Bean的规则即可,下面我们以订购例程为例,看下主要类的定义。

@Data
public class Address {

    private String street1;
    private String street2;
    private String city;
    private String state;
    private String postCode;
    private String country;
}
@Data
public class Customer {
    private long customerNumber;
    private String firstName;
    private String lastName;
    private List middleNames;
}
@Data
public class Order {
  private long orderNumber;
  private Customer customer;
  private Address billTo;
  private Shipping shipping;
  private Address shipTo;
  private Float total;
}
public enum Shipping {
    STANDARD_MAIL, PRIORITY_MAIL, INTERNATIONAL_MAIL, DOMESTIC_EXPRESS, INTERNATIONAL_EXPRESS
}

POJO对象定义完成之后,通过Ant脚本来生成XML和 POJO对象的绑定关系文件,同时也附加生成XML的Schema定义文件。

进行到这里我们先来测试一下JiBx类库的使用。

public class TestOrder {
    private IBindingFactory factory = null;
    private StringWriter writer = null;
    private StringReader reader = null;
    private final static String CHARSET_NAME = "UTF-8";

    private String encode2Xml(Order order) throws JiBXException, IOException {
     //关注1
        factory = BindingDirectory.getFactory(Order.class);
     //关注2
        writer = new StringWriter();
        IMarshallingContext mctx = factory.createMarshallingContext();
        mctx.setIndent(2);
        mctx.marshalDocument(order, CHARSET_NAME, null, writer);
        String xmlStr = writer.toString();
        writer.close();
        System.out.println(xmlStr.toString());
        return xmlStr;
    }

    private Order decode2Order(String xmlBody) throws JiBXException {
        reader = new StringReader(xmlBody);
        IUnmarshallingContext uctx = factory.createUnmarshallingContext();
        Order order = (Order) uctx.unmarshalDocument(reader);
        return order;
    }

    public static void main(String[] args) throws JiBXException, IOException {
        TestOrder test = new TestOrder();
        Order order = new Order();
        order.setOrderNumber(123);
        Customer customer = new Customer();
        customer.setFirstName("ali");
        customer.setMiddleNames(Arrays.asList("baba"));
        customer.setLastName("taobao");
        order.setCustomer(customer);
        Address address = new Address();
        address.setCity("南京市");
        address.setCountry("中国");
        address.setPostCode("123321");
        address.setState("江苏省");
        address.setStreet1("龙眠大道");
        address.setStreet2("INTERNATIONAL_MAIL");
        order.setBillTo(address);
        order.setShipTo(address);
        order.setShipping(Shipping.INTERNATIONAL_MAIL);
        order.setTotal(33f);
        String body = test.encode2Xml(order);
        Order order2 = test.decode2Order(body);
        System.out.println(order2);
    }
}

     首先看关注1,根据Order的 Class实例构造IBindingFactory对象。再看关注2,创建新的StringWriter对象,通过IBindingFactory构造Marshalling上下文,最后通过marshalDocument将Order序列化为StringWriter,通过StringWriter 的 toString()方法可以返回String类型的XML对象。
    解码与编码类似,不同的是它使用StringReader 来读取 String 类型的XML对象,然后通过unmarshalDocument方法将其反序列化为Order对象。

执行结果如下图所示:

 (图片来自《Netty权威指南》)

通过上面的执行结果我们可以发现,XML序列化和反序列化后的结果与预期一致,我们开发的JiBx应用可以正常工作。

四. HTTP+XML编解码框架开发

下面我们基于程序的源代码来进行分析:如何在 Netty 提供的HTTP基础协议栈上进行扩展和封装,实现HTTP+XML 协议栈的开发。

1. HTTP+XML请求消息编码类

       对于上层业务侧,构造订购请求消息后,以HTTP+XML协议将消息发送给服务端,如果要实现对业务零侵入或者尽可能少的侵入,协议层和应用层应该解耦。
      考虑到HTTP+XML协议栈需要一定的定制扩展能力,例如通过HTTP消息头携带业务自定义字段,所以,应该允许业务利用Netty的 HTTP协议栈接口自行构造私有的HTTP消息头。
      和我们前面学习的一样,HTTP+XML的协议编码仍然采用ChannelPipeline中增加对应的编码handler类实现。

      下面我们来一起看下HTTP+XML请求消息编码类的源码实现。

public class HttpXmlRequestEncoder extends AbstractHttpXmlEncoder {

    @Override
    protected void encode(ChannelHandlerContext ctx, Object o,List out) throws Exception {
        HttpXmlRequest msg = (HttpXmlRequest)o;
        //首先调用父类的encode0,将业务需要发送的POJO对象Order实例通过JiBx序列化为XML字符串
        //随后将它封装成Netty的ByteBuf。
        ByteBuf body = encode0(ctx, msg.getBody());
        FullHttpRequest request = msg.getRequest();
        //对消息头进行判断,如果业务自定义和定制了消息头,则使用业务侧设置的HTTP消息头,
        //如果业务侧没有设置,则构造新的HTTP消息头。
        if (request == null) {
            //用来构造和设置默认的HTTP消息头,由于通常情况下HTTP通信双方更关注消息体本身,所以这里采用了硬编码的方式,
            //如果要产品化,可以做成XML配置文件,允许业务自定义配置,以提升定制的灵活性。
            request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,HttpMethod.GET, "/do", body);
            HttpHeaders headers = request.headers();
            headers.set(HttpHeaders.Names.HOST, InetAddress.getLocalHost().getHostAddress());
            headers.set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
            headers.set(HttpHeaders.Names.ACCEPT_ENCODING,
                    HttpHeaders.Values.GZIP.toString() + ','
                            + HttpHeaders.Values.DEFLATE.toString());
            headers.set(HttpHeaders.Names.ACCEPT_CHARSET,"ISO-8859-1,utf-8;q=0.7,*;q=0.7");
            headers.set(HttpHeaders.Names.ACCEPT_LANGUAGE, "zh");
            headers.set(HttpHeaders.Names.USER_AGENT,"Netty xml Http Client side");
            headers.set(HttpHeaders.Names.ACCEPT,"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        }
        //由于请求消息消息体不为空,也没有使用Chunk方式,所以在HTTP消息头中设置消息体的长度Content-Length,
        //完成消息体的XML序列化后将重新构造的HTTP请求消息加入到out中,
        //由后续Netty的HTTP请求编码器继续对HTTP请求消息进行编码。
        HttpHeaders.setContentLength(request, body.readableBytes());
        out.add(request);
    }
}

HTTP+XML请求消息编码类中,我们首先来看下面的代码:

 ByteBuf body = encode0(ctx, msg.getBody());

这里首先调用父类的encode0,将业务需要发送的POJO对象Order实例通过JiBx序列化为XML字符串,随后将它封装成Netty的ByteBuf。

        FullHttpRequest request = msg.getRequest();

 随后对消息头进行判断,如果业务自定义和定制了消息头,则使用业务侧设置的HTTP消息头,
 如果业务侧没有设置,则构造新的HTTP消息头。在构造新的消息头的代码中,由于通常情况下HTTP通信双方更关注消息体本身,所以这里采用了硬编码的方式。

 HttpHeaders.setContentLength(request, body.readableBytes());

 最后由于请求消息消息体不为空,也没有使用Chunk方式,所以在HTTP消息头中设置消息体的长度Content-Length,完成消息体的XML序列化后将重新构造的HTTP请求消息加入到out中,由后续Netty的HTTP请求编码器继续对HTTP请求消息进行编码。

下面来看一下父类AbstractHttpXmlEncoder的实现:

public abstract class AbstractHttpXmlEncoder extends MessageToMessageEncoder {
    IBindingFactory factory = null;
    StringWriter writer = null;
    final static String CHARSET_NAME = "UTF-8";
    final static Charset UTF_8 = Charset.forName(CHARSET_NAME);

    protected ByteBuf encode0(ChannelHandlerContext ctx, Object body) throws Exception {
        //在此将业务的Order实例序列化为XML字符串。
        factory = BindingDirectory.getFactory(body.getClass());
        writer = new StringWriter();
        IMarshallingContext mctx = factory.createMarshallingContext();
        mctx.setIndent(2);
        mctx.marshalDocument(body, CHARSET_NAME, null, writer);
        String xmlStr = writer.toString();
        writer.close();
        writer = null;
        //将XML字符串包装成Netty的ByteBuf并返回,实现了HTTP请求消息的XML编码。
        ByteBuf encodeBuf = Unpooled.copiedBuffer(xmlStr, UTF_8);
        return encodeBuf;
    }

在 前面JiBx分析中,已经介绍了XML序列化和反序列化的相关类库使用。这里类主要是将Order实例序列化为XML字符串。最后将XML字符串包装成Netty 的 ByteBuf并返回,实现了HTTP请求消息的XML 编码。

再来看一下HttpXmlRequest是如何实现的:

public class HttpXmlRequest {

    private FullHttpRequest request;
    private Object body;

    public HttpXmlRequest(FullHttpRequest request, Object body) {
        this.body = body;
        this.request = request;
    }
}

 从上面的代码中可以看出,这个类包含两个成员变量FullHttpRequest和编码对象Object,用于实现和协议栈之间的解耦。

2.HTTP+XML请求消息解码类

HTTP服务端接收到HTTP+XML请求消息后,需要从HTTP消息体中获取请求码流,通过JiBx框架对它进行反序列化,得到请求POJO对象,然后对结果进行封装,回调到业务handler对象,业务得到的就是解码后的POJO对象和HTTP消息头。

下面是这个类的具体实现,代码分析直接注释在代码中:

public class HttpXmlRequestDecoder extends AbstractHttpXmlDecoder {

    public HttpXmlRequestDecoder(Class clazz) {
        this(clazz, false);
    }
    //HttpXmlRequestDecoder有两个参数,分别为需要解码的对象的类型信息和是否打印HTTP消息体码流的码流开关,码流开关默认关闭。
    public HttpXmlRequestDecoder(Class clazz, boolean isPrint) {
        super(clazz, isPrint);
    }

    @Override
    protected void decode(ChannelHandlerContext arg0, Object o, List arg2) throws Exception {
        FullHttpRequest arg1 = (FullHttpRequest)o;
        //首先对HTTP请求消息本身的解码结果进行判断,如果已经解码失败,再对消息体进行二次解码已经没有意义。
        if (!arg1.getDecoderResult().isSuccess()) {
            //如果HTTP消息本身解码失败,则构造处理结果异常的HTTP应答消息返回给客户端。
            sendError(arg0, BAD_REQUEST);
            return;
        }
        //通过HttpXmlRequest和反序列化后的Order对象构造HttpXmlRequest实例,最后将它添加到解码结果List列表中。
        HttpXmlRequest request = new HttpXmlRequest(arg1, decode0(arg0,arg1.content()));
        arg2.add(request);
    }

    private static void sendError(ChannelHandlerContext ctx,
                                  HttpResponseStatus status) {
                   //省略...  
   }
}

继续看下它的父类AbstractHttpXmlDecoder的实现。

分析直接注释在代码中:

public abstract class AbstractHttpXmlDecoder extends MessageToMessageDecoder {
    private IBindingFactory factory;
    private StringReader reader;
    private Class clazz;
    private boolean isPrint;
    private final static String CHARSET_NAME = "UTF-8";
    private final static Charset UTF_8 = Charset.forName(CHARSET_NAME);

    protected AbstractHttpXmlDecoder(Class clazz) {
        this(clazz, false);
    }
    protected AbstractHttpXmlDecoder(Class clazz, boolean isPrint) {
        this.clazz = clazz;
        this.isPrint = isPrint;
    }
    protected Object decode0(ChannelHandlerContext arg0, ByteBuf body)
            throws Exception {
        //从HTTP的消息体中获取请求码流,然后通过JiBx类库将XML转换成POJO对象。
        factory = BindingDirectory.getFactory(clazz);
        String content = body.toString(UTF_8);
        //根据码流开关决定是否打印消息体码流。
        //增加码流开关往往是为了方便问题定位,在实际项目中,需要打印到日志中。
        if (isPrint) {
            System.out.println("The body is : " + content);
        }
        reader = new StringReader(content);
        IUnmarshallingContext uctx = factory.createUnmarshallingContext();
        Object result = uctx.unmarshalDocument(reader);
        reader.close();
        reader = null;
        return result;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        // 释放资源
        //如果解码发生异常,要判断StringReader是否已经关闭,
        //如果没有关闭,则关闭输入流并通知JVM对其进行垃圾回收。
        if (reader != null) {
            reader.close();
            reader = null;
        }
    }
}

3. HTTP+XML响应消息编码类

对于响应消息,用户可能并不关心HTTP消息头之类的,它将业务处理后的POJO对象丢给HTTP+XML协议栈,由基础协议栈进行后续的处理。为了降低业务的定制开发难度,我们首先封装一个全新的HTTP XML应答对象,它的实现如下:


//它包含两个成员变量:FullHttpResponse和Object,Object就是业务需要发送的应答POJO对象。
public class HttpXmlResponse {
    private FullHttpResponse httpResponse;
    private Object result;
    public HttpXmlResponse(FullHttpResponse httpResponse, Object result) {
        this.httpResponse = httpResponse;
        this.result = result;
    }
    public final FullHttpResponse getHttpResponse() {
        return httpResponse;
    }
    public final void setHttpResponse(FullHttpResponse httpResponse) {
        this.httpResponse = httpResponse;
    }
    public final Object getResult() {
        return result;
    }
    public final void setResult(Object result) {
        this.result = result;
    }
}

这个类主要包含两个成员变量:FullHttpResponse和Object,Object就是业务需要发送的应答POJO对象。

下面是应答消息的XML编码类实现:


public class HttpXmlResponseEncoder extends AbstractHttpXmlEncoder {

    protected void encode(ChannelHandlerContext ctx, Object o, List out) throws Exception {
        HttpXmlResponse msg = (HttpXmlResponse) o;
        ByteBuf body = encode0(ctx, msg.getResult());
        FullHttpResponse response = msg.getHttpResponse();
//对应答消息进行判断
        if (response == null) {
            response = new DefaultFullHttpResponse(HTTP_1_1, OK, body);
        } else {
            response = new DefaultFullHttpResponse(msg.getHttpResponse()
                    .getProtocolVersion(), msg.getHttpResponse().getStatus(),
                    body);
        }
//设置消息体内容格式为“text/xml”
        response.headers().set(CONTENT_TYPE, "text/xml");
        setContentLength(response, body.readableBytes());
        out.add(response);
    }
}

      这个类的实现比较简单,首先对应答消息进行判断,如果业务侧已经构造了HTTP应答消息,则利用业务已有应答消息重新复制一个新的HTTP应答消息。无法重用业务侧自定义HTTP应答消息的主要原因,是 Netty 的 DefaultFullHttpResponse没有提供动态设置消息体 content的接口,只能在第一次构造的时候设置内容。由于这个局限,导致我们的实现有点麻烦。
     最后设置消息体内容格式为“text/xml”,然后在消息头中设置消息体的长度,把编码后的DefaultFullHttpResponsc对象添加到编码结果列表中,由后续Netty的HTTP编码类进行二次编码。

4.HTTP+XML应答消息解码

客户端接收到HTTP+XML应答消息后,对消息进行解码,获取HttpXmlResponse对象,源码如下。

public class HttpXmlResponseDecoder extends AbstractHttpXmlDecoder {

    public HttpXmlResponseDecoder(Class clazz) {
        this(clazz, false);
    }
    public HttpXmlResponseDecoder(Class clazz, boolean isPrintlog) {
        super(clazz, isPrintlog);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx,Object o, List out) throws Exception {
//关注
        DefaultFullHttpResponse msg = (DefaultFullHttpResponse)o;
        HttpXmlResponse resHttpXmlResponse = new HttpXmlResponse(msg, decode0(
                ctx, msg.content()));
        out.add(resHttpXmlResponse);
    }
}

我们看关注部分,是通过DefaultFullHttpResponse和 HTTP应答消息反序列化后的POJO对象构造HttpXmlResponse,并将其添加到解码结果列表中。

五. HTTP + XML  客户端 与 服务端 开发

客户端的功能如下:
(1)发起 HTTP连接请求;
(2)构造订购请求消息,将其编码成XML,通过HTTP协议发送给服务端;
(3)接收HTTP服务端的应答消息,将XML应答消息反序列化为订购消息POJO对象;(4)关闭HTTP连接。

先来看启动类:

public class HttpXmlClient {

    public void connect(int port) throws Exception {
        // 配置客户端NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer() {
                        @Override
                        public void initChannel(Channel ch)
                                throws Exception {
                            ch.pipeline().addLast("http-decoder",new HttpResponseDecoder());
                            ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536));
                            // XML解码器
                            ch.pipeline().addLast("xml-decoder",new HttpXmlResponseDecoder(Order.class,true));
                            ch.pipeline().addLast("http-encoder",new HttpRequestEncoder());
                            ch.pipeline().addLast("xml-encoder",new HttpXmlRequestEncoder());
                            ch.pipeline().addLast("xmlClientHandler",new HttpXmlClientHandle());
                        }
                    });

            // 发起异步连接操作
            ChannelFuture f = b.connect(new InetSocketAddress(port)).sync();

            // 等待客户端链路关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放NIO线程组
            group.shutdownGracefully();
        }

      我们先来看一下,在channelPipeline上添加了哪些 “handler”:

      首先在ChannelPipeline 中新增了HttpResponseDecoder,它负责将二进制码流解码成为HTTP的应答消息。

      随后新增了HttpObjectAggregator,它负责将1个HTTP请求消息的多个部分合并成一条完整的 HTTP 消息。

    然后将前面开发的 XML解码器HttpXmIResponseDecoder添加到ChannelPipeline中,它有两个参数,分别是解码对象的类型信息和码流开关,这样就实现了HTTP+XML应答消息的自动解码。
    最后将HttpRequestEncoder编码器添加到ChannelPipeline中时,需要注意顺序,编码的时候是按照从尾到头的顺序调度执行的,它后面放的是我们自定义开发的HTTP+XML请求消息编码器HttpXmlRequestEncoder。

最后是业务的逻辑编排类HttpXmlClientHandle。

public class HttpXmlClientHandle extends SimpleChannelInboundHandler {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        HttpXmlRequest request = new HttpXmlRequest(null,OrderFactory.create(123));
        ctx.writeAndFlush(request);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx,Object o) throws Exception {
        HttpXmlResponse msg = (HttpXmlResponse)o;
        System.out.println("The client receive response of http header is : " + msg.getHttpResponse().headers().names());
        System.out.println("The client receive response of http body is : " + msg.getResult());
    }
}

客户端的实现简单。首先构造HttpXmlRequest对象,调用ChannclHandlerContext的 writeAndFlush 发送HttpXmlRequest。
messageReceived用于接收服务端的应答消息,从接口看,它接收到的已经是自动解码后的 HttpXmIResponse对象了。最后将POJO消息打印出来,这里在测试的时候可以与服务端发送的原始对象进行比对,两者的内容将完全一致。

最后,来看看订购对象工厂类的实现:

public class OrderFactory {
    public static Order create(long orderID) {
        Order order = new Order();
        order.setOrderNumber(orderID);
        order.setTotal(9999.999f);
        Address address = new Address();
        address.setCity("南京市");
        address.setCountry("中国");
        address.setPostCode("123321");
        address.setState("江苏省");
        address.setStreet1("龙眠大道");
        order.setBillTo(address);
        Customer customer = new Customer();
        customer.setCustomerNumber(orderID);
        customer.setFirstName("李");
        customer.setLastName("林峰");
        order.setCustomer(customer);
        order.setShipping(Shipping.INTERNATIONAL_MAIL);
        order.setShipTo(address);
        return order;
    }
}

HTTP服务端的功能如下:

( 1)接收HTTP客户端的连接;
(2)接收HTTP客户端的XML请求消息,并将其解码为POJO对象;(3)对POJO对象进行业务处理,构造应答消息返回;
(4)通过HTTP+XML的格式返回应答消息;
(5)主动关团HTTP连接。
服务端监听主程序和我们前面,在组件分析中,分析过的启动流程类似,这里不再赘述,直接看HttpXmlServerHandler的实现(省略异常处理代码):

public class HttpXmlServerHandler extends SimpleChannelInboundHandler {

    @Override
    public void messageReceived(final ChannelHandlerContext ctx,Object o) throws Exception {
        HttpXmlRequest xmlRequest = (HttpXmlRequest)o;
        HttpRequest request = xmlRequest.getRequest();
//关注1
        Order order = (Order) xmlRequest.getBody();
        System.out.println("Http server receive request : " + order);
//关注2
        dobusiness(order);
//关注3
        ChannelFuture future = ctx.writeAndFlush(new HttpXmlResponse(null,
                order));
        if (!isKeepAlive(request)) {
            future.addListener(new GenericFutureListener() {
                public void operationComplete (Future future)throws Exception {
                    ctx.close();
                }
            });
        }
    }

    private void dobusiness(Order order) {
        order.getCustomer().setFirstName("狄");
        order.getCustomer().setLastName("仁杰");
        List midNames = new ArrayList();
        midNames.add("李元芳");
        order.getCustomer().setMiddleNames(midNames);
        Address address = order.getBillTo();
        address.setCity("洛阳");
        address.setCountry("大唐");
        address.setState("河南道");
        address.setPostCode("123456");
        order.setBillTo(address);
        order.setShipTo(address);
    }

//省略....

}

我们专注messageReceived方法:

这里通过messageReceived的方法入参HttpXmlRequest,可以看出服务端业务处理类接收到的已经是解码后的业务消息了。

关注1 用于获取请求消息对象。随后将它打印出来,可以与客户端发送的原始消息进行对比。

关注2 对订购请求消息进行业务逻辑编排.

关注3 用于发送应答消息,并且在发送成功之后主动关闭HTTP连接。
最后的异常处理的代码没有给出,这部分代码主要负责在发生异常并且链路没有关闭的情况下,构造内部异常消息发送给客户端,发送完成之后关闭HTTP链路。


到此,HTTP+XML的协议栈开发工作全部完成。

我们来简单的看一下测试的结果:

服务端请求消息码流输出:

服务端解码后的业务对象输出:

客户端响应消息码流输出:

客户端解码后的业务对象输出:

测试结果表明,HTTP+XML协议栈功能正常,达到了设计预期。

六.小结

    这两次的博客,第一篇关于协议的博客着重分析了HTTP协议以及如何使用Netty 的 HTTP协议栈开发基于 HTTP的应用程序。今天的这个博客主要通过对HTTP+XML协议栈的开发的源代码进行分析,研究了如何基于Netty提供的HTTP协议栈做二次定制开发。

   第一次进行这个分析的时候,还是不是很清晰。通过两次的分析,感觉自己稍微有点“入门”了。关于协议的二次开发,主要思想是分析现在需要的功能和已经有的技术可以解决的部分,再来找出还需要做的部分,针对这部分进行开发。然后就是具体针对协议的请求响应的编码解码的实现,在这部分中,我们也看到了对前面基础的应用,尤其是责任链模式发挥了很大的作用,很大程度减轻了后期的工作,我们只需要将需要的handler添加到链上,后面的工作就不需要我们担心了。

   关于协议这部分,目前还是比较基础的一个状态 ,后面的分析会更锻炼自己从上层往下看,可以把握整体的结构。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值