首页 新闻 会员 周边

WebServer传输大文件致客户端自动关闭

1
悬赏园豆:5 [待解决问题]

程序运行在云服务器上, Ubuntu 20.04LTS系统,用浏览器测试能正常打开页面,请求一般的html文本和几十kb的小图片无问题,接着放了一个1.63MB( 1714387字节)的网上找的图过去,客户端图没加载完就自动断连了,应用上看没问题,但我的设计是响应头默认Connection: keep-alive, 客户端自动断连了明显跟设计不符,遂开始debug。

<img src="https://rhino-picbase.oss-cn-hangzhou.aliyuncs.com/img/image-20230113191218513.png" alt="image-20230113191218513" style="zoom:50%;" />

首先排除SIGPIPE,因为自己strace并没有看到有sigpipe,再者客端断后服务器依然正常。strace一看是客户端自己断连触发了服务端EventLoop上的EPOLLRDHUP事件,到这就开始盲区搞不懂了。

自己之后瞎搞了半天,改函数打日志什么的就不说了。

自己还了写客户端模拟发送HTTP报文测试,显示normal close,读了80000多字节,喂喂你都没有读完耶,怎么就读到0了?

int main(int argc, char *argv[]){
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    if(argc < 3){
        printf("usage:%s ip port\n", argv[0]);
        return;
    }
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(sockfd != -1);
    struct sockaddr_in serv_addr;
    socklen_t serv_addr_len = sizeof(serv_addr); 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(ip);
    serv_addr.sin_port = htons(port);
    assert(connect(sockfd, (sockaddr*)&serv_addr, serv_addr_len) != -1);
    
    char req[1024] = {0}; 
    const char *path = "image.png";
    snprintf(req, 45 + strlen(path),"GET /%s HTTP/1.1\r\nConnection: keep-alive\r\n\r\n",path);
    char buf[1024] = {0};
    size_t wlen = -1;
    size_t nread = 0;
    do{
        size_t wlen = write(sockfd,req, strlen(req));
        printf("write: %s\n", req);
        size_t rlen = read(sockfd, buf, sizeof(buf));
        buf[rlen] = 0;
        nread += rlen;
        printf("read: %s\n", buf);
        if(rlen > 0){
        }
        else if(rlen == 0){
            perror("normal close");
            break;
        }
        else{
            perror("read error:");
            break;
        }
    }while(wlen > 0);
	printf("total read: %d\n", nread);
    close(sockfd);
    return 0;
}

把服务端和客户端分别抽到两个虚拟机排除SSL的因素, 然后两个端设tcpdump抓包输出文件,结果wireshark一看还是客户端自己给服务端发了FIN报文,始终搞不懂为什么。希望有大佬给出建议打破我的unknown unknown。

整个写响应流程涉及的代码如下:

void WebServer::EventLoop(){
    int timeoutMS = -1;
    while(1){
        int nevent = epoll_wait(epollfd_, events_, MAX_EVENTS, timeoutMS);
        for(int i = 0; i < nevent; ++i){
            int sockfd = events_[i].data.fd;
            uint32_t events = events_[i].events;
            if(sockfd == listenfd_){
                //add new connection
                dealNewConn();
            }
            else if(events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){
                assert(connMap_[sockfd]);
                dealCloseConn(connMap_[sockfd]);
            }
            else if(events & EPOLLIN){
                //read request/close connection
                assert(connMap_[sockfd]);
                dealRead(connMap_[sockfd]);
            }
            else if(events & EPOLLOUT){
                //send response
                assert(connMap_[sockfd]);
                dealWrite_debug(connMap_[sockfd]);
            }
        }

    }
}

void WebServer::dealWrite_debug(const HttpConnectionPtr& client){
    //正常这里应该是扔线程池,这里debug排除了多线程干扰
    onWrite_debug(client);
}

void WebServer::onWrite_debug(const HttpConnectionPtr& client){
    //确保用于写的连接还在,防止SF,连接用智能指针管理时尤其注意!
    assert(client);
    int writeErrno = 0;
    ssize_t nwrite = client->writeOut_debug(&writeErrno);
    const string& clientAddr = client->getClientAddr().getIpPort();
    printf("write %ld byte to %s\n",nwrite,clientAddr.c_str());
    if(client->toWriteBytes() == 0){
        //传输完成
        if(client->isKeepAlive()){
            onProcess(client);
            return;
        }
    }
    else{
        //传输未完成, 场景包括:正在写的时候对端断开连接
        if(writeErrno == EAGAIN || writeErrno == EWOULDBLOCK){
            epoll_modfd(epollfd_, client->getSocket(), EPOLLOUT | connEvent_);
            return;
        }
    }
    dealCloseConn(client);
}

void WebServer::dealCloseConn(const HttpConnectionPtr& client){
    int connfd = client->getSocket();
    perror("status");
    epoll_delfd(epollfd_, connfd);
    connMap_.erase(connfd);
}

