通过识别Content-Length和Transfer-Encoding实现C++ socket正确接收HTTP数据

众所周知,HTTP在运输层是TCP协议,所以在socket编程中,一般是初始化socket,解析ip,connect,send,recv的步骤。

send请求头倒是容易,但在recv时就会发生问题。

recv需要传入一个接收大小,但在HTTP协议中,头部并没有包大小,所以这个大小一般作为缓冲区大小使用,例如传入1024 bytes这种。

HTTP丢包的问题

首先我以为通过判断recv返回值,可以得知包是否接收完全,但实践发现,这种方式会产生丢包。例如,包大小实际是2000 B,在第一次recv时,接收到了1024 B,程序继续接收,又接收到100 B,由于100<1024,程序认为已经接收完,就跳出循环了。但实际上还有876 B的数据没来得及进入socket缓冲区,程序就直接返回了。

解决这个问题,非常不优雅的做法就是在recv后加sleep,在网络畅通的情况下,稍微sleep几十毫秒,给数据拷进缓冲区留一点时间,就可以接收完全。但这个方法既不优雅也不可靠。网络拥堵时一样会失效。

实际上,recv的返回值只能说明本次从缓冲区取了多少字节,并不担保包已经结束,也不保证下一个分片什么时候到来。

解决方法

可以注意到HTTP的响应有两种格式,都是带有长度数据的。一种是Content-Length字段,后面直接带的就是正文长度。另一种是Transfer-Encoding: chunked,在这种格式下,正文部分为:

1a2<CRLF>
正文<CRLF>
51b<CRLF>
正文<CRLF>
0<CRLF>
<CRLF>

其中<CRLF>代表rn。格式就是一行16进制长度,跟着数据,需要注意的是数据后面这个<CRLF>是不算在长度里的。

由此就可以得出程序逻辑了:先recv数据,在数据里找第一次出现的rnrn(也就是响应头和正文的分界点),如果没找到就继续recv,找到就进行切分,把响应头和正文都切出来。再解析响应头,区分是Content-Length类型还是Transfer-Encoding类型。

如果是Content-Length类型,就计算上一步切分出的正文长度是否接收完全,没接收完就继续接收剩余数据。因为知道剩余多少字节,所以就不存在recv阻塞的问题了。

如果是Transfer-Encoding类型,就不断切分长度行和正文部分,根据长度行识别分块大小,直到长度行为0,确保最后的rn接收完毕,结束读取。

代码

上代码,这是调用部分:

main.cpp :

#include <string>
#include <iostream>

#include "MyInitSock.h"
#include "MyHTTP.h"

MyInitSock myInitSock;

using namespace std;

int main(int argc, char* argv[])
{
    try
    {
        MyHTTP http;
        string website = "www.163.com";
        string ip = DnsParse(website);

        //连接
        http.Connect(ip, 80);

        //设置请求头
        http.request_header.host = website;
        http.request_header.url = "/";
        http.request_header.user_agent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36";

        //发送
        http.SendGet();

        //接收
        http.RecvHTTP(10240);

        //http.content = MyBase64::Unicode2GBK(MyBase64::UTF8toUnicode(http.content));

        //显示结果
        cout << http.response.raw_response_header;
        cout << http.response.content;
    }
    catch (runtime_error e)
    {
        cout << e.what();
    }
    catch (invalid_argument e)
    {
        cout << e.what();
    }

    system("pause");

    return 0;
}

MyInitSock代码就不上了,内容就是初始化socket。

MyHTTP.h :

#pragma once
#include "MySocket.h"

#include <unordered_map>

class MyHTTP :
    public MySocket
{
public:
    MyHTTP() :MySocket() {}

    //请求头
    struct RequestHeader
    {
        std::string host;
        std::string url;
        std::string user_agent;
    };
    RequestHeader request_header;

    //响应数据
    struct Response
    {
        std::string version, state_code, phrase;//版本 状态码 短语
        std::unordered_map<std::string, std::string> header;//响应头
        std::string raw_response_header;//响应头原始数据
        std::string content;//正文
    };
    Response response;

    //发送请求头
    void SendGet();

    //接收响应头
    void RecvHTTP(int bufsize = 1024, int flags = 0) throw(std::runtime_error,std::invalid_argument);
};


MySocket的代码就不贴了,内容就是对socket中几个函数的简单封装,gethostname,connect这几个。

MyHTTP.cpp :

#include "MyHTTP.h"

#ifdef _DEBUG
#include <iostream>
#endif

using namespace std;

