Socket文件传输2
OVERVIEW
在socket文件传输1中使用了一种在buff缓冲数组后添加\0
的方式实现文件的传输。
- 实现方法2:根据TCP在数据传输过程中进行粘包与拆包的过程,修改文件传输功能的实现。
客户端向服务端连续不断的发送数据包时,服务端接受的数据可能会出现几种情况:
- case1:客户端发送的包和服务端接受的包大小刚好是相同的(整包),这时能够完全的打印出包中的内容。
- case2:如果真正需要传输的数据比一个packet小,需要将packet存满tcp才会对齐进行发送,等待数据填满packet
- case3:客户端发送的包大于服务端接受的包,则需要将发送的数据包进行截断,
- packet_t:一份新的packet与原来能够完整打包数据的包一样大小。
- packet_pre:存放截断后的数据包
- offset:当前数据所占用的包的位置,如果包中新添加了数据需要更新offset:
(offset += recv_size;)
。
- TCP传输的是字节流,其并没有一定的大小,会出现粘包与拆包的情况。
- UDP是基于报文来发送消息的,其首部有一个用于记录报文长度的字段16位,指定报文传输的长度(数据边界),因此UDP没有粘包与拆包的情况。
没什么好说的,直接上代码。
//1.server.c
#include "head.h"
#include "common.h"
#define MAXUSER 100
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
struct UserInfo {
int newfd;
struct sockaddr_in client;
};
/* 传输文件的信息 */
struct Packet {
char filename[50];
ssize_t size;//文件大小
char content[1024];//文件主体内容
};
//TCP拆包、粘包操作接受数据并进行写入文件
int recv_file(int sockfd, const char *dir) {
struct Packet packet;//数据传输packet
struct Packet packet_t;//数据缓冲packet
struct Packet packet_pre;//数据处理packet
ssize_t packet_size = sizeof(packet);
ssize_t offset = 0;
ssize_t recv_size = 0;
//1.创建文件夹
if (mkdir(dir, 0755) < 0) {
if (errno == EEXIST) {
printf("dir exist!\n");
} else {
perror("mkdir");
return -1;
}
}
//2.循环接收发来的数据进行拆包、粘包操作并将其写入目标文件中
FILE *fp;
int cnt = 0;//数据recv的次数
while (1) {
//3.对接受到的数据包进行拆包、粘包操作(凑整包)
memcpy((char *)&packet, &packet_pre, offset);//将case3中的包剩余部分packet_pre重新加入到新的packet中
while ((recv_size = recv(sockfd, (void *)&packet_t, packet_size, 0)) > 0) {
if (recv_size + offset == packet_size) {
/* case1:整包的情况 */
printf("<case1> Packet size fit.\n");
memcpy((char *)&packet + offset, &packet_t, recv_size);//将packet_t中的数据转移到包中的位置(恰好full packet)
offset = 0;//offset置为0
break;
} else if (recv_size + offset < packet_size) {
/* case2:粘包的情况 */
printf("<case2> Packet Assembly.\n");
memcpy((char *)&packet + offset, &packet_t, recv_size);//将packet_t中的数据转移到包中的位置
offset += recv_size;
} else {
/* case3:拆包的情况 */
printf("<case3> Packet Fragmentation.\n");
memcpy((char *)&packet + offset, &packet_t, packet_size - offset);//将packet_t中的部分数据转移到包中的位置
memcpy((char *)&packet_pre, (char *)&packet_t + (packet_size - offset), recv_size - (packet_size - offset));//将packet_t中的剩余的数据转移到packet_pre中
offset = recv_size - (packet_size - offset);
break;
}
}
//4.若第一次接受packet需要创建文件
if (cnt == 0) {
printf("saving file %s to ./%s/ ...\n", packet.filename, dir);
char filepath[512] = {0};
sprintf(filepath, "./%s/%s", dir, packet.filename);
if ((fp = fopen(filepath, "w+")) == NULL) {
perror("fopen");
return -1;
}
}
cnt++;
//5.文件数据写入
ssize_t total_size;//已经写入文件的数据大小
ssize_t write_size;//每次写入的数据大小
if (packet.size - total_size >= packet_size) {//前n次数据写入
write_size = fwrite(packet.content, 1, sizeof(packet.content), fp);
} else {//最后一次数据写入(写入数据size小于packet_size)
write_size = fwrite(packet.content, 1, packet.size - total_size, fp);
}
total_size += write_size;
printf("writing...\n");
if (total_size >= packet.size) {//若数据全部写入完成直接退出
printf("saving success.\n");
break;
}
}
fclose(fp);
return 0;
}
void *worker(void *arg) {
//1.对传入的arg参数进行解封装
struct UserInfo userInfo = *(struct UserInfo *)arg;//进行参数的类型转换
int sockfd = userInfo.newfd;
struct sockaddr_in client = userInfo.client;//将参数client进行解封装
int port = ntohs(client.sin_port);
char ip[20] = {0}; strcpy(ip, inet_ntoa(client.sin_addr));
//2.循环对packet包进行接收并将数据写入目标文件中 调用recv_file函数
//设置存储文件的路径为 端口号:ip地址
char dir[50];
sprintf(dir, "%d:", port); strcat(dir, ip);
recv_file(sockfd, dir);
// if (rsize > 0) printf("<receive %ldbyte> %s:%d: \n%s\n", rsize, ip, port, packet.content);
// else { close(sockfd); break; }
printf("<server> : %s:%d has left!\n", ip, port);
close(sockfd);
}
int main(int argc, char *argv[]) {
//./a.out -p port
//1.命令行解析
if (argc != 3) {
fprintf(stderr, "Usage : %s -p port", argv[0]);
exit(1);
}
int opt;
int port;
while ((opt = getopt(argc, argv, "p:")) != -1) {
switch (opt) {
case 'p':
port = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -p port\n", argv[0]);
exit(1);
}
}
//2.创建socket
int server_listen;//监听文件描述符
if ((server_listen = socketCreate(port)) < 0) handle_error("socketCreate");
//3.accept循环的接受客户端对server的连接
int sockfd;//accept文件描述符
pthread_t tid[MAXUSER + 5] = {0};
struct UserInfo userInfo[MAXUSER + 5];
bzero(&userInfo, sizeof(userInfo));
while (1) {
int newfd;//新建的文件描述符用于接收accept返回的结果
struct sockaddr_in client;//用于存放临时建立连接的客户端的信息
socklen_t len = sizeof(client);
if ((newfd = accept(server_listen, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//4.发送一个ack
int ack = 1;
if (send(newfd, (void *)&ack, sizeof(int), 0) == -1) handle_error("ack send");
printf("ack send finish.\n");
//5.创建多线程来接受消息recv
userInfo[newfd].client = client;
userInfo[newfd].newfd = newfd;//这样所使用的文件描述符 就是当前进程所打开的文件描述符中最小、未被使用的fd
pthread_create(&tid[newfd], NULL, worker, (void *)&userInfo[newfd]);
}
close(server_listen);
close(sockfd);
return 0;
}
/* 传输文件的信息 */
struct Packet {
char filename[50];
ssize_t size;//文件大小
char content[1024];//文件主体内容
};
//TCP拆包、粘包操作接受数据并进行写入文件
int recv_file(int sockfd, const char *dir) {
struct Packet packet;//数据传输packet
struct Packet packet_t;//数据缓冲packet
struct Packet packet_pre;//数据处理packet
ssize_t packet_size = sizeof(packet);
ssize_t offset = 0;
ssize_t recv_size = 0;
//1.创建文件夹
if (mkdir(dir, 0755) < 0) {
if (errno == EEXIST) {
printf("dir exist!\n");
} else {
perror("mkdir");
return -1;
}
}
//2.循环接收发来的数据进行拆包、粘包操作并将其写入目标文件中
FILE *fp;
int cnt = 0;//数据recv的次数
while (1) {
//3.对接受到的数据包进行拆包、粘包操作(凑整包)
memcpy((char *)&packet, &packet_pre, offset);//将case3中的包剩余部分packet_pre重新加入到新的packet中
while ((recv_size = recv(sockfd, (void *)&packet_t, packet_size, 0)) > 0) {
if (recv_size + offset == packet_size) {
/* case1:整包的情况 */
printf("<case1> Packet size fit.\n");
memcpy((char *)&packet + offset, &packet_t, recv_size);//将packet_t中的数据转移到包中的位置(恰好full packet)
offset = 0;//offset置为0
break;
} else if (recv_size + offset < packet_size) {
/* case2:拆包的情况 */
printf("<case2> Packet Fragmentation.\n");
memcpy((char *)&packet + offset, &packet_t, recv_size);//将packet_t中的数据转移到包中的位置
offset += recv_size;
} else {
/* case3:粘包的情况 */
printf("<case3> Packet Assembly.\n");
memcpy((char *)&packet + offset, &packet_t, packet_size - offset);//将packet_t中的部分数据转移到包中的位置
memcpy((char *)&packet_pre, (char *)&packet_t + (packet_size - offset), recv_size - (packet_size - offset));//将packet_t中的剩余的数据转移到packet_pre中
offset = recv_size - (packet_size - offset);
break;
}
}
//4.若第一次接受packet需要创建文件
if (cnt == 0) {
printf("saving file %s to ./%s/ ...\n", packet.filename, dir);
char filepath[512] = {0};
sprintf(filepath, "./%s/%s", dir, packet.filename);
if ((fp = fopen(filepath, "w+")) == NULL) {
perror("fopen");
return -1;
}
}
cnt++;
//5.文件数据写入
ssize_t total_size;//已经写入文件的数据大小
ssize_t write_size;//每次写入的数据大小
if (packet.size - total_size >= packet_size) {//前n次数据写入
write_size = fwrite(packet.content, 1, sizeof(packet.content), fp);
} else {//最后一次数据写入(写入数据size小于packet_size)
write_size = fwrite(packet.content, 1, packet.size - total_size, fp);
}
total_size += write_size;
printf("writing...\n");
if (total_size >= packet.size) {//若数据全部写入完成直接退出
printf("saving success.\n");
break;
}
}
fclose(fp);
return 0;
}
void *worker(void *arg) {
//1.对传入的arg参数进行解封装
struct UserInfo userInfo = *(struct UserInfo *)arg;//进行参数的类型转换
int sockfd = userInfo.newfd;
struct sockaddr_in client = userInfo.client;//将参数client进行解封装
int port = ntohs(client.sin_port);
char ip[20] = {0}; strcpy(ip, inet_ntoa(client.sin_addr));
//2.循环对packet包进行接收并将数据写入目标文件中 调用recv_file函数
//设置存储文件的路径为 端口号:ip地址
char dir[50];
sprintf(dir, "%d:", port); strcat(dir, ip);
recv_file(sockfd, dir);
// if (rsize > 0) printf("<receive %ldbyte> %s:%d: \n%s\n", rsize, ip, port, packet.content);
// else { close(sockfd); break; }
printf("<server> : %s:%d has left!\n", ip, port);
close(sockfd);
}
int main(int argc, char *argv[]) {
//./a.out -p port
//1.命令行解析
if (argc != 3) {
fprintf(stderr, "Usage : %s -p port", argv[0]);
exit(1);
}
int opt;
int port;
while ((opt = getopt(argc, argv, "p:")) != -1) {
switch (opt) {
case 'p':
port = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -p port\n", argv[0]);
exit(1);
}
}
//2.创建socket
int server_listen;//监听文件描述符
if ((server_listen = socketCreate(port)) < 0) handle_error("socketCreate");
//3.accept循环的接受客户端对server的连接
int sockfd;//accept文件描述符
pthread_t tid[MAXUSER + 5] = {0};
struct UserInfo userInfo[MAXUSER + 5];
bzero(&userInfo, sizeof(userInfo));
while (1) {
int newfd;//新建的文件描述符用于接收accept返回的结果
struct sockaddr_in client;//用于存放临时建立连接的客户端的信息
socklen_t len = sizeof(client);
if ((newfd = accept(server_listen, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//4.发送一个ack
int ack = 1;
if (send(newfd, (void *)&ack, sizeof(int), 0) == -1) handle_error("ack send");
printf("ack send finish.\n");
//5.创建多线程来接受消息recv
userInfo[newfd].client = client;
userInfo[newfd].newfd = newfd;//这样所使用的文件描述符 就是当前进程所打开的文件描述符中最小、未被使用的fd
pthread_create(&tid[newfd], NULL, worker, (void *)&userInfo[newfd]);
}
close(server_listen);
close(sockfd);
return 0;
}
//2.client.c
#include "head.h"
#include "common.h"
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int sockfd;
struct Packet {
char filename[50];
ssize_t size;//文件大小
char content[1024];//文件主体内容
};
int send_file(int sockfd, const char *filename) {
//1.封装需要发送文件的文件名filename与文件大小size
struct Packet packet;
memset(&packet, 0, sizeof(packet));
strcpy(packet.filename, filename);//传输的文件名
FILE *fp = fopen(filename, "r");//传输的文件大小
fseek(fp, 0, SEEK_END);
packet.size = ftell(fp);
fseek(fp, 0, SEEK_SET);//将指针重新定位到文件的开始
//2.send发送文件内容信息
while (1) {
ssize_t psize;//每次fread的数据大小
if ((psize = fread(packet.content, 1, sizeof(packet.content), fp)) < 0) break;//将文件数据读入到content中 读不到数据则直接break循环
//只要向文件描述符中写入 tcp服务就会帮助发送消息
//With a zero flags argument, send is equivalent to write(2).
send(sockfd, (void *)&packet, sizeof(packet), 0);
memset(packet.content, 0, sizeof(packet.content));
}
fclose(fp);
return 0;
}
//ctrl+c信号处理
void closeSock(int signum) {
send(sockfd, "I am leaving...", 27, 0);
close(sockfd);//关闭客户端文件描述符
exit(0);
}
int main(int argc, char *argv[]) {
//./a.out ip port filename
if (argc != 4) {
fprintf(stderr, "Usage : %s ip port filename\n", argv[0]);
exit(1);
}
int port = atoi(argv[2]);
char ip[20] = {0}; strcpy(ip, argv[1]);
char filename[50]; strcpy(filename, argv[3]);
signal(SIGINT, closeSock);
//1.建立连接connect
if ((sockfd = socketConnect(ip, port)) < 0) handle_error("socketConnect");
printf("connect sccuess!\n");
//2.接受一个ack
int ack = 0;
int rsize;
if (rsize = recv(sockfd, (void *)&ack, sizeof(int), 0) == -1) handle_error("ack recv");
if (ack != 1) {//若从服务端没有接受到ack直接关闭文件描述符
printf("ack accpte fail. ack = %d\n", ack);
close(sockfd);
exit(0);
}
printf("ack accpte success. ack = %d\n", ack);
//3.将本地文件发送出去
printf("sending file to server...\n");
send_file(sockfd, filename);
//4.断开连接
close(sockfd);
return 0;
}
编译1.server.c与2.client.c文件,在终端窗口执行启动server程序,
在另外一个窗口启动客户端程序,将1.server.c文件进行发送。
在server的窗口中可以看到出现了6次凑包的过程,最终数据成功写入./port:ip
目录下(写入位置在server.c中第76行filepath
)
血的教训:如果使用fwrite写入文件出现乱码的现象,大概率是写入的
filesize
设置出现错误,而不是文件字符集编码的问题。
- 包的大小是:
ssize_t packet_size = sizeof(packet);
,而不是ssize_t packet_size = sizeof(packet.content);
- 由于包大小判断错误,导致写入文件后出现大量乱码问题,且文件写入不完整。