zoukankan      html  css  js  c++  java
  • 文件传输

    文件传输

     

    前面的案例都是传输字符串.另一种常见的情况,就是在服务端和client之间传输文件.

     

    计入,client显示了一个菜单:当输入S1,S2S3,分别向服务端发送文件Client01.jpg,Client02.jpg,Client02.jpg;当输入R1,R2R3,分别从服务端接受文件Server01.jpg,Server02.jpg,Server03.jpg.那么,怎样完毕这项功能呢?

     

    方法1:类似FTP协议,服务端监听两个port:一个称为控制port,用于接受各种命令字符串(仅仅是接受或发送文件);一个称为数据port,用于传输实际数据,就是发送和接收文件

     

    方法2:服务端仅仅监听一个port,用于接受来自client的命令字符串,称为控制port.client在发出命令字符串之前,先开辟一个本地port专用于收发文件,然后向服务端发送命令字符串,当中包括了client专用于收发数据的port号.服务端接收到请求之后,连接至client专用于收发文件的port,然后数据传输,结束后关闭连接.

     

    如今仅仅关注两种方式中的数据port,当用法1的时候,服务端的数据port须要同一时候为多个client的多次请求服务,因此须要採用异步方式;用法2,服务端与client在每次数据传输时都会又一次建立新的连接,在传输完毕后马上关闭,因此在数据port上传输文件时不须要採用异步方式.可是在控制port仍然须要使用异步方式.

     

    咱们用法2来实现文件的接受.

     

     

    订立协议

     

    1.发送文件

     

    先看一下发送文件的情况:假设要将Client01.jpg由client发往服务端,能够归纳为下面几个步骤:

    (1).client对port进行监听,如果port号为8005.

    (2).如果客户输入了S1,则发送以下的控制字符串到服务端:[file=Client01.jpg,mode=send.port=8005].

    (3)服务端收到以后,依据clientIP和port号与该client建立连接

    (4)client向网络流羞辱数据,開始发送文件.

    (5)传送完成之后client和服务端关闭数据port的连接.

     

    类似之前的规则,订立的公布文件协议能够为:[file=Client01.jpg,mode=send,port=8005].可是,因为他是一个普通的字符串,在前面我们採用了正則表達式的方法来获得当中的有效值,但还有更好地办法----使用XML.对于上面的语句,我们能够写成这种XML:

     

    <protocol><file name=”Client01.jpg” mode=senf,port=”8005” /></protocol>


    这样处理起来就会方便多了,由于处理XML格式有更丰富的类型支持.接下来看一下接收文件的流程及其协议.

     

    2.接收文件

     

    接收文件与发送文件实际上全然类似,差别仅仅是:当发送文件时,由client向网络流写入数据;当接收文件时,由服务端想网络流介入数据.接受文件的过程例如以下:

    (1).client对port进行监听,如果port号为8006.

    (2).如果client输入了R1,则发送控制字符串:<protocol><file name=”Server01.jpg” mode=”receive” port=”8006”/></protocol>到服务端.

    (3).服务端接收到以后,依据clientIP和port号与该client建立连接.

    (4).服务端向网络流写入数据,開始发送文件.

    (5).传送完成后client和服务端关闭数据port的连接.

     

     

    协议处理类的实现

     

    在開始编写实际的服务端client代码之前,首先要编写处理协议的类,这个类须要两个功能:一是方便获取完整的协议信息,由于前面说过,服务端可能将client的多次独立请求拆分或合并.比方,client连续发送了两条控制信息到服务端,而服务端将它们合并了,则须要先拆开再分别处理.二是方便获取节点信息,由于协议是XML格式,所以还须要一个类专门对XML进行处理,获得节点的值.

     

    1.ProtocolHandler辅助类

     

    先看下ProtocolHandler,它与前面说过的ResquestHandler作用同样.须要注意的是必须将它声明为实例的,而非静态的,这是由于每一个TcpClient都须要相应一个ProtocolHandler,它内部维护的partialProtocol不能共享,在协议发送不完整的情况下,这个变量用于暂时保存被截断的字符串.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading.Tasks;
     
    namespace Server
    {
        public class ProtocolHandler
        {
            private string partialProtocol;//保存不完整的协议
            public ProtocolHandler()
            {
                partialProtocol = "";
            }
            public string[] GetProtocol(string input)
            {
                return GetProtocol(input,null);
            }
            //获得协议
            private string[] GetProtocol(string input, List<string>outputList)
            {
                if (outputList==null)
                {
                    outputList = new List<string>();
                }
                if (string.IsNullOrEmpty(input))
                {
                    return outputList.ToArray();
                }
                if (!string.IsNullOrEmpty(partialProtocol))
                {
                    input = partialProtocol + input;
                }
     
                string pattern = "(^<protocol>.*?

    </protocol>)"; //假设有匹配,说明已经找到了,是完整的协议 if (Regex.IsMatch(input,pattern)) { //获取匹配的值 string match = Regex.Match(input,pattern).Groups[0].Value; outputList.Add(match); partialProtocol = ""; //缩短input的长度 input = input.Substring(match.Length); //递归调用 GetProtocol(input,outputList); } else { //假设不匹配,说明协议长度不够 //那么先缓存,然后等待下一次请求 partialProtocol = input; } return outputList.ToArray(); } } }



    由于这个类不是重点,这里贴下代码就够了.

     

    2.FileRequestType枚举类型和FileProtocol结构

     

    由于XML是以字符串的形式进行传输,为了方便使用,最好构建一个强类型对它们进行操作,这样会方便非常多.首先能够定义FileRequestMode枚举类型,用来表示是发送文件还是接受文件:

        public enum FileRequestMode
        {
            Send=0,
            Receive
        }


    接下来再定义一个FileProtocol结构,用来为整个协议字符串提供强类型的訪问,注意这里覆盖了基类的ToString()方法,这样在client就不须要再手工去编写XML,仅仅要在FileProtocol结构值上调用ToString()即可了:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
     
    namespace Server
    {
        public struct FileProtocol
        {
            private readonly FileRequestMode mode;
            private readonly int port;
            private readonly string fileName;
     
            public FileProtocol(FileRequestMode mode, int port, string fileName)
            {
                this.mode = mode;
                this.port = port;
                this.fileName = fileName;
            }
            public FileRequestMode Mode { get { return mode; } }
     
            public int Port
            {
                get
                {
                    return port;
                }
            }
     
            public string FileName
            {
                get
                {
                    return fileName;
                }
            }
     
            public override string ToString()
            {
                return string.Format("<protocol><file name="{0}" mode="{1}" port="{2}"/></protocol>",fileName,mode,port);
            }
        }
    }


     

    3.ProtocolHelper辅助类

     

    这个类专用于将XML格式的协议映射为上面定义的强类型对象,这里没有增加try/catch异常处理,由于协议对用户来说是不可见的,而client应该总是发送正确的协议:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Xml;
     
    namespace Server
    {
        public class ProtocolHelper
        {
            private XmlNode fileNode;
            private XmlNode root;
     
            public ProtocolHelper(string protocol)
            {
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(protocol);
                root = doc.DocumentElement;
                fileNode = root.SelectSingleNode("file");
            }
     
            //此时的protocol已定位单条完整的protocol
            private FileRequestMode GetFileMode()
            {
                string mode = fileNode.Attributes["mode"].Value;
                mode = mode.ToLower();
                if (mode=="send")
                {
                    return FileRequestMode.Send;
                }
                else
                {
                    return FileRequestMode.Receive;
                }
            }
     
            //获取单条协议包括的信息
            public FileProtocol GetProtocol()
            {
                FileRequestMode mode = GetFileMode();
                string fileName = "";
                int port = 0;
                fileName = fileNode.Attributes["name"].Value;
                port = Convert.ToInt32(fileNode.Attributes["port"].Value);
     
                return new FileProtocol(mode,port,fileName);
            }
        }
    }


    关于XML部分的内容,我们会具体解说.

     

     

    client发送文件

     

    我们还是讲一个问题分为两部分来处理:先是client发送文件,然后是服务端接收文件.

     

    1.client的实现

     

    不着急实现clientS1,R1等用户菜单,首先完毕发送文件这一功能为前面的Client类再加入一个SendFile()方法.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading.Tasks;
    using System.Net;
    using System.Threading;
    using System.IO;
    using Server;
    namespace Client
    {
        public class Client
        {
            private const int BufferSize = 8192;
            private byte[] buffer;
            private TcpClient client;
            private NetworkStream streamToServer;
     
            public Client()
            {
                try
                {
                    client = new TcpClient();
                    client.Connect("192.168.3.19",8500);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    throw;
                }
                buffer = new byte[BufferSize];
     
                //打印连接到的server信息
                Console.WriteLine("Server Connected! Local: {0}-->Server:{1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
                streamToServer = client.GetStream();
            }
     
            //发送消息到server
            public void SendMessage(string msg)
            {
                byte[] temp = Encoding.Unicode.GetBytes(msg);//获得缓存
                try
                {
                    streamToServer.Write(temo,0,temp.Length);
                    Console.WriteLine("Sent: {0}", msg);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    throw;
                }
            }
     
            //发送文件--异步方法
            public void BeginSendFile(string filePath)
            {
                ParameterizedThreadStart start = new ParameterizedThreadStart(BeginSendFile);
                start.BeginInvoke(filePath,null,null);
            }
     
            private void BeginSendFile(object obj)
            {
                string filePath = obj as string;
                SendFile(filePath);
            }
            //发送文件
            public void SendFile(string filePath)
            {
                IPAddress ip = IPAddress.Parse("192.168.3.19");
                TcpListener listener = new TcpListener(ip,0);
                listener.Start();
     
                //获取本地监听的port号
                IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
                int listeningPort = endPoint.Port;
     
                //获取发送的协议字符串
                string fileName = Path.GetFileName(filePath);
                FileProtocol protocol = new FileProtocol(FileRequestMode.Send,listeningPort,fileName);
                string pro = protocol.ToString();
     
                SendMessage(pro);//发送协议到服务端
     
                //中断,等待远程连接
                TcpClient localClient = listener.AcceptTcpClient();
                Console.WriteLine("Start sending file...");
                NetworkStream stream = localClient.GetStream();
     
                //创建文件流
                FileStream fs = new FileStream(filePath,FileMode.Open,FileAccess.Read);
     
                byte[] fileBuffer = new byte[1024];
                //每次传输1KB
     
                int bytesRead;
                int totalBytes = 0;
     
                //创建获取文件发送状态的类
                SendStatus status = new SendStatus();
     
                //将文件流转写入网络流
                try
                {
                    do
                    {
                        Thread.Sleep(10);//模拟远程传输视觉效果,暂停10秒
                        bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);
                        stream.Write(fileBuffer, 0, bytesRead);
                        totalBytes += bytesRead;
                        status.PrintStatus(totalBytes);
                    } while (bytesRead > 0);
                    Console.WriteLine("Total {0} bytes sent ,Done!", totalBytes);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                finally
                {
                    stream.Dispose();
                    fs.Dispose();
                    localClient.Close();
                    listener.Stop();
                }
            }
        }
    }
     


    SendStatus:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
     
    namespace Client
    {
        public class SendStatus
        {
            private FileInfo info;
            private long fileBytes;
     
            public SendStatus(string filePath)
            {
                info = new FileInfo(filePath);
                fileBytes = info.Length;        
            }
     
            public void PrintStatus(int sent)
            {
                string percent = GetPercent(sent);
                Console.WriteLine("Sending {0} bytes, {1}% ...",sent,percent);
            }
            //获得文件发送的百分比
            private string GetPercent(int sent)
            {
                decimal allBytes = Convert.ToDecimal(fileBytes);
                decimal currentSent = Convert.ToDecimal(sent);
                decimal percent = (currentSent / allBytes) * 100;
     
                percent = Math.Round(percent,1);//保留一位小数
     
                if (percent.ToString()=="100.0")
                {
                    return "100";
                }
                else
                {
                    return percent.ToString();
                }
            }
        }
    }
     


    主程序:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
     
    namespace Client
    {
        class Program
        {
            static void Main(string[] args)
            {
                ConsoleKey key;
                Client c = new Client();
                string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg";
                if (File.Exists(filePath))
                {
                    c.BeginSendFile(filePath);
                }
                Console.WriteLine("
    
    按Q退出");
                do
                {
                    key = Console.ReadKey(true).Key;
                } while (key!=ConsoleKey.Q);            
            }
        }
    }
     


    这段代码游侠买几个地方须要注意:

    1.)Main()方法中能够看到,图片的位置为应用程序所在的文件夹.假设处于调试模式,那么在项目的Bin文件夹下的Debug文件夹中防止三种图片Client01.jpg,Client02.jpg,Client03.jpg,作为測试目的.

    2.)client提供了一个SendFile()和两个BeginSendFile()方法,分别用于同步和异步传输,当中私有的BeginSendFile()方法仅仅是个辅助方法,实际上对于发送文件这种操作,集合总是须要使用异步方式.

    3.)另外编写了一个SendStatus,用来记录和打印完毕的状态,已经发送了多少字节.

     

     

    2.服务端的实现

     

    服务端要完毕的就是解析协议,连接至client,依据协议内容,推断时发送文件还是接收文件,以下是代码部分:

     

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
     
    namespace Server
    {
        public class Server
        {
            private TcpClient client;
            private NetworkStream streamToClient;
            private const int BufferSize = 8192;
            private byte[] buffer;
            private ProtocolHandler handler;
     
     
            public Server(TcpClient client)
            {
                this.client = client;
     
                //打印连接到的客户端信息
                Console.WriteLine("ClientConnected! Local :{0}<--Client: {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
     
                //获得流
                streamToClient = client.GetStream();
                buffer = new byte[BufferSize];
     
                handler = new ProtocolHandler();
                    
            }
     
            //開始进行读取
            public void BeginRead()
            {
                AsyncCallback callBack = new AsyncCallback(OnReadComplete);
                streamToClient.BeginRead(buffer,0,BufferSize,callBack,null);
            }
            //读取完毕时进行回调
            private void OnReadComplete(IAsyncResult ar)
            {
                int bytesRead = 0;
                try
                {
                    bytesRead = streamToClient.EndRead(ar);
                    Console.WriteLine("Reading data,{0} bytes...",bytesRead);
                    if (bytesRead==0)
                    {
                        Console.WriteLine("Client offline.");
                        return;
                    }
                    string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                    Array.Clear(buffer,0,buffer.Length);//清空缓存,避免脏读
     
                    //获取protocol数组
                    string[] protocolArray = handler.GetProtocol(msg);
                    foreach (string pro in protocolArray)
                    {
                        //这里异步调用,不然这里会比較耗时
                        ParameterizedThreadStart start = new ParameterizedThreadStart(handleProtocol);
                        start.BeginInvoke(pro,null,null);
                    }
                    //再次调用BeginRead(),完毕时调用自身,形成无限循环
                    AsyncCallback callBack = new AsyncCallback(OnReadComplete);
                    streamToClient.BeginRead(buffer,0,BufferSize,callBack,null);
                }
                catch (Exception ex)
                {
                    if (streamToClient!=null)
                    {
                        streamToClient.Dispose();
                    }
                    client.Close();
                    Console.WriteLine(ex.Message);                
                }
            }
            //处理protocol
            private void handleProtocol(object obj)
            {
                string pro = obj as string;
                ProtocolHelper helper = new ProtocolHelper(pro);
                FileProtocol protocol = helper.GetProtocol();
     
                if (protocol.Mode==FileRequestMode.Send)
                {
                    //客户端发送文件,对服务端来说则是接收文件
                    receiveFile(protocol);
                }
                else if (protocol.Mode==FileRequestMode.Receive)
                {
                    //客户端接收文件,对服务端来说是发送文件
                    //sendFile(protocol);
                }
            }
            //接收文件
            private void receiveFile(FileProtocol protocol)
            {
                //获取远程客户端的位置
                IPEndPoint endPoint = client.Client.RemoteEndPoint as IPEndPoint;
                IPAddress ip = endPoint.Address;
     
                //使用新port,获得远程用于接收文件的port
                endPoint = new IPEndPoint(ip,protocol.Port);
     
                //连接到远程客户端
                TcpClient localClient;
                try
                {
                    localClient = new TcpClient();
                    localClient.Connect(endPoint);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("无法连接到客户端 --> {0}",endPoint);
                    return;
                }
     
                //获取发送文件的流
                NetworkStream streamToCLient = localClient.GetStream();
     
                //随机生成一个在当前文件夹下的文件名
                string path = Environment.CurrentDirectory + "/" + generateFileName(protocol.FileName);
     
                byte[] fileBuffer = new byte[1024];//每次收1KB
                FileStream fs = new FileStream(path,FileMode.CreateNew,FileAccess.Write);
     
                //从缓存Buffer中读入到文件流中
                int bytesRead;
                int totalBytes = 0;
                do
                {
                    bytesRead = streamToCLient.Read(buffer,0,BufferSize);
     
                    fs.Write(buffer, 0, bytesRead);
                    totalBytes += bytesRead;
                    Console.WriteLine("Receiving {0} bytes ...",totalBytes);
                } while (bytesRead>0);
                Console.WriteLine("Total {0} bytes received,Done! ",totalBytes);
     
                streamToClient.Dispose();
                fs.Dispose();
                localClient.Close();
            }
            //随机获取一个图片名称
            private string generateFileName(string fileName)
            {
                DateTime now = DateTime.Now;
                return string.Format("{0}_{1}_{2}_{3}",now.Minute,now.Second,now.Millisecond,fileName);
            }
        }
    }
     

     

    主程序例如以下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading.Tasks;
     
    namespace Server
    {    
        class Program
        {
            static void Main(string[] args)
            {
     
                Console.WriteLine("Server is running...");
                IPAddress ip = IPAddress.Parse("192.168.1.113");
                TcpListener listener = new TcpListener(ip,8500);
                listener.Start();
                Console.WriteLine("Start Listening...");
                while (true)
                {
                    //获取一个连接,同步方法,在此处中断
                    TcpClient client = listener.AcceptTcpClient();
                    Server wapper = new Server(client);
                    wapper.BeginRead();
                }
            }
        }
    }
     


    这段代码须要注意这么几点:

    1.在OnReadComplete()回调方法的foreach循环中,使用托付异步调用了handlerProtocol()方法,这是由于handlerProtocol即即将运行的是一个读取或接收文件的操作,也就是一个相对耗时的操作.

    2.在handlerProtocol()方法中,能够体会到定义ProtocolHelper类和FileProtocol结构的优点,假设不定义它们,这里将是杂乱的处理XML以及类型转换的代码.

    3.handlerProtocol()方法中进行了一个条件推断,注意sendFile()被屏蔽了,这里还没有为服务单发送文件至client.

    4.receiveFile()方法是实际接受client发来文件的方法,这里没有什么特别之处.须要注意的是文件存储的路径,它保存在了当前程序运行的文件夹下,文件的名称使用GetFileName()方法生成一个与时间有关的随机名称.

     

    最后我想说一点,由于楼主中途换个一个教室,程序怎么都出错,结果搞了半天发现是IP地址出了问题,每一个教室的IP地址可能不同,另一点须要注意,别忘了加入对解决方式的引用,要不然会报错.

     

    无论怎么说,程序算是完毕了,楼主这里用了一个40M大小的文件进行測试(原本是一个avi,楼主改了一下后缀名)....程序是如此的慢,为啥呢?由于咱们一次仅仅传输1KB...

     

    到如今为止,咱们关于网络编程的内容算是搞一个段落了,开头第一部分解说了TCP协议,套接字,即时通信程序三种开发模式,以及两个基本操作----监听port和连接远程server.

     

    第二部分是一个简单的实例:从client传输字符串到server,server接受并处理字符串,在将处理过的字符串发回给client.

     

    第三部分是异步传输和文本边界,这是对第二部分的强化.

     

    第四部分是发送文件.

  • 相关阅读:
    hdu.1226.超级密码(bfs)
    zstu.2512. Moving Tables(贪心)
    zoj.3868.GCD Expectation(数学推导>>容斥原理)
    acdream.LCM Challenge(数学推导)
    acdream.Triangles(数学推导)
    acdream.A Very Easy Triangle Counting Game(数学推导)
    acdream.Bet(数学推导)
    acdream.郭式树(数学推导)
    EntityFramework(转自wiki)
    石墨烯(转自wiki)
  • 原文地址:https://www.cnblogs.com/yfceshi/p/7283527.html
Copyright © 2011-2022 走看看