飞鸽传书源码分析六-文件传输续

在飞鸽传书源码分析五-文件传输这篇文章中分析了,如何将要发送的文件先传至要传送到的机器上。此时接收端的机器已经“知道”要接收文件了。本篇文章详细介绍飞鸽传书文件的传输过程。为了简化,我们选择只传输一个文件进行分析。

1.接收文件的结构

1.1 RecvFileObj结构

struct RecvFileObj {
    ConnectInfo *conInfo;  //网络连接信息
    FileInfo    *fileInfo; //文件信息

    BOOL        isDir;    //要接收的是不是文件夹
    FileInfo    curFileInfo; //

    _int64      offset;  //当前已经传输文件的offset
    _int64      woffset;
    char        *recvBuf; //接收到的信息
    HANDLE      hFile;    //为接收文件创建的handle
    HANDLE      hThread;  //为接收文件创建的线程handle

    int         infoLen;
    int         dirCnt;

    _int64      totalTrans; //已经传文件字节数
    DWORD       startTick;  //开始时间戳
    DWORD       lastTick;   //最后时间戳
    int         totalFiles; //总文件数
    FileStatus  status;     //文件状态
    char        saveDir[MAX_PATH]; //接收文件存储的位置,
    char        path[MAX_PATH];
    char        info[MAX_BUF];  // for dirinfo buffer
};

1.2 ConnectInfo结构

struct ConnectInfo : public TListObj {
    SOCKET  sd;     //socket
    ULONG   addr;   //要发送文件的ip地址
    int     port;   //要发送文件的端口
    BOOL    server; //是否为服务器
    BOOL    complete; //连接是否完成
    DWORD   startTick;
    DWORD   lastTick;
};

2.接收端向发送端发起tcp连接

在飞鸽传书源码分析五-文件传输最后点击按钮后会出现一个选择保存文件所在路径的对话框。

case FILE_BUTTON:
        ...
        else if (fileObj) {
            TSaveCommonDlg  dlg(shareInfo, cfg, this);
            if (dlg.Exec())
            {
                memset(fileObj, 0, sizeof(RecvFileObj));
                strncpyz(fileObj->saveDir, cfg->lastSaveDir, MAX_PATH);
                SaveFile();
            }
        }
        ...
