同步傳輸字串

見證大牛成長之路發表於2016-04-19

同步傳輸字串

 

接下來考慮著一種情況,完成一個簡單的文字通訊:

(1).客戶端將字串傳送到服務端,服務端接受字串並顯示

(2).服務端將字串由英文的小寫轉換為大寫,然後發回給客戶端,客戶端接受並顯示.

 

客戶端傳送,服務端接受並輸出

 

1.服務端程式

 

可以在TcpClient上呼叫GetStream()方法來獲得連線到遠端計算機的網路流NetworkStream.當在客戶端呼叫時,它獲得連線服務端的流;當在服務端呼叫時,它獲得連線客戶端的流.

 

先看服務端的程式碼實現:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Net;
using System.Net.Sockets;
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BufferSize = 8192;//快取大小,8192位元組
 
            Console.WriteLine("Server is running...");
            IPAddress ip = new IPAddress(new byte[] { 192, 168, 3, 19 });
            TcpListener listener = new TcpListener(ip,1621);
 
            listener.Start();//開始監聽
 
            Console.WriteLine("Start Listening...");
 
            //獲取一個連線,中斷方法
            TcpClient remoteClient = listener.AcceptTcpClient();
 
            //列印連線到的客戶端資訊
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
 
            //獲得流,並寫入buffer中
 
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            int byteRead = streamToClient.Read(buffer,0,BufferSize);
 
            //獲得請求的字串
            string msg = Encoding.Unicode.GetString(buffer,0,byteRead);
            Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出\n\n");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
        }
    }
}


等一下,這裡樓主有個問題需要問一下,在使用netstat命令檢視埠時,你知道根據埠的狀態判斷哪些埠是可以用來監聽的,那些埠不能用來監聽嗎?


 

這段程式的上半部分上一次說過了,remoteClient.GetStream()方法獲取到了連線至客戶端的流,然後從流中讀出資料並儲存在了buffer,隨後使用Encoding.Unicode.GetString()方法,從快取中獲取到實際的字串.最後將字串顯示在了控制檯中.這段程式碼有個地方需要注意:如果能夠讀取的字串的總位元組數大於BufferSize,就會出現字串截斷現象,只能讀取到不完整的字串.這是因為快取的位元組數是有限的,在本例中是8192.如果傳遞的資料位元組數比較大,例如圖片,音訊,檔案,則必須採用”分次讀取然後轉存”的方式,程式碼如下:

            //獲取字串
            byte buffer = new byte[BufferSize];
            int bytesRead;
            MemoryStream ms = new MemoryStream();
            do
            {
                bytesRead = streamToClient.Read(buffer,0,BufferSize);
            } while (bytesRead>0);


我們們沒有使用”分次讀取轉存”的方式,為啥呢?因為:樓主的水平有限,不想誤人子弟.

 

說實話,8192已經很多了.當使用Unicode編碼時,8192位元組可以儲存4096個漢字和英文字元.使用不同的編碼方式,佔用的位元組數有很大的差異.

 

現在不對客戶端在任何修改,先除錯執行伺服器,在執行客戶端,會發現服務端在列印完”Client Connected ! Local:192.168.3.19:1621<-- Client:192.168.3.19:4044”之後,再次被阻塞了,沒有繼續執行,也沒有輸出”Reading data,{0} bytes...”等任何字元.可見,AcceptTcpClient()方法類似,Read()方法也是同步的,只有當客戶端傳送資料的時候,服務端才會讀取資料,執行此方法,否則會一直等待下去.

 

2.客戶端程式

 

