14.9 Socket 高效文件传输

网络上的文件传输功能也是很有必要实现一下的,网络传输文件的过程通常分为客户端和服务器端两部分。客户端可以选择上传或下载文件,将文件分块并逐块发送到服务器,或者从服务器分块地接收文件。服务器端接收来自客户端的请求,根据请求类型执行对应的操作,并根据发送的文件名或其他标识来确定要传输的文件。

在实现文件传输之前,需要先打开要传输的文件,并获取文件的大小信息,也可以通过其他方式获取文件的信息。在客户端和服务器端都准备就绪后,可以通过套接字来发送文件数据。在传输文件的过程中,可以将文件分解为若干个数据包进行传输,以减少数据传输中的丢包或传输错误。每个数据包的长度可以根据实际情况进行选择,通常选择1024字节或更大,也可以设置成更小的值。传输文件的过程中,还需要实现一定的错误处理机制,例如检测传输过程中的超时、丢包、不完整数据等情况,并在必要时进行错误重传或协商其他解决方案。

首先无论时服务端还是客户端都需要封装两个函数,其中GetFileName()函数用于当用户传入文件的具体路径信息时自动获取到该文件的文件名,第二个函数GetFileSize()则用于传入文件路径并自动获取到该文件的字节数。

// 传入路径得到文件名
char* GetFileName(char* Path)
{
if (strchr(Path, '\\'))
{
char ch = '\\';
char* ref = strrchr(Path, ch) + 1;
return ref;
}
else
{
char ch = '/';
char* ref = strrchr(Path, ch) + 1;
return ref;
}
}

// 获取文件大小
int GetFileSize(std::string FileName)
{
FILE* pointer = NULL;
pointer = fopen(FileName.c_str(), "rb");
if (pointer != NULL)
{
fseek(pointer, 0, SEEK_END);
int size = ftell(pointer);
fclose(pointer);
return size;
}
return 0;
}

接着我们来看一下RecvFile()接收文件函数是如何实现的,首先第一个发送用于向服务端发出我需要下载具体的那个目录下的文件,接着服务端会返回该目录文件的长度,此时我们通过fopen()创建一个新文件,并以此循环接收该文件的长度,每次接收成功后自动的fwrite写出到文件中,当文件被接收完毕后,则通过fclose(pointer)保存并关闭文件。

// 接收文件
bool RecvFile(SOCKET ptr, char* LocalPath, char* RemoteFile)
{
// 发送需要下载的文件路径
send(ptr, RemoteFile, strlen(RemoteFile), 0);

// 接收文件长度
long long file_size = 0;
recv(ptr, (char*)&file_size, sizeof(int), 0);
if (file_size <= 0)
{
return false;
}

// 保存文件到指定目录下
char *file_name = GetFileName(RemoteFile);
char file_all_name[1024] = { 0 };

strcat(file_all_name, LocalPath);
strcat(file_all_name, file_name);

std::cout << "生成保存路径: " << file_all_name << std::endl;
FILE* pointer = fopen(file_all_name, "wb");
char buffer[1024] = { 0 };

if (pointer != NULL)
{
long long length = 0;
long long total_length = 0;

// 循环接收字节数据,每次接收1024字节
while ((length = recv(ptr, buffer, 1024, 0)) > 0)
{
// 写出文件并判断是否写出成功
if (fwrite(buffer, sizeof(char), length, pointer) < length)
{
break;
}

// 每次累加递增
total_length += length;
memset(buffer, 0, 1024);

// 判断文件长度是否全部接收完毕
if (total_length >= file_size)
{
std::cout << "文件接收完毕, 接收字节数: " << total_length << std::endl;
fclose(pointer);
return true;
}
}
fclose(pointer);
}
return false;
}

对于SendFile()发送文件而言首先我们接收到客户端传来的文件路径,并通过该路径得到该文件的具体长度,第一次调用发送函数将文件的长度传递给客户端,此时打开我们所需要发送的文件,并通过循环的方式向客户端传输,当数据包传输完毕后则自动关闭文件。

// 发送指定文件
bool SendFile(SOCKET ptr)
{
// 接收文件路径
char file_path[1024] = { 0 };
recv(ptr, file_path, 1024, 0);

// 得到文件长度并发送给服务端
long long file_size = GetFileSize(file_path);

if (file_size <= 0)
{
return false;
}
send(ptr, (char*)&file_size, sizeof(int), 0);
std::cout << "发送文件长度: " << file_size << std::endl;

// 循环发送数据
char buffer[1024] = { 0 };
FILE* pointer = fopen(file_path, "rb");
if (pointer != NULL)
{
long long length = 0;
long long total_length = 0;

// 循环发送数据
while ((length = fread(buffer, sizeof(char), 1024, pointer)) > 0)
{
send(ptr, buffer, length, 0);
memset(buffer, 0, 1024);
total_length += length;
}

if (total_length == file_size)
{
return true;
}
}
return false;
}

14.9.1 服务端实现

如下代码展示了如何使用Winsock进行TCP协议的文件传输。首先使用WSAStartup函数对Winsock库进行初始化。然后创建一个socket,设置IP地址、端口号等信息,并将该socket和本地服务端的地址绑定起来。接下来对该socket进行监听,等待客户端的连接请求。

当有客户端连接请求到来时,accept函数会接收请求,并创建一个新的socket与客户端进行通信。在与客户端通信的过程中,可以通过sendrecv函数进行数据的传输,实现文件的上传和下载功能。此处的代码调用RecvFile函数,该函数为自定义实现的接收文件函数,负责接收数据并将接收到的文件存储到指定的路径下。

int main(int argc, char* argv[])
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
exit(1);

// 声明并初始化一个服务端(本地)的地址结构
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = INADDR_ANY;
server_addr.sin_port = htons(8087);

// 创建socket
SOCKET m_Socket = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == m_Socket)
exit(1);

// 绑定socket和服务端(本地)地址
if (SOCKET_ERROR == bind(m_Socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)))
exit(1);

// 监听
if (SOCKET_ERROR == listen(m_Socket, 10))
exit(1);

sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);

SOCKET m_New_Socket = accept(m_Socket, (sockaddr*)&client_addr, &client_addr_len);

// 接收远程d://lyshark.exe放到本地的d://11g/目录下
bool ref = RecvFile(m_New_Socket, (char*)"d://11g/", (char*)"d://lyshark.exe");
std::cout << "接收状态: " << ref << std::endl;

closesocket(m_New_Socket);
closesocket(m_Socket);

WSACleanup();
system("pause");
return 0;
}

14.9.2 客户端实现

如下客户端代码实现了一个基于TCP协议的文件传输客户端。首先使用WSAStartup函数对Winsock库进行初始化。然后创建一个socket,并设置服务端的IP地址和端口号。之后通过connect函数与服务端建立连接,连接成功后调用SendFile函数进行文件传输,将指定的文件发送到服务端。文件传输完成后,关闭socket连接,清除Winsock资源。

int main(int argc, char* argv[])
{
while (true)
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
exit(1);

// 创建socket
SOCKET c_Socket = socket(AF_INET, SOCK_STREAM, 0);

//指定服务端的地址
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8087);

if (SOCKET_ERROR != connect(c_Socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)))
{
bool ref = SendFile(c_Socket);
std::cout << "文件发送状态: " << ref << std::endl;
}
closesocket(c_Socket);
WSACleanup();
Sleep(1000);
}
return 0;
}

文件传输功能代码就这些,其实理解起来并不难,读者可自行编译并运行上述代码,运行后则可接收远程d://lyshark.exe文件,并放到本地的d://11g/目录下,输出效果图如下;