HTTP1.1认识chunked编码以及使用socket对chunked解码(Java)

1 篇文章 0 订阅
1 篇文章 0 订阅

HTTP1.1认识chunked编码以及使用socket对chunked解码(Java)

最近在恶补android的网络方面,练习分别使用socket和HttpURLConnection下载、上传文件。在HttpURLConnection上基本都没什么问题,然而HttpURLConnection封装得太好了,只是学会了使用这个还算不上学会网络。要想更深入地学习网络,就不可避免地要接触到socket了。然而在用socket时,第一次遇到了chunked编码,让我非常头疼。

在HTTP1.1的头部信息中有Content-Length这一项,它表明了即将传输的数据正文的大小,以字节为单位。这一般没什么问题,但是很多时候其实服务器无法预先知道你的数据的大小,这个时候就要遇到chunked编码。

chunked意味分块,表示服务器将会把数据分块传输,一般有chunked的信息的消息头部是这样的:

HTTP/1.1 200 OK
Date: Wed, 01 Mar 2017 02:31:20 GMT
Server: Apache/2.4.23 (Win64) PHP/5.6.25
X-Powered-By: PHP/5.6.25
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

可以看到,原本应该有的Content-Length不见了,变为了Transfer-Encoding。
我的服务器脚本是这样写的

<?php
/**
 * Created by PhpStorm.
 * User: zu
 * Date: 2017/2/22
 * Time: 14:50
 */

/**
下载文件的服务器脚本。 
 */


//判断post请求中是否有file_name这个变量,如果有就下载该文件
if(empty($_POST["file_name"]))
{
    echo "NO_FILE_NAME\n";
    print_r($_POST);
    exit();
}

/*由于文件是存储在windows系统上,文件名都是GB2312编码,所以还是要转换一下文件名,看存放资源的父文件夹在不在*/
$path = iconv("utf-8", "GB2312", "F:\\NetEaseMusic\\download\\");
if(!file_exists($path))
{
    echo "文件夹不存在\n";
    print_r($path);
    exit();
}

$file_map = array();
$files = scandir($path);
for($i = 0; $i < count($files); $i++ )
{
    $file_map[iconv("GB2312", "utf-8", $files[$i])] = $files[$i];
}

if(!array_key_exists($_POST["file_name"], $file_map))
{
    echo "FILE_KEY_NOT_FOUND\n";
    print_r($file_map);
    exit();
}

/*拼接出文件路径然后转码,用来寻找文件。*/
$path = iconv("utf-8", "GB2312", "F:\\NetEaseMusic\\download\\".$_POST["file_name"]);
//$path = "F:\\NetEaseMusic\\download\\".$file_map[$_POST["file_name"]];
if (!file_exists ( $path )) {
    echo "FILE_NOT_FOUND\n";
    echo "F:\\NetEaseMusic\\download\\".$_POST["file_name"]."\n";
    print($path);
    exit ();
}

/*下面的代码用于断点续传,如果客户端发送了有效的range信息,就从range开始发送文件,而不是从头开始*/
$file_size = filesize($path);

$begin = 0;
$end = 0;

