這個系列文章將會對C#的網(wǎng)絡編程常用方法和編程技巧進行記錄。本文主要記錄基于TCP/IP協(xié)議的本地客戶端與華為云電腦上運行的服務端進行通信的編程過程。
C#常用網(wǎng)絡通信類
IPEndPoint類
EndPoint可以理解為終端,IPEndPoint理解為以IP方式解釋這個終端
這個類是EndPoint抽象類的實現(xiàn)類。
Socket對象有兩個類型,一個是RemoteEndPoint類,另一個是LocalEndPoint類,即一個是遠程終端,另一個是本地終端。
屬性Address:使用IPv4表示的地址;
屬性Port:使用int表示的端口號(0-65535),一般數(shù)值選擇10000以上會比較好,可以避免其他應用占用沖突的問題。
Socket類
Socket類既可以用在服務端的開發(fā),也可以用在客戶端的開發(fā),構造時需要三個參數(shù):
1.參數(shù)AddressFamily:指定使用的IPv4地址InterNetwork;
2.參數(shù)SocketType:指定使用流式傳輸Stream;
3.參數(shù)ProtocolType:指定協(xié)議類型Tcp
類方法:
1.Bind()方法:綁定IP與端口,這樣就成為了服務器,可以監(jiān)聽指定IP的特定端口;
2.Listen()方法:置為監(jiān)聽狀態(tài),參數(shù)是允許的最大掛起數(shù)。
3.Accept()方法:接收客戶端的連接,返回一個Socket對象,此方法會阻塞當前的線程,即如果程序主線程執(zhí)行到Accept()方法的時候,主線程會停止并且等待客戶端的連接而導致后面的代碼無法執(zhí)行,因此在使用的過程中一般需要配合多線程操作執(zhí)行這個方法,結合多線程的尾遞歸來允許接收多個客戶端的連接。
4.Receive()方法:接收客戶端發(fā)送過來的消息,以字節(jié)為單位進行操作,此方法同樣會阻塞當前的線程,故同樣需要開啟新的線程來執(zhí)行這個方法。
5.Send()方法:發(fā)送消息,以字節(jié)為單位。
在本地計算機創(chuàng)建客戶端
首先需要創(chuàng)建一個Socket對象,通過如下代碼:
public class ClientControl
{
private Socket clientSocket;
public ClientControl() // 方法名稱與類名相同,即為構造函數(shù)
{
clientControl = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
}
接下來我們需要創(chuàng)建一個連接服務端的方法,這樣我們就可以調用這個連接方法并且傳入ip地址的字符串和整數(shù)型的端口號就可以了。
public void Connect(string ip, int port)
{
clientSocket.Connect(ip, port);
}
在云端計算機創(chuàng)建服務端
首先需要創(chuàng)建一個Socket對象,通過如下代碼:
public class ServerControl
{
private Socket serverSocket;
public ServerControl() // 方法名稱與類名相同,即為構造函數(shù)
{
serverControl = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
}
接下來我們需要啟動服務器,一般會把下面的代碼添加到一個新的方法中,然后在服務端的主函數(shù)中調用這個方法來啟動服務器的相關服務。
serverSocket.Bind(new IPEndPoint(IPAddress.Parse("xxx.xx.xx.xxx"), xxxxx/*端口號*/));
這里通過parse方法將一個字符串IP地址轉換成IPAddress所需要的十六進制表示的IP地址,但是這樣做的弊端是如果這個程序轉移到其他機器上運行,還需要注意修改這里的IP地址的設置值,因此我們還可以嘗試下面的方法:
serverSocket.Bind(new IPEndPoint(IPAddress.Any, xxxxx/*端口號*/));
接下來我們需要服務器對我們設置的端口進行監(jiān)聽
serverSocket.Listen(10); // 開啟監(jiān)聽,最大掛起數(shù)設置為10
以上內容設置完成之后,我們就需要使能客戶端的連接,接收客戶端的連接,并且返回這個客戶端的Socket對象
Socket client = serverSocket.Accept(); // 注意,這個Accept方法會掛起當前線程
如果我們想獲得連接的客戶端的ip地址、分配的端口等屬性信息,我們可以通過返回的Socket對象來查看,但是Socket對象無法直接獲得ip和端口等屬性,我們需要調用Socket對象中的
RemoteEndPoint方法,因為從服務器的角度來說,客戶端屬于遠端的終端。另外一個值得注意的地方就是Socket.RemoteEndPoint是一個EndPoint類型的抽象類,仍然無法直接獲得ip地址和端口等屬性信息,因此我們需要將這個抽象類用IPEndPoint的類型來解釋:
IPEndPoint point = client.RemoteEndPoint as IPEndPoint;
此時,我們就可以獲得point對象的Address屬性和Port端口屬性
Console.WriteLine(point.Address); // 控制臺輸出客戶端ip地址
Console.WriteLine(point.Port); // 控制臺輸出客戶端端口
通過上面的在客戶端和服務器端分別創(chuàng)建工程代碼,我們就成功實現(xiàn)了一個最簡單的服務端與客戶端的連接,現(xiàn)在服務端啟動調試,然后再啟動客戶端的調試,如果沒有報錯,那么就實現(xiàn)了最簡單基于TCP/IP通信協(xié)議的通信過程。
存在的問題
通過上面的代碼創(chuàng)建的客戶端和服務器,由于我們在編程的時候沒有采用多線程和循環(huán)結構,因此服務端最多只能連接一個客戶端,并且雙方都不具備發(fā)送消息的功能,因此下面我們還需要進行完善。
多線程——解決服務端連接多個客戶端的問題
之前說到過,Accept()方法是會阻塞當前的線程的,因此我們需要將這個方法放入新的線程之中。
第一步、將服務端接收客戶端的代碼封裝成一個方法
private void Accept()
{
Socket client = serverSocket.Accept(); // 注意,這個Accept方法會掛起當前線程
Accept(); // 尾遞歸循環(huán)執(zhí)行代碼
}
第二步、在服務端的開啟服務方法中啟動多線程,當然了,如果你把所有綁定端口監(jiān)聽端口等操作都放在了構造函數(shù)中的話,那么你可以在構造函數(shù)中啟動多線程
Thread threadAccept = new Thread(Accept); // 將前面我們寫的Accept方法添加到多線程進程之中
threadAccept.IsBackground = true; // 設置此線程是否是背景線程:如果設置為true表示主線程結束的時候此背景線程會立即結束,
//如果設置為false則即使主線程結束,此線程也不會結束。
threadAccept.Start(); // 啟動線程
這樣當我們啟動主機后,在啟動多個客戶端,服務端都可以響應。
客戶端向服務端發(fā)送消息
通過send方法發(fā)送消息,此方法接收一個字節(jié)數(shù)組byte[]類型作為參數(shù),因此我們需要把字符串類型的數(shù)據(jù)轉換成字節(jié)數(shù)組類型。
public void Send(string msg)
{
clientSocket.Send(Encoding.UTF8.GetBytes(msg)); // 將字符串轉換成字節(jié)數(shù)組
}
服務器接收客戶端的消息
類似地,在服務端我們通過receive方法來接收來自客戶端的消息,這個方法接收一個字節(jié)數(shù)組作為參數(shù),同時返回一個int類型的值表示接收到的字節(jié)個數(shù),接收到的內容將會存放在傳入的字節(jié)數(shù)組參數(shù)之中。
byte[] msg = new byte[1024]; // 創(chuàng)建一個字節(jié)數(shù)組
int msgLen = client.Receive(msg);
strMsg = Encoding.UTF8.GetString(msg, 0, msgLen); // 將接收到的字符數(shù)組轉換為字符串,第二個參數(shù)表示轉換起始為止
// 第三個參數(shù)表示轉換結束位置
值得注意的是Receive方法也會阻塞當前的線程,只要程序執(zhí)行到這個指令,就會一直等待客戶端發(fā)來消息,如果客戶端不發(fā)送就會一直等待,后續(xù)代碼無法繼續(xù)執(zhí)行,因此我們仍然需要將這個方法放入新的線程之中,和前面開啟新線程的方法類似,區(qū)別在于我們需要向這個方法之中傳入client對象,因為新線程的數(shù)量隨著客戶端接入的數(shù)量增加,不同的線程應該處理不同的客戶端,對于需要傳遞參數(shù)的多線程的開啟,需要注意多線程中傳遞的參數(shù)只能是object對象,在線程實現(xiàn)方法中,我們需要對object對象進行解釋,例子如下:
private void Receive(object obj) // 線程實現(xiàn)方法
{
Socket client = obj as Socket; // 將obj參數(shù)解釋為Socket對象
byte[] msg = new byte[1024]; // 創(chuàng)建一個字節(jié)數(shù)組
int msgLen = client.Receive(msg);
strMsg = Encoding.UTF8.GetString(msg, 0, msgLen); // 將接收到的字符數(shù)組轉換為字符串,第二個參數(shù)表示轉換起始為止
// 第三個參數(shù)表示轉換結束位置
Receive(client) // 尾遞歸
}
多線程的啟動如下:
Thread threadAccept = new Thread(Receive); // 將前面我們寫的Accept方法添加到多線程進程之中
threadAccept.IsBackground = true; // 設置此線程是否是背景線程:如果設置為true表示主線程結束的時候此背景線程會立即結束,
//如果設置為false則即使主線程結束,此線程也不會結束。
threadAccept.Start(client); // 啟動線程,并且傳遞參數(shù)client
解決問題——客戶端斷開連接
上面的代碼可以實現(xiàn)客戶端向服務端發(fā)送消息,服務端接收消息,但是如果遇到客戶端主動關閉程序下線,服務端就會拋出異常,問題發(fā)生的地方就在于服務端的Receive方法處,因為client對象已經斷開,receive方法就會出現(xiàn)錯誤,因此在這個地方我們需要進行
trycatch的異常處理環(huán)節(jié)。
try
{
byte[] msg = new byte[1024]; // 創(chuàng)建一個字節(jié)數(shù)組
int msgLen = client.Receive(msg);
strMsg = Encoding.UTF8.GetString(msg, 0, msgLen);
}
catch(Exception ex)
{
//…………下線處理………………
}
服務端向客戶端發(fā)送消息
與客戶端向服務端發(fā)送消息一樣,通過client.Send()方法向客戶端發(fā)送消息。
客戶端接收服務端發(fā)送消息
與前面一樣,通過clientSocket.Receive()方法接收服務器消息,注意需要開啟新線程防止阻塞。