国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
C#網(wǎng)絡編程(訂立協(xié)議和發(fā)送文件) - Part.4

C#網(wǎng)絡編程(訂立協(xié)議和發(fā)送文件) - Part.4

文件傳輸

前面兩篇文章所使用的范例都是傳輸字符串,有的時候我們可能會想在服務端和客戶端之間傳遞文件。比如,考慮這樣一種情況,假如客戶端顯示了一個菜單,當我們輸入S1、S2或S3(S為Send縮寫)時,分別向服務端發(fā)送文件Client01.jpg、Client02.jpg、Client03.jpg;當我們輸入R1、R2或R3時(R為Receive縮寫),則分別從服務端接收文件Server01.jpg、Server02.jpg、Server03.jpg。那么,我們該如何完成這件事呢?此時可能有這樣兩種做法:

  • 類似于FTP協(xié)議,服務端開辟兩個端口,并持續(xù)對這兩個端口偵聽:一個用于接收字符串,類似于FTP的控制端口,它接收各種命令(接收或發(fā)送文件);一個用于傳輸數(shù)據(jù),也就是發(fā)送和接收文件。
  • 服務端只開辟一個端口,用于接收字符串,我們稱之為控制端口。當接到請求之后,根據(jù)請求內(nèi)容在客戶端開辟一個端口專用于文件傳輸,并在傳輸結(jié)束后關(guān)閉端口。

現(xiàn)在我們只關(guān)注于上面的數(shù)據(jù)端口,回憶一下在第二篇中我們所總結(jié)的,可以得出:當我們使用上面的方法一時,服務端的數(shù)據(jù)端口可以為多個客戶端的多次請求服務;當我們使用方法二時,服務端只為一個客戶端的一次請求服務,但是因為每次請求都會重新開辟端口,所以實際上還是相當于可以為多個客戶端的多次請求服務。同時,因為它只為一次請求服務,所以我們在數(shù)據(jù)端口上傳輸文件時無需采用異步傳輸方式。但在控制端口我們?nèi)匀恍枰褂卯惒椒绞健?/p>

從上面看出,第一種方式要好得多,但是我們將采用第二種方式。至于原因,你可以回顧一下Part.1(基本概念和操作)中關(guān)于聊天程序模式的講述,因為接下來一篇文章我們將創(chuàng)建一個聊天程序,而這個聊天程序采用第三種模式,所以本文的練習實際是對下一篇的一個鋪墊。

1.訂立協(xié)議

1.1發(fā)送文件

我們先看一下發(fā)送文件的情況,如果我們想將文件client01.jpg由客戶端發(fā)往客戶端,那么流程是什么:

  1. 客戶端開辟數(shù)據(jù)端口用于偵聽,并獲取端口號,假設為8005。
  2. 假設客戶端輸入了S1,則發(fā)送下面的控制字符串到服務端:[file=Client01.jpg, mode=send, port=8005]。
  3. 服務端收到以后,根據(jù)客戶端ip和端口號與該客戶端建立連接。
  4. 客戶端偵聽到服務端的連接,開始發(fā)送文件。
  5. 傳送完畢后客戶端、服務端分別關(guān)閉連接。

此時,我們訂立的發(fā)送文件協(xié)議為:[file=Client01.jpg, mode=send, port=8005]。但是,由于它是一個普通的字符串,在上一篇中,我們采用了正則表達式來獲取其中的有效值,但這顯然不是一種好辦法。因此,在本文及下一篇文章中,我們采用一種新的方式來編寫協(xié)議:XML。對于上面的語句,我們可以寫成這樣的XML:

<protocol><file name="client01.jpg" mode="send" port="8005" /></protocol>

這樣我們在服務端就會好處理得多,接下來我們來看一下接收文件的流程及其協(xié)議。

NOTE:這里說發(fā)送、接收文件是站在客戶端的立場說的,當客戶端發(fā)送文件時,對于服務器來收,則是接收文件。

1.2接收文件