if(isset($_SERVER["HTTP_RANGE"]))
{
    $temp = $_SERVER["HTTP_RANGE"];
    if(preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i',$temp, $matcher))
    {
        $begin = intval($matcher[0]);
        $end = intval($matcher[1]);
    }
}
if($begin == 0)
{
    header("HTTP/1.1 200 OK");
}else{
    header("HTTP/1.1 206 Partial Content");
}


//header("Content-type: application/octet-stream");
//header("content-length: ".$file_size);

//header("Accept-Ranges: bytes");
//header("Accept-Length:".$file_size);
//header("Content-Disposition: attachment; filename=".$path);

/*不停读取文件并将数据流发送出去*/
$file = fopen($path, "r");
fseek($file, $begin, 0);
while(!feof($file))
{
    echo fread($file, 1024);
}
exit();
?>

可以看出服务器在向客户端返回文件的时候,其实只是脚本一直在读文件并且把数据流传送给服务器。而服务器必然会有个缓存区域,用来存储脚本输出的内容。如果发送的数据小于缓存区域,那么服务器会计算数据大小并且自动在响应头中添加Content-Length;如果缓存区满而脚本仍然不断输出,并且此时脚本也没有通知服务器大小是多少,那么服务器就会使用chunked编码方式。也就是说,如果发送一个几kb的文本,那么服务器就会自动得出Content-Length并添加到响应头中。但是如果是几个G的大文件,在没有手动通知服务器Content-Length的情况下,就会使用chunked编码。
注意上面的脚本中,如果将header("content-length: ".$file_size);这一行取消注释,那么服务器就不会使用chunked编码,而且响应头中会包含刚才设置的content-length。

而在使用socket的情况下对chunked进行解码,原理说起来其实很简单,只是实际操作时需要考虑的情况比较多。下面先看chunked编码规则:

HTTP/1.1 200 OK\r\n
Date: Wed, 01 Mar 2017 02:31:20 GMT\r\n
Server: Apache/2.4.23 (Win64) PHP/5.6.25\r\n
X-Powered-By: PHP/5.6.25\r\n
Transfer-Encoding: chunked\r\n
Content-Type: text/html; charset=UTF-8\r\n
\r\n
chunked-length\r\n
chunked-body\r\n
...
chunked-length\r\n
chunked-body\r\n
0\r\n
\r\n

这是使用socket获得的完整的使用chunked编码的数据,保留了所有信息。可以看出来,报头仍然是以\r\n结尾的,接下来是数据。一个chunk块的结构则是

chunked-length\r\n
chunked-body\r\n

chunked-length是表示chunked-body的字节数量的十六进制数字,千万注意是十六进制。这个长度只包含chunked-body的长度,不包含前后跟的\r\n。最后在所有块都传输完毕后,会再传输一个空的块来通知客户端数据发送完毕。

要注意解码的时候不能以\r\n为判断依据,只能以chunked-length。因为也许正文中也含有\r\n
我的目标是写一个能够在边下载边解码的程序。因此重点在于读取数据时发生的各种情况,比如可能会把chunked-length给截断、或者把\r\n给截断。这都会导致无法读取到正确的数据,而且是一步错步步错。下面上代码。

这是通过socket下载文件的方法,也可以解码chunked编码。

    private void downloadFileBySocketWithChunke(String urlString, String fileName)
    {

        try{
            /**拼接post请求,注意发送的post数据要进行编码,否则服务器无法识别到。而头部则可不编码*/
            StringBuilder sb = new StringBuilder();
            String data = URLEncoder.encode("file_name", "utf-8") + "=" +  URLEncoder.encode(fileName, "utf-8");
//            String data = URLEncoder.encode("file_name="+fileName, "utf-8");
            sb.append("POST " + urlString + " HTTP/1.1\r\n");
            sb.append("Host: 10.206.68.242\r\n");
            sb.append("Content-Type: application/x-www-form-urlencoded\r\n");
            sb.append("Content-Length: " + data.length() + "\r\n");
            sb.append("\r\n");
            sb.append(data + "\r\n");

            String temp = sb.toString();

//            sb.append( URLEncoder.encode("file_name", "utf-8") + "=" +  URLEncoder.encode(fileName, "utf-8") + "\r\n");

            System.out.println(temp);

            URL url = new URL(urlString);
            Socket socket = new Socket(url.getHost(), url.getPort());
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "utf-8"));
            /**将post请求通过socket发送到服务器*/
            writer.write(sb.toString());
            writer.flush();


            File file = new File("./" + fileName);
            DataOutputStream out = null;
            DataInputStream in = null;
            try{
                out = new DataOutputStream(new FileOutputStream(file));
                in = new DataInputStream(socket.getInputStream());
                /**缓存从socket读取的数据的buffer*/
                byte[] buffer = new byte[1024];
                /**本次从socket读取了多少字节*/
                int readBytes = 0;
                /**从buffer中向外取数据时当前读取的位置*/
                int readPosition = 0;

                /**是否已经获得了头部信息*/
                boolean getHead = false;
                /**当前是否为chunk编码*/
                boolean chunked = false;
                /**缓存头部信息的*/
                StringBuilder headTemp = new StringBuilder();
                /**完整的头部信息*/
                String head = null;
                /**该chunk块的大小*/
                int chunkedSize = 0;
                /**该chunk块已经读取到的大小*/
                int readSize = 0;

                /**
                 * 用于存储上一轮读取中遗留的信息。假如在上一轮读取完了一个chunk块,要提取下一个chunk块的大小信息时,发现
                 * 这些信息时被截断了,那就要把这些信息存在这里,待下一轮从socket读取后,将这些信息拼接上去再分析。
                 * */
                byte[] chunkedSizeBuffer = new byte[32];
                /**存储在chunkedSizeBuffer里的有效信息的长度*/
                int chunkedSizeBit = 0;

                /**不断从socket中读取*/
                while((readBytes = in.read(buffer)) != -1)
                {
                    readPosition = 0;
                    /**没有获得头部就先获得头部,头部与正文以\r\n\r\n区分,\r的十六进制数字是0x0d,而\n是0x0a*/
                    if(!getHead)
                    {
                        /**找出一个byte[]在另一个byte[]中的序号,返回-1是没找到*/
                        int position = findByte(buffer,0, readBytes, new byte[]{0x0d,0x0a,0x0d,0x0a});
                        if(position == -1)
                        {
                            /**没找到证明这次读取的全都是头部信息,放入头部缓存*/
                            headTemp.append(new String(buffer));
                            continue;
                        }else
                        {
                            /**找到则分析头部信息*/
                            byte[] headBytes = new byte[position];
                            System.arraycopy(buffer, 0, headBytes, 0, position);
                            headTemp.append(new String(headBytes));
                            head = headTemp.toString();
                            getHead = true;
                            String[] infoList = head.split("\r\n");

                            if(!infoList[0].split(" ")[1].equals("200"))
                            {
                                throw new RuntimeException("连接失败,状态码为" + infoList[0].split(" ")[1]);
                            }

                            /**查询是否为chunked编码*/
                            for(String s : infoList)
                            {
                                if (s.toLowerCase().contains("chunked"))
                                {
                                    chunked = true;
                                    break;
                                }
                            }
                            /**将读取位置向后移4个字节,到达正文。*/
                            readPosition = position + 4;

                        }
                    }

                    if(!chunked)
                    {
                        /**如果不是chunk,直接写入*/
                        out.write(buffer, readPosition, readBytes - readPosition);
                    }else
                    {
                        while(readPosition < readBytes)
                        {
                            /**判断该chunk块是否已读取完毕,如果是,就要获取下一个chunk块的长度信息*/
                            if(chunkedSize == readSize)
                            {
//                                System.out.println("chunkedSize == readSize");
                                /**如果buffer中未读的字节数小于10,就判断这次是把chunk长度信息给截断了,就放在
                                 * chunkedSizeBuffer里等待下一轮读取后拼接再分析
                                 * */
                                if(readBytes - readPosition < 10)
                                {
//                                    System.out.println("readBytes - readPosition < 10");
                                    for(chunkedSizeBit = 0; chunkedSizeBit < readBytes - readPosition; chunkedSizeBit++)
                                    {
                                        chunkedSizeBuffer[chunkedSizeBit] = buffer[readPosition + chunkedSizeBit];
                                    }
                                    readPosition = readBytes;
//                                    System.out.println("readPosition = " + readPosition + ",readBytes = " + readBytes);
                                    continue;
                                }

                                /**如果chunkedSizeBit不为0,说明在上一轮分析中有被截断在遗留信息在chunkedSizeBuffer里,
                                 * 需要和这次buffer里的数据先拼接*/
                                if(chunkedSizeBit != 0)
                                {
//                                    System.out.println("chunkedSizeBit != 0, chunkedSizeBit = " + chunkedSizeBit);
                                    byte[] a = new byte[chunkedSizeBit + readBytes];
                                    System.arraycopy(chunkedSizeBuffer, 0, a, 0, chunkedSizeBit);
                                    System.arraycopy(buffer, 0, a, chunkedSizeBit, readBytes);
                                    buffer = a;
                                    readBytes = chunkedSizeBit + readBytes;
                                    chunkedSizeBit = 0;
                                }
                                /**判断下buffer里的下一个读取数据是否是长度前面的\r\n,如果是要剔除掉*/
                                if(buffer[readPosition] == 0x0d && buffer[readPosition + 1] == 0x0a)
                                {
//                                    System.out.println("buffer[readPosition] == 0x0d && buffer[readPosition + 1] == 0x0a");
                                    readPosition += 2;
                                }
                                /**获取长度,如果超出32个还未读取到长度后面的\r\n,就说明读取出错。*/
                                int count = 0;
                                byte r1 = 0;
                                byte n1 = 0;
                                byte[] size = new byte[32];
                                while((r1 = buffer[readPosition++]) != 0x0d)
                                {
                                    size[count] = r1;
                                    count++;
                                    if(count >= 32)
                                    {
                                        System.out.println("read /r error");
                                        System.out.println(new String(buffer, readPosition - count, readBytes - (readPosition - count)));
                                        return;
                                    }

                                }
                                if((n1 = buffer[readPosition++]) != 0x0a)
                                {
                                    System.out.println("read /n error");
                                    System.out.println(new String(buffer, readPosition - count, readBytes - (readPosition - count)));
                                    return;
                                }
//                                System.out.println("chunked size:" + new String(size, 0, count));
                                /**千万注意是十六进制*/
                                chunkedSize = Integer.parseInt(new String(size, 0, count), 16);
                                readSize = 0;

                            }
                            /**以下是将buffer里的内容按照长度写到文件里,分两种情况。需要注意的是readPosition和readSize要
                             * 及时变化。*/
                            if(readBytes - readPosition >= chunkedSize - readSize)
                            {
//                                System.out.println("readBytes - readPosition >= chunkedSize - readSize");
                                out.write(buffer, readPosition, chunkedSize - readSize);
                                readPosition += chunkedSize - readSize;
                                readSize = chunkedSize;
                            }else
                            {
//                                System.out.println("readBytes - readPosition < chunkedSize - readSize");
                                out.write(buffer, readPosition, readBytes - readPosition);
                                readSize += readBytes - readPosition;
                                readPosition = readBytes;
                            }
                        }

                    }


                }
                out.flush();
            }catch (Exception e1)
            {
                e1.printStackTrace();
            }finally {
                try{
                    if(in != null)
                    {
                        in.close();
                    }
                    if(out != null)
                    {
                        out.flush();
                        out.close();
                    }
                }catch (Exception e2)
                {
                    e2.printStackTrace();
                }
            }
            socket.close();

        }catch (Exception e)
        {
            e.printStackTrace();
        }

    }