ssize_t HttpConnection::writeOut_debug(int *saveErrno){
    printf("Buffer write: \n");
    cout << string(outbuffer_.peek(), outbuffer_.readableBytes()) << endl;
    ssize_t len = -1;
    ssize_t nwrite = 0;
    do{
        len = writev(sockfd_, iov_, iovCnt_);
        if(iov_[0].iov_len + iov_[1].iov_len == 0){ break; }
        if(len <= 0){ saveErrno = &errno; }
        else if(static_cast<size_t>(len) <= iov_[0].iov_len){
            iov_[0].iov_base = (uint8_t*) iov_[0].iov_base + len;
            iov_[0].iov_len -= len;
            outbuffer_.retrieve(len);
        }
        else{
            iov_[1].iov_base = (uint8_t*) iov_[1].iov_base + (len - iov_[0].iov_len);
            iov_[1].iov_len -= (len - iov_[0].iov_len);
            if(iov_[0].iov_len) {
                outbuffer_.retrieveAll();
                iov_[0].iov_len = 0;
            }
        }
        nwrite += len;
        cout << "write: " << len << " " << "write bytes left: " << toWriteBytes() << endl;
    }while(HttpConnection::isET && toWriteBytes() > 1024);
    return nwrite;
}


//write_debug写响应操作前置的流程函数
bool HttpConnection::process(){
    if(inbuffer_.readableBytes() <= 0)
        return false;
    HttpRequest req;
    if(!httpParser_.parseRequest(inbuffer_,req))
        return false;
    assert(inbuffer_.readableBytes() == 0);
    inbuffer_.retrieveAll();

    string headkey = req.getHeader("Connection");
    if(headkey == string("keep-alive")){
        keepAlive_ = true;
    }
    else if(headkey == string("close")){
        close_ = true;
    }

    HttpResponse resp(close_);
    makeResponse(req,resp);
    return true;
}

void HttpConnection::makeResponse(HttpRequest& req,HttpResponse& resp){
    string path(srcDir_);
    string file = (req.getPath() == "/") ? "/index.html" : req.getPath();
    iovCnt_ = 1;

    cout << "path: " << path + file << endl;
    struct stat statbuf;
    if(stat(string(path + file).data(),&statbuf) >= 0 && !S_ISDIR(statbuf.st_mode)){
        iovCnt_++;    
        int filefd = open(string(path + file).data(), O_RDONLY);
        fileLen_ = statbuf.st_size;
        mmFile_ = mmap(0, fileLen_, PROT_READ, MAP_PRIVATE, filefd, 0);
        assert(mmFile_ != (void*)-1);
        close(filefd);
        resp.setFile(true);
    }

    if(resp.isHaveFile()){
        resp.setLine(HttpResponse::k200Ok);
        resp.setContentType(MimeType::getFileType(file));
        resp.setContentLength(fileLen_);
    }   
    else{
        resp.setLine(HttpResponse::k404NotFound);
        resp.setCloseConnection(true);
    }
    resp.appendAllToBuffer(outbuffer_);
    iov_[0].iov_base = const_cast<char*>(outbuffer_.peek());
    iov_[0].iov_len = outbuffer_.readableBytes();
    iov_[1].iov_base = mmFile_;
    iov_[1].iov_len = fileLen_;
}

HttpConnection类头部涉及部分:

class HttpConnection : noncopyable{
    public:
        HttpConnection(int sockfd, const INetAddress& peerAddr, const INetAddress& hostAddr);
        ~HttpConnection() { onClose();}
        ssize_t readIn(int* saveErrno);
        bool process();
       	。。。。
    public:
        static bool isET;
        static const char* srcDir_;
    private:
        void makeResponse(HttpRequest&,HttpResponse&);
   		。。。
        void *mmFile_;
        size_t fileLen_;
        
        struct iovec iov_[2];
        size_t iovCnt_;

        Buffer inbuffer_;
        Buffer outbuffer_;
        HttpParser httpParser_;
};

输出效果如下:

# Buffer read 的输出函数没包括在给出的代码里面 算补充吧
Buffer read: 
GET /image.png HTTP/1.1
Host: sss.sss.sss.sss:9006
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

# 正式打印部分
path: /home/LinuxC++/Project/re_webserver/root/image.png
Buffer write: 
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 1714387
Content-Type: image/png


write: 143080 to write bytes left: 1571400
write 143080 byte to ccc.ccc.ccc.ccc:35521
status: Invalid argument
connection ccc.ccc.ccc.ccc:35521 -> sss.sss.sss.sss:9006 closed
connection ccc.ccc.ccc.ccc:35550 -> sss.sss.sss.sss:9006

Note: 发现不同浏览器请求的行为不一样,Chrome除了请求页面还请求favicon.ico,而且似乎会自动检测是不是静态资源,是就自动断连,估计是我Chrome上插件的影响,Firefox就不会,后面的输出结果以Firefox为准。注意是Linux上的Firefox,试过Windows上的Firefox,不知为什么原因请求完资源后客端也会自动断连。

另一种开始的主页 另一种开始 | 初学一级 | 园豆:119
提问于:2023-01-13 19:27
< >
分享
所有回答(1)
0

可以参考一下 Kestrel 的实现源码 aspnetcore/src/Servers/Kestrel/Core/src/Internal/Http/

dudu | 园豆:30994 (高人七级) | 2023-01-14 14:52
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册