C# TCP/UDP 通訊程式碼講解
阿新 • • 發佈:2019-01-26
之前寫一個檔案傳輸軟體的時候不過的瞭解這 TCP和UDP的 一些概念和 知識,趁著目前還熱乎著,寫下來溫習一下。。第一次寫部落格 有不好的地方歡迎指出來。
首先介紹下我之前寫的思路。大家都知道 UDP是沒有連線是不可靠的,而TCP是面向連線可靠的連線。雖然TCP面向連線並且可靠但是同時效率也是相對沒有UDP好,但是檔案的傳輸是不允許丟失的就是必須可靠所以選擇用TCP,當然也可以用UDP將檔案打包傳送然後再檢驗。
TCP還有一個需要UDP的地方因為我們自己的電腦都不是固定的IP所以不能一直給TCP指定一個伺服器IP 一般自己寫的程式都是指定自己的電腦為伺服器這樣就導致每次IP都在改變所以這時候就需要 UDP的廣播,通過UDP的廣播將伺服器的IP傳送到客戶端然後客戶端接收到IP再建立TCP的連線。。差不多就是這樣一個思路。
接下來講解程式碼:
/// <summary>
/// 傳送廣播資料包
/// </summary>
public virtual void UDPSendInfo(String SendMsg)
{
UDPS = new Thread(UDPSendThread);
UDPS.IsBackground = true;
UDPS.Start(SendMsg);
}
這個是UDP傳送資訊的函式通過 執行緒來發送 將要傳送的資訊 通過引數 SeendMsg 傳送給執行緒
根據上面講的思路我們要利用UDP傳送的是我們的IP地址所以 SendMsg 裡面裝的是我們的IP地址。 作為伺服器這邊就 隔一段時間就傳送 一次 IP地址資訊讓客戶端能夠接收到。IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, UDPport); 這句話說明 傳送的是廣播資訊。/// <summary> /// UDP傳送資訊 /// </summary> protected void UDPSendThread(Object SendMsg) { UDPSend = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一個Scoket實習,採用UDP傳輸 while (true) { String IpMsg = (String)SendMsg; //IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, UDPport);//初始化一個傳送廣播和指定埠的網路埠例項 IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, UDPport); UDPSend.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);//設定該scoket例項的傳送形式 byte[] buffer = System.Text.Encoding.UTF8.GetBytes(IpMsg + "-"); UDPSend.SendTo(buffer, iep); Thread.Sleep(10); //間隔一定時間傳送一次 } }
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(IpMsg + "-");
這個是吧資訊轉換為位元組流資訊
/// <summary> /// 獲取本地IP /// </summary> /// <param name="pos"></param> /// <returns></returns> public static String GetLocalIP(int pos = 1) { try { // 獲得本計算機的名稱 string hostName = Dns.GetHostName(); // 通過名字獲得IP地址 IPHostEntry hostInfo = Dns.GetHostByName(hostName); IPAddress[] address = hostInfo.AddressList; // 建立 ipAddress 陣列來存放字串形式的IP地址 string[] ipAddress = new string[address.Length]; for (int index = 0; index < address.Length; index++) { ipAddress[index] = address[index].ToString(); } pos = ipAddress.Length - 1; return Dns.Resolve(Dns.GetHostName()).AddressList[pos].ToString(); } catch (Exception exc) { //Error_Info = exc.Message; //WriteLogger(exc.ToString()); return ""; } }
通過這個函式獲取自己計算機的IP地址 以用於廣播發送給客戶端
pos = ipAddress.Length - 1;
return Dns.Resolve(Dns.GetHostName()).AddressList[pos].ToString();
這2句還是比較關鍵的 經過測試這獲取的就是自己的公網IP 如果是路由器連著就是 路由器分配給自己的IP 總之一般都是可以用的。
/// <summary>
/// UDP接受廣播資訊
/// </summary>
public virtual void UDPRecInfo()
{
UDPR = new Thread(UDPRecThread);
UDPR.IsBackground = true;
UDPR.Start();
}
接下來是 負責接收的這邊。同樣利用執行緒來負責接收 以免阻塞主執行緒卡死 並且設定為後臺執行緒。這裡也沒有其他好說的
/// <summary>
/// UDP接受執行緒
/// </summary>
protected void UDPRecThread()
{
UDPRec = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一個Scoket協議
IPEndPoint iep = new IPEndPoint(IPAddress.Any, UDPport);
//IPEndPoint iep = new IPEndPoint(IPAddress.Any, UDPport);//初始化一個偵聽區域網內部所有IP和指定埠
EndPoint ep = (EndPoint)iep;
UDPRec.Bind(iep);//繫結這個例項
byte[] buffer = new byte[2048];//設定緩衝資料流
UDPRec.ReceiveFrom(buffer, ref ep);//接收資料,並確把資料設定到緩衝流裡
serveripaddress = System.Text.Encoding.UTF8.GetString(buffer); //獲取ip地址
serveripaddress = serveripaddress.Split('-')[0];
UDPRec.Close();
Start(); //啟動
UDPR.Abort(); //釋放資源
}
這裡也是 基本上 註釋 講的差不多了 serveripaddress = serveripaddress.Split('-')[0];
這個地方要注意一下前面我封裝的時候就是 在IP最後加了一個 “-”作為 結束標識。可以自己定義
UDPport
這個就是一個埠 基本上TCP和UDP 都是樣的通過埠來識別是哪個應用程式來接受這段資訊,基本模式也是埠 + IP地址的組合 就可以通訊。
(IPAddress.Any
這個則是偵聽所有的
/// <summary>
/// 開啟前傳送UDP電報將本身的serveripadress傳送給客戶端,和SearchStart 選取一個就好
/// </summary>
/// <returns></returns>
public override bool SearchStart()
{
UDPSendInfo(serveripaddress);
// 建立負責監聽的套接字,注意其中的引數;
socketWatch = new Socket(IPAddress.Parse(serveripaddress).AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 獲得文字框中的IP物件;
IPAddress address = IPAddress.Parse(serveripaddress);
// 建立包含ip和埠號的網路節點物件;
IPEndPoint endPoint = new IPEndPoint(address, port);
try
{
// 將負責監聽的套接字繫結到唯一的ip和埠上;
socketWatch.Bind(endPoint);
}
catch (SocketException se)
{
Error_Info = "異常:" + se.Message;
return false;
}
// 設定監聽佇列的長度;
socketWatch.Listen(clent_lenght);
// 建立負責監聽的執行緒;
threadWatch = new Thread(WatchConnecting);
threadWatch.IsBackground = true;
threadWatch.Start();
File_Rec(); //處理流執行緒
_is_start = true;
return true;
}
這個是啟動TCP 的執行緒 serveripaddress
這個變數就是之前我們獲取到的本機的IP地址,將它與我們開啟的TCP進行繫結,然後threadWatch = new Thread(WatchConnecting);
threadWatch.IsBackground = true;
threadWatch.Start();
用這個建立負責監聽是否有客戶端連線 的執行緒
/// <summary>
/// 伺服器監聽函式,進行對子客戶端的監聽
/// </summary>
private void WatchConnecting()
{
while (true) // 持續不斷的監聽客戶端的連線請求;
{
try {
// 開始監聽客戶端連線請求,Accept方法會阻斷當前的執行緒;
Socket sokConnection = socketWatch.Accept(); // 一旦監聽到一個客戶端的請求,就返回一個與該客戶端通訊的 套接字;
// 想列表控制元件中新增客戶端的IP資訊;
//lbOnline.Items.Add(sokConnection.RemoteEndPoint.ToString());
// 將與客戶端連線的 套接字 物件新增到集合中;
String skey = sokConnection.RemoteEndPoint.ToString();
AddNewClient(skey, sokConnection); //新增一個新的客戶端的資訊
SendInitNameInfo(sokConnection, skey); //給新連線的客戶端 初始化其他客戶端的資訊和給其他客戶端新增新客戶端的資訊
Thread thr = new Thread(RecMsg); // 為每一個接收訊息的連結單獨建立一個執行緒
thr.IsBackground = true;
thr.Start(sokConnection);
dictThread.Add(skey, thr); // 將新建的執行緒 新增 到執行緒的集合中去。
//sendInitInfo();//傳送初始化資訊;
}catch(Exception ex)
{
Error_Info = ex.Message;
WriteLogger(ex.ToString());
}
}
}
建立一個死迴圈來不斷 監聽是否有客戶端連線進來如果有客戶端連線進來則 給每個 連線客戶端建立一個訊息執行緒,然後將這個這個客戶端的埠資訊和物件資訊存入Dictionary字典 一般都是用這個來儲存的當然 選擇自己最擅長用的 自己覺得最好用的就好。
AddNewClient(skey, sokConnection); //新增一個新的客戶端的資訊
我這裡則是封裝為一個函數了將這個過程。
SendInitNameInfo(sokConnection, skey); //給新連線的客戶端 初始化其他客戶端的資訊和給其他客戶端新增新客戶端的資訊
這個是我自己的處理邏輯 就不解釋了。
/// <summary>
/// 監聽到後資訊的檢測函式
/// </summary>
/// <param name="sokConnectionparn"></param>
void RecMsg(object sokConnectionparn) // 負責接收訊息的執行緒
{
Socket sokClient = sokConnectionparn as Socket; //套接字物件
String skey = sokClient.RemoteEndPoint.ToString(); //套接字資訊
while (true)
{
// 定義一個2M的快取區;
byte[] arrMsgRec = new byte[Msg_send_size];
// 將接受到的資料存入到輸入 arrMsgRec中;
int length = -1;
try
{
length = sokClient.Receive(arrMsgRec,0, Msg_send_size,SocketFlags.None); // 接收資料,並返回資料的長度;
}
catch (SocketException se)
{
Error_Info = "異常:" + se.Message;
/* // 從 通訊套接字 集合中刪除被中斷連線的通訊套接字;
dict.Remove(sokClient.RemoteEndPoint.ToString());
//從名字列表刪除被中斷的套接字的名字
ClientNameList.Remove(sokClient.RemoteEndPoint.ToString());*/
///刪除一箇中斷的客戶端資訊
DelClient(skey);
// 從通訊執行緒集合中刪除被中斷連線的通訊執行緒物件;
dictThread.Remove(skey);
// 從列表中移除被中斷的連線IP
//lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
break;
}
catch (Exception ex)
{
Error_Info = "異常:" + ex.Message;
/* // 從 通訊套接字 集合中刪除被中斷連線的通訊套接字;
dict.Remove(sokClient.RemoteEndPoint.ToString());
//從名字列表刪除被中斷的套接字的名字
ClientNameList.Remove(sokClient.RemoteEndPoint.ToString());*/
///刪除一箇中斷的客戶端資訊
DelClient(skey);
// 從通訊執行緒集合中刪除被中斷連線的通訊執行緒物件;
dictThread[skey].Abort();
dictThread.Remove(skey);
// 從列表中移除被中斷的連線IP
//lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
break;
}
try {
//Infomation_Select(arrMsgRec, length); //進行處理資訊篩選
AddRecInfo(arrMsgRec);
SendByteToAll(arrMsgRec, sokClient); //轉發
//SendFileByte(arrMsgRec); //轉發
} catch (Exception ex)
{
Error_Info = ex.Message;
}
}
}
這個是每個客戶端對應 的一個接受訊息的執行緒 負責接收一個客戶端傳送的訊息同時負責將訊息 轉發給其他同時線上的客戶端 如果客戶端連線斷開則Error_Info = "異常:" + se.Message;
/* // 從 通訊套接字 集合中刪除被中斷連線的通訊套接字;
dict.Remove(sokClient.RemoteEndPoint.ToString());
//從名字列表刪除被中斷的套接字的名字
ClientNameList.Remove(sokClient.RemoteEndPoint.ToString());*/
///刪除一箇中斷的客戶端資訊
DelClient(skey);
// 從通訊執行緒集合中刪除被中斷連線的通訊執行緒物件;
dictThread.Remove(skey);
// 從列表中移除被中斷的連線IP
//lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
break;
刪除這個客戶端的訊息同時break跳出死迴圈結束這個執行緒
length = sokClient.Receive(arrMsgRec,0, Msg_send_size,SocketFlags.None); // 接收資料,並返回資料的長度;
這裡表示要衝 Socket 接收快取中 一次提取多少長度的內容出來Msg_send_size
代表要一次提取的內容長度,所以請在傳送的時候也 一次傳送同樣大小的資料包,因為本人制作的是檔案傳輸 一般檔案都不是一下子能傳完的所以必須將檔案分包傳送,然後自己封裝每個檔案包的 頭和尾加上判斷是否接受完畢和傳送完畢。當全部接受的時候 然後寫回到磁碟。訊息傳送的邏輯和封裝都是自己自定義
/// <summary>
/// 傳送檔案流
/// </summary>
/// <param name="msg">檔案流資訊</param>
public override void SendFileByte(byte [] msg)
{
try
{
foreach (Socket s in dict.Values)
{
if (s.Connected)
s.Send(msg, Msg_send_size, SocketFlags.None);
}
}catch(Exception ex)
{
Error_Info = ex.Message;
}
}
伺服器傳送流訊息, 傳送的資訊都是 以位元組流的形式傳送。提取每個線上的客戶端都給他們傳送一遍。注意傳送的資訊量的大小Msg_send_size
<strong><span style="font-size:32px;">客戶端:</span></strong>
/// <summary>
/// 啟動客戶端
/// </summary>
/// <returns></returns>
public override Boolean Start()
{
IPAddress ip = IPAddress.Parse(serveripaddress);
IPEndPoint endPoint = new IPEndPoint(ip, port);
sockClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
Error_Info += "與伺服器連線中……\r\n";
//MessageBox.Show("與伺服器連線中……");
sockClient.Connect(endPoint);
}
catch (SocketException se)
{
Error_Info = "連線錯誤:" + se.Message;
WriteLogger(se.ToString());
return false;
//this.Close();
}
Error_Info = "與伺服器連線成功!!!\r\n";
//MessageBox.Show("與伺服器連線成功!!!");
threadClient = new Thread(RecMsg); //客戶端接收資訊
threadClient.IsBackground = true;
threadClient.Priority = ThreadPriority.Highest;
threadClient.Start(); //開啟執行緒
Thread intname = new Thread(InitThread);
intname.IsBackground = true;
intname.Start();
File_Rec(); //處理留資訊 執行緒
_is_start = true;
return true;
}
當客戶端接收到伺服器傳送的UDP資料包的時候 上面我們已經看到客戶端 接收UDP資料包並且解析出來的伺服器的IP地址這個時候就可以使用解析出來的IP地址serveripaddress
與客戶端這邊的TCPSocket進行綁定了然後開啟接收伺服器傳送的訊息的執行緒/// <summary>
/// 接受伺服器傳送來的訊息
/// </summary>
protected void RecMsg()
{
while (true)
{
if (!sockClient.Connected)
{
Error_Info = "和伺服器連線斷開.....\r\n";
WriteLogger(Error_Info);
break;
}
// 定義一個Msg_send_size的快取區;
byte[] arrMsgRec = new byte[Msg_send_size];
// 將接受到的資料存入到輸入 arrMsgRec中;
int length = -1;
try
{
length = sockClient.Receive(arrMsgRec , 0, Msg_send_size, SocketFlags.None); // 接收資料,並返回資料的長度;
}
catch (SocketException se)
{
Error_Info += "異常;" + se.Message;
WriteLogger(se.ToString());
return;
}
catch (Exception se)
{
Error_Info += "異常:" + se.Message;
WriteLogger(se.ToString());
return;
}
try
{
//Infomation_Select(arrMsgRec, length); //資訊篩選
AddRecInfo(arrMsgRec);
}
catch (Exception ex)
{
Error_Info = ex.Message;
WriteLogger(ex.ToString());
}
}
}
這邊接收的基本和伺服器一個樣沒什麼好說的同樣要注意Msg_send_size
/// <summary>
/// 傳送檔案流
/// </summary>
/// <param name="msg">檔案流資訊</param>
public override void SendFileByte(byte []msg)
{
try {
sockClient.Send(msg);//傳送位元組流
}catch(Exception ex)
{
Error_Info = ex.Message;
}
}
接下來是客戶端傳送 位元組流資訊基本上看下就明白了。
目前就先這樣 有問題歡迎大家提出來