在飞鸽传书源码分析五-文件传输这篇文章中分析了,如何将要发送的文件先传至要传送到的机器上。此时接收端的机器已经“知道”要接收文件了。本篇文章详细介绍飞鸽传书文件的传输过程。为了简化,我们选择只传输一个文件进行分析。
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
};
struct ConnectInfo : public TListObj {
SOCKET sd; //socket
ULONG addr; //要发送文件的ip地址
int port; //要发送文件的端口
BOOL server; //是否为服务器
BOOL complete; //连接是否完成
DWORD startTick;
DWORD lastTick;
};
在飞鸽传书源码分析五-文件传输最后点击按钮后会出现一个选择保存文件所在路径的对话框。
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)。
飞鸽传书在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;
}
飞鸽使用的是一个线程用于文件发送处理。线程回调函数是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;
}