View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufUtil;
20  import io.netty.buffer.Unpooled;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.channel.FileRegion;
23  import io.netty.handler.codec.MessageToMessageEncoder;
24  import io.netty.util.CharsetUtil;
25  import io.netty.util.internal.StringUtil;
26  
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map.Entry;
30  
31  import static io.netty.buffer.Unpooled.directBuffer;
32  import static io.netty.buffer.Unpooled.unreleasableBuffer;
33  import static io.netty.handler.codec.http.HttpConstants.CR;
34  import static io.netty.handler.codec.http.HttpConstants.LF;
35  
36  /**
37   * Encodes an {@link HttpMessage} or an {@link HttpContent} into
38   * a {@link ByteBuf}.
39   *
40   * <h3>Extensibility</h3>
41   *
42   * Please note that this encoder is designed to be extended to implement
43   * a protocol derived from HTTP, such as
44   * <a href="http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
45   * <a href="http://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
46   * To implement the encoder of such a derived protocol, extend this class and
47   * implement all abstract methods properly.
48   */
49  public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageToMessageEncoder<Object> {
50      static final int CRLF_SHORT = (CR << 8) | LF;
51      private static final int ZERO_CRLF_MEDIUM = ('0' << 16) | CRLF_SHORT;
52      private static final byte[] ZERO_CRLF_CRLF = { '0', CR, LF, CR, LF };
53      private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(2).writeByte(CR).writeByte(LF));
54      private static final ByteBuf ZERO_CRLF_CRLF_BUF = unreleasableBuffer(directBuffer(ZERO_CRLF_CRLF.length)
55              .writeBytes(ZERO_CRLF_CRLF));
56      private static final float HEADERS_WEIGHT_NEW = 1 / 5f;
57      private static final float HEADERS_WEIGHT_HISTORICAL = 1 - HEADERS_WEIGHT_NEW;
58      private static final float TRAILERS_WEIGHT_NEW = HEADERS_WEIGHT_NEW;
59      private static final float TRAILERS_WEIGHT_HISTORICAL = HEADERS_WEIGHT_HISTORICAL;
60  
61      private static final int ST_INIT = 0;
62      private static final int ST_CONTENT_NON_CHUNK = 1;
63      private static final int ST_CONTENT_CHUNK = 2;
64      private static final int ST_CONTENT_ALWAYS_EMPTY = 3;
65  
66      @SuppressWarnings("RedundantFieldInitialization")
67      private int state = ST_INIT;
68  
69      /**
70       * Used to calculate an exponential moving average of the encoded size of the initial line and the headers for
71       * a guess for future buffer allocations.
72       */
73      private float headersEncodedSizeAccumulator = 256;
74  
75      /**
76       * Used to calculate an exponential moving average of the encoded size of the trailers for
77       * a guess for future buffer allocations.
78       */
79      private float trailersEncodedSizeAccumulator = 256;
80  
81      @Override
82      protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
83          ByteBuf buf = null;
84          if (msg instanceof HttpMessage) {
85              if (state != ST_INIT) {
86                  throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
87              }
88  
89              @SuppressWarnings({ "unchecked", "CastConflictsWithInstanceof" })
90              H m = (H) msg;
91  
92              buf = ctx.alloc().buffer((int) headersEncodedSizeAccumulator);
93              // Encode the message.
94              encodeInitialLine(buf, m);
95              state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY :
96                      HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
97  
98              sanitizeHeadersBeforeEncode(m, state == ST_CONTENT_ALWAYS_EMPTY);
99  
100             encodeHeaders(m.headers(), buf);
101             ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
102 
103             headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
104                                             HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
105         }
106 
107         // Bypass the encoder in case of an empty buffer, so that the following idiom works:
108         //
109         //     ch.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
110         //
111         // See https://github.com/netty/netty/issues/2983 for more information.
112         if (msg instanceof ByteBuf) {
113             final ByteBuf potentialEmptyBuf = (ByteBuf) msg;
114             if (!potentialEmptyBuf.isReadable()) {
115                 out.add(potentialEmptyBuf.retain());
116                 return;
117             }
118         }
119 
120         if (msg instanceof HttpContent || msg instanceof ByteBuf || msg instanceof FileRegion) {
121             switch (state) {
122                 case ST_INIT:
123                     throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
124                 case ST_CONTENT_NON_CHUNK:
125                     final long contentLength = contentLength(msg);
126                     if (contentLength > 0) {
127                         if (buf != null && buf.writableBytes() >= contentLength && msg instanceof HttpContent) {
128                             // merge into other buffer for performance reasons
129                             buf.writeBytes(((HttpContent) msg).content());
130                             out.add(buf);
131                         } else {
132                             if (buf != null) {
133                                 out.add(buf);
134                             }
135                             out.add(encodeAndRetain(msg));
136                         }
137 
138                         if (msg instanceof LastHttpContent) {
139                             state = ST_INIT;
140                         }
141 
142                         break;
143                     }
144 
145                     // fall-through!
146                 case ST_CONTENT_ALWAYS_EMPTY:
147 
148                     if (buf != null) {
149                         // We allocated a buffer so add it now.
150                         out.add(buf);
151                     } else {
152                         // Need to produce some output otherwise an
153                         // IllegalStateException will be thrown as we did not write anything
154                         // Its ok to just write an EMPTY_BUFFER as if there are reference count issues these will be
155                         // propagated as the caller of the encode(...) method will release the original
156                         // buffer.
157                         // Writing an empty buffer will not actually write anything on the wire, so if there is a user
158                         // error with msg it will not be visible externally
159                         out.add(Unpooled.EMPTY_BUFFER);
160                     }
161 
162                     break;
163                 case ST_CONTENT_CHUNK:
164                     if (buf != null) {
165                         // We allocated a buffer so add it now.
166                         out.add(buf);
167                     }
168                     encodeChunkedContent(ctx, msg, contentLength(msg), out);
169 
170                     break;
171                 default:
172                     throw new Error();
173             }
174 
175             if (msg instanceof LastHttpContent) {
176                 state = ST_INIT;
177             }
178         } else if (buf != null) {
179             out.add(buf);
180         }
181     }
182 
183     /**
184      * Encode the {@link HttpHeaders} into a {@link ByteBuf}.
185      */
186     protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) {
187         Iterator<Entry<CharSequence, CharSequence>> iter = headers.iteratorCharSequence();
188         while (iter.hasNext()) {
189             Entry<CharSequence, CharSequence> header = iter.next();
190             HttpHeadersEncoder.encoderHeader(header.getKey(), header.getValue(), buf);
191         }
192     }
193 
194     private void encodeChunkedContent(ChannelHandlerContext ctx, Object msg, long contentLength, List<Object> out) {
195         if (contentLength > 0) {
196             String lengthHex = Long.toHexString(contentLength);
197             ByteBuf buf = ctx.alloc().buffer(lengthHex.length() + 2);
198             buf.writeCharSequence(lengthHex, CharsetUtil.US_ASCII);
199             ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
200             out.add(buf);
201             out.add(encodeAndRetain(msg));
202             out.add(CRLF_BUF.duplicate());
203         }
204 
205         if (msg instanceof LastHttpContent) {
206             HttpHeaders headers = ((LastHttpContent) msg).trailingHeaders();
207             if (headers.isEmpty()) {
208                 out.add(ZERO_CRLF_CRLF_BUF.duplicate());
209             } else {
210                 ByteBuf buf = ctx.alloc().buffer((int) trailersEncodedSizeAccumulator);
211                 ByteBufUtil.writeMediumBE(buf, ZERO_CRLF_MEDIUM);
212                 encodeHeaders(headers, buf);
213                 ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
214                 trailersEncodedSizeAccumulator = TRAILERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
215                                                  TRAILERS_WEIGHT_HISTORICAL * trailersEncodedSizeAccumulator;
216                 out.add(buf);
217             }
218         } else if (contentLength == 0) {
219             // Need to produce some output otherwise an
220             // IllegalStateException will be thrown
221             out.add(encodeAndRetain(msg));
222         }
223     }
224 
225     /**
226      * Allows to sanitize headers of the message before encoding these.
227      */
228     protected void sanitizeHeadersBeforeEncode(@SuppressWarnings("unused") H msg, boolean isAlwaysEmpty) {
229         // noop
230     }
231 
232     /**
233      * Determine whether a message has a content or not. Some message may have headers indicating
234      * a content without having an actual content, e.g the response to an HEAD or CONNECT request.
235      *
236      * @param msg the message to test
237      * @return {@code true} to signal the message has no content
238      */
239     protected boolean isContentAlwaysEmpty(@SuppressWarnings("unused") H msg) {
240         return false;
241     }
242 
243     @Override
244     public boolean acceptOutboundMessage(Object msg) throws Exception {
245         return msg instanceof HttpObject || msg instanceof ByteBuf || msg instanceof FileRegion;
246     }
247 
248     private static Object encodeAndRetain(Object msg) {
249         if (msg instanceof ByteBuf) {
250             return ((ByteBuf) msg).retain();
251         }
252         if (msg instanceof HttpContent) {
253             return ((HttpContent) msg).content().retain();
254         }
255         if (msg instanceof FileRegion) {
256             return ((FileRegion) msg).retain();
257         }
258         throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
259     }
260 
261     private static long contentLength(Object msg) {
262         if (msg instanceof HttpContent) {
263             return ((HttpContent) msg).content().readableBytes();
264         }
265         if (msg instanceof ByteBuf) {
266             return ((ByteBuf) msg).readableBytes();
267         }
268         if (msg instanceof FileRegion) {
269             return ((FileRegion) msg).count();
270         }
271         throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
272     }
273 
274     /**
275      * Add some additional overhead to the buffer. The rational is that it is better to slightly over allocate and waste
276      * some memory, rather than under allocate and require a resize/copy.
277      * @param readableBytes The readable bytes in the buffer.
278      * @return The {@code readableBytes} with some additional padding.
279      */
280     private static int padSizeForAccumulation(int readableBytes) {
281         return (readableBytes << 2) / 3;
282     }
283 
284     @Deprecated
285     protected static void encodeAscii(String s, ByteBuf buf) {
286         buf.writeCharSequence(s, CharsetUtil.US_ASCII);
287     }
288 
289     protected abstract void encodeInitialLine(ByteBuf buf, H message) throws Exception;
290 }