接下來編寫客戶端想服務端傳送字串的程式碼,與服務端類似,先獲取連線到服務端的流,將字串儲存到buffer,再將快取寫入流.寫入流這一過程,相當於將訊息發往服務端.

        static void Main(string[] args)
        {
            #region MyRegion
            /*
 
            Console.WriteLine("Client is running...");
            TcpClient client;
            for (int i = 0; i <= 2; i++)
            {
                try
                {
                    client = new TcpClient();
                    //與伺服器建立連線
                    client.Connect(IPAddress.Parse("192.168.3.19"), 1621);
 
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    return;
                }
                //列印連線到的服務端資訊
                Console.WriteLine("Server Connected ! Local:{0} -->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
            }
            //按Q退出
            Console.WriteLine("\n\n輸入\"Q\"鍵退出. ");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key != ConsoleKey.Q);*/
            #endregion
            Console.WriteLine("Client is running...");
            TcpClient client;
            try
            {
                client = new TcpClient();
                //與伺服器建立連線
                client.Connect(IPAddress.Parse("192.168.1.120"),1621);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return;
            }
 
            //列印連線到的服務端資訊
            Console.WriteLine("Server Connected! Local:{0}-->Server:{1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
 
            string msg = "Hello,readers!";
 
            NetworkStream streamToServer = client.GetStream();
 
            byte[] buffer = Encoding.Unicode.GetBytes(msg);//獲得快取
            streamToServer.Write(buffer,0,buffer.Length);
 
            Console.WriteLine("Sent: {0}",msg);
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
 
        }


現在再次執行程式,得到的輸出為:

//服務端:
Server is running...
Start Listening...
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:5465
Received: Hello,readers! [28bytes]
//客戶端:
Client is running...
Server Connected! Local:192.168.1.120:5465-->Server:192.168.1.120:1621
Sent: Hello,readers!


 

可以看到,已經成功的傳送和接受了一串字串,是不是有點成就感了?QQ不過如此了了的事.但不要高興的太早,對於即時通訊程式來說,在客戶端和服務端之間,應該是可以不間斷的接受和傳送訊息的.但是上面的程式碼只能接受客戶端傳送的一條訊息,因為程式碼已經執行完畢,控制外也已經輸出了”按Q退出”.無法再繼續執行任何的後續動作.此時如果在開啟一個客戶端,那麼出現的情況是:客戶端可以與伺服器建立連線,也就是”netstat -a”顯示為ESTABLISHED,這是作業系統所知道的;但是由於服務端的程式已經執行到了最後一步,只能輸入Q退出,無法再執行任何動作.

 

回想一下,前面說過,當一個服務端需要接受多個客戶端連線時,所採用的處理辦法是:AcceptTcpClient()方法放在一個while迴圈中.類似的,當服務端需要同一個客戶端的多次請求進行處理時,可以將Read()方法也放入到一個do/while迴圈中.

 

綜合起來就只有四種情況:

 

第一種:如果不使用do/while迴圈,服務端只有一個listener.AcceptTcpClient()方法和一個TcpClient.GetStream().Read()方法,則服務端只能處理來自一個客戶端的一條請求.

 

第二種:如果使用一個do/while迴圈,並將listener.AcceptTcpClient(0方法和TcpClient.GetStream().Read()方法放在迴圈中,那麼服務端將可以處理多個客戶端的一條請求.

 

第三種:使用一個do/while迴圈,並將listener.AcceptTcpClient()方法放在迴圈內,TcpClient.GetStream().Read()方法放在迴圈外,那麼可以處理一個客戶端的多條請求.

 

第四種:使用兩個do/while迴圈,對它們分別進行巢狀,那麼結果是啥?你肯定會說,可以處理多個客戶端的多個請求.事實上不是這樣的.因為內層的do/while迴圈總是在為一個客戶端服務,它會中斷在TcpClient.GetStream().Read()方法的位置,而無法執行完畢.即時可以通過某種方式讓內層迴圈退出,例如,當客戶端向服務端傳送”exit”字串時,服務端也只能挨個對客戶端提供服務.如果服務端想並行的對多個客戶端的多個請求進行服務,那麼服務端就需要採用多執行緒.主執行緒,即執行外層的do/while迴圈的執行緒,它在AcceptTcpClient()獲取到一個TcpClient之後,必須將內層的do/while迴圈交給其他的執行緒去處理,然後主執行緒快速的重新回到listener.AcceptTcpClient()的位置,來響應其他的客戶端?明白了嗎?是不是有點暈,樓主也有點暈,沒關係.我們們一個一個講解.

 

我們們先來看第二種和第三種情況.

對於第二種情況,按照上面描述的那些對程式碼做一些改動,服務端程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BufferSize = 8192;//快取大小,8192位元組
 
            Console.WriteLine("Server is running...");
            IPAddress ip = new IPAddress(new byte[] { 192, 168, 1, 120 });
            TcpListener listener = new TcpListener(ip,1621);
 
            listener.Start();//開始監聽
 
            Console.WriteLine("Start Listening...");
            do
            {
                //獲取一個連線,中斷方法
                TcpClient remoteClient = listener.AcceptTcpClient();
                //列印連線到的客戶端資訊
                Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
 
                //獲得流,並寫入buffer中
                NetworkStream streamToClient = remoteClient.GetStream();
                byte[] buffer = new byte[BufferSize];
                int bytesRead = streamToClient.Read(buffer,0,BufferSize);
 
                //獲得請求的字串
                string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);
 
            } while (true);
            /*
            //獲取一個連線,中斷方法
            TcpClient remoteClient = listener.AcceptTcpClient();
 
            //列印連線到的客戶端資訊
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
 
            //獲得流,並寫入buffer中
 
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            int byteRead = streamToClient.Read(buffer,0,BufferSize);
 
            //獲得請求的字串
            string msg = Encoding.Unicode.GetString(buffer,0,byteRead);
            Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出\n\n");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
            */
           /* //獲取字串
            byte buffer = new byte[BufferSize];
            int bytesRead;
            MemoryStream ms = new MemoryStream();
            do
            {
                bytesRead = streamToClient.Read(buffer,0,BufferSize);
            } while (bytesRead>0);*/
        }
    }
}