BOOL TRecvDlg::SaveFile(void)
{
    int     target;
    //处理客户端选中的将要接收的文件
    for (target=0; target < shareInfo->fileCnt; target++)
        if (shareInfo->fileInfo[target]->IsSelected())
            break;
    if (target == shareInfo->fileCnt)
        return  FALSE;

    memset(fileObj, 0, (char *)&fileObj->totalTrans - (char *)fileObj);
    fileObj->conInfo = new ConnectInfo;
    //将要接收的文件信息
    fileObj->fileInfo = shareInfo->fileInfo[target];
    ...
    if (ConnectRecvFile())
        SetDlgItemText(FILE_BUTTON, PREPARETRANS_MSGSTR);
    ...
    return  TRUE;
}
BOOL TRecvDlg::ConnectRecvFile(void)
{
    memset(fileObj->conInfo, 0, sizeof(ConnectInfo));
    fileObj->conInfo->addr = msg.hostSub.addr;
    fileObj->conInfo->port = msg.hostSub.portNo;

    if (msgMng->Connect(hWnd, fileObj->conInfo) != TRUE)
        return  FALSE;

    if (fileObj->conInfo->complete)
        StartRecvFile();

    return  TRUE;
}
BOOL TRecvDlg::ConnectRecvFile(void)
{
    memset(fileObj->conInfo, 0, sizeof(ConnectInfo));
    //发送端的ip地址
    fileObj->conInfo->addr = msg.hostSub.addr;
    //发送端的端口
    fileObj->conInfo->port = msg.hostSub.portNo;
    //连接发送端,将待发送文件机器作为server,接收文件端作为client
    if (msgMng->Connect(hWnd, fileObj->conInfo) != TRUE)
        return  FALSE;

    if (fileObj->conInfo->complete)
        StartRecvFile();

    return  TRUE;
}
BOOL TRecvDlg::StartRecvFile(void)
{
    char    buf[MAX_PATH], tcpbuf[MAX_BUF];
    wsprintf(buf, fileObj->isDir ? "%x:%x:" : "%x:%x:%x:", msg.packetNo, fileObj->fileInfo->Id(), OFFSET);
    fileObj->conInfo->complete = TRUE;
    msgMng->MakeMsg(tcpbuf, fileObj->isDir ? IPMSG_GETDIRFILES : IPMSG_GETFILEDATA, buf);
    ...

    //向server发送一条IPMSG消息
    if (::send(fileObj->conInfo->sd, tcpbuf, strlen(tcpbuf), 0) < (int)strlen(tcpbuf))
        return  EndRecvFile(), FALSE;
    ...
    //设置当前接收文件夹的信息为从server发送过来的文件信息
    if (fileObj->isDir == FALSE)
        fileObj->curFileInfo = *fileObj->fileInfo;
    ...
    DWORD   id; 
    fileObj->hThread = (HANDLE)~0;
    // 创建一个线程用于接收文件处理
    if ((fileObj->hThread = ::CreateThread(NULL, 0, RecvFileThread, this, 0, &id)) == NULL)
    {
        EndRecvFile();
        return  FALSE;
    }

    return  TRUE;
}
DWORD WINAPI TRecvDlg::RecvFileThread(void *_recvDlg)
{
    TRecvDlg    *recvDlg = (TRecvDlg *)_recvDlg;
    RecvFileObj *fileObj = recvDlg->fileObj;
    fd_set      rfd;
    timeval     tv;
    int         sock_ret;
    BOOL        (TRecvDlg::*RecvFileFunc)(void) =
                fileObj->isDir ? &TRecvDlg::RecvDirFile : &TRecvDlg::RecvFile;

    FD_ZERO(&rfd);
    FD_SET(fileObj->conInfo->sd, &rfd);

    for (int waitCnt=0; waitCnt < 120 && fileObj->hThread != NULL; waitCnt++)
    {
        tv.tv_sec = 1, tv.tv_usec = 0;
        if ((sock_ret = ::select(fileObj->conInfo->sd + 1, &rfd, NULL, NULL, &tv)) > 0)
        {
            waitCnt = 0;
            if ((recvDlg->*RecvFileFunc)() != TRUE)
                break;
            if (fileObj->status == FS_COMPLETE)
                break;
        }
        else if (sock_ret == 0) {
            FD_ZERO(&rfd);
            FD_SET(fileObj->conInfo->sd, &rfd);
            fileObj->conInfo->lastTick = ::GetTickCount();
            recvDlg->PostMessage(WM_RECVDLG_FILEBUTTON, 0, 0);
        }
        else if (sock_ret == SOCKET_ERROR) {
            break;
        }
    }
    recvDlg->CloseRecvFile(fileObj->status == FS_COMPLETE ? TRUE : FALSE);
    if (fileObj->status != FS_COMPLETE)
        fileObj->status = FS_ERROR;

    recvDlg->PostMessage(WM_TCPEVENT, fileObj->conInfo->sd, FD_CLOSE);
    ::ExitThread(0);
    return  0;
}

根据函数的调用关系,总算找到了发送接收文件的处理函数”RecvFileThread”,但是这个函数并不真正处理文件信息,在RecvFileThread函数中,我们看到有一个select函数。上面的select函数作用是如果当前tcp socket中有可读的数据,那么select就会返回>0的一个数。表示发送端已经向接收端发送的数据。当有数据可读时就交给recvDlg->*RecvFileFunc所指向的函数完成数据的处理(这里我们只考虑单个文件的传输,所以RecvFileFunc所指就是TRecvDlg::RecvFile)。

3.文件接收端的处理

飞鸽传书在windows使用的网络是多路复用select模型。

