start of encoder

This commit is contained in:
Greg Wilkins 2014-06-08 19:55:56 +02:00
parent 9e639d9236
commit fdf73adf39
3 changed files with 484 additions and 8 deletions

View File

@ -232,6 +232,37 @@ public class HpackContext
}
}
public void unuseReferenceSet()
{
Entry entry = _refSet._refSetNext;
while(entry!=_refSet)
{
entry._used=false;
entry=entry._refSetNext;
}
}
public void removedUnusedReferences(ByteBuffer buffer)
{
Entry entry = _refSet._refSetNext;
while(entry!=_refSet)
{
Entry next = entry._refSetNext;
if (!entry.isUsed())
{
// encode the reference to remove it
buffer.put((byte)0x80);
NBitInteger.encode(buffer,7,index(entry));
entry.removeFromRefSet();
}
entry=next;
}
}
public Iterator<Entry> iterateReferenceSet()
{
return new Iterator<Entry>()
@ -350,11 +381,11 @@ public class HpackContext
/* ------------------------------------------------------------ */
public static class Entry
{
int _index;
final HttpField _field;
int _index;
Entry _refSetNext=this;
Entry _refSetPrev=this;
boolean _refSetUsed;
boolean _used;
Entry()
{
@ -382,7 +413,8 @@ public class HpackContext
throw new IllegalStateException("evicted");
if (_refSetNext!=this)
return;
_used=true;
_refSetNext=ctx._refSet;
_refSetPrev=ctx._refSet._refSetPrev;
ctx._refSet._refSetPrev._refSetNext=this;
@ -402,6 +434,7 @@ public class HpackContext
_refSetPrev._refSetNext=_refSetNext;
_refSetNext=this;
_refSetPrev=this;
_used=false;
}
}
@ -429,6 +462,18 @@ public class HpackContext
{
return String.format("{%s,%d,%s,%x}",isStatic()?"S":"D",_index,_field,hashCode());
}
public void used()
{
_used=true;
}
public boolean isUsed()
{
return _used;
}
}
public static class StaticEntry extends Entry

View File

