首页 > 编程语言 >c#使用TCP协议在局域网中传输数据

c#使用TCP协议在局域网中传输数据

时间:2024-07-05 21:27:01浏览次数:13  
标签:文件 字节 c# TCP 传输数据 buffer new byte networkStream

现实中会遇到一种情况,需要从一台电脑上将文件转移到另一台电脑时,通常会选用网络进行传输,或者使用移动存储设备进行传输。但前者传输速度受限,后者需要跑来跑去非常得麻烦。

一般这种情况,两台电脑连接的都是同一个网络,处在同一个局域网中,如果使用局域网来传输文件,岂不是不会受到上述两种方式的限制,何不美哉。

收集资料后得知,TCP连接需要接收方启动TCP服务,监听发送过来的消息。

启动TCP服务器代码如下,其中TcpHelper.ReceiveDataInTcp方法用来处理接收到的请求,具体实现,接下来再说。

 Task.Run(async () =>
{
    try
    {
        //启动TCP监听
        TcpListener listener = new TcpListener(IPAddress.Any, 5000);
        listener.Start();
        while (true)
        {
            try
            {
                TcpClient client = await listener.AcceptTcpClientAsync();
                //处理接收到的请求
                TcpHelper.ReceiveDataInTcp(client);
            }
            catch (Exception ex)
            {

            }
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"启动TCP服务失败:" + ex.Message);
    }
});

通常使用TCP传输的数据有两种,第一种是传输一个字符串,另一个传输文件,为了区别这两种类型数据,我将发送的NetworkStream数据中的第一个字节作为传输的数据的类型标识符,1表示传输的是字符串,2表示传输的是文件。

以下为使用TCP发送字符串的代码,将要发送的NetworkStream类型数据的第一个字节设置为0x01,表示发送的数据类型为字符串,在发送之前需要先于目标计算机进行连接。

        /// <summary>
        /// 发送字符串
        /// 消息的第一个字节用来表示数据类型:0x01 表示字符串,0x02 表示文件
        /// </summary>
        /// <param name="serverIp">目标IP地址</param>
        /// <param name="serverPort">目标端口地址</param>
        /// <param name="message">要发送的消息</param>
        /// <returns></returns>
        public static async Task SendStringAsync(string serverIp, int serverPort, string message, TcpClient client)
        {
            using (client)
            {
                await client.ConnectAsync(serverIp, serverPort);
                using (NetworkStream networkStream = client.GetStream())
                {
                    byte[] dataType = new byte[] { 0x01 }; // 字符串类型
                    await networkStream.WriteAsync(dataType, 0, dataType.Length);

                    byte[] data = Encoding.UTF8.GetBytes(message);
                    await networkStream.WriteAsync(data, 0, data.Length);
                }
            }
        }

如果需要发送文件的话,步骤要比发送字符串麻烦许多。在发送的数据中我们不仅要让目标计算机知道我们发送的数据的类型,还要让目标计算机知道我们传输的文件的名称,所以我们需要把带后缀(拓展名)的文件名称一起写入到NetworkStream类型数据中进行传输。

如果只是将文件名称一起写入会导致出现一个问题,目标计算机在接收我方发送的信息时,由于不知道文件名称所占的长度,导致文件名称以及文件本身都无法正确地读取。因此在传输之前需要将文件名称的长度也一起进行传输。

将文件的长度写入到NetworkStream类型数据中,也会占用一定的长度,如果占用的长度不固定,也会导致目标计算机无法正确地读取数据。查询资料后得知,windows默认情况下一个文件的最大名称长度是256个字符,如果解开限制的话,长度可能会更长,因此只使用一个字节来保存文件名称的长度并不合适,于是便使用两个字节保存文件名称的长度,传输文件的代码如下:

