C++使用TCP SOCKET发送超大文件(超过2G)
前几天有一个网友提出问题,如何使用socket传输超大文件。
之前虽然知道理论上该怎么处理,但并未在实际工作中使用过,毕竟现成的工具实在是太多了,没有自己开发的必要。但是想着既然给他回复了一些文字,何不写个demo让他看,不是更加直观吗?说干就干。
首先是服务端的开发。
1.首先要让客户端知道我们要发送的文件是多大,这就要在服务建立连接的时候,先将文件大小通过socket发送给客户端。
2.其次需要让客户端知道文件名是什么,这样客户端在保存的时候可以动态保存,而不是写死一个文件名。
3.最后就是文件内容的发送了,当然选择读取文件二进制,客户端使用二进制方式再保存最为稳妥。
服务端代码如下,windows下开发,包含了WinSock2.h头文件。
#include <iostream>
#include <stdio.h>
#include <WinSock2.h>
using namespace std;
//缓存大小设置不能超过2M
#define BUFF_SIZE (1024 * 1024)
#define FILE_NAME_LENGTH 1024
int s; /* socket for accepting connections */
int ns; /* socket connected to client */
int exitFunc() {
closesocket(s);
closesocket(ns);
}
off64_t getFileSize(char *filePath) {
FILE *f;
f = fopen(filePath, "rb");
if (NULL == f) {
printf("getFileSize fopen error\n");
return -1;
}
if (0 != fseeko64(f, 0, SEEK_END)) {
printf("getFileSize fseek error\n");
return -1;
}
off64_t fileSize = ftello64(f);
if (fileSize < 0) {
printf("ftell error\n");
}
printf("fileSize:%lld\n", fileSize);
fclose(f);
return fileSize;
}
char *getFileName(char *filePath) {
bool bFound = false;
char *buff = new char[1024];
memset(buff, 0, 1024);
while (!bFound) {
int lastIndex = 0;
for (int i = 0; i < strlen(filePath); ++i) {
if (filePath[i] == '\\' || filePath[i] == '/') {
lastIndex = i;
}
}
for (int i = lastIndex + 1; i < strlen(filePath); ++i) {
buff[i - lastIndex - 1] = filePath[i];
}
bFound = true;
}
return buff;
}
int main(int argc, char **argv)
{
_onexit(exitFunc);
unsigned short port; /* port server binds to */
char buff[BUFF_SIZE]; /* buffer for sending & receiving data */
struct sockaddr_in client; /* client address information */
struct sockaddr_in server; /* server address information */
int namelen; /* length of client name */
char *filePath = new char[FILE_NAME_LENGTH];
//检查是否传入端口参数
if (argc < 2)
{
fprintf(stderr, "Usage: %s port\n", argv[0]);
exit(1);
}
//第一个参数是端口号
port = (unsigned short) atoi(argv[1]);
//如果有第二个参数 第二个参数需要是文件的详细路径 否则需要自己指定路径
if (argc > 2) {
filePath = argv[2];
printf("filePath from arg:%s\n", filePath);
} else {
//char *filePath = "D:\\Download\\qt-opensource-windows-x86-5.12.5.exe";
//char *filePath = "D:\\Download\\ideaIC-2019.3.3.exe";
filePath = "D:\\Download\\settings.xml";
}
off64_t fileSize = getFileSize(filePath);
printf("fileSize:%lld\n", fileSize);
char *fileName = getFileName(filePath);
printf("fileName:%s\n", fileName);
WSADATA wsadata;
WSAStartup(0x202, &wsadata);
//创建socket服务
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("socket error\n");
exit(2);
}
//socket和服务地址绑定
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = INADDR_ANY;
if (bind(s, (struct sockaddr *)&server, sizeof(server)) < 0)
{
printf("bind error\n");
exit(3);
}
//监听服务,只允许一个客户端连接
if (listen(s, 1) != 0)
{
printf("listen error\n");
exit(4);
}
//等待连接
namelen = sizeof(client);
while (true) {
//循环 一直等待客户端的连接
if ((ns = accept(s, (struct sockaddr *)&client, &namelen)) == -1)
{
printf("accept error\n");
exit(5);
}
//有客户端连接过来之后 将指定文件发送给客户端
FILE *f;
f = fopen(filePath, "rb");
if (f == NULL) {
printf("file:%s doesn't exist\n", filePath);
exit(6);
}
off64_t sendSize = 0;
//先将文件大小的数据发送给客户端
lltoa(fileSize, buff, 10);
if (send(ns, buff, sizeof(buff), 0) < 0) {
printf("send fileSize to client error\n");
exit(7);
}
//再将文件名发送给客户端
printf("sizeof:%d strlen:%d\n", sizeof(fileName), strlen(fileName));
if (send(ns, fileName, strlen(fileName), 0) < 0) {
printf("send fileName to client error\n");
exit(7);
}
while (sendSize < fileSize) {
memset(buff, 0, 1024 * 1024);
size_t iread = fread(buff, sizeof(char), BUFF_SIZE, f);
printf("iread:%d\n", iread);
if (iread < 0) {
printf("fread error\n");
fclose(f);
break;
}
int iSend = send(ns, buff, iread, 0);
if (iSend < 0) {
printf("send error\n");
fclose(f);
break;
}
sendSize += iSend;
printf("fileSize:%lld iSend:%d sendSize:%lld\n", fileSize, iSend, sendSize);
fseeko64(f, sendSize, SEEK_SET);
}
fclose(f);
}
printf("Server ended successfully\n");
exit(0);
}
客户端的开发
按照服务端的开发思路,客户端需要先接收文件大小,再接收文件名,最后接收文件内容。
代码如下:
#include <iostream>
#include <stdio.h>
#include <WinSock2.h>
#include <time.h>
using namespace std;
//缓存大小设置不能超过2M
#define BUFF_SIZE (1024 * 1024)
#define FILE_NAME_LENGTH 1024
int s; /* client socket */
int exitFunc() {
closesocket(s);
return 0;
}
/*
* Client Main.
*/
int main(int argc, char** argv)
{
_onexit(exitFunc);
WSADATA wsadata;
WSAStartup(0x202, &wsadata);
printf("start...\n");
unsigned short port; //服务端口
char buf[BUFF_SIZE]; //缓存
struct hostent *hostnm; //服务地址信息
struct sockaddr_in server; //服务sockaddr信息
//传入两个参数,顺序是服务器地址和端口
if (argc != 3)
{
fprintf(stderr, "Usage: %s hostname port\n", argv[0]);
exit(1);
}
//第一个参数是服务器地址
hostnm = gethostbyname(argv[1]);
if (hostnm == (struct hostent *) 0)
{
fprintf(stderr, "Gethostbyname failed\n");
exit(2);
}
//第二个参数是端口号
port = (unsigned short) atoi(argv[2]);
//put the server information into the server structure.
//The port must be put into network byte order.
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = *((unsigned long *)hostnm->h_addr);
//创建socket
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("Socket error\n");
exit(3);
}
//准备连接服务端
printf("ready to connet to server ...\n");
if (connect(s, (struct sockaddr *)&server, sizeof(server)) < 0)
{
printf("Connect error\n");
exit(4);
}
//先接收文件大小
int iRecv = 0;
memset(buf, 0, BUFF_SIZE);
iRecv = recv(s, buf, BUFF_SIZE, 0);
if (iRecv < 0) {
printf("recv fileSize error\n");
exit(5);
}
off64_t totalFileSize = atoll(buf);
printf("totalFileSize:%lld\n", totalFileSize);
//再接收文件名
memset(buf, 0, BUFF_SIZE);
iRecv = recv(s, buf, BUFF_SIZE, 0);
if (iRecv < 0) {
printf("recv fileName error\n");
exit(5);
}
char fileName[FILE_NAME_LENGTH];
memset(fileName, 0, FILE_NAME_LENGTH);
memcpy(fileName, buf, strlen(buf));
printf("recv fileName:%s\n", fileName);
//接收文件 将文件保存到指定位置
char *filePath = new char[FILE_NAME_LENGTH];
memset(filePath, 0, FILE_NAME_LENGTH);
char *basePath = "D:\\client\\";
memcpy(filePath, basePath, strlen(basePath));
strcat(filePath, fileName);
printf("filePath:%s\n", filePath);
FILE *f = NULL;
f = fopen(filePath, "wb");
if (f == NULL) {
printf("file:%s doesn't exist and failed to create\n", filePath);
exit(5);
}
off64_t fileRecv = 0;
time_t start;
start = time(NULL);
while (fileRecv < totalFileSize) {
memset(buf, 0, BUFF_SIZE);
iRecv = recv(s, buf, BUFF_SIZE, 0);
if (iRecv < 0)
{
printf("Recv error\n");
exit(6);
}
if (iRecv == 0) {
break;
}
fileRecv += iRecv;
time_t end = time(NULL);
time_t cost = end - start;
//动态计算出传输完需要用时多久
time_t totalTime = 0;
//计算出剩余时间
time_t leftTime = 0;
if (cost != 0) {
totalTime = totalFileSize / (fileRecv / cost);
leftTime = (totalFileSize - fileRecv) / (fileRecv / cost);
}
printf("totalFileSize:%lld recv file size:%lld, totalTime:%d 's, leftTime:%d 's\n", totalFileSize, fileRecv, totalTime, leftTime);
fwrite(buf, sizeof(char), iRecv, f);
}
fclose(f);
printf("Client Ended Successfully\n");
exit(0);
}
运行服务端时通过参数传入端口号,也可在代码中写死端口号。
运行客户端时通过参数传入服务地址、服务端口号,也可在代码中写死。
在这个过程中遇到一个有趣事情就是,比较小的文件传输没问题,但是大小超过2G的文件刚开始使用fseek和ftell获取文件大小时获取到的文件大小为-1,后来查询文档才知道要使用其他接口,fseeko64和ftello64。
由此来看,很多时候虽然理论上知道怎么做,但实际做起来,并没有想象中的那么顺利。这在现实生活中又何尝不是呢。