/*
寫一個同步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);
}
}
}
運行結果:
這種一對一的方式,在實際開發中幾乎是不存在,這只是一個概念,一個流程,接下來會有些難度了。恩恩,今天就這樣了~~~
地方、