/// <summary>
/// 发送文件
/// 消息的第一个字节用来表示数据类型:0x01 表示字符串,0x02 表示文件
/// </summary>
/// <param name="serverIp">目标IP地址</param>
/// <param name="serverPort">目标端口地址</param>
/// <param name="filePath">要发送的文件的地址</param>
/// <returns></returns>
public static async Task SendFileAsync(string serverIp, int serverPort, string filePath)
{
    int BufferSize = 2048;
    using (TcpClient client = new TcpClient())
    {
        await client.ConnectAsync(serverIp, serverPort);
        using (NetworkStream networkStream = client.GetStream())
        {
            //第一个字节用来表示要传递的数据类型
            byte[] dataType = new byte[] { 0x02 }; // 文件类型
            await networkStream.WriteAsync(dataType, 0, dataType.Length);

            //获取目标文件带后缀的名称
            string fileName = Path.GetFileName(filePath);
            byte[] fileNameInByte = Encoding.UTF8.GetBytes(fileName);

            // 保存目标文件名称的长度(用2个字节)
            ushort fileNameLength = (ushort)fileNameInByte.Length;
            byte[] fileNameLengthBytes = BitConverter.GetBytes(fileNameLength);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(fileNameLengthBytes);
            }
            // 将目标文件的名称长度写入到流中
            await networkStream.WriteAsync(fileNameLengthBytes, 0, fileNameLengthBytes.Length);

            //将文件的名称写入流中
            networkStream.Write(fileNameInByte, 0, fileNameInByte.Length);


            byte[] buffer = new byte[BufferSize];
            using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                int bytesRead;
                while ((bytesRead = await fileStream.ReadAsync(buffer, 0, BufferSize)) > 0)
                {
                    await networkStream.WriteAsync(buffer, 0, bytesRead);
                }
            }
        }
    }
}

