1. 为什么要使用Http Trunked协议?
一般http通信时会使用content_length头信息来表示服务器发送的文档内容长度,这是因为我们已经提前知道了文档内容的长度,但
有时候我们无法提前知道我们需要传输的文档的长度,这时我们就要采用分块传输的方式来发送内容,也就是通过我们的http trunked协议。
Http1.1x协议的chunked编码方式,可以确保接收端能够准确的判断不定长内容收取是否完整。
2. http RFC文档中的chunked编码格式
chunked编码一般使用若干个chunk串联而成,最后一个chunk的长度为0,表示chunk数据结束。每个chunked分为头部和正文,头部指
定下一段正文的长度,正文只的是实际内容。通过/r/n分隔符来分隔各个部分。
Chunked编码格式:
Chunked-Body = *chunk
last-chunk
trailer
CRLF
chunk = chunk-size [ chunk-extension ] CRLF
chunk-data CRLF
chunk-size = 1*HEX
last-chunk = 1*("0") [ chunk-extension ] CRLF
chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)
trailer = *(entity-header CRLF)
Chunked-Body表示经过chunked编码后的报文体。报文体可以分为chunk, last-chunk,trailer和结束符四部分。chunk的数量在报文
体中最少可以为0,无上限;每个chunk的长度是自指定的,即,起始的数据必然是16进制数字的字符串,代表后面chunk-data的长度
(字节数)。这个16进制的字符串第一个字符如果是“0”,则表示chunk-size为0,该chunk为last-chunk,无chunk-data部分。可选的
chunk-extension由通信双方自行确定,如果接收者不理解它的意义,可以忽略。
trailer是附加的在尾部的额外头域,通常包含一些元数据,后文中会给出具体的例子。
3. Http RFC文档中chunked协议解码
length := 0 //长度计数器置0
read chunk-size, chunk-extension (if any) and CRLF
//读取chunk-size, chunk-extension和CRLF
while(chunk-size > 0 ) { //表明不是last-chunk
read chunk-data and CRLF //读chunk-size大小的chunk-data,skip CRLF
append chunk-data to entity-body //将此块chunk-data追加到entity-body后
read chunk-size and CRLF //读取新chunk的chunk-size 和 CRLF
}
read entity-header //entity-header的格式为name:valueCRLF,如果为空即只有CRLF
while (entity-header not empty) //即,不是只有CRLF的空行
{
append entity-header to existing header fields
read entity-header
}
Content-Length:=length //将整个解码流程结束后计算得到的新报文体length
//作为Content-Length域的值写入报文中
Remove "chunked" from Transfer-Encoding //同时从Transfer-Encoding中域值去除 chunked这个标记
从这段伪代码中,我们可以看出chunked协议还是比较简单的,用任何一门语言实现起来都很方便。下面我们给出Java版实现的例子。
1 private ByteBuffer m_chunkbody = ByteBuffer.allocate(100*1024); 2 public byte[] m_buffer; 3 protected BufferedInputStream m_bis = getInputStream(); 4 BisBuffer bb = new BisBuffer(m_bis, 100);//BisBuffer每次读一百个字节 5 int contentLength = 0; 6 boolean isDataOverLimit = false; 7 byte b; 8 while (true) { // 头判断 9 b = (byte)bb.read();//每次读取一个字节 10 if (b == 'T' || b == 't') { 11 byte[] keyBuffer = new byte[50]; 12 idx = 0; 13 while ((keyBuffer[idx++] = (byte)bb.read()) != ' ') // "transfer-Encoding: " 14 if (keyBuffer[idx - 1] == '\n') 15 continue; 16 String encoding = new String(keyBuffer, 0, idx - 1); 17 if (encoding.equalsIgnoreCase("ransfer-Encoding:")) { 18 byte[] chuckBuffer = new byte[8]; 19 idx = 0; 20 while ((chuckBuffer[idx++] = (byte)bb.read()) != '\r'); 21 String chunked = new String(chuckBuffer, 0, idx - 1); // chunked协议标记 22 logger.info(chunked); 23 if (!chunked.equalsIgnoreCase("chunked")) 24 throw new Exception("not chunked!"); 25 b = (byte)bb.read(); // \n 26 } else if(encoding.equalsIgnoreCase("railer:")){ 27 byte[] trailerBuffer = new byte[50]; 28 idx = 0; 29 while ((trailerBuffer[idx++] = (byte)bb.read()) != '\r'); 30 String trailer = new String(trailerBuffer, 0, idx - 1); // trailer 31 String[] trailerArr = trailer.split(","); 32 if (!(trailerArr[0].trim()).equalsIgnoreCase("data_over_limit")) 33 throw new Exception("trailer doesn't have data_over_limit!"); 34 b = (byte)bb.read(); // \n 35 }else { 36 while ((b = (byte)bb.read()) != '\n') ; // \n 37 } 38 } else if (b == '\r') { 39 //到此说明接下来是chunked-body相关内容,。 40 b = (byte)bb.read(); // \n 41 byte[] lensize = new byte[Integer.SIZE]; 42 idx = 0; 43 while((lensize[idx++] = (byte)bb.read()) != '\r'); 44 b = (byte)bb.read(); // \n 45 int chunksize = Integer.parseInt(new String(lensize,0,idx-1),16); 46 //n个chunked包的解析 47 while(chunksize > 0){ 48 contentLength += chunksize;//add len 49 if(contentLength < 0 || contentLength > 100*1024) 50 throw new Exception(contentLength+" LENGTH TOO LARGE!"); 51 52 byte[] temp = new byte[chunksize]; 53 idx = 0; 54 while(idx != chunksize){ 55 temp[idx++] = bb.read(); 56 } 57 m_chunkbody.put(temp);//append chunk-data 58 //读取下一个chunk-data 59 idx = 0; 60 b = (byte)bb.read(); // \r 61 b = (byte)bb.read(); // \n 62 while((lensize[idx++] = (byte)bb.read()) != '\r'); 63 chunksize = Integer.parseInt(new String(lensize,0,idx-1),16); 64 b = (byte)bb.read(); // \n 65 } 66 b = (byte)bb.read(); // \r 67 b = (byte)bb.read(); // \n 68 } else { 69 if(b == 's'){ 70 //end 读取完chunk-body,最后将trailer数据读取出来 71 byte[] trailerBuffer = new byte[50]; 72 idx = 0; 73 while ((trailerBuffer[idx++] = (byte)bb.read()) != '\r'); 74 String trailer = new String(trailerBuffer, 0, idx - 1); // trailer 75 int length = trailer.length(); 76 trailer = "s" + trailer; 77 String tailerKey = "data_over_limit: "; 78 if (!trailer.startsWith(tailerKey)) 79 throw new Exception("data_over_limit ERROR!"); 80 String isOverLimit = trailer.substring(tailerKey.length(),length+1); 81 if(isOverLimit.equalsIgnoreCase("true")){ 82 isDataOverLimit = true; 83 }else 84 isDataOverLimit = false; 85 b = (byte)bb.read(); // \n 86 break; 87 } 88 while ((b = (byte)bb.read()) != '\n') ; // 其他头字段 89 } 90 } 91 //组装chunk-body的内容,即chunk-size对应的chunk-data的所有块的组合。 92 m_chunkbody.flip();//反转 93 m_buffer = m_chunkbody.array(); 94 m_chunkbody.clear();//清空缓冲区
4 . Chunked协议发送端数据组装
首先来看一下http普通协议和http trunked协议header头部信息的异同。普通http头部信息如下所示:
Post xxx http/1.1
Accept-Language: en-us
Accept: */*
Host: xxx.xxx
User-Agent: xxx HTTP Client
Content-Length: 1024
Http Trunked协议头部信息:
Post xxx http/1.1
Accept-Language: en-us
Accept: */*
Host: xxx.xxx
User-Agent: xxx HTTP Client
Transfer-Encoding: chunked
Trailer: data_over_limit
从上面我们可以看到普通http协议header包含了长度信息,chunked协议是没有长度的,需要再客户端全部chunk数据解析后才
能得到传输信息的具体长度。
头部信息的组装通过java代码来实现如下:
1 public byte[] creatHttpHeader(){ 2 StringBuilder sb = new StringBuilder(100); 3 sb.append("POST xxx http/1.1").append("\r\n"); 4 sb.append("Accept-Language: en-us").append("\r\n"); 5 sb.append("Accept: */*").append("\r\n"); 6 sb.append("Host: xxx.xxx").append("\r\n"); 7 sb.append("User-Agent: xxx HTTP Client").append("\r\n"); 8 sb.append("Transfer-Encoding: chunked").append("\r\n"); 9 sb.append("Trailer: data_over_limit").append("\r\n"); 10 sb.append("\r\n"); // mark header over 11 return sb.toString().getBytes("US-ASCII"); 12 }
Trailer信息Java代码实现:
1 public byte[] createChunkedTrailer(){ 2 StringBuilder sb = new StringBuilder(100); 3 sb.append("data_over_limit: true\r\n"); 4 return sb.toString().getBytes("US-ASCII"); 5 }
传输内容chunked组装java代码实现:
1 public void send(InputStream fileInputStream) throws IOException { 2 OutputStream requestStream = socket.getOutputStream(); 3 ChunkedOutputStream chunkedBodyStream = new ChunkedOutputStream(requestStream); 4 int chunkSize = 2048; 5 this.requestStream.write(createHttpHeader()); 6 7 byte[] buf = new byte[chunkSize]; 8 int readed = 0; 9 int size = 0; 10 while ((readed = fileInputStream.read(buf)) != -1) { 11 size += readed; 12 } 13 this.chunkedBodyStream.finish(); 14 this.requestStream.write(createChunkedTrailer()); 15 }
在介绍一下ChunkedOutputStream这个类,这个类是httpclient-3.0.1.jar里面的一个类,源代码我们可以拿到,代码实现的很简
洁,有兴趣的同学可以好好看看,可以去网上获取httpclient的源代码。
传输内容组装好之后,就可以通过套接字发送到客户端去了,第三节中的代码就可以解析从这里发送过去的数据,怎么样,很简单吧
,看过之后大家都会使用了吧。