然後啟動多個客戶端程式,在服務端可以看到這樣的情況:

Server is running...
Start Listening...
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6737
Received: Hello,readers! [28bytes]
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6742
Received: Hello,readers! [28bytes]
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6754
Received: Hello,readers! [28bytes]


 

現在將第二種情況變為第三種情況,只需要將do向下挪動幾行就可以了:

            //獲取一個連線,中斷方法
            TcpClient remoteClient = listener.AcceptTcpClient();
            //列印連線到的客戶端資訊
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
            //獲得流,並寫入buffer中
            NetworkStream streamToClient = remoteClient.GetStream();
            do
            {                
                byte[] buffer = new byte[BufferSize];
                int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
 
                //獲得請求的字串
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);
 
            } while (true);


然後再改動一下客戶端,讓它可以連續傳送多個字串到到伺服器.當按下回車的時候傳送字串,輸入”Q”的時候,跳出迴圈:

            Console.WriteLine("Client is running...");
            TcpClient client;
            try
            {
                client = new TcpClient();
                //與伺服器建立連線
                client.Connect(IPAddress.Parse("192.168.1.120"), 1621);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return;
            }
 
            //列印連線到的服務端資訊
            Console.WriteLine("Server Connected! Local:{0}-->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
 
 
 
            NetworkStream streamToServer = client.GetStream();
 
            string msg;
            do
            {
                Console.Write("Sent:");
                msg = Console.ReadLine();
                if (!String.IsNullOrEmpty(msg) && msg != "Q")
                {
                    byte[] buffer = Encoding.Unicode.GetBytes(msg);
                    try
                    {
                        //發往伺服器
                        streamToServer.Write(buffer, 0, buffer.Length);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        return;
                    }
                }
            } while (msg != "Q");


接下來先執行服務端,再執行客戶端,輸入以下字串來進行測試,應該能夠看到預期的結果.

 

注意:如果再開啟一個客戶端,該客戶端雖然可以成功的連線服務端,也可以傳送字串,但是服務端不會做任何的處理和顯示.

 

這裡有一點需要注意:當客戶端在TcpClient例項上呼叫Close()方法,或者在流上呼叫Didpose()方法時,服務端的streamToClient.Read()方法會持續返回0,但是不丟擲異常,所以會產生一個無限迴圈.服務端不斷的重新整理顯示”Received: [0 bytes]”,因此,服務端在呼叫streamToClient.Read()方法後,應加上一個如下判斷:

                int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                if (bytesRead==0)
                {
                    Console.WriteLine("Client offline");
                    break;
                }


如果直接關閉掉客戶端,或者客戶端執行完畢但沒有呼叫stream.Dispose()或者TcpClient.Close(),切服務端此時仍阻塞在Read()方法處,則會在服務端丟擲異常:未經處理的異常:  System.IO.IOException: 無法從傳輸連線中讀取資料:遠端主機強迫關閉了一個現有的連線。

 

因此,服務端的streamToClient.Read()方法需要寫在一個try/catch.下面是改進的服務端程式碼:

            do
            {
                try
                {
                    byte[] buffer = new byte[BufferSize];
                    int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                    if (bytesRead == 0)
                    {
                        Console.WriteLine("Client offline");
                        break;
                    }
                    //獲得請求的字串
                    string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                    Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    break;
                }
            } while (true);


同樣的,如果服務端在連線到客戶端之後呼叫remoteClient.Close(),則客戶端在呼叫streamToServer.Write()時也會丟擲異常.因此,他們的讀寫操作都必須放入try/catch塊中.

 

 

服務端傳送,客戶端接受並顯示

 

1.服務端程式

 

到現在為止,客戶端已經能傳送字串到服務端,服務端能接受並顯示.接下來,再進行進一步處理,使服務端將字串有英文小寫改為英文大寫,然後發回給客戶端,客戶端接受並顯示.此時服務端和客戶端的角色和上面進行了一下對調:對於服務端來說,就好像剛才的客戶端一樣,將字串寫入到流中,而客戶端則同服務端一樣,接受並顯示.

 

服務端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BufferSize = 8192;//快取大小,8192位元組
 
            Console.WriteLine("Server is running...");
            IPAddress ip = new IPAddress(new byte[] { 192,168,1,120});
 
            TcpListener listener = new TcpListener(ip, 1621);
            //開始監聽
            listener.Start();
            Console.WriteLine("Start Listening...");
 
            //獲取一個連線,中斷方法
            TcpClient remoteClient = listener.AcceptTcpClient();
 
            //列印連線到的客戶端資訊
            Console.WriteLine("Client Connected! Local:{0}<--Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
 
            //獲得流
            NetworkStream streamToClient = remoteClient.GetStream();
 
            do
            {
                try
                {
                    byte[] buffer = new byte[BufferSize];
                    int bytesRead = streamToClient.Read(buffer,0,BufferSize);
                    if (bytesRead==0)
                    {
                        Console.WriteLine("Client offline");
                        break;
                    }
                    //獲得請求的字串
                    string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                    Console.WriteLine("Received: {0} [{1} bytes]",msg,bytesRead);
 
                    //轉換為大寫
 
                    msg = msg.ToUpper();
                    buffer = Encoding.Unicode.GetBytes(msg);
                    streamToClient.Write(buffer,0,buffer.Length);
 
                    Console.WriteLine("Sent: {0}",msg);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    break;
                }
            } while (true);
            streamToClient.Dispose();
            remoteClient.Close();
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
        }
    }
}


