c#網絡編程學習筆記02_Tcp編程(中)_簡單的同步tcp聊天程序

/*

寫一個同步tcp程序,功能爲,客戶端發送一個字符串給服務器,服務器將字符串打印,服務器再將字符串全部轉化爲大寫字母,發送給客戶端,然後客戶端接受打印

*/

參考大牛地址:http://www.tracefact.net/CSharp-Programming/Network-Programming-Part2.aspx


回憶一下編寫服務端的一般步驟:

1.取得服務器的ip和端口號,創建TcpListener對象,調用Start方法開始監聽。

2.利用TcpListener的AcceptTcpClient方法得到監聽的客戶端TcpClient對象,利用GetStream得到NetStream對象,即字節流。然後可選取其他流處理方法進行字符或者字節流處理。

3.進行通信。

4. 關閉流。關閉監聽。

上代碼:

服務器端:

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;
            Console.Write("Server is running!");
            IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
            TcpListener listener = new TcpListener(ip, 8500);
            listener.Start();
            Console.WriteLine("服務器開始偵聽");
            TcpClient remoteClient = listener.AcceptTcpClient();
            Console.WriteLine("Client conneted! {0}----{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

            //獲得流,並且寫到buffer中
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
            Console.WriteLine("Reading data, {0} bytes。。。",bytesRead);

            //獲得請求的字符串
            string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received: {0}", msg);


            //按下Q退出
            Console.WriteLine("按下Q鍵退出");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key != ConsoleKey.Q);
        }
    }
}

客戶端程序1:測試連接:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;


namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Client is running:");
            TcpClient client = new TcpClient();

            try
            {
                client.Connect("localhost", 8500);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                
                throw;
            }
            Console.WriteLine("Server Conneted!{0} --- > {1}", client.Client.LocalEndPoint.ToString(), client.Client.RemoteEndPoint.ToString());
            Console.Read();
        }
    }
}

先運行服務器,再運行客戶端發現:


連接成功!!,但是,,我們發現服務器端並沒有執行流處理,即int bytesRead = streamToClient.Read(buffer, 0, BufferSize);說明Read方法是一個同步方法,這裏沒有接受字符串,發生了阻塞。我們接下來開始發送字符串,完善客戶端程序。

客戶端程序二:完善版

 <span style="white-space:pre">		</span>const int BufferSize = 8192;
            Console.Write("Server is running!");
            IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
            TcpListener listener = new TcpListener(ip, 8500);
            listener.Start();
            Console.WriteLine("服務器開始偵聽");
            TcpClient remoteClient = listener.AcceptTcpClient();
            Console.WriteLine("Client conneted! {0}----{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

            //獲得流,並且寫到buffer中
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            //同步的方法,會阻塞,不要擔心順序問題。
           
            int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
            Console.WriteLine("Reading data, {0} bytes。。。", bytesRead);

            //獲得請求的字符串
            string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received: {0}", msg);


            //按下Q退出
            Console.WriteLine("按下Q鍵退出");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
                remoteClient.Close();
                streamToClient.Close();
            } while (key != ConsoleKey.Q);

運行結果:


恩。。看上去挺爽了,但是我在這裏要強調兩個問題!!!

問題1

記得在程序結束的時候關閉流,關閉連接。

重要的重要的重要的問題2:

我在寫這段代碼的時候腦袋中突然出現一個問題,在服務器和客戶單進行連接後,服務器和客戶端的代碼都在向下執行,爲什麼客戶端寫入了數據後,服務器一定就能接收到呢?爲什麼不是服務器先接受然後爲空?過了幾分鐘,我明白了,NetStream中的write和read都是同步方法,都會阻塞的。。(沃日...),嚴格來說,與AcceptTcpClient()方法類似,這個Read()方法也是同步的,只有當客戶端發送數據的時候,服務端纔會讀取數據、運行此方法,否則它便會一直等待。。

問題三:這個程序只能實現一個客戶端,並且只能發送一條信息。