接收文件與發(fā)送文件實際上完全類似,區(qū)別只是由客戶端向網(wǎng)絡流寫入數(shù)據(jù),還是由服務端向網(wǎng)絡流寫入數(shù)據(jù)。

  1. 客戶端開辟數(shù)據(jù)端口用于偵聽,假設為8006。
  2. 假設客戶端輸入了R1,則發(fā)送控制字符串:<protocol><file name="Server01.jpg" mode="receive" port="8006" /></protocol>到服務端。
  3. 服務端收到以后,根據(jù)客戶端ip和端口號與該客戶端建立連接。
  4. 客戶端建立起與服務端的連接,服務端開始網(wǎng)絡流中寫入數(shù)據(jù)。
  5. 傳送完畢后服務端、客戶端分別關(guān)閉連接。

2.協(xié)議處理類的實現(xiàn)

和上面一章一樣,在開始編寫實際的服務端客戶端代碼之前,我們首先要編寫處理協(xié)議的類,它需要提供這樣兩個功能:1、方便地幫我們獲取完整的協(xié)議信息,因為前面我們說過,服務端可能將客戶端的多次獨立請求拆分或合并。比如,客戶端連續(xù)發(fā)送了兩條控制信息到服務端,而服務端將它們合并了,那么則需要先拆開再分別處理。2、方便地獲取我們所想要的屬性信息,因為協(xié)議是XML格式,所以還需要一個類專門對XML進行處理,獲得字符串的屬性值。

2.1 ProtocalHandler輔助類

我們先看下ProtocalHandler,它與上一篇中的RequestHandler作用相同。需要注意的是必須將它聲明為實例的,而非靜態(tài)的,這是因為每個TcpClient都需要對應一個ProtocalHandler,因為它內(nèi)部維護的patialProtocal不能共享,在協(xié)議發(fā)送不完整的情況下,這個變量用于臨時保存被截斷的字符串。

public class ProtocolHandler {

    private string partialProtocal; // 保存不完整的協(xié)議
   
    public ProtocolHandler() {
        partialProtocal = "";      
    }

    public string[] GetProtocol(string input) {
        return GetProtocol(input, null);
    }
   
    // 獲得協(xié)議
    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(partialProtocal))
            input = partialProtocal + input;

        string pattern = "(^<protocol>.*?</protocol>)";

        // 如果有匹配,說明已經(jīng)找到了,是完整的協(xié)議
        if (Regex.IsMatch(input, pattern)) {

            // 獲取匹配的值
            string match = Regex.Match(input, pattern).Groups[0].Value;
            outputList.Add(match);
            partialProtocal = "";

            // 縮短input的長度
            input = input.Substring(match.Length);

            // 遞歸調(diào)用
            GetProtocol(input, outputList);

        } else {
            // 如果不匹配,說明協(xié)議的長度不夠,
            // 那么先緩存,然后等待下一次請求
            partialProtocal = input;
        }

        return outputList.ToArray();
    }
}

因為現(xiàn)在它已經(jīng)不是本文的重點了,所以我就不演示對于它的測試了,本文所附帶的代碼中含有它的測試代碼(我在ProtocolHandler中添加了一個靜態(tài)類Test())。

2.2 FileRequestType枚舉和FileProtocol結(jié)構(gòu)

因為XML是以字符串的形式在進行傳輸,為了方便使用,我們最好構(gòu)建一個強類型來對它們進行操作,這樣會方便很多。我們首先可以定義FileRequestMode枚舉,它代表是發(fā)送還是接收文件:

public enum FileRequestMode {
    Send = 0,
    Receive
}

接下來我們再定義一個FileProtocol結(jié)構(gòu),用來為整個協(xié)議字符串提供強類型的訪問,注意這里覆蓋了基類的ToString()方法,這樣在客戶端我們就不需要再手工去編寫XML,只要在結(jié)構(gòu)值上調(diào)用ToString()就OK了,會方便很多。

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);
    }
}

2.3 ProtocolHelper輔助類