上面的程式碼大家應該很熟悉了,主要的變化是轉換字母大小寫,並寫入到流中的操作.

 

2.客戶端程式碼

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
 
namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("CLient is running...");
            TcpClient client;
            const int BufferSize = 8192;
 
            try
            {
                client = new TcpClient();
                //與伺服器建立連線
                client.Connect(IPAddress.Parse("192.168.3.19"), 9322);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return;
            }           
            //列印連線到的服務端資訊
            Console.WriteLine("Server Connected! Local: {0} --> Server: {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
 
            NetworkStream streamToServer = client.GetStream();
 
            string msg;
 
            do
            {
                Console.Write("Sent:");
                msg = Console.ReadLine();
 
                if (!string.IsNullOrEmpty(msg)&&msg!="Q")
                {
                    byte[] buffer = Encoding.Unicode.GetBytes(msg);
                    try
                    {
                        //發往伺服器
                        streamToServer.Write(buffer,0,buffer.Length);
                        int bytesRead;
                        buffer = new byte[BufferSize];
 
                        //接受並顯示伺服器回傳的字串
                        bytesRead = streamToServer.Read(buffer,0,BufferSize);
 
                        if (bytesRead==0)
                        {
                            Console.WriteLine("Server offline");
                            break;
                        }
                        msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                        Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        break;
                    }
                }
            } while (msg!="Q");
            streamToServer.Dispose();
            client.Close();
        }
    }
}
 


先執行一下服務端,然後執行客戶端,就會看到相應的輸出.

 

 

這樣大家應該會對C#的網路程式設計有了一定得了解,當然了,這是隻是網路程式設計中的皮毛.因為到目前為止,我們們的操作都是同步操作,上面的程式碼只能作為入門使用.在實際中,一個服務端只能為一個客戶端提供服務的情況幾乎不存在.

 

下面我們們要看非同步的網路程式設計之前,先學習一下在不同的編碼方式中英文的大小,以及TCP快取導致的文字邊界問題.


相關文章