C#網(wǎng)絡(luò)編程之Socket編程
一:什么是SOCKET
socket的英文原義是“孔”或“插座”。作為進程通信機制,取后一種意思。通常也稱作“套接字”,用于描述IP地址和端口,是一個通信鏈的句柄(其實就是兩個程序通信用的)。
socket非常類似于電話插座。以一個電話網(wǎng)為例:電話的通話雙方相當于相互通信的2個程序,電話號碼就是ip地址。任何用戶在通話之前,首先要占有一部電話機,相當于申請一個socket;同時要知道對方的號碼,相當于對方有一個固定的socket。然后向?qū)Ψ綋芴柡艚校喈斢诎l(fā)出連接請求。對方假如在場并空閑,拿起電話話筒,雙方就可以正式通話,相當于連接成功。雙方通話的過程,是一方向電話機發(fā)出信號和對方從電話機接收信號的過程,相當于向socket發(fā)送數(shù)據(jù)和從socket接收數(shù)據(jù)。通話結(jié)束后,一方掛起電話機相當于關(guān)閉socket,撤銷連接。
套接字分類
為了滿足不同程序?qū)νㄐ刨|(zhì)量和性能的要求,一般的網(wǎng)絡(luò)系統(tǒng)都提供了以下3種不同類型的套接字,以供用戶在設(shè)計程序時根據(jù)不同需要來選擇:
流式套接字(SOCK_STREAM)
:提供了一種可靠的、面向連接的雙向數(shù)據(jù)傳輸服務(wù)。實現(xiàn)了數(shù)據(jù)無差錯,無重復(fù)的發(fā)送,內(nèi)設(shè)流量控制,被傳輸?shù)臄?shù)據(jù)被看做無記錄邊界的字節(jié)流。在TCP/IP協(xié)議簇中,使用TCP實現(xiàn)字節(jié)流的傳輸,當用戶要發(fā)送大批量數(shù)據(jù),或?qū)?shù)據(jù)傳輸?shù)目煽啃杂休^高要求時使用流式套接字。
數(shù)據(jù)報套接字(SOCK_DGRAM)
:提供了一種無連接、不可靠的雙向數(shù)據(jù)傳輸服務(wù)。數(shù)據(jù)以獨立的包形式被發(fā)送,并且保留了記錄邊界,不提供可靠性保證。數(shù)據(jù)在傳輸過程中可能會丟失或重復(fù),并且不能保證在接收端數(shù)據(jù)按發(fā)送順序接收。在TCP/IP協(xié)議簇中,使用UDP實現(xiàn)數(shù)據(jù)報套接字。
原始套接字(SOCK_RAW)
:該套接字允許對較低層協(xié)議(如IP或ICMP)進行直接訪問。一般用于對TCP/IP核心協(xié)議的網(wǎng)絡(luò)編程。
二:SOCKET相關(guān)概念
1、端口
在Internet上有很多這樣的主機,這些主機一般運行了多個服務(wù)軟件,同時提供幾種服務(wù)。每種服務(wù)都打開一個Socket,并綁定到一個端口上,不同的端口對應(yīng)于不同的服務(wù)(應(yīng)用程序),因此,在網(wǎng)絡(luò)協(xié)議中使用端口號識別主機上不同的進程。
例如:http使用80端口,F(xiàn)TP使用21端口。
2、協(xié)議
2.1 TCP:
TCP是一種面向連接的、可靠的,基于字節(jié)流的傳輸層通信協(xié)議。為兩臺主機提供高可靠性的數(shù)據(jù)通信服務(wù)。它可以將源主機的數(shù)據(jù)無差錯地傳輸?shù)侥繕酥鳈C。當有數(shù)據(jù)要發(fā)送時,對應(yīng)用進程送來的數(shù)據(jù)進行分片,以適合于在網(wǎng)絡(luò)層中傳輸;當接收到網(wǎng)絡(luò)層傳來的分組時,它要對收到的分組進行確認,還要對丟失的分組設(shè)置超時重發(fā)等。為此TCP需要增加額外的許多開銷,以便在數(shù)據(jù)傳輸過程中進行一些必要的控制,確保數(shù)據(jù)的可靠傳輸。因此,TCP傳輸?shù)男时容^低。
2.1.1 TCP的工作過程
TCP是面向連接的協(xié)議,TCP協(xié)議通過三個報文段完成類似電話呼叫的連接建立過程,這個過程稱為三次握手,如圖所示:
第一次握手:建立連接時,客戶端發(fā)送SYN包(SEQ=x)到服務(wù)器,并進入SYN_SEND狀態(tài),等待服務(wù)器確認。
第二次握手:服務(wù)器收到SYN包,必須確認客戶的SYN(ACK=x+1),同時自己也發(fā)送一個SYN包(SEQ=y),即SYN+ACK包,此時服務(wù)器進入SYN_RECV狀態(tài)。
第三次握手:客戶端收到服務(wù)器的SYN+ACK包,向服務(wù)器發(fā)送確認包ACK(ACK=y+1),此包發(fā)送完畢,客戶端和服務(wù)器進入Established狀態(tài),完成三次握手。
2.1.2 傳輸數(shù)據(jù)
一旦通信雙方建立了TCP連接,連接中的任何一方都能向?qū)Ψ桨l(fā)送數(shù)據(jù)和接收對方發(fā)來的數(shù)據(jù)。TCP協(xié)議負責(zé)把用戶數(shù)據(jù)(字節(jié)流)按一定的格式和長度組成多個數(shù)據(jù)報進行發(fā)送,并在接收到數(shù)據(jù)報之后按分解順序重新組裝和恢復(fù)用戶數(shù)據(jù)。
利用TCP傳輸數(shù)據(jù)時,數(shù)據(jù)是以字節(jié)流的形式進行傳輸?shù)摹?/p>
2.1.3 連接的終止
建立一個連接需要三次握手,而終止一個連接要經(jīng)過四次握手,這是由TCP的半關(guān)閉(half-close)造成的。具體過程如圖所示:
2.1.4 TCP的主要特點
TCP最主要的特點如下。
(1) 是面向連接的協(xié)議。
(2) 端到端的通信。每個TCP連接只能有兩個端點,而且只能一對一通信,不能一點對多點直接通信。
(3) 高可靠性。通過TCP連接傳送的數(shù)據(jù),能保證數(shù)據(jù)無差錯、不丟失、不重復(fù)地準確到達接收方,并且保證各數(shù)據(jù)到達的順序與其發(fā)出的順序相同。
(4) 全雙工方式傳輸。
(5) 數(shù)據(jù)以字節(jié)流的方式傳輸。
(6) 傳輸?shù)臄?shù)據(jù)無消息邊界。
2.1.5 同步與異步
同步工作方式是指利用TCP編寫的程序執(zhí)行到監(jiān)聽或接收語句時,在未完成工作(偵聽到連接請求或收到對方發(fā)來的數(shù)據(jù))前不再繼續(xù)往下執(zhí)行,線程處于阻塞狀態(tài),直到該語句完成相應(yīng)的工作后才繼續(xù)執(zhí)行下一條語句。
異步工作方式是指程序執(zhí)行到監(jiān)聽或接收語句時,不論工作是否完成,都會繼續(xù)往下執(zhí)行。
2.2 UDP
UDP是一種簡單的、面向數(shù)據(jù)報的無連接的協(xié)議,提供的是不一定可靠的傳輸服務(wù)。所謂“無連接”是指在正式通信前不必與對方先建立連接,不管對方狀態(tài)如何都直接發(fā)送過去。這與發(fā)手機短信非常相似,只要知道對方的手機號就可以了,不要考慮對方手機處于什么狀態(tài)。UDP雖然不能保證數(shù)據(jù)傳輸?shù)目煽啃?,但?shù)據(jù)傳輸?shù)男瘦^高。
2.2.1 UDP與TCP的區(qū)別
(1) UDP可靠性不如TCP
TCP包含了專門的傳遞保證機制,當數(shù)據(jù)接收方收到發(fā)送方傳來的信息時,會自動向發(fā)送方發(fā)出確認消息;發(fā)送方只有在接收到該確認消息之后才繼續(xù)傳送其他信息,否則將一直等待直到收到確認信息為止。與TCP不同,UDP并不提供數(shù)據(jù)傳送的保證機制。如果在從發(fā)送方到接收方的傳遞過程中出現(xiàn)數(shù)據(jù)報的丟失,協(xié)議本身并不能做出任何檢測或提示。因此,通常人們把UDP稱為不可靠的傳輸協(xié)議。
(2) UDP不能保證有序傳輸
UDP不能確保數(shù)據(jù)的發(fā)送和接收順序。對于突發(fā)性的數(shù)據(jù)報,有可能會亂序。
2.2.2 UDP的優(yōu)勢
(1) UDP速度比TCP快
由于UDP不需要先與對方建立連接,也不需要傳輸確認,因此其數(shù)據(jù)傳輸速度比TCP快得多。對于強調(diào)傳輸性能而不是傳輸完整性的應(yīng)用(比如網(wǎng)絡(luò)音頻播放、視頻點播和網(wǎng)絡(luò)會議等),使用UDP比較合適,因為它的傳輸速度快,使通過網(wǎng)絡(luò)播放的視頻音質(zhì)好、畫面清晰。
(2) UDP有消息邊界
發(fā)送方UDP對應(yīng)用程序交下來的報文,在添加首部后就向下直接交付給IP層。既不拆分,也不合并,而是保留這些報文的邊界。使用UDP不需要考慮消息邊界問題,這樣使得UDP編程相比TCP,在對接收到的數(shù)據(jù)的處理方面要方便的多。在程序員看來,UDP套接字使用比TCP簡單。UDP的這一特征也說明了它是一種面向報文的傳輸協(xié)議。
(3) UDP可以一對多傳輸
由于傳輸數(shù)據(jù)不建立連接,也就不需要維護連接狀態(tài)(包括收發(fā)狀態(tài)等),因此一臺服務(wù)器可以同時向多個客戶端傳輸相同的消息。利用UDP可以使用廣播或組播的方式同時向子網(wǎng)上的所有客戶進程發(fā)送消息,這一點也比TCP方便。
其中,速度快是UDP的首要優(yōu)勢
由于TCP協(xié)議中植入了各種安全保障功能,在實際執(zhí)行的過程中會占用大量的系統(tǒng)開銷,無疑使速度受到嚴重影響。反觀UDP,由于拋棄了信息可靠傳輸機制,將安全和排序等功能移交給上層應(yīng)用完成,極大地降低了執(zhí)行時間,使速度得到了保證。簡而言之,UDP的“理念”就是“不顧一切,只為更快地發(fā)送數(shù)據(jù)”。
三:socket一般應(yīng)用模式:
四:SOCKET通信基本流程圖:
根據(jù)socket通信基本流程圖,總結(jié)通信的基本步驟:
服務(wù)器端:
第一步:創(chuàng)建一個用于監(jiān)聽連接的Socket對像;
第二步:用指定的端口號和服務(wù)器的ip建立一個EndPoint對像;
第三步:用socket對像的Bind()方法綁定EndPoint;
第四步:用socket對像的Listen()方法開始監(jiān)聽;
第五步:接收到客戶端的連接,用socket對像的Accept()方法創(chuàng)建一個新的用于和客戶端進行通信的socket對像;
第六步:通信結(jié)束后一定記得關(guān)閉socket;
客戶端:
第一步:建立一個Socket對像;
第二步:用指定的端口號和服務(wù)器的ip建立一個EndPoint對像;
第三步:用socket對像的Connect()方法以上面建立的EndPoint對像做為參數(shù),向服務(wù)器發(fā)出連接請求;
第四步:如果連接成功,就用socket對像的Send()方法向服務(wù)器發(fā)送信息;
第五步:用socket對像的Receive()方法接受服務(wù)器發(fā)來的信息 ;
第六步:通信結(jié)束后一定記得關(guān)閉socket;
五:示例程序
服務(wù)端界面:
代碼實現(xiàn)如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Threading; using System.IO; namespace SocketServer { public partial class FrmServer : Form { public FrmServer() { InitializeComponent(); } //定義回調(diào):解決跨線程訪問問題 private delegate void SetTextValueCallBack(string strValue); //定義接收客戶端發(fā)送消息的回調(diào) private delegate void ReceiveMsgCallBack(string strReceive); //聲明回調(diào) private SetTextValueCallBack setCallBack; //聲明 private ReceiveMsgCallBack receiveCallBack; //定義回調(diào):給ComboBox控件添加元素 private delegate void SetCmbCallBack(string strItem); //聲明 private SetCmbCallBack setCmbCallBack; //定義發(fā)送文件的回調(diào) private delegate void SendFileCallBack(byte[] bf); //聲明 private SendFileCallBack sendCallBack; //用于通信的Socket Socket socketSend; //用于監(jiān)聽的SOCKET Socket socketWatch; //將遠程連接的客戶端的IP地址和Socket存入集合中 Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>(); //創(chuàng)建監(jiān)聽連接的線程 Thread AcceptSocketThread; //接收客戶端發(fā)送消息的線程 Thread threadReceive; /// <summary> /// 開始監(jiān)聽 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Start_Click(object sender, EventArgs e) { //當點擊開始監(jiān)聽的時候 在服務(wù)器端創(chuàng)建一個負責(zé)監(jiān)聽IP地址和端口號的Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //獲取ip地址 IPAddress ip=IPAddress.Parse(this.txt_IP.Text.Trim()); //創(chuàng)建端口號 IPEndPoint point=new IPEndPoint(ip,Convert.ToInt32(this.txt_Port.Text.Trim())); //綁定IP地址和端口號 socketWatch.Bind(point); this.txt_Log.AppendText("監(jiān)聽成功"+" \r \n"); //開始監(jiān)聽:設(shè)置最大可以同時連接多少個請求 socketWatch.Listen(10); //實例化回調(diào) setCallBack = new SetTextValueCallBack(SetTextValue); receiveCallBack = new ReceiveMsgCallBack(ReceiveMsg); setCmbCallBack = new SetCmbCallBack(AddCmbItem); sendCallBack = new SendFileCallBack(SendFile); //創(chuàng)建線程 AcceptSocketThread = new Thread(new ParameterizedThreadStart(StartListen)); AcceptSocketThread.IsBackground = true; AcceptSocketThread.Start(socketWatch); } /// <summary> /// 等待客戶端的連接,并且創(chuàng)建與之通信用的Socket /// </summary> /// <param name="obj"></param> private void StartListen(object obj) { Socket socketWatch = obj as Socket; while (true) { //等待客戶端的連接,并且創(chuàng)建一個用于通信的Socket socketSend = socketWatch.Accept(); //獲取遠程主機的ip地址和端口號 string strIp=socketSend.RemoteEndPoint.ToString(); dicSocket.Add(strIp, socketSend); this.cmb_Socket.Invoke(setCmbCallBack, strIp); string strMsg = "遠程主機:" + socketSend.RemoteEndPoint + "連接成功"; //使用回調(diào) txt_Log.Invoke(setCallBack, strMsg); //定義接收客戶端消息的線程 Thread threadReceive = new Thread(new ParameterizedThreadStart(Receive)); threadReceive.IsBackground = true; threadReceive.Start(socketSend); } } /// <summary> /// 服務(wù)器端不停的接收客戶端發(fā)送的消息 /// </summary> /// <param name="obj"></param> private void Receive(object obj) { Socket socketSend = obj as Socket; while (true) { //客戶端連接成功后,服務(wù)器接收客戶端發(fā)送的消息 byte[] buffer = new byte[2048]; //實際接收到的有效字節(jié)數(shù) int count = socketSend.Receive(buffer); if (count == 0)//count 表示客戶端關(guān)閉,要退出循環(huán) { break; } else { string str = Encoding.Default.GetString(buffer, 0, count); string strReceiveMsg = "接收:" + socketSend.RemoteEndPoint + "發(fā)送的消息:" + str; txt_Log.Invoke(receiveCallBack, strReceiveMsg); } } } /// <summary> /// 回調(diào)委托需要執(zhí)行的方法 /// </summary> /// <param name="strValue"></param> private void SetTextValue(string strValue) { this.txt_Log.AppendText(strValue + " \r \n"); } private void ReceiveMsg(string strMsg) { this.txt_Log.AppendText(strMsg + " \r \n"); } private void AddCmbItem(string strItem) { this.cmb_Socket.Items.Add(strItem); } /// <summary> /// 服務(wù)器給客戶端發(fā)送消息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Send_Click(object sender, EventArgs e) { try { string strMsg = this.txt_Msg.Text.Trim(); byte[] buffer = Encoding.Default.GetBytes(strMsg); List<byte> list = new List<byte>(); list.Add(0); list.AddRange(buffer); //將泛型集合轉(zhuǎn)換為數(shù)組 byte[] newBuffer = list.ToArray(); //獲得用戶選擇的IP地址 string ip = this.cmb_Socket.SelectedItem.ToString(); dicSocket[ip].Send(newBuffer); } catch (Exception ex) { MessageBox.Show("給客戶端發(fā)送消息出錯:"+ex.Message); } //socketSend.Send(buffer); } /// <summary> /// 選擇要發(fā)送的文件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Select_Click(object sender, EventArgs e) { OpenFileDialog dia = new OpenFileDialog(); //設(shè)置初始目錄 dia.InitialDirectory = @""; dia.Title = "請選擇要發(fā)送的文件"; //過濾文件類型 dia.Filter = "所有文件|*.*"; dia.ShowDialog(); //將選擇的文件的全路徑賦值給文本框 this.txt_FilePath.Text = dia.FileName; } /// <summary> /// 發(fā)送文件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_SendFile_Click(object sender, EventArgs e) { List<byte> list = new List<byte>(); //獲取要發(fā)送的文件的路徑 string strPath = this.txt_FilePath.Text.Trim(); using (FileStream sw = new FileStream(strPath,FileMode.Open,FileAccess.Read)) { byte[] buffer = new byte[2048]; int r = sw.Read(buffer, 0, buffer.Length); list.Add(1); list.AddRange(buffer); byte[] newBuffer = list.ToArray(); //發(fā)送 //dicSocket[cmb_Socket.SelectedItem.ToString()].Send(newBuffer, 0, r+1, SocketFlags.None); btn_SendFile.Invoke(sendCallBack, newBuffer); } } private void SendFile(byte[] sendBuffer) { try { dicSocket[cmb_Socket.SelectedItem.ToString()].Send(sendBuffer, SocketFlags.None); } catch (Exception ex) { MessageBox.Show("發(fā)送文件出錯:"+ex.Message); } } private void btn_Shock_Click(object sender, EventArgs e) { byte[] buffer = new byte[1] { 2}; dicSocket[cmb_Socket.SelectedItem.ToString()].Send(buffer); } /// <summary> /// 停止監(jiān)聽 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_StopListen_Click(object sender, EventArgs e) { socketWatch.Close(); socketSend.Close(); //終止線程 AcceptSocketThread.Abort(); threadReceive.Abort(); } } }
客戶端界面
代碼實現(xiàn)如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net.Sockets; using System.Net; using System.Threading; using System.IO; namespace SocketClient { public partial class FrmClient : Form { public FrmClient() { InitializeComponent(); } //定義回調(diào) private delegate void SetTextCallBack(string strValue); //聲明 private SetTextCallBack setCallBack; //定義接收服務(wù)端發(fā)送消息的回調(diào) private delegate void ReceiveMsgCallBack(string strMsg); //聲明 private ReceiveMsgCallBack receiveCallBack; //創(chuàng)建連接的Socket Socket socketSend; //創(chuàng)建接收客戶端發(fā)送消息的線程 Thread threadReceive; /// <summary> /// 連接 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Connect_Click(object sender, EventArgs e) { try { socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ip = IPAddress.Parse(this.txt_IP.Text.Trim()); socketSend.Connect(ip, Convert.ToInt32(this.txt_Port.Text.Trim())); //實例化回調(diào) setCallBack = new SetTextCallBack(SetValue); receiveCallBack = new ReceiveMsgCallBack(SetValue); this.txt_Log.Invoke(setCallBack, "連接成功"); //開啟一個新的線程不停的接收服務(wù)器發(fā)送消息的線程 threadReceive = new Thread(new ThreadStart(Receive)); //設(shè)置為后臺線程 threadReceive.IsBackground = true; threadReceive.Start(); } catch (Exception ex) { MessageBox.Show("連接服務(wù)端出錯:" + ex.ToString()); } } /// <summary> /// 接口服務(wù)器發(fā)送的消息 /// </summary> private void Receive() { try { while (true) { byte[] buffer = new byte[2048]; //實際接收到的字節(jié)數(shù) int r = socketSend.Receive(buffer); if (r == 0) { break; } else { //判斷發(fā)送的數(shù)據(jù)的類型 if (buffer[0] == 0)//表示發(fā)送的是文字消息 { string str = Encoding.Default.GetString(buffer, 1, r - 1); this.txt_Log.Invoke(receiveCallBack, "接收遠程服務(wù)器:" + socketSend.RemoteEndPoint + "發(fā)送的消息:" + str); } //表示發(fā)送的是文件 if (buffer[0] == 1) { SaveFileDialog sfd = new SaveFileDialog(); sfd.InitialDirectory = @""; sfd.Title = "請選擇要保存的文件"; sfd.Filter = "所有文件|*.*"; sfd.ShowDialog(this); string strPath = sfd.FileName; using (FileStream fsWrite = new FileStream(strPath, FileMode.OpenOrCreate, FileAccess.Write)) { fsWrite.Write(buffer, 1, r - 1); } MessageBox.Show("保存文件成功"); } } } } catch (Exception ex) { MessageBox.Show("接收服務(wù)端發(fā)送的消息出錯:" + ex.ToString()); } } private void SetValue(string strValue) { this.txt_Log.AppendText(strValue + "\r \n"); } /// <summary> /// 客戶端給服務(wù)器發(fā)送消息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Send_Click(object sender, EventArgs e) { try { string strMsg = this.txt_Msg.Text.Trim(); byte[] buffer = new byte[2048]; buffer = Encoding.Default.GetBytes(strMsg); int receive = socketSend.Send(buffer); } catch (Exception ex) { MessageBox.Show("發(fā)送消息出錯:" + ex.Message); } } private void FrmClient_Load(object sender, EventArgs e) { Control.CheckForIllegalCrossThreadCalls = false; } /// <summary> /// 斷開連接 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_CloseConnect_Click(object sender, EventArgs e) { //關(guān)閉socket socketSend.Close(); //終止線程 threadReceive.Abort(); } } }
到此這篇關(guān)于C#網(wǎng)絡(luò)編程之Socket編程的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C# 實現(xiàn)簡易的串口監(jiān)視上位機功能附源碼下載
這篇文章主要介紹了C# 實現(xiàn)簡易的串口監(jiān)視上位機功能,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11