這明顯是不行的,我們如何實現一個客戶端,多條消息呢?

 當我們需要一個服務端對同一個客戶端的多次請求服務時,可以將Read()方法放入到do/while循環中

現在,我們大致可以得出這樣幾個結論:

  • 如果不使用do/while循環,服務端只有一個listener.AcceptTcpClient()方法和一個TcpClient.GetStream().Read()方法,則服務端只能處理到同一客戶端的一條請求。
  • 如果使用一個do/while循環,並將listener.AcceptTcpClient()方法和TcpClient.GetStream().Read()方法都放在這個循環以內,那麼服務端將可以處理多個客戶端的一條請求。
  • 如果使用一個do/while循環,並將listener.AcceptTcpClient()方法放在循環之外,將TcpClient.GetStream().Read()方法放在循環以內,那麼服務端可以處理一個客戶端的多條請求。
  • 如果使用兩個do/while循環,對它們進行分別嵌套,那麼結果是什麼呢?結果並不是可以處理多個客戶端的多條請求。因爲裏層的do/while循環總是在爲一個客戶端服務,因爲它會中斷在TcpClient.GetStream().Read()方法的位置,而無法執行完畢。即使可以通過某種方式讓裏層循環退出,比如客戶端往服務端發去“exit”字符串時,服務端也只能挨個對客戶端提供服務。如果服務端想執行多個客戶端的多個請求,那麼服務端就需要採用多線程。主線程,也就是執行外層do/while循環的線程,在收到一個TcpClient之後,必須將裏層的do/while循環交給新線程去執行,然後主線程快速地重新回到listener.AcceptTcpClient()的位置,以響應其它的客戶端
對於第四種,我們在之後的博文裏面講述,現在搞下第二種和第三種。
對於第二種,我們改下服務端的代碼,客戶端不變:
 do
            {
                TcpClient remoteClient = listener.AcceptTcpClient();
                Console.WriteLine("Client conneted! {0}----{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

                //獲得流,並且寫到buffer中
                NetworkStream streamToClient = remoteClient.GetStream();
                byte[] buffer = new byte[BufferSize];
                //同步的方法,會阻塞,不要擔心順序問題。

                int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                Console.WriteLine("Reading data, {0} bytes。。。", bytesRead);

                //獲得請求的字符串
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received: {0}", msg);

            } while (true);

然後啓動多個客戶端,結果:

下面說第三種,要稍微改動下客戶端和服務器的代碼:

服務端:

do
            {
                remoteClient = listener.AcceptTcpClient();
                Console.WriteLine("Client conneted! {0}----{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
                //獲得流,並且寫到buffer中
                streamToClient = remoteClient.GetStream();
                byte[] buffer = new byte[BufferSize];
                //同步的方法,會阻塞,不要擔心順序問題。

                int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                Console.WriteLine("Reading data, {0} bytes。。。", bytesRead);

                //獲得請求的字符串
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received: {0}", msg);

            } while (true);

客戶端:

     
<pre name="code" class="csharp">using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;


namespace Client3
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpClient client=null;
            NetworkStream streamToServer = null;
            ConsoleKey key;
client = new TcpClient();
            client.Connect("localhost", 8500);
            streamToServer = client.GetStream();
		do 
	{ 
		key = Console.ReadKey(true).Key;
		 try { if (key == ConsoleKey.S) 
		{ 
			Console.WriteLine("Input the message : ");
			 string msg = Console.ReadLine();
			 byte[] buffer = Encoding.Unicode.GetBytes(msg); 
			streamToServer.Write(buffer, 0, buffer.Length); 
			Console.WriteLine("send : {0}", msg); } } 
		catch (Exception e) 
		 Console.WriteLine(e.Message); 
		throw; 
		} 
		} while (key!=ConsoleKey.K); 
	} }}





運行結果:

這裏還需要注意一點,當客戶端在TcpClient實例上調用Close()方法,或者在流上調用Dispose()方法,服務端的streamToClient.Read()方法會持續地返回0,但是不拋出異常,所以會產生一個無限循環;而如果直接關閉掉客戶端,或者客戶端執行完畢但沒有調用stream.Dispose()或者TcpClient.Close(),如果服務器端此時仍阻塞在Read()方法處,則會在服務器端拋出異常:“遠程主機強制關閉了一個現有連接”。因此,我們將服務端的streamToClient.Read()方法需要寫在一個try/catch中。同理,如果在服務端已經連接到客戶端之後,服務端調用remoteClient.Close(),則客戶端會得到異常“無法將數據寫入傳輸連接: 您的主機中的軟件放棄了一個已建立的連接。”;而如果服務端直接關閉程序的話,則客戶端會得到異常“無法將數據寫入傳輸連接: 遠程主機強迫關閉了一個現有的連接。”。因此,它們的讀寫操作必須都放入到try/catch塊中。

/*-----------------------------------------------無敵分割線--------------------------------------------------*/

回到開始,完成那個程序。

客戶端發送到服務端是搞定了,接下來搞定字符處理,併發送回客戶端打印。

具體做法就是和客戶端發送給服務器一樣。除此之外,我們最好對流的操作加上lock(這是個什麼東西)。

服務器:

 const int bufferSize = 8192;
            TcpListener listener;
            TcpClient client;
            NetworkStream stream;
            IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
            
            try
            {
                listener = new TcpListener(ip, 8500);
                listener.Start();
                Console.WriteLine("開始監聽");
                byte[] buffer = new byte[bufferSize];
                client = listener.AcceptTcpClient();
                stream = client.GetStream();
                lock (stream)
                {
                    int readnum = stream.Read(buffer, 0, bufferSize);
                }
                

                string msg = Encoding.Unicode.GetString(buffer);
                Console.WriteLine("Msg = {0}", msg);

                msg = msg.ToUpper();
                buffer = Encoding.Unicode.GetBytes(msg);
                lock (stream)
                {
                    stream.Write(buffer, 0, bufferSize);
                }
                
            }
            catch (Exception)
            {

                throw;
            }
            stream.Close();
            client.Close();
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
客戶端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;


namespace Client
{
    class Program
    {
        //接下來我們編寫客戶端向服務器發送字符串的代碼,與服務端類似,
        //他先獲取連接服務器端的流,將字符串保存在緩存中,寫入流這一個過程,相當於將消息發送服務端
        static void Main(string[] args)
        {
            Console.WriteLine("Client is running:");
            TcpClient client =null;

            try
            {
                client = new TcpClient();
                client.Connect("localhost", 8500);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                
                throw;
            }
            //打印連接到服務器的信息
            Console.WriteLine("Server Conneted!{0} --- > {1}", client.Client.LocalEndPoint.ToString(), client.Client.RemoteEndPoint.ToString());

            string msg = "\"What a Big Shit\"";
            NetworkStream streamToServer = client.GetStream();

            byte[] buffer = Encoding.Unicode.GetBytes(msg);
            lock (streamToServer)
            {
                streamToServer.Write(buffer, 0, buffer.Length);
            }
            
            Console.WriteLine("Sent: {0}", msg);

            lock (streamToServer)
            {
                streamToServer.Read(buffer, 0, buffer.Length);
            }
            msg = Encoding.Unicode.GetString(buffer);
            Console.WriteLine("轉換後: {0}", msg);
            
            //按下q退出
            ConsoleKey key;
            Console.WriteLine("按下q退出");
            do
            {
                key = Console.ReadKey(true).Key;
                client.Close();
                streamToServer.Close();
            } while (key != ConsoleKey.Q);
        }
    }
}


運行結果:



這種一對一的方式,在實際開發中幾乎是不存在,這只是一個概念,一個流程,接下來會有些難度了。恩恩,今天就這樣了~~~

地方、

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章