void MyHTTP::RecvHTTP(int bufsize, int flags) throw(std::runtime_error)
{
    if (bufsize <= 0)
        throw runtime_error("bufsize<=0");

    char* buf = new(nothrow) char[bufsize + 1];
    if (buf == nullptr)
        throw runtime_error("memory is not enough.");

    unique_ptr<char> up_buf(buf);

    string& raw_head = response.raw_response_header;
    string& content = response.content;
    auto dic = response.header;

    raw_head.clear();
    content.clear();
    while (1)
    {
        int rs = recv(m_socket, buf, bufsize, flags);

        if (rs <= 0)
            throw runtime_error("Recieve error. Error code:" + to_string(WSAGetLastError()));

        buf[rs] = 0;
        raw_head += buf;

        //以两组CRLF切分请求头和内容
        auto pos = raw_head.find("rnrn");
        if (pos != string::npos)
        {
            content += raw_head.substr(pos + 4);//从CRLF后截取
            raw_head.erase(raw_head.begin() + pos + 2, raw_head.end());//带上1组CRLF截取
            break;
        }
    }

    //识别第一行
    //格式:版本 状态码 短语
    size_t start = 0;
    auto pos = raw_head.find("rn", start);
    if (pos != string::npos)
    {
        stringstream ss(raw_head.substr(start, pos - start));
        ss >> response.version >> response.state_code >> response.phrase;
        start = pos + 2;
    }
    else
        throw runtime_error("Can not parse the first line of request head:" + raw_head.substr(start, pos - start));

    //解析请求头
    dic.clear();
    while (1)
    {
        auto pos = raw_head.find("rn", start);

        //以CRLF切分
        if (pos != string::npos)
        {
            string line = raw_head.substr(start, pos - start);//得到1行

            //切分出key和value
            auto pos_space = line.find(": ");
            if (pos_space != string::npos)
            {
                string key = line.substr(0, pos_space);
                string value = line.substr(pos_space + 2);
                dic[key] = value;
            }
            else
            {
                throw runtime_error("Can not parse the line:" + line);
            }

            start = pos + 2;//设置起始点
        }
        else
        {
            break;
        }
    }

    //接收正文
    const char sz_content_length[] = "Content-Length";
    auto it_content_length = dic.find(sz_content_length);
    if (it_content_length != dic.end())
    {
        //length模式
        int content_length = stoi(dic[sz_content_length]);

        int remain = content_length - content.length();
        if (remain < 0)//实际大小>标记大小
            throw runtime_error("Field Content-Length is less than real content length.");

        //接收剩余部分
        while (remain)
        {
            int rs = recv(m_socket, buf, bufsize, flags);
            if (rs <= 0)//若接收数据小于标记值,此处rs=-1
                throw runtime_error("Recieve error. Error code:" + to_string(WSAGetLastError()));

            buf[rs] = 0;
            content += buf;
            remain -= rs;
        }
    }
    else
    {
        //chunked模式
        const char sz_transfer_encoding[] = "Transfer-Encoding";
        auto it_transfer_encoding = dic.find(sz_transfer_encoding);
        if (it_transfer_encoding != dic.end() && dic[sz_transfer_encoding] == "chunked")
        {
            string temp = content;
            content.clear();

            int state = 0;
            while (1)
            {
                auto pos = temp.find("rn");
                if (pos != string::npos)
                {
                    //此处保证 temp 以chunked大小开始

                    string s_len = temp.substr(0, pos);//得到大小
                    cout << s_len << endl;
                    int len = stoi(s_len,nullptr,16);
                    temp = temp.substr(pos + 2);//截掉大小,content现在是纯内容

                    int remain = len - temp.length();
                    if (remain <= -2)//缓冲数据超出大小截取点,直接进行截取
                    {
                        //第一处正式读取
                        content+=temp.substr(0, len);
                        temp=temp.substr(len+2);//越过结尾的rn
                    }
                    else
                    {
                        //接收不足部分
                        while (1)
                        {
                            int rs = recv(m_socket, buf, bufsize, flags);
                            if (rs <= 0)
                                throw runtime_error("Recieve error. Error code:" + to_string(WSAGetLastError()));

                            buf[rs] = 0;
                            temp += buf;
                            remain -= rs;

                            if (remain <= -2)//[len长度的chunk]后会额外跟一组rn,要越过rn需要至少多接收2B
                            {
                                //第二处正式读取
                                content += temp.substr(0, len);
                                temp = temp.substr(len + 2);//越过结尾的rn
                                break;
                            }
                        }
                    }

                    //接收完本分块
                    if (len == 0)//最后一个chunk以0rnrn结尾
                    {
                        //以下两行仅用于测试结尾分块是否逻辑正确
                        //正确的话此处socket缓冲区应无数据,recv应始终阻塞
                        //int rs = recv(m_socket, buf, bufsize, flags);
                        //if (rs <= 0)
                        //  throw runtime_error("Recieve error. Error code:" + to_string(WSAGetLastError()));
                        break;
                    }
                }
                else
                {
                    int rs = recv(m_socket, buf, bufsize, flags);
                    if (rs <= 0)
                        throw runtime_error("Recieve error. Error code:" + to_string(WSAGetLastError()));

                    buf[rs] = 0;
                    temp += buf;
                }
            }
        }

    }
}

void MyHTTP::SendGet()
{
    if (request_header.url.empty())
        request_header.url = "/";
    string header = "GET " + request_header.url + " HTTP/1.1rn"
        "Host: " + request_header.host + "rn"
        "user-agent: " + request_header.user_agent + "rn"
        "rn";
    Send(header);
}

稍微麻烦点的逻辑是,响应头和内容部分处处都有断片的情况,就是在recv的过程中要不停进行切分和合并,耗费的逻辑比较多。如果每次都只recv 1B,就不存在这个问题了,但我测试过,每次recv 1B,效率低得惊人。

我已经尽量避免逻辑错误了,RecvHTTP中的bufsize可大可小,设置成10240可以,设置成1B也可以正常工作,就是效率很低。

效果

在这里插入图片描述
在这里插入图片描述
可以看到第1部分我把各个分块的长度输出出来了。第2部分输出的响应头。第3部分输出的正文。测试表明能够让recv不阻塞,尽量快地得到正文数据。

不知道成熟的库里是怎么实现的,是否也是我这种方法。感谢各位批评指正。

原文链接: https://www.cnblogs.com/tomwillow/p/15483697.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    通过识别Content-Length和Transfer-Encoding实现C++ socket正确接收HTTP数据

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/360282

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 下午1:11
下一篇 2023年3月2日 下午1:11

相关推荐