现在我们已经可以发送数据了,接下来是接收数据以及处理接收数据的方法,即文章开头的ReceiveDataInTcp方法,代码如下。

        /// <summary>
        /// TCP接收信息
        /// </summary>
        /// <returns></returns>
        public static async Task ReceiveDataInTcp(TcpClient client)
        {
            await Task.Delay(0);//进入异步
            int BufferSize = 2048;
            try
            {
                using (client)
                using (NetworkStream networkStream = client.GetStream())
                {
                    //分配一个缓冲区 buffer 存储读取的数据
                    byte[] buffer = new byte[BufferSize];
                    int bytesRead = networkStream.Read(buffer, 0, 1);

                    if (bytesRead > 0)
                    {
                        //根据请求第一个字节判断是文件还是字符串数据
                        byte dataType = buffer[0];

                        if (dataType == 0x01) // 字符串
                        {
                            GetStringInTcp(networkStream);
                        }
                        else if (dataType == 0x02) // 文件
                        {
                            GetFileInTcp(client, networkStream);                            
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("TCP接收数据时发生错误:" + ex.Message);
            }
        }

GetStringInTcp代码如下:

        /// <summary>
        ///从TCP请求中获取到字符串数据
        /// </summary>
        /// <param name="client"></param>
        /// <param name="networkStream"></param>
        /// <returns></returns>
        public static async Task GetStringInTcp(NetworkStream networkStream)
        {
            int BufferSize = 2048;
            try
            {
                //分配一个缓冲区 buffer 存储读取的数据
                byte[] buffer = new byte[BufferSize];
                int bytesRead = 0;//用来统计每次取到的数据长度
                StringBuilder receivedData = new StringBuilder();
                while ((bytesRead = networkStream.Read(buffer, 0, BufferSize)) > 0)
                {
                    receivedData.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
                }
                Debug.WriteLine("Received string: " + receivedData.ToString());
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"An error occurred: {ex.Message}");
            }
        }

GetFileInTcp代码如下:

        /// <summary>
        /// 从TCP请求中获取到文件
        /// </summary>
        /// <param name="client"></param>
        /// <param name="networkStream"></param>
        /// <returns></returns>
        public static async Task GetFileInTcp(TcpClient client, NetworkStream networkStream)
        {
            int BufferSize = 2048;
            try
            {
                byte[] buffer = new byte[BufferSize];
                int bytesRead;

                // 读取文件名长度(2个字节)
                byte[] fileNameLengthBytes = new byte[2];
                await networkStream.ReadAsync(fileNameLengthBytes, 0, 2);

                //这个属性返回一个布尔值,表示当前系统的字节序。如果返回 true,说明当前系统是小端字节序;如果返回 false,说明是大端字节序
                if (BitConverter.IsLittleEndian)
                {
                    //这段代码的作用是确保字节序正确。具体来说,它用于在需要时调整字节顺序,以确保在不同字节序的系统之间正确读取和解释文件名长度。
                    Array.Reverse(fileNameLengthBytes);
                }
                //保存 16 位(2 字节)无符号整数,值的范围为 0 到 65,535
                ushort fileNameLength = BitConverter.ToUInt16(fileNameLengthBytes, 0);

                // 读取文件名
                bytesRead = networkStream.Read(buffer, 0, fileNameLength);
                string fileName = Encoding.UTF8.GetString(buffer, 0, fileNameLength);

                //处理接收文件的名称
                int fileNameIndex = 1;
                //获取不包括扩展名(后缀)的文件名
                string fileNameWithoutExtension=Path.GetFileNameWithoutExtension(fileName);
                //获取文件的扩展名
                string fileExtension = Path.GetExtension(fileName);
                while (File.Exists(AppDomain.CurrentDomain.BaseDirectory + "downLoad\\" + fileName))
                {
                    fileName = fileNameWithoutExtension + $"{fileNameIndex}"+ fileExtension;
                    fileNameIndex++;
                }

                // 接收文件内容并保存
                using (FileStream fileStream = new FileStream(AppDomain.CurrentDomain.BaseDirectory + "downLoad\\" + fileName, FileMode.Create, FileAccess.Write))
                {
                    while ((bytesRead = networkStream.Read(buffer, 0, BufferSize)) > 0)
                    {
                        fileStream.Write(buffer, 0, bytesRead);
                    }
                }
                Debug.WriteLine($"File '{fileName}' received successfully.");
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"An error occurred: {ex.Message}");
            }
        }

此时,我们已经能够正常地使用TCP在局域网中进行通信了,但是还不够完美。使用互联网传输文件的时候,我们能够看到文件的名称,大小以及下载的进度。
在上述的代码中,传输的数据中已经有文件的名称了,我们还需要在传输的数据中加上文件的大小。我在此使用long类型来保存一个文件大小的字节数,并添加到传输的数据中即可(由于修改了传输数据的结构,接收数据的时候也要做出对应的修改)。

               //将文件的大小写到流中
               FileInfo fileInfo = new FileInfo(filePath);
               //fileInfo.Length值类型是long,转换出来的byte[]长度与sizeOf(Long)一致
               byte[] fileLengthInByte =BitConverter.GetBytes(fileInfo.Length);
               networkStream.Write(fileLengthInByte, 0, fileLengthInByte.Length);

现在传递的数据中已经包含了文件的名称,文件大小以及文件本身,现在还需要获取到传输文件时的进度。为实现这个功能我有两个想法,一个是使用Progress类型,另一个是使用yield return。最终我选择了后者(yield return需要语言版本为c#10及以上)。
通过接收返回的IAsyncEnumerable类型值来更新进度,需要注意一点,要对返回数据的速度做出限制,如果返回数据过快,更新UI时会对UI线程造成巨大的压力,因此我在此使用了Stopwatch类型来记录运行的时间,每超过100ms才返回一次值。发送文件的代码如下:

     /// <summary>
     /// 测试带进度的发送文件
     /// </summary>
     /// <param name="serverIp"></param>
     /// <param name="serverPort"></param>
     /// <param name="filePath"></param>
     /// <returns></returns>
     public static async IAsyncEnumerable<long> SendFileAsyncWithProgress(string serverIp, int serverPort, string filePath)
     {
         int bufferSize = 2048;
         long count = 0;//用来保存已发送的总量
         var yieldStopwatch = Stopwatch.StartNew();//用来统计已执行的时间
         using (TcpClient client = new TcpClient())
         {
             await client.ConnectAsync(serverIp, serverPort);
             using (NetworkStream networkStream = client.GetStream())
             {
                 //第一个字节用来表示要传递的数据类型
                 byte[] dataType = new byte[] { 0x02 }; // 文件类型
                 await networkStream.WriteAsync(dataType, 0, dataType.Length);

                 //获取目标文件带后缀的名称
                 string fileName = Path.GetFileName(filePath);
                 byte[] fileNameInByte = Encoding.UTF8.GetBytes(fileName);

                 // 保存目标文件名称的长度(用2个字节)
                 ushort fileNameLength = (ushort)fileNameInByte.Length;
                 byte[] fileNameLengthBytes = BitConverter.GetBytes(fileNameLength);
                 if (BitConverter.IsLittleEndian)
                 {
                     Array.Reverse(fileNameLengthBytes);
                 }
                 // 将目标文件的名称长度写入到流中
                 await networkStream.WriteAsync(fileNameLengthBytes, 0, fileNameLengthBytes.Length);

                 //将文件的名称写入流中
                 networkStream.Write(fileNameInByte, 0, fileNameInByte.Length);

                 //将文件的大小写到流中
                 FileInfo fileInfo = new FileInfo(filePath);
                 //fileInfo.Length值类型是long,转换出来的byte[]长度与sizeOf(Long)一致
                 byte[] fileLengthInByte =BitConverter.GetBytes(fileInfo.Length);
                 networkStream.Write(fileLengthInByte, 0, fileLengthInByte.Length);

                 //发送文件
                 byte[] buffer = new byte[bufferSize];
                 using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                 {
                     int bytesRead;
                     while ((bytesRead = await fileStream.ReadAsync(buffer, 0, bufferSize)) > 0)
                     {
                         await networkStream.WriteAsync(buffer, 0, bytesRead);
                         count += bytesRead;
                         //每100ms返回一次进度
                         if (yieldStopwatch.ElapsedMilliseconds >= 100)
                         {
                             //重置计时器
                             yieldStopwatch.Restart();
                             yield return count;
                         }
                     }
                 }
             }
         }
         //完成就返回-1
         yield return -1;
     }

接收文件的代码如下(建议将获取文件名称,文件大小以及进度的功能分别拆分成3个方法)。

        /// <summary>
        /// 带进度的接收文件
        /// </summary>
        /// <param name="networkStream"></param>
        /// <returns></returns>
        public static async IAsyncEnumerable<long> GetFileInTcpWithProgress(NetworkStream networkStream)
        {

            var yieldStopwatch = Stopwatch.StartNew();//用来统计已执行的时间
            long count = 0;//用来保存已接收的总量
            int BufferSize = 2048;

            byte[] buffer = new byte[BufferSize];
            int bytesRead;

            // 读取文件名长度(2个字节)
            byte[] fileNameLengthBytes = new byte[2];
            await networkStream.ReadAsync(fileNameLengthBytes, 0, 2);

            //这个属性返回一个布尔值,表示当前系统的字节序。如果返回 true,说明当前系统是小端字节序;如果返回 false,说明是大端字节序
            if (BitConverter.IsLittleEndian)
            {
                //这段代码的作用是确保字节序正确。具体来说,它用于在需要时调整字节顺序,以确保在不同字节序的系统之间正确读取和解释文件名长度。
                Array.Reverse(fileNameLengthBytes);
            }
            //保存 16 位(2 字节)无符号整数,值的范围为 0 到 65,535
            ushort fileNameLength = BitConverter.ToUInt16(fileNameLengthBytes, 0);

            // 读取文件名
            bytesRead = networkStream.Read(buffer, 0, fileNameLength);
            string fileName = Encoding.UTF8.GetString(buffer, 0, fileNameLength);

            //处理接收文件的名称
            int fileNameIndex = 1;
            //获取不包括扩展名(后缀)的文件名
            string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
            //获取文件的扩展名
            string fileExtension = Path.GetExtension(fileName);
            while (File.Exists(AppDomain.CurrentDomain.BaseDirectory + "downLoad\\" + fileName))
            {
                fileName = fileNameWithoutExtension + $"{fileNameIndex}" + fileExtension;
                fileNameIndex++;
            }

            // 接收文件内容并保存
            using (FileStream fileStream = new FileStream(AppDomain.CurrentDomain.BaseDirectory + "downLoad\\" + fileName, FileMode.Create, FileAccess.Write))
            {
                while ((bytesRead = networkStream.Read(buffer, 0, BufferSize)) > 0)
                {
                    count += bytesRead;
                    fileStream.Write(buffer, 0, bytesRead);
                    //每隔100ms返回一次进度
                    if (yieldStopwatch.ElapsedMilliseconds >= 100)
                    {
                        yield return count;
                        yieldStopwatch.Restart();
                    }
                }
                //接收完成就返回-1
                yield return -1;
            }
            Debug.WriteLine($"File '{fileName}' received successfully.");
        }

如果使用图形化界面,可以使用UDP广播发送自己的IP地址以及正在监听的端口,让其他计算机能够发现当前计算机,于是就可以不用手动填写IP地址和端口号。

标签:文件,字节,c#,TCP,传输数据,buffer,new,byte,networkStream
From: https://blog.csdn.net/KingOfBadLuck/article/details/140211484

相关文章

  • Python基于卷积神经网络分类模型(CNN分类算法)实现时装类别识别项目实战
    说明:这是一个机器学习实战项目(附带数据+代码+文档+视频讲解),如需数据+代码+文档+视频讲解可以直接到文章最后获取。1.项目背景在深度学习领域,卷积神经网络(ConvolutionalNeuralNetworks,CNNs)因其在图像识别和分类任务上的卓越表现而备受关注。CNNs能够自动检测图像中的特......
  • Python实现ABC人工蜂群优化算法优化循环神经网络分类模型(LSTM分类算法)项目实战
    说明:这是一个机器学习实战项目(附带数据+代码+文档+视频讲解),如需数据+代码+文档+视频讲解可以直接到文章最后获取。1.项目背景人工蜂群算法(ArtificialBeeColony,ABC)是由Karaboga于2005年提出的一种新颖的基于群智能的全局优化算法,其直观背景来源于蜂群的采蜜行为,蜜蜂根......
  • Linux remoteproc子系统(基于STM32MP157)概览
    remoteproc(RemoteProcessorFramework)用于管理异构远程处理器设备。这些设备通常在非对称多处理(AsymmetricMultiProcessing,AMP)配置中,可能运行不同的操作系统实例,包括Linux或其他实时操作系统的变体。remoteproc框架允许不同平台或架构控制远程处理器(例如,开启电源、加载固件......
  • 学习笔记(0):重拾Halcon
    目录前言教学视频前言了解我的人可能知道,我其实很想回去全职做外贸,但是大环境不好,淘宝做了3个月,1688做了1个月。我只能说销量很惨淡。现在打算还是老老实实上班去了。教学视频我之前找一个B站UP主,买了一下他的教学视频。600块钱,总共有40集,大概10个小时。大概需要一个星期学完,......
  • Golang channel底层是如何实现的?(深度好文)
    Hi你好,我是k哥。大厂搬砖6年的后端程序员。我们知道,Go语言为了方便使用者,提供了简单、安全的协程数据同步和通信机制,channel。那我们知道channel底层是如何实现的吗?今天k哥就来聊聊channel的底层实现原理。同时,为了验证我们是否掌握了channel的实现原理,本文也收集了channel的高......
  • 第8章:Electron 剪贴版和消息通知
    在本章中,我们将介绍如何在Electron应用中与操作系统进行集成。这些操作包括剪贴板操作、通知系统、原生对话框等功能。8.1剪贴板操作Electron提供了clipboard模块,允许我们在应用中访问和操作剪贴板内容。以下是一些基本的剪贴板操作示例。8.1.1复制文本到剪贴板我......
  • 417、基于51单片机的热水器(燃气,温度,LCD1602,阀门PID)(程序+Proteus仿真+原理图+流程图+
    毕设帮助、开题指导、技术解答(有偿)见文未目录方案选择单片机的选择显示器选择方案一、设计功能二、Proteus仿真图单片机模块设计三、原理图四、程序源码资料包括:需要完整的资料可以点击下面的名片加下我,找我要资源压缩包的百度网盘下载地址及提取码。方案选择......
  • Javascript中Object、Array、String
    Object在JavaScript中,Object 类型是一种复杂的数据类型,用于存储键值对集合。它提供了多种方法来操作这些键值对,以及执行其他常见的操作。这里,我列出了一些 Object 类型的常见方法或特性,它们在日常编程中非常有用:属性访问点符号(.):如果属性名是一个有效的标识符(例如,没有空格......
  • autoware.universe源码略读(3.5)--perception:compare_map_segmentation/crosswalk_tra
    autoware.universe源码略读3.5--perception:compare_map_segmentation/crosswalk_traffic_light_estimatorcompare_map_segmentationcompare_elevation_map_filter_nodedistance_based_compare_map_filter_nodeletvoxel_based_approximate_compare_map_filter_nodeletvox......
  • 「杂题乱刷2」CF1454F Array Partition
    题目链接CF1454FArrayPartition解题思路我们发现显然第一个和第三个区间的值区间随着长度的增大而增大。于是我们就可以枚举第一个区间的右端点位置,然后现在问题就转化成了找到一个断点来确定第二,三个区间的长度,由于前文提到的第三个区间的值区间随着长度的增大而增大,于是我......