BOOL TRecvDlg::RecvFile(void)
{
    //wresid用于表示recvBuf前面多少字节用于每次接收数据后固定写到文件中。
    int     wresid = (int)(fileObj->offset - fileObj->woffset);
    //remain剩下的未接收文件大小
    _int64  remain = fileObj->curFileInfo.Size() - fileObj->offset;
    int     size = 0;

    if (remain > cfg->TransMax - wresid)
        remain = cfg->TransMax - wresid;

    //接收到的数据从recvBuf+wresid内存处开始存放
    if ((size = ::recv(fileObj->conInfo->sd, fileObj->recvBuf + wresid, (int)remain, 0)) <= 0)
        return  FALSE;

    //如果接收数据的文件还没创建,则调用OpenRecvFile创建一个文件用于将接收到的数据写到文件中。
    if (fileObj->hFile == INVALID_HANDLE_VALUE)
        if (OpenRecvFile() == FALSE)
            return  FALSE;

    //固定数据+接收到的数据
    wresid += size;
    //当前接收的数据大小为cfg->TransMax(数据还未传完),或者已经接收数据(fileObj->offset)+当前接收数据(size)大于等文件大小
    //将接收到的数据(加上前面固定写入的字符)写入到文件中。
    if (fileObj->offset + size >= fileObj->curFileInfo.Size() || cfg->TransMax == wresid)
    {
        DWORD   wsize;
        if (::WriteFile(fileObj->hFile, fileObj->recvBuf, wresid, &wsize, 0) != TRUE || wresid != (int)wsize)
            return  MessageBox(WRITEFAIL_MSGSTR), FALSE;
        fileObj->woffset += wresid;//总写入到文件中数据大小(包括固定字符--如果有)
    }
    fileObj->offset += size;//从server已接收到的数据大小

    DWORD   nowTick = ::GetTickCount();

    if (nowTick - fileObj->conInfo->lastTick >= 1000)
    {
        fileObj->conInfo->lastTick = nowTick;
        PostMessage(WM_RECVDLG_FILEBUTTON, 0, 0);
    }

    //如果接收到数据大小大于等于服务器原文件大小。表示文件传输完成。
    if (fileObj->offset >= fileObj->curFileInfo.Size())
        fileObj->status = fileObj->isDir ? FS_ENDFILE : FS_COMPLETE;

    return  TRUE;
}

4.文件发送端的处理

飞鸽使用的是一个线程用于文件发送处理。线程回调函数是SendFileThread,这个函数中调用了SendFile函数,SendFile就是处理文件信息读取及发送到网络上的任务

BOOL TMainWin::SendFile(SendFileObj *obj)
{
    if (obj == NULL || obj->hFile == INVALID_HANDLE_VALUE)
        return  FALSE;

    int     size = 0;
    _int64  remain = obj->fileSize - obj->offset;
    int     transMax = cfg->TransMax - (int)(obj->offset % cfg->TransMax);
    //将磁盘上已经映射到内存的文件数据发送给接收端
    if (remain > 0 && (size = ::send(obj->conInfo->sd, obj->mapAddr + (obj->offset % cfg->ViewMax), (int)(remain > transMax ? transMax : remain), 0)) < 0)
        return  FALSE;

    //计算已经发送的字节数
    obj->offset += size;

    //文件发送完成
    if (obj->offset == obj->fileSize)
        obj->status = obj->command == IPMSG_GETDIRFILES ? FS_ENDFILE : FS_COMPLETE;
    else if ((obj->offset % cfg->ViewMax) == 0) 
    {
        //还有剩余的数据未发送
        ::UnmapViewOfFile(obj->mapAddr);
        remain = obj->fileSize - obj->offset;
        //将剩余数据部分(或全部映射到内存)
        obj->mapAddr = (char *)::MapViewOfFile(obj->hMap, FILE_MAP_READ, (int)(obj->offset >> 32), (int)obj->offset, (int)(remain > cfg->ViewMax ? cfg->ViewMax : remain));
    }

    obj->conInfo->lastTick = ::GetTickCount();

    return  TRUE;
}

你可能感兴趣的:(C++,文件传输,飞鸽传书源码)