【Linux系统与网络编程】16:Socket文件传输2 Socket文件传输2

2023年3月29日

Socket文件传输2


OVERVIEW

  • Socket文件传输2

在socket文件传输1中使用了一种在buff缓冲数组后添加的方式实现文件的传输。

  • 实现方法2:根据TCP在数据传输过程中进行粘包与拆包的过程,修改文件传输功能的实现。

客户端向服务端连续不断的发送数据包时,服务端接受的数据可能会出现几种情况:

  • case1:客户端发送的包和服务端接受的包大小刚好是相同的(整包),这时能够完全的打印出包中的内容。
  • case2:如果真正需要传输的数据比一个packet小,需要将packet存满tcp才会对齐进行发送,等待数据填满packet
  • case3:客户端发送的包大于服务端接受的包,则需要将发送的数据包进行截断,
  • packet_t:一份新的packet与原来能够完整打包数据的包一样大小。
  • packet_pre:存放截断后的数据包
  • offset:当前数据所占用的包的位置,如果包中新添加了数据需要更新offset:(offset += recv_size;)
  1. TCP传输的是字节流,其并没有一定的大小,会出现粘包与拆包的情况。
  2. 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(" 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(" Packet Assembly.n");
				memcpy((char *)&packet + offset, &packet_t, recv_size);//将packet_t中的数据转移到包中的位置
				offset += recv_size;
			} else {
				/* case3:拆包的情况 */
				printf(" 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(" %s:%d: n%sn", rsize, ip, port, packet.content);
	// else { close(sockfd); break; }
	printf(" : %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 portn", 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(" %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(" 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(" Packet Fragmentation.n");
				memcpy((char *)&packet + offset, &packet_t, recv_size);//将packet_t中的数据转移到包中的位置
				offset += recv_size;
			} else {
				/* case3:粘包的情况 */
				printf(" 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(" %s:%d: n%sn", rsize, ip, port, packet.content);
	// else { close(sockfd); break; }
	printf(" : %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 portn", 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(" %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 filenamen", 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 = %dn", ack);
		close(sockfd);
		exit(0);
	}
	printf("ack accpte success. ack = %dn", 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);
  • 由于包大小判断错误,导致写入文件后出现大量乱码问题,且文件写入不完整。

服务器托管,北京服务器托管,服务器租用 http://www.hhisp.net

hackdl

咨询热线/微信 13051898268