使用TCP协议进行C ++文件传输

时间:2018-11-12 16:13:48

标签: c++ sockets client-server file-transfer

我目前正在编写一个服务器和客户端应用程序,试图传输屏幕截图,但无法正常工作。我是这样实现的。

SOCKET sock;
char buf[4096];

DWORD WINAPI  thread_function()
{
    bool file_transfer = false;
    bool loop = true;
   while (1)
   {
       ZeroMemory(buf, 4096);
       int bytesReceived = recv(sock, buf, 4096, 0);
       if (bytesReceived > 0)
       {
           std::string received(buf, 0, bytesReceived);
           if (received == "Sending file.")
           {
               file_transfer = true;
           }

           if (file_transfer == false)
           {
           std::cout << "\nSERVER> " << std::string(buf, 0, bytesReceived) << std::endl;  
           std::cout << "> ";
           }
           else if (file_transfer == true)
           {
               loop = true;
               TCHAR *szfname = "screenshot.bmp";
               FILE* f = fopen(szfname, "wb");
               if (NULL == f)
               {
                   std::cerr << "Error opening file" << std::endl;
                   return 1;
               }
               while ((bytesReceived = recv(sock, buf, 4096, 0)) > 0 && loop == true)
               {
                   received = buf;
                   if (received == "File transfer completed !")
                   {
                       loop = false;
                       std::cout << "File transfer completed !" << std::endl;
                       std::cout << "> ";
                   }
                   else
                   {
                   fwrite(buf, 1, bytesReceived, f);
                   }
               }
               file_transfer = false;
           }
       }
   }
}

我以此调用函数

CreateThread(0, 0, (LPTHREAD_START_ROUTINE)thread_function, 0, 0, 0);

问题是我认为这不是一种很干净的方法,而且效果也不理想。收到文件后,我无法正确接收服务器正在发送的内容。

这是我认为可以的服务器代码。

            send(clientSocket, TEXT("Attempting to take a screenshot."), sizeof(TEXT("Attempting to take a screenshot...")), 0);
            HWND win = GetDesktopWindow();
            HDC dc = GetDC(win);
            if (HDCToFile("screenshot.bmp", dc, { 0, 0, 1920, 1080 }) == true)
            {
                send(clientSocket, TEXT("Sending file."), sizeof(TEXT("Sending file.")), 0);
                FILE *fp = fopen("screenshot.bmp", "rb");
                if (fp == NULL)
                {
                    std::cerr << "Error : Cannot open file." << std::endl;
                    return 1;
                }
                while (1)
                {
                   char buff[4096] = { 0 };
                    int nread = fread(buff, 1, 4096, fp);
                    if (nread > 0)
                    {
                        send(clientSocket, buff, sizeof(buff), 0);
                    }
                    if (nread < 4096)
                    {
                        if (feof(fp))
                        {
                            std::cout << "File transfer completed !" << std::endl;
                            send(clientSocket, TEXT("File transfer completed !"), sizeof(TEXT("File transfer completed !")), 0);
                        }
                        if (ferror(fp))
                            std::cerr << "Error reading." << std::endl;
                        break;
                    }
                }
            }
            else
            {
                send(clientSocket, TEXT("Screen capture failed...."), sizeof(TEXT("Screen capture failed....")), 0);
            }

感谢您的时间和帮助。

1 个答案:

答案 0 :(得分:1)

TCP是流协议。它没有消息的概念,因此当服务器发送"Sending file."时,字符串和要发送的文件的开头之间没有分隔。所有内容仅在下一个字节之后才进入流,而当网络堆栈确定时间到时,通常是因为已填充数据包或it's been too long since data was last added,所以发送了一个数据包,其中可能包含多个消息。

所以

int bytesReceived = recv(sock, buf, 4096, 0);

很有可能读取完整的4096字节,Attempting to take a screenshot.\0Sending file.\0加上位图的前四千个字节。客户端代码使用该字符串,并丢弃其余缓冲区。

您需要建立一个位于套接字和文件写入之间的通信协议。有很多不同的方法来处理此问题。读取字符串的常见技巧是

  1. 在写入字符串之前写出字符串的长度,以便协议处理程序知道要提前读取多少字节

发件人

uint16_t len = str.length(); // size is exactly 16 bits
len = htons(len); // endian is known
int sent = send(sock, (char*)&len, sizeof(len), 0);
// test sent for success (did not fail, sent all the bytes)
sent = send(sock, str.c_str(), len, 0);
// test sent for success (did not fail, sent all the bytes) 
// may need to loop here if the string is super long.

接收器

uint16_t len;
int recd = recv(sock, (char*)&len, sizeof(len), MSG_WAITALL);
// test recd for success (did not fail, read all the bytes)
// MSG_WAITALL will read exactly the right number of bytes or die trying. 
len = ntohs(len); // ensure correct endian
std::string msg(len, ' '); // allocate a big enough string
char * msgp = &msg[0]; // or msg.data() if C++17 or better. 
                       // Never seen &msg[0] fail, but this is not guaranteed by C++
while (len) // sometimes you want an extra exit condition here to bail out early
{
    recd = recv(sock, msgp, len, 0);
    // test recd for success 
    len -= recd;
    msgp += recd;
 } 
  1. 插入一个Canary值,以便协议处理程序知道何时停止读取。空终止符在这里工作良好。协议会一直进行读取,直到找到null为止,并保留所读取内容的其余部分供以后使用。这里没有代码示例,因为可以用很多很多不同的方式来实现。
  2. 不使用字符串,而是发送整数代码消息。例如:


enum messageID
{
    TAKING_SCREENSHOT,
    SENDING_FILE,
    EATING_COOOOOOKIE_OM_NOM_NOM
};

好!这样可以正确地移动琴弦。假设我那里没有错误。这个想法是正确的,但是实际的代码来自内存,可能包含脑筋。

您想要的是一堆函数,每种发送的数据类型都有一个。这些功能中的每一个都可以并且应该分别进行测试,以便在将它们集成到程序中时,程序看起来像

sendString(sock, "Attempting to take a screenshot.");
if (getBitmap("screenshot.bmp"))
{
    sendString(sock, "Sending file.");
    sendBitmap(sock, "screenshot.bmp");
}

receiveString(sock);
std::string command = receiveString(sock);
if (command == "Sending file.")
{
    receiveBitmap(sock, "screenshot.bmp");
}
else if (command == "Eating coooooookie! Om! Nom! Nom!")
{
    OmNomNom(sock);
}

您所能获得的几乎是万无一失的。

注意:

服务器中存在一个错误:int nread = fread(buff, 1, 4096, fp);获取读取的字节数,但是send(clientSocket, buff, sizeof(buff), 0);始终尝试发送完整的缓冲区,而不管读取了多少字节,因此会将垃圾发送到客户端。此外,send可能会失败,并且未对此进行检查。始终检查返回码。人们除非重要就不会把它们放在那里。