這個類專用于將XML格式的協(xié)議映射為我們上面定義的強類型對象,這里我沒有加入try/catch異常處理,因為協(xié)議對用戶來說是不可見的,而且客戶端應該總是發(fā)送正確的協(xié)議,我覺得這樣可以讓代碼更加清晰:

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");
    }

    // 此時的protocal一定為單條完整protocal
    private FileRequestMode GetFileMode() {
        string mode = fileNode.Attributes["mode"].Value;
        mode = mode.ToLower();
        if (mode == "send")
            return FileRequestMode.Send;
        else
            return FileRequestMode.Receive;
    }

    // 獲取單條協(xié)議包含的信息
    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);
    }
}

OK,我們又耽誤了點時間,下面就讓我們進入正題吧。

3.客戶端發(fā)送數(shù)據(jù)

3.1 服務端的實現(xiàn)

我們還是將一個問題分成兩部分來處理,先是發(fā)送數(shù)據(jù),然后是接收數(shù)據(jù)。我們先看發(fā)送數(shù)據(jù)部分的服務端。如果你從第一篇文章看到了現(xiàn)在,那么我覺得更多的不是技術(shù)上的問題而是思路,所以我們不再將重點放到代碼上,這些應該很容易就看懂了。

class Server {
    static void Main(string[] args) {
        Console.WriteLine("Server is running ... ");
        IPAddress ip = IPAddress.Parse("127.0.0.1");
        TcpListener listener = new TcpListener(ip, 8500);

        listener.Start();           // 開啟對控制端口 8500 的偵聽
        Console.WriteLine("Start Listening ...");

        while (true) {
            // 獲取一個連接,同步方法,在此處中斷
            TcpClient client = listener.AcceptTcpClient();             
            RemoteClient wapper = new RemoteClient(client);
            wapper.BeginRead();
        }
    }
}

public class RemoteClient {
    private TcpClient client;
    private NetworkStream streamToClient;
    private const int BufferSize = 8192;
    private byte[] buffer;
    private ProtocolHandler handler;
   
    public RemoteClient(TcpClient client) {
        this.client = client;

        // 打印連接到的客戶端信息
        Console.WriteLine("\nClient Connected!{0} <-- {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);
    }

    // 再讀取完成時進行回調(diào)
    private void OnReadComplete(IAsyncResult ar) {
        int bytesRead = 0;
        try {
            lock (streamToClient) {
                bytesRead = streamToClient.EndRead(ar);
                Console.WriteLine("Reading data, {0} bytes ...", bytesRead);
            }
            if (bytesRead == 0) throw new Exception("讀取到0字節(jié)");

            string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Array.Clear(buffer,0,buffer.Length);        // 清空緩存,避免臟讀

            // 獲取protocol數(shù)組
            string[] protocolArray = handler.GetProtocol(msg);
            foreach (string pro in protocolArray) {
                // 這里異步調(diào)用,不然這里可能會比較耗時
                ParameterizedThreadStart start =
                    new ParameterizedThreadStart(handleProtocol);
                start.BeginInvoke(pro, null, null);
            }

            // 再次調(diào)用BeginRead(),完成時調(diào)用自身,形成無限循環(huán)
            lock (streamToClient) {
                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) {
            // 客戶端發(fā)送文件,對服務端來說則是接收文件
            receiveFile(protocol);
        } else if (protocol.Mode == FileRequestMode.Receive) {
            // 客戶端接收文件,對服務端來說則是發(fā)送文件
            // sendFile(protocol);
        }
    }

    private void receiveFile(FileProtocol protocol) {
        // 獲取遠程客戶端的位置
        IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint;
        IPAddress ip = endpoint.Address;
       
        // 使用新端口號,獲得遠程用于接收文件的端口
        endpoint = new IPEndPoint(ip, protocol.Port);

        // 連接到遠程客戶端
        TcpClient localClient;
        try {
            localClient = new TcpClient();
            localClient.Connect(endpoint);
        } catch {
            Console.WriteLine("無法連接到客戶端 --> {0}", endpoint);
            return;
        }

        // 獲取發(fā)送文件的流
        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
        );
    }
}

這里應該沒有什么新知識,需要注意的地方有這么幾個:

  • 在OnReadComplete()回調(diào)方法中的foreach循環(huán),我們使用委托異步調(diào)用了handleProtocol()方法,這是因為handleProtocol即將執(zhí)行的是一個讀取或接收文件的操作,也就是一個相對耗時的操作。
  • 在handleProtocol()方法中,我們深切體會了定義ProtocolHelper類和FileProtocol結(jié)構(gòu)的好處。如果沒有定義它們,這里將是不堪入目的處理XML以及類型轉(zhuǎn)換的代碼。
  • handleProtocol()方法中進行了一個條件判斷,注意sendFile()方法我屏蔽掉了,這個還沒有實現(xiàn),但是我想你已經(jīng)猜到它將是后面要實現(xiàn)的內(nèi)容。
  • receiveFile()方法是實際接收客戶端發(fā)來文件的方法,這里沒有什么特別之處。需要注意的是文件存儲的路徑,它保存在了當前程序執(zhí)行的目錄下,文件的名稱我使用generateFileName()生成了一個與時間有關(guān)的隨機名稱。

3.2客戶端的實現(xiàn)

我們現(xiàn)在先不著急實現(xiàn)客戶端S1、R1等用戶菜單,首先完成發(fā)送文件這一功能,實際上,就是為上一節(jié)SendMessage()加一個姐妹方法SendFile()。

class Client {
    static void Main(string[] args) {
        ConsoleKey key;

        ServerClient client = new ServerClient();
        string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg";

        if(File.Exists(filePath))
            client.BeginSendFile(filePath);
       
        Console.WriteLine("\n\n輸入\"Q\"鍵退出。");
        do {
            key = Console.ReadKey(true).Key;
        } while (key != ConsoleKey.Q);
    }
}

public class ServerClient {
    private const int BufferSize = 8192;
    private byte[] buffer;
    private TcpClient client;
    private NetworkStream streamToServer;

    public ServerClient() {
        try {
            client = new TcpClient();
            client.Connect("localhost", 8500);      // 與服務器連接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        buffer = new byte[BufferSize];

        // 打印連接到的服務端信息
        Console.WriteLine("Server Connected!{0} --> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);

        streamToServer = client.GetStream();
    }

    // 發(fā)送消息到服務端
    public void SendMessage(string msg) {

        byte[] temp = Encoding.Unicode.GetBytes(msg);   // 獲得緩存
        try {
            lock (streamToServer) {
                streamToServer.Write(temp, 0, temp.Length); // 發(fā)往服務器
            }
            Console.WriteLine("Sent: {0}", msg);
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
    }

    // 發(fā)送文件 - 異步方法
    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);
    }

    // 發(fā)送文件 -- 同步方法
    public void SendFile(string filePath) {

        IPAddress ip = IPAddress.Parse("127.0.0.1");
        TcpListener listener = new TcpListener(ip, 0);
        listener.Start();

        // 獲取本地偵聽的端口號
        IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
        int listeningPort = endPoint.Port;

        // 獲取發(fā)送的協(xié)議字符串
        string fileName = Path.GetFileName(filePath);
        FileProtocol protocol =
            new FileProtocol(FileRequestMode.Send, listeningPort, fileName);
        string pro = protocol.ToString();

        SendMessage(pro);       // 發(fā)送協(xié)議到服務端

        // 中斷,等待遠程連接
        TcpClient localClient = listener.AcceptTcpClient();
        Console.WriteLine("Start sending file...");
        NetworkStream stream = localClient.GetStream();

        // 創(chuàng)建文件流
        FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);          
        byte[] fileBuffer = new byte[1024];     // 每次傳1KB
        int bytesRead;
        int totalBytes = 0;

        // 創(chuàng)建獲取文件發(fā)送狀態(tài)的類
        SendStatus status = new SendStatus(filePath);

        // 將文件流轉(zhuǎn)寫入網(wǎng)絡流
        try {
            do {
                Thread.Sleep(10);           // 為了更好的視覺效果,暫停10毫秒:-)
                bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);                 
                stream.Write(fileBuffer, 0, bytesRead);
                totalBytes += bytesRead;            // 發(fā)送了的字節(jié)數(shù)
                status.PrintStatus(totalBytes); // 打印發(fā)送狀態(tài)
            } while (bytesRead > 0);
            Console.WriteLine("Total {0} bytes sent, Done!", totalBytes);
        } catch {
            Console.WriteLine("Server has lost...");
        }
       
