.NET平臺下C# socket通信
https://blog.csdn.net/subin_iecas/article/details/80289915
在開始介紹socket前先補充補充基礎知識,在此基礎上理解網絡通信纔會順理成章。
TCP/IP:Transmission Control Protocol/Internet Protocol,傳輸控制協議/因特網互聯協議,又名網絡通訊協議。
簡單來說:TCP控制傳輸數據,負責發現傳輸的問題,一旦有問題就發出信號,要求重新傳輸,直到所有數據安全正確地傳輸到目的地,而IP是負責給因特網中的每一臺電腦定義一個地址,以便傳輸。從協議分層模型方面來講:TCP/IP由:網絡接口層(鏈路層)、網絡層、傳輸層、應用層。它和OSI的七層結構以及對於協議族不同,下圖簡單表示:
現階段socket通信使用TCP、UDP協議,相對應UDP來說,TCP則是比較安全穩定的協議了。本文只涉及到TCP協議來說socket通信。一般建立TCP需要三次握手才能建立,而斷開連接則需要四次握手。整個過程如下圖所示,在握手基礎上延伸socket通信的基本過程。
表1 TCP/IP結構
圖1 TCP/IP關係圖
圖2 三次握手 四次握手關係
在此基礎上,socket連接過程:
服務器監聽:服務器端socket並不定位具體的客戶端socket,而是處於等待監聽狀態,實時監控網絡狀態。
客戶端請求:客戶端clientSocket發送連接請求,目標是服務器的serverSocket。爲此,clientSocket必須知道serverSocket的地址和端口號,進行掃描發出連接請求。
連接確認:當服務器socket監聽到或者是受到客戶端socket的連接請求時,服務器就響應客戶端的請求,建議一個新的socket,把服務器socket發送給客戶端,一旦客戶端確認連接,則連接建立。
注:在連接確認階段:服務器socket即使在和一個客戶端socket建立連接後,還在處於監聽狀態,仍然可以接收到其他客戶端的連接請求,這也是一對多產生的原因。
下圖簡單說明連接過程:
代碼可以詳見,附件中的內容,不過唯一值得說一下的事情是,其實在TCP/IP傳輸的數據都應該是以字節爲單位的。比如說傳送50個double類型的數據就是傳送400個字節的數組。所以在這個過程中,我們首先需要將這一類的數據首先轉化成爲字節數組才能進行傳遞。在這個過程中,LabVIEW主要是通過一個節點完成的轉化。這個函數的主要的操作就是將任意類型的數據轉化爲字節數組然後在進行數據傳輸。
這裏又要進行一個說明,由於C#本身的原因,所以在字節存儲格式的時候都是由小端進行存儲的,但是在TCP/IP傳輸格式的時候標準默認的時候都是用大端方式進行傳輸的,所以拿到的數據不能直接進行解析。由於這個原因這邊編寫一個可以完成大小端轉化的類,方便用戶在TCP/IP以及其他串口等類型的時候進行使用。其中GetBytes可以完成多種類型的轉化,包括數值和數組。使用十分方便。
下圖所示是範例運行的過程,其中127.0.0.1,是使用本機IP號的時候IP地址,其中一個是Server端一個是Client端。
同時與LabVIEW Simple TCP的範例可以共同使用,相互做Server與Client都沒有問題。如下圖所示。
介紹了通信的過程以及機制,但實際上這中間簡單的TCP的通信在實際應用中是比較的,利用C# TCP多線程的應用案例,這邊一起來分析一下多線程的代碼,大家也可以在文章附帶的Demo進行嘗試。
我們點擊啓動服務按鈕,服務器:
// 創建負責監聽的套接字,注意其中的參數;
socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 獲得文本框中的IP對象;
IPAddress address = IPAddress.Parse(txtIp.Text.Trim());
// 創建包含ip和端口號的網絡節點對象;
IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
try
{
// 將負責監聽的套接字綁定到唯一的ip和端口上;
socketWatch.Bind(endPoint);
}
catch (SocketException se)
{
MessageBox.Show("異常:"+se.Message);
return;
}
首先我們創建負責監聽的套接字, 在Bind綁定後,我們創建了負責監聽的線程。代碼如下:
// 設置監聽隊列的長度;
socketWatch.Listen(10);
// 創建負責監聽的線程;
threadWatch = new Thread(WatchConnecting);
threadWatch.IsBackground = true;
threadWatch.Start();
ShowMsg("服務器啓動監聽成功!");
btnBeginListen.Enabled = false;
其中 WatchConnecting方法是負責監聽新客戶端請求的。然後讓我們看一下WatchConnecting的代碼。
/// <summary>
/// 監聽客戶端請求的方法;
/// </summary>
void WatchConnecting()
{
while (true) // 持續不斷的監聽客戶端的連接請求;
{
// 開始監聽客戶端連接請求,Accept方法會阻斷當前的線程;
Socket sokConnection = socketWatch.Accept(); // 一旦監聽到一個客戶端的請求,就返回一個與該客戶端通信的 套接字;
// 想列表控件中添加客戶端的IP信息;
lbOnline.Items.Add(sokConnection.RemoteEndPoint.ToString());
// 將與客戶端連接的 套接字 對象添加到集合中;
dict.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection);
ShowMsg("客戶端連接成功!");
Thread thr = new Thread(RecMsg);
thr.IsBackground = true;
thr.Start(sokConnection);
dictThread.Add(sokConnection.RemoteEndPoint.ToString(), thr); // 將新建的線程 添加 到線程的集合中去。
}
}
這個線程是一直存在的,主要的任務就是監聽是否有Client與Server端進行連接,如果連接成功則會另開一個線程”RecMsg”。在該線程中則主要是得到字符數據的處理,包括接受數據以及發送數據。
void RecMsg(object sokConnectionparn)
{
Socket sokClient = sokConnectionparn as Socket;
while (true)
{
// 定義一個2M的緩存區;
byte[] arrMsgRec = new byte[1024 * 1024 * 2];
// 將接受到的數據存入到輸入 arrMsgRec中;
int length = -1;
try
{
length = sokClient.Receive(arrMsgRec); // 接收數據,並返回數據的長度;
}
catch (SocketException se)
{
ShowMsg("異常:" + se.Message);
// 從 通信套接字 集合中刪除被中斷連接的通信套接字;
dict.Remove(sokClient.RemoteEndPoint.ToString());
// 從通信線程集合中刪除被中斷連接的通信線程對象;
dictThread.Remove(sokClient.RemoteEndPoint.ToString());
// 從列表中移除被中斷的連接IP
lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
break;
}
catch (Exception e)
{
ShowMsg("異常:" + e.Message);
// 從 通信套接字 集合中刪除被中斷連接的通信套接字;
dict.Remove(sokClient.RemoteEndPoint.ToString());
// 從通信線程集合中刪除被中斷連接的通信線程對象;
dictThread.Remove(sokClient.RemoteEndPoint.ToString());
// 從列表中移除被中斷的連接IP
lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
break;
}
if (arrMsgRec[0] == 0) // 表示接收到的是數據;
{
string strMsg = System.Text.Encoding.UTF8.GetString(arrMsgRec,1, length-1);// 將接受到的字節數據轉化成字符串;
ShowMsg(strMsg);
}
if (arrMsgRec[0] == 1) // 表示接收到的是文件;
{
SaveFileDialog sfd = new SaveFileDialog();
if (sfd.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
{// 在上邊的 sfd.ShowDialog() 的括號裏邊一定要加上 this 否則就不會彈出 另存爲 的對話框,而彈出的是本類的其他窗口,,這個一定要注意!!!【解釋:加了this的sfd.ShowDialog(this),“另存爲”窗口的指針才能被SaveFileDialog的對象調用,若不加thisSaveFileDialog 的對象調用的是本類的其他窗口了,當然不彈出“另存爲”窗口。】
string fileSavePath = sfd.FileName;// 獲得文件保存的路徑;
// 創建文件流,然後根據路徑創建文件;
using (FileStream fs = new FileStream(fileSavePath, FileMode.Create))
{
fs.Write(arrMsgRec, 1, length - 1);
ShowMsg("文件保存成功:" + fileSavePath);
}
}
}
}
}
其實這就是建立多線程TCP通信的主要過程,這裏值得注意的就是其實監聽線程監聽的一直都是一個固定的端口,在應用層如果建立建立連接了,則連接不會使用監聽的端口號,而會使用另一個空閒的端口號,這樣才能保證一直連接監聽一直使用一個固定端口號,從而使得連接也變得更加容易。Demo有數據交換的,也有類似於通信的,大家都可以參考。