@ -20,16 +20,49 @@
package org.eclipse.jetty.hpack;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import org.eclipse.jetty.hpack.HpackContext.Entry;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.ByteBufferPool;
public class HpackEncoder
{
private final static EnumSet<HttpHeader> __NEVER_INDEX =
EnumSet.of(HttpHeader.SET_COOKIE,
HttpHeader.SET_COOKIE2);
private final static EnumSet<HttpHeader> __DO_NOT_HUFFMAN =
EnumSet.of(HttpHeader.COOKIE,
HttpHeader.SET_COOKIE,
HttpHeader.SET_COOKIE2,
HttpHeader.AUTHORIZATION,
HttpHeader.CONTENT_MD5,
HttpHeader.PROXY_AUTHENTICATE,
HttpHeader.PROXY_AUTHORIZATION);
private final static EnumSet<HttpHeader> __USE_REFERENCE_SET =
EnumSet.of(HttpHeader.ACCEPT,
HttpHeader.ACCEPT_CHARSET,
HttpHeader.ACCEPT_ENCODING,
HttpHeader.ACCEPT_LANGUAGE,
HttpHeader.ACCEPT_RANGES,
HttpHeader.ALLOW,
HttpHeader.AUTHORIZATION,
HttpHeader.CACHE_CONTROL,
HttpHeader.CONTENT_LANGUAGE,
HttpHeader.COOKIE,
HttpHeader.DATE,
HttpHeader.HOST,
HttpHeader.SERVER,
HttpHeader.SERVLET_ENGINE,
HttpHeader.USER_AGENT);
private final ByteBufferPool _byteBufferPool;
private final HpackContext _context;
public HpackEncoder(ByteBufferPool byteBufferPool)
{
@ -41,15 +74,38 @@ public class HpackEncoder
this._byteBufferPool = byteBufferPool;
_context=new HpackContext(maxHeaderTableSize);
}
public HpackContext getContext()
{
return _context;
}
/*
public ByteBufferPool.Lease encode(HttpFields fields)
{
return new ByteBufferPool.Lease(_byteBufferPool);
}
private boolean encode(ByteBuffer buffer, HttpField field)
*/
public void encode(ByteBuffer buffer, HttpFields fields)
{
// Clear the used bits
_context.unuseReferenceSet();
// Add all the known fields
for (HttpField field : fields)
{
encode(buffer,field);
}
_context.removedUnusedReferences(buffer);
}
private void encode(ByteBuffer buffer, HttpField field)
{
// TODO currently we do not check if there is enough space, so we will always
// return true or fail nastily.
// Is there an entry for the field?
Entry entry = _context.get(field);
@ -57,12 +113,15 @@ public class HpackEncoder
{
// if entry is already in the reference set, then nothing more to do.
if (entry.isInReferenceSet())
return true;
{
entry.used();
return;
}
// Is this as static field
if (entry.isStatic())
{
// TODO Policy decision to make!
// TODO Strategy decision to make!
// Should we add to reference set or just always send as indexed?
// Let's always send as indexed to reduce churn in header table!
// BUGGER! Can't do that. Oh well all the static fields have small values, so
@ -77,9 +136,107 @@ public class HpackEncoder
// Add the value
buffer.put(entry.getStaticHuffmanValue());
return;
}
// So we can add the entry to the reference Set and emit the index;
_context.addToRefSet(entry);
int index=_context.index(entry);
// TODO pregenerate indexes?
buffer.put((byte)0x80);
NBitInteger.encode(buffer,7,index);
return;
}
// Must be a new entry, so we will have to send literally.
// TODO Strategy decision to make!
// What fields will we put in the reference set and what fields will we huffman encode?
// Let's make these decisions by lookup of known fields
HttpHeader header = field.getHeader();
final boolean never_index;
final boolean huffman;
final boolean reference;
final int name_bits;
final byte mask;
if (header==null)
{
never_index=false;
huffman=true;
reference=true;
name_bits = 6;
mask=(byte)0x40;
}
else if (__USE_REFERENCE_SET.contains(header))
{
reference=true;
never_index=false;
huffman=__DO_NOT_HUFFMAN.contains(header);
name_bits = 6;
mask=(byte)0x40;
}
else
{
reference=false;
never_index=__NEVER_INDEX.contains(header);
huffman=__DO_NOT_HUFFMAN.contains(header);
name_bits = 4;
mask=never_index?(byte)0x01:(byte)0x00;
}
// Add the mask bits
buffer.put(mask);
// Look for a name Index
Entry name_entry = _context.get(field.getName());
if (name_entry!=null)
NBitInteger.encode(buffer,name_bits,_context.index(name_entry));
else
{
// Encode the name always with huffman
buffer.put((byte)0x80);
NBitInteger.encode(buffer,7,Huffman.octetsNeeded(field.getName()));
Huffman.encode(buffer,field.getName());
}
// Add the literal value
String value=field.getValue();
if (huffman)
{
// huffman literal value
buffer.put((byte)0x80);
NBitInteger.encode(buffer,7,Huffman.octetsNeeded(value));
Huffman.encode(buffer,field.getValue());
}
else
{
// add literal assuming iso_8859_1
buffer.put((byte)0x80);
NBitInteger.encode(buffer,7,value.length());
for (int i=0;i<value.length();i++)
{
char c=value.charAt(i);
if (c<' '|| c>127)
throw new IllegalArgumentException();
buffer.put((byte)c);
}
}
// If we want the field referenced, then we add it to our
// table and reference set.
if (reference)
{
Entry new_entry=_context.add(field);
if (new_entry!=null)
_context.addToRefSet(new_entry);
}
return;
}

View File

@ -0,0 +1,274 @@
//
// ========================================================================
// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.hpack;
import static org.junit.Assert.assertThat;
import java.nio.ByteBuffer;
import java.util.HashSet;
import org.eclipse.jetty.hpack.HpackContext.Entry;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.util.BufferUtil;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
/* ------------------------------------------------------------ */
/**
*/
public class HpackEncoderTest
{
@Test
public void testUnknownFieldsContextManagement()
{
HpackEncoder encoder = new HpackEncoder(null,38*5);
HttpFields fields = new HttpFields();
HttpField[] field =
{
new HttpField("fo0","b0r"),
new HttpField("fo1","b1r"),
new HttpField("fo2","b2r"),
new HttpField("fo3","b3r"),
new HttpField("fo4","b4r"),
new HttpField("fo5","b5r"),
new HttpField("fo6","b6r"),
new HttpField("fo7","b7r"),
new HttpField("fo8","b8r"),
new HttpField("fo9","b9r"),
new HttpField("foA","bAr"),
};
// Add 4 entries
for (int i=0;i<=3;i++)
fields.add(field[i]);
// encode them
ByteBuffer buffer = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,pos);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// All are in the header table
Assert.assertEquals(4,encoder.getContext().size());
// All are in the reference set
HashSet<HttpField> refSet = new HashSet<>();
for (Entry entry : encoder.getContext().getReferenceSet())
refSet.add(entry.getHttpField());
Assert.assertEquals(4,refSet.size());
for (int i=0;i<=3;i++)
Assert.assertTrue(refSet.contains(field[i]));
// encode exact same fields again!
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// nothing should be encoded!
assertThat(buffer.remaining(),Matchers.is(0));
// All are in the header table
Assert.assertEquals(4,encoder.getContext().size());
// All are in the reference set
refSet.clear();
for (Entry entry : encoder.getContext().getReferenceSet())
refSet.add(entry.getHttpField());
Assert.assertEquals(4,refSet.size());
for (int i=0;i<=3;i++)
Assert.assertTrue(refSet.contains(field[i]));
// Add 4 more fields
for (int i=4;i<=7;i++)
fields.add(field[i]);
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// max header table size reached
Assert.assertEquals(5,encoder.getContext().size());
// last 5 in reference set
refSet.clear();
for (Entry entry : encoder.getContext().getReferenceSet())
refSet.add(entry.getHttpField());
Assert.assertEquals(5,refSet.size());
for (int i=3;i<=7;i++)
Assert.assertTrue(refSet.contains(field[i]));
// remove some fields
for (int i=0;i<=7;i+=2)
fields.remove(field[i].getName());
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// max header table size reached
Assert.assertEquals(5,encoder.getContext().size());
// last 5 in reference set
refSet.clear();
for (Entry entry : encoder.getContext().getReferenceSet())
refSet.add(entry.getHttpField());
Assert.assertEquals(4,refSet.size());
for (int i=0;i<=7;i++)
{
if (i%2==1)
Assert.assertTrue(refSet.contains(field[i]));
else
Assert.assertFalse(refSet.contains(field[i]));
}
// remove another fields
fields.remove(field[1].getName());
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// max header table size reached
Assert.assertEquals(5,encoder.getContext().size());
// last 5 in reference set
refSet.clear();
for (Entry entry : encoder.getContext().getReferenceSet())
refSet.add(entry.getHttpField());
Assert.assertEquals(3,refSet.size());
for (int i=2;i<=7;i++)
{
if (i%2==1)
Assert.assertTrue(refSet.contains(field[i]));
else
Assert.assertFalse(refSet.contains(field[i]));
}
// re add the field
fields.add(field[1]);
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// max header table size reached
Assert.assertEquals(5,encoder.getContext().size());
// last 5 in reference set
refSet.clear();
for (Entry entry : encoder.getContext().getReferenceSet())
refSet.add(entry.getHttpField());
Assert.assertEquals(4,refSet.size());
for (int i=0;i<=7;i++)
{
if (i%2==1)
Assert.assertTrue(refSet.contains(field[i]));
else
Assert.assertFalse(refSet.contains(field[i]));
}
}
@Test
public void testDoNotReferenceStatics()
{
HpackEncoder encoder = new HpackEncoder(null,38*5);
ByteBuffer buffer = BufferUtil.allocate(4096);
HttpFields fields = new HttpFields();
fields.put(":method","POST");
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// empty header table
Assert.assertEquals(0,encoder.getContext().size());
}
@Test
public void testNeverIndexSetCookie()
{
HpackEncoder encoder = new HpackEncoder(null,38*5);
ByteBuffer buffer = BufferUtil.allocate(4096);
HttpFields fields = new HttpFields();
fields.put("set-cookie","some cookie value");
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// empty header table
Assert.assertEquals(0,encoder.getContext().size());
// encode again
BufferUtil.clearToFill(buffer);
encoder.encode(buffer,fields);
BufferUtil.flipToFlush(buffer,0);
// something was encoded!
assertThat(buffer.remaining(),Matchers.greaterThan(0));
// empty header table
Assert.assertEquals(0,encoder.getContext().size());
}
}