        stream.Dispose();
        fs.Dispose();
        localClient.Close();
        listener.Stop();
    }
}

接下來我們來看下這段代碼,有這么兩點需要注意一下:

  • 在Main()方法中可以看到,圖片的位置為應用程序所在的目錄,如果你跟我一樣處于調(diào)試模式,那么就在解決方案的Bin目錄下的Debug目錄中放置三張圖片Client01.jpg、Client02.jpg、Client03.jpg,用來發(fā)往服務端。
  • 我在客戶端提供了兩個SendFile()方法,和一個BeginSendFile()方法,分別用于同步和異步傳輸,其中私有的SendFile()方法只是一個輔助方法。實際上對于發(fā)送文件這樣的操作我們幾乎總是需要使用異步操作。
  • SendMessage()方法中給streamToServer加鎖很重要,因為SendFile()方法是多線程訪問的,而在SendFile()方法中又調(diào)用了SendMessage()方法。
  • 我另外編寫了一個SendStatus類,它用來記錄和打印發(fā)送完成的狀態(tài),已經(jīng)發(fā)送了多少字節(jié),完成度是百分之多少,等等。本來這個類的內(nèi)容我是直接寫入在Client類中的,后來我覺得它執(zhí)行的工作已經(jīng)不屬于Client本身所應該執(zhí)行的領域之內(nèi)了,我記得這樣一句話:當你覺得類中的方法與類的名稱不符的時候,那么就應該考慮重新創(chuàng)建一個類。我覺得用在這里非常恰當。

下面是SendStatus的內(nèi)容:

// 即時計算發(fā)送文件的狀態(tài)
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);
    }

    // 獲得文件發(fā)送的百分比
    public string GetPercent(int sent){    

        decimal allBytes = Convert.ToDecimal(fileBytes);
        decimal currentSent = Convert.ToDecimal(sent);

        decimal percent = (currentSent / allBytes) * 100;
        percent = Math.Round(percent, 1);   //保留一位小數(shù)
       
        if (percent.ToString() == "100.0")
            return "100";
        else
            return percent.ToString();
    }
}

3.3程序測試

接下里我們運行一下程序,來檢查一下輸出,首先看下服務端:

接著是客戶端,我們能夠看到發(fā)送的字節(jié)數(shù)和進度,可以想到如果是圖形界面,那么我們可以通過擴展SendStatus類來創(chuàng)建一個進度條:

最后我們看下服務端的Bin\Debug目錄,應該可以看到接收到的圖片:

本來我想這篇文章就可以完成發(fā)送和接收,不過現(xiàn)在看來沒法實現(xiàn)了,因為如果繼續(xù)下去這篇文章就太長了,我正嘗試著盡量將文章控制在15頁以內(nèi)。那么我們將在下篇文章中再完成接收文件這一部分。

本站僅提供存儲服務,所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
C#網(wǎng)絡編程(訂立協(xié)議和發(fā)送文件)
C#中的socket編程入門實例
C#網(wǎng)絡編程(同步傳輸字符串) - Part.2 - C# 編程 - TraceFact....
用C#實現(xiàn)基于TCP協(xié)議的網(wǎng)絡通訊--51編程網(wǎng)
C#線程系列講座(3):線程池和文件下載服務器
Orleans[NET Core 3.1] 學習筆記(四)( 1 )創(chuàng)建項目
更多類似文章 >>
生活服務
分享 收藏 導長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服