这是获取一个byte[]在另一个byte[]中的索引的方法,很简单就不写注释了。

    private int findByte(byte[] src, byte[] mark)
    {
        return findByte(src, 0, src.length, mark);
    }

    private int findByte(byte[] src, int start, int length, byte[] mark)
    {
        if(length < mark.length || length > src.length)
        {
            return -1;
        }
        for(int i = start; i < length - mark.length; i++)
        {
            if(src[i] == mark[0])
            {
                for(int j = 0; j < src.length; j++)
                {
                    if(src[j + i] == mark[j])
                    {
                        if(j == mark.length - 1)
                        {
                            return i;
                        }
                    }else
                    {
                        break;
                    }
                }
            }
        }
        return -1;
    }

以上就是一个使用socket进行下载并支持chunked解码的例子。当然这个还不是最好的,实际使用过程中下载一个十几MB的文件时和直接使用socket不进行解码的速度差不多,不过下载一个1.74G的电影时会多几秒,当然这是我在本机实验下载的,传输速度很快的。如果是实用的话,网络环境应该会成为瓶颈而不是解码耗时。然而最快的还是HttpURLConnection,快非常多。当然这可能是socket的固有缺陷,毕竟用socket不解码时下载也很慢。
还有另一种思路,就是将buffer的大小设置为本个chunk块的大小,然后只要从socket中读数据并向里填即可,只要buffer满了就说明该块读取完毕。不会出现将chunk块截断的问题。如果时间充裕我会在下一篇博客中实现这种方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值