HTTP chunked 인 코딩 데이터 흐름 분석 알고리즘
HTTP 전송 과정 에서 Content - Length 는 응답 메시지 의 크기 를 나타 내 는 데 사 용 됩 니 다. 보통 Content - Length 가 있 는 데 이 터 는 그림, 미디어 자원, 정적 텍스트, 웹 페이지 이 고 문서 도 있 습 니 다.보통 Content - Length 는 클 라 이언 트, 서버 에서 이용 할 수 있 습 니 다.
1. Content - Length 를 클 라 이언 트 에 사용 할 때 POST, PUT, DELETE 등 과 함께 서버 에 데 이 터 를 보 내 고 데이터 길 이 를 서버 에 보고 합 니 다.
2. Content - Length 를 서버 에 사용 할 때 HEAD, HTTP 304 등 을 제외 하고 Client 에 데이터 길 이 를 보고 합 니 다.
3. 물론 문 제 는 일부 데 이 터 는 동적 변화 이다. 예 를 들 어 동적 페이지 처리 데이터, Server 자체 가 데이터 의 총 길 이 를 확정 하지 못 하고 세 션 길이 만 알 기 때문에 Content - Length 를 사용 하지 않 았 다.이때 Transfer - Encoding: Chunked 는 무대 에 올 라 세 션 을 보 내 고 세 션 을 전달 하 는 Content - Length 에 따라 데 이 터 를 보 냈 다.마찬가지 로 Content - Length 는 영원히 chunked 와 함께 사용 할 수 없다 는 것 을 알 수 있다.
2. Transfer - Encoding: Chunked 인 코딩 의 데이터 형식 입 니 다.
HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Transfer-Encoding: chunked\r
Connection:keep-Alive\r
\r
25\r
;
This is the data in the first chunk\r
\r
1A\r
and this is the second one
\r
0\r
\r
보기 에는 매우 평범 하 다. 사실 우 리 는 3, 5, 7, 9 행 에 중점 을 두 었 다.
세 번 째 줄 은 그의 전송 인 코딩 이 chunked, 다섯 번 째, 일곱 번 째, 아홉 번 째 는 chunked 인 코딩 의 다음 부분 인 Content - Length 지시 로 16 진 데이터 로 표시 되 었 음 을 설명 했다.
, \r
,0x25 10 37, “This is the data in the first chunk\r
” 37
,0x1A=26,“and this is the second one” 26
, 0x0=0, , Client , -1
\r
0\r
\r
예제 도 를 사용 하여 이 사이 의 관 계 를 설명 하 다.
3. Transfer - Encoding: Chunked 인 코딩 분석
표준 해석 위조 코드
length := 0//
read chunk-size, chunk-extension (if any) and CRLF//
while (chunk-size > 0) {// , 0
read chunk-data and CRLF// ,
append chunk-data to entity-body//
length := length + chunk-size//
read chunk-size and CRLF//
}
read entity-header//
while (entity-header not empty) {
append entity-header to existing header fields
read entity-header
}
Content-Length := length//
Remove "chunked" from Transfer-Encoding// Transfer-Encoding
1. chunked 일반적인 해석
우 리 는 아래 코드 를 직접 볼 수 있다.
URL url=new URL("http://localhost:8080/test/ios.php");
EchoURLConnection connection=(EchoURLConnection)url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
PrintWriter pw = new PrintWriter(new OutputStreamWriter(connection.getOutputStream()));
pw.write("name=zhangsan&password=123456");
pw.flush();
InputStream stream = connection.getInputStream();
int len = -1;
byte[] buf = new byte[1];
byte[] outBuf = new byte[]{0x3A}; // “ ”, http Response
int count = 0;
// Header
while((len=stream.read(buf, 0, buf.length))>-1)
{
int outLength = outBuf.length+len;
byte[] tempBuf = outBuf;
outBuf = new byte[outLength];
System.arraycopy(tempBuf, 0, outBuf,0, tempBuf.length);
System.arraycopy(buf, 0, outBuf,outBuf.length-1, len);
if(buf[0]==0x0D || buf[0]==0x0A)
{
count++;
if(count==4)
{
break;
}
}else{
count = 0;
}
}
String headerString = new String(outBuf, 0, outBuf.length);
String[] splitLine = headerString.trim().split("\r
");
// header Map
Map<String, String> headerMap = new HashMap<String, String>();
boolean isCkunked = false;
for (String statusLine : splitLine) {
String[] nameValue = statusLine.split(":");
if(nameValue[0].equals(""))
{
headerMap.put("", nameValue[1]);
}else{
headerMap.put(nameValue[0], nameValue[1]);
}
if("Transfer-Encoding".equalsIgnoreCase(nameValue[0]))
{
String value = nameValue[1].trim();
isCkunked = value.equalsIgnoreCase("chunked"); // chunked
}
}
System.out.println(headerMap);
if(isCkunked)
{
int chunkedNumber = 0; // , CRLF( )
byte[] readBuf = null; // chunked Content-Length
byte[] contentBuf = new byte[0]; //
long currentLength = 0;
while((len=stream.read(buf, 0,buf.length))>=0)
{
if(readBuf==null)
{
readBuf = new byte[]{buf[0]};
}
else
{
int outLength = readBuf.length+len;
byte[] tempBuf = readBuf;
readBuf = new byte[outLength];
System.arraycopy(tempBuf, 0, readBuf,0, tempBuf.length);
System.arraycopy(buf, 0, readBuf,readBuf.length-1, len);
}
if(buf[0]==0x0D|| buf[0]==0x0A) //
{
chunkedNumber++;
//readBuf.length>currentLength chunked Content-Length
if(chunkedNumber==2&&readBuf.length>currentLength)
{
byte[] tmpContentBuf = contentBuf;
contentBuf = new byte[(int) (tmpContentBuf.length+currentLength)];
System.arraycopy(tmpContentBuf, 0, contentBuf,0, tmpContentBuf.length);
System.arraycopy(readBuf, 0, contentBuf,tmpContentBuf.length, (int) currentLength);
String lineNo = "0x"+ (new String(readBuf,(int)currentLength, readBuf.length-(int)currentLength)).trim();
if(lineNo.equals("0x"))
{
currentLength = 0; // chunked Content-Length
}else{
currentLength = Long.decode(lineNo); // chunked Content-Length
if(currentLength==0) // currentLength 0,
{
System.out.println(new String(contentBuf, 0,contentBuf.length));
break;
}
}
System.out.println(currentLength+"--"+lineNo);
chunkedNumber = 0;
readBuf = null;
}
}else{
chunkedNumber = 0; // CRLF, 0
}
}
}else{
// chunked
StringBuilder sb = new StringBuilder();
while((len=stream.read(buf, 0,buf.length))>-1)
{
sb.append(new String(buf, 0, len));
}
System.out.println(sb.toString());
}
pw.close();
stream.close();
2. 해석 클래스 로 봉인
물론 위의 분석 방식 은 이식 성 이 좋 지 않 기 때문에 개선 이 필요 합 니 다. 우 리 는 대리 방식 을 통 해 InputStream 을 분석 합 니 다.
public class HttpInputStream extends InputStream {
private InputStream hostStream; // input
private boolean isCkunked = false;
private int readMetaSize = 0; //
private long contentLength = 0; //
private long chunkedNextLength = -1L; //
private long chunkedCurrentLength = 0L; //
private final Map<String, Object> httpHeaders = new HashMap<String, Object>();
public HttpInputStream(InputStream inputStream) throws IOException
{
this.hostStream = inputStream;
parseHeader();
}
public int getReadMetaSize() {
return readMetaSize;
}
public long getContentLength()
{
return contentLength;
}
/**
*
* @throws IOException
*/
private void parseHeader() throws IOException
{
if(this.hostStream!=null)
{
int len = -1;
byte[] buf = new byte[1];
byte[] outBuf = new byte[]{0x3A};
int count = 0;
while((len=read(buf, 0, buf.length))>-1)
{
int outLength = outBuf.length+len;
byte[] tempBuf = outBuf;
outBuf = new byte[outLength];
System.arraycopy(tempBuf, 0, outBuf,0, tempBuf.length);
System.arraycopy(buf, 0, outBuf,outBuf.length-1, len);
if(buf[0]==0x0D || buf[0]==0x0A)
{
count++;
if(count==4)
{
break;
}
}else{
count = 0;
}
}
String headerString = new String(outBuf, 0, outBuf.length);
String[] splitLine = headerString.trim().split("\r
");
for (String statusLine : splitLine) {
String[] nameValue = statusLine.split(":");
if(nameValue[0].equals(""))
{
httpHeaders.put("", nameValue[1]);
}else{
httpHeaders.put(nameValue[0], nameValue[1]);
}
if("Transfer-Encoding".equalsIgnoreCase(nameValue[0]))
{
String value = nameValue[1].trim();
isCkunked = value.equalsIgnoreCase("chunked");
}
}
}
}
public Map<String, Object> getHttpHeaders() {
return httpHeaders;
}
/**
*
*/
@Override
public int read() throws IOException {
if(!isCkunked)
{
/**
* chunked
*/
return hostStream.read();
}
/**
* chunked
*/
return readChunked();
}
private int readChunked() throws IOException {
byte[] chunkedFlagBuf = new byte[0]; // chunked length
int crlf_nums = 0;
int byteCode = -1;
if(chunkedNextLength==-1L) // -1 chunkedNextLength , chunked length
{
byteCode = hostStream.read();
readMetaSize++;
while(byteCode!=-1)
{
int outLength = chunkedFlagBuf.length+1;
byte[] tempBuf = chunkedFlagBuf;
chunkedFlagBuf = new byte[outLength];
System.arraycopy(tempBuf, 0, chunkedFlagBuf,0, tempBuf.length);
System.arraycopy(new byte[]{(byte) byteCode}, 0, chunkedFlagBuf,chunkedFlagBuf.length-1, 1);
if(byteCode==0x0D || byteCode==0x0A) //
{
crlf_nums++;
if(crlf_nums==2) // 2,
{
String lineNo = "0x"+ (new String(chunkedFlagBuf,0,chunkedFlagBuf.length)).trim();
chunkedNextLength = Long.decode(lineNo);
contentLength+=chunkedNextLength;
if(chunkedNextLength>0)
{
byteCode = hostStream.read();
readMetaSize++;
}
break;
}
}else{
crlf_nums=0;
}
byteCode = hostStream.read();
readMetaSize++;
}
}
else if(chunkedNextLength>0) //
{
if(chunkedCurrentLength<chunkedNextLength)
{
byteCode = hostStream.read();
readMetaSize++;
chunkedCurrentLength++; // ,
}
if(chunkedCurrentLength==chunkedNextLength){ // , chunkedCurrentLength
chunkedNextLength = -1L;
chunkedCurrentLength = 0L;
}
}else{
// , header
getHttpHeaders().put("Content-Length", ""+contentLength);
return -1;//chunked -1, chunkedLength=0 -1
}
return byteCode;
}
@Override
public long skip(long n) throws IOException {
return hostStream.skip(n);
}
@Override
public boolean markSupported() {
return hostStream.markSupported();
}
@Override
public int available() throws IOException {
return hostStream.available();
}
@Override
public synchronized void mark(int readlimit) {
hostStream.mark(readlimit);
}
@Override
public void close() throws IOException {
hostStream.close();
}
@Override
public synchronized void reset() throws IOException {
hostStream.reset();
}
아주 간단 하지 않 습 니까? 이 단계 까지 우 리 는 사실 HttpResponse 의 가장 70% 기능 을 완 성 했 습 니 다. 나머지 30% 는 캐 시, 방향 변경, httpCookie 와 같은 구성 요소 입 니 다.
사용 방식
URL url=new URL("http://localhost:8080/test/ios.php");
EchoURLConnection connection=(EchoURLConnection)url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
PrintWriter pw = new PrintWriter(new OutputStreamWriter(connection.getOutputStream()));
pw.write("name=zhangsan&password=123456");
pw.flush();
InputStream stream = connection.getInputStream();
HttpInputStream his = new HttpInputStream(stream);
System.out.println(his.getHttpHeaders());
int len = 0;
byte[] buf = new byte[127];
StringBuilder sb = new StringBuilder();
while((len=his.read(buf, 0, buf.length))!=-1)
{
sb.append(new String(buf, 0, len));
}
System.out.println(sb.toString());