需求
客户端(安卓手机)每帧(一秒30帧)向服务端传送想要的文件名,服务端向客户端传送文件
过程
在传文件这个过程中,主要困在了传文件总会少几KB的bug。
导致传输的中间文件不可用。
经过探究,主要在于以下几个知识点(或者未知点):
- 服务器和客户端的缓冲数组大小没有说非要一致大小,原来以为服务器发送用1024字节的数组分块发,那客户端也要用1024字节的数组分块接受。但不是,两边可以不一致。
客户端每次会不一定能接受多少,比如服务端发送了一个1024的块,客户端用1024的缓冲接,发现只接受了880的字节,下一次再接会是144的字节。不一定能接多少。但TCP能保证这些字节的顺序及块的顺序(实践得来)
-
由于第一条,所以服务器不用纠结与数组的大小,直接用了一个函数,就传走了文件
sendfile
-
客户端 是c# unity android
服务器传输了639KB的中间文件,但客户端每次就接受了636KB的文件,少了3KB,导致中间文件不可用。
经过探究,(错误,会造成文件损耗)原本客户端每次接受一个块后,就用binarywriter 往创建好的文件写入这部分数据。
(正确,没有损耗)后来先把文件大小传输过来
并使用list 存储每次接收到的字节块,然后接受完毕后,用File.writeallbyte全部写入文件。就没有任何损失且可以正常解码。
服务端的代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include<string>
#include<iostream>
#include<vector>
#include<sys/stat.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#define PORT 8081 //服务器端监听端口号
#define MAX_BUFFER 1024 //数据缓冲区最大值
using namespace std;
// void sendfile(string filepath,string filename,int client_sockfd)
// /* 传输一个文件
// in:
// filename: 文件名,客户端将以同样的文件名存储
// client_sockfd: 已经绑定端口并监听的socket文件句柄号
// */
// {
// string start="START";
// char buf[1024] = { 0 };
// strcpy(buf, start.c_str());
// write(client_sockfd,buf,MAX_BUFFER);
// memset(buf, 0, sizeof(buf));
// string name =filename;
// strcpy(buf, name.c_str());
// write(client_sockfd,buf,MAX_BUFFER);
// memset(buf, 0, sizeof(buf));
// int bytes;
// FILE* fp =NULL;
// if((fp = fopen(&filepath[0], "rb"))==NULL)
// {
// printf("打开文件失败");
// exit(0);
// }
// while ((bytes = fread(buf, 1, 1024, fp) )> 0)//读取文件内容,每次一个1024字节
// {
// // sleep(1);
// //cout<<bytes<<endl;
// // for(auto byte:buf)
// // {
// // cout<<byte<<" ";
// // }
// // cout<<endl;`
// write(client_sockfd,buf,MAX_BUFFER);//把buf里存的东西send给服务端
// memset(buf, 0, sizeof(buf));
// }
// sleep(1);
// string end="END";
// strcpy(buf, end.c_str());
// // for(auto byte:buf)
// // {
// // cout<<byte<<" ";
// // }
// // cout<<endl;
// write(client_sockfd,buf,MAX_BUFFER);
// memset(buf, 0, sizeof(buf));
// }
int main()
{
struct sockaddr_in server_addr, client_addr;
int server_sockfd, client_sockfd;
int size, write_size;
int filehandle;
char buffer[MAX_BUFFER];
struct stat obj;
if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) //创建Socket
{
perror("Socket Created Failed!\n");
exit(1);
}
printf("Socket Create Success!\n");
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
bzero(&(server_addr.sin_zero), 8);
int opt = 1;
int res = setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //设置地址复用
if (res < 0)
{
perror("Server reuse address failed!\n");
exit(1);
}
if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) //绑定本地地址
{
perror("Socket Bind Failed!\n");
exit(1);
}
printf("Socket Bind Success!\n");
if (listen(server_sockfd, 5) == -1) //监听
{
perror("Listened Failed!\n");
exit(1);
}
printf("Listening ....\n");
socklen_t len = sizeof(client_addr);
printf("waiting connection...\n");
if ((client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len)) == -1) //等待客户端连接
{
perror("Accepted Failed!\n");
exit(1);
}
printf("connection established!\n");
printf("waiting message...\n");
// 开始传文件
//发送一个文件名
//bytes[]
// vector<string> filenames={"test1.txt","test2.txt","test3.txt","test4.txt"};
// for(auto filename:filenames)
// {
// string filepath ="output/"+filename;
// cout<<filename<<"已经传送完毕"<<endl;
// }
char buf[1024] = { 0 };
int total_byte=0;
int total_bags=0;
for(;;)
{
memset(buf,0,1024);
int rec_len = recv(client_sockfd,buf,sizeof(buf),0);
buf[rec_len]='\0';
if(rec_len<=0)
{
break;
}
cout<<"recive_len:"<<rec_len<<endl;
string filename=buf;
cout<<"接受到的文件名:"<<filename<<endl;
memset(buf, 0, sizeof(buf));
stat(filename.c_str(), &obj);
// FILE* fp =NULL;
// if((fp = fopen(&filename[0], "rb"))==NULL)
// {
// printf("打开文件失败");
// exit(0);
// }
// 发送文件大小
string filesize=to_string(obj.st_size);
cout<<filesize<<endl;
strcpy(buf, filesize.c_str());
write_size = write(client_sockfd, buf, MAX_BUFFER);
memset(buf, 0, sizeof(buf));
int fd=open(filename.c_str(), O_RDONLY);
int ret=sendfile(client_sockfd,fd,NULL,obj.st_size);
if(ret<0)
{
cout<<"发送文件失败"<<endl;
}
cout<<ret<<endl;
//sendf(client_sockfd,buf,MAX_BUFFER,0);
// int bytes=0;
// while ((bytes = fread(buf, 1,1024, fp) )> 0)//读取文件内容,每次一个4096字节
// {
// total_byte+=bytes;
// total_bags+=1;
// cout<<bytes<<endl;
// // for(auto byte:bytes)
// // {
// // cout<<byte<<" ";
// // }
// send(client_sockfd,buf,MAX_BUFFER,0);//把buf里存的东西send给服务端
// memset(buf, 0, sizeof(buf));
// }
close(filehandle);
}
close(client_sockfd); //关闭Socket
close(server_sockfd);
cout<<"total_byte:"<<total_byte<<endl;
cout<<"total_bags:"<<total_bags<<endl;
return 0;
}
客户端的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System;
using System.IO;
using static UnityEngine.Networking.UnityWebRequest;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.InteropServices;
using UnityEngine.XR;
using System.Linq;
public class TransferFile : MonoBehaviour
{
string editString = "hello wolrd"; //编辑框文字
Socket serverSocket; //服务器端socket
IPAddress ip; //主机ip
IPEndPoint ipEnd;
string recvStr; //接收的字符串
string sendStr; //发送的字符串
byte[] recvData = new byte[1024]; //接收的数据,必须为字节
byte[] sendData = new byte[1024]; //发送的数据,必须为字节
int recvLen; //接收的数据长度
Thread connectThread; //连接线程
public string filename;
public string filepath;
public string record_filename;
public string record_filepath;
int total_bytes;
int total_bags;
public int is_name = 0; // 0 代表准备接受下个文件
// 1 代表准备接受文件名
// 2 代表准备接受数据文件
BinaryWriter writer = null;
FileStream fs;
FileStream record_fs;
void init_log_file(string log_file_name)
{
record_filename = log_file_name; //log文件的名字
record_filepath = Application.persistentDataPath + "/" + record_filename;
File.WriteAllText(record_filepath, "");//初始化文件
}
void log_file(string debug_txt)
{
string millenSecond = DateTime.UtcNow.Millisecond.ToString();
//string content = string.Format("time:{0}_{1}\n {2}\n", DateTime.Now, millenSecond, debug_txt);
string content = string.Format("{0}\n", debug_txt);
// XXXXXXXXXX是debug的内容
File.AppendAllText(record_filepath, content);
}
//初始化
void InitSocket()
{
//定义服务器的IP和端口,端口与服务器对应
ip = IPAddress.Parse("81.6213218.123.212141"); //可以是局域网或互联网ip,此处是本机
ipEnd = new IPEndPoint(ip, 8081);
//开启一个线程连接,必须的,否则主线程卡死
try
{
connectThread = new Thread(new ThreadStart(SocketReceive));
connectThread.Start();
//log_file("连接成功");
}
catch {
//log_file("连接失败");
}
}
void SocketConnet()
{
if (serverSocket != null)
serverSocket.Close();
//定义套接字类型,必须在子线程中定义
print("ready to connect");
//连接
try
{
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Connect(ipEnd);
//log_file("connect success");
//log_file("连接成功时的serversocket"+serverSocket.ToString());
}
catch(Exception ex) {
Debug.Log("connect fail");
//log_file("connect fail");
}
输出初次连接收到的字符串
//recvLen = serverSocket.Receive(recvData);
//recvStr = Encoding.ASCII.GetString(recvData, 0, recvLen);
//print(recvStr);
}
void SocketSend(string sendStr)
{
//清空发送缓存
sendData = new byte[1024];
//数据类型转换
sendData = Encoding.ASCII.GetBytes(sendStr);
//发送
serverSocket.Send(sendData, sendData.Length, SocketFlags.None);
}
void SocketReceive()
{
SocketConnet();
//不断接收服务器发来的数据
try
{
SocketSend(filename);
//log_file("发送文件名成功");
}
catch (Exception e)
{
//og_file("发送文件名失败");
}
// 接受文件的大小
recvData = new byte[1024];
recvLen = serverSocket.Receive(recvData, 0, recvData.Length, SocketFlags.None);
log_file(Encoding.UTF8.GetString(recvData));
int filesize=int.Parse(Encoding.UTF8.GetString(recvData));
log_file("文件的大小:" + filesize);
int recv_size = 0;
//fs = new FileStream(filepath, FileMode.Create, FileAccess.Write);
//writer = new BinaryWriter(fs);
//log_file("创建文件流成功"); `
//List<byte> bytelist = new List<byte>();
//bytelist.AddRange(recvData);
//不断接收服务器发来的数据
total_bags = 0;
List<byte> byte_ls = new List<byte>();
while (recv_size<filesize)
{
recvData = new byte[1024];
int real_len=cut_byte(recvData);
recvLen = serverSocket.Receive(recvData,0,recvData.Length, SocketFlags.None);
recv_size += recvLen;
log_file(recvLen.ToString());
log_file("recv_size:"+recv_size.ToString());
total_bags += 1;
try
{
log_file("截取之前长度:"+recvData.Length);
recvData=recvData.Skip(0).Take(recvLen).ToArray();
log_file("截取之后长度:"+recvData.Length);
byte_ls.AddRange(recvData);
log_file("byte_ls长度:" + byte_ls.Count);
}
catch(Exception e)
{
log_file("未成功写入文件");
}
//log_file(total_bytes.ToString());
//log_file(total_bags.ToString());
}
log_file("最终字节数:" + byte_ls.Count);
log_file("最终字节数total_byte" + total_bytes);
File.WriteAllBytes(filepath, byte_ls.ToArray());
log_file("接受完毕");
}
int cut_byte(byte[] buffer)
{
int i = 0;
for (; i < buffer.Length; i++)
{
if (buffer[i] == '\0')
{
break;
}
}
return i;
}
void SocketQuit()
{
//关闭线程
if (connectThread != null)
{
connectThread.Interrupt();
connectThread.Abort();
}
//最后关闭服务器
if (serverSocket != null)
serverSocket.Close();
print("diconnect");
}
// Start is called before the first frame update
void Start()
{
init_log_file("Transfer_file_debug.txt");
filename = "longdress.drc";
filepath = Application.persistentDataPath + "/" + filename;
//File.WriteAllText(filepath, "");//初始化文件
InitSocket();
}
void OnDestroy()
{
//fs.Flush();
//fs.Close();
SocketQuit();
}
// Update is called once per frame
void Update()
{
}
}