C# Socket編程
此博客,轉自.NET開發菜鳥的博客——C# Socket編程
一:什麽是SOCKET
socket的英文原義是“孔”或“插座”。作為進程通信機制,取後一種意思。通常也稱作“套接字”,用於描述IP地址和端口,是一個通信鏈的句柄(其實就是兩個程序通信用的)。
socket非常類似於電話插座。以一個電話網為例:電話的通話雙方相當於相互通信的2個程序,電話號碼就是ip地址。任何用戶在通話之前,首先要占有一部電話機,相當於申請一個socket;同時要知道對方的號碼,相當於對方有一個固定的socket。然後向對方撥號呼叫,相當於發出連接請求。對方假如在場並空閑,拿起電話話筒,雙方就可以正式通話,相當於連接成功。雙方通話的過程,是一方向電話機發出信號和對方從電話機接收信號的過程,相當於向socket發送數據和從socket接收數據。通話結束後,一方掛起電話機相當於關閉socket,撤銷連接。
1、套接字分類
為了滿足不同程序對通信質量和性能的要求,一般的網絡系統都提供了以下3種不同類型的套接字,以供用戶在設計程序時根據不同需要來選擇:
流式套接字(SOCK_STREAM):提供了一種可靠的、面向連接的雙向數據傳輸服務。實現了數據無差錯,無重復的發送,內設流量控制,被傳輸的數據被看做無記錄邊界的字節流。在TCP/IP協議簇中,使用TCP實現字節流的傳輸,當用戶要發送大批量數據,或對數據傳輸的可靠性有較高要求時使用流式套接字。
數據報套接字(SOCK_DGRAM):提供了一種無連接、不可靠的雙向數據傳輸服務。數據以獨立的包形式被發送,並且保留了記錄邊界,不提供可靠性保證。數據在傳輸過程中可能會丟失或重復,並且不能保證在接收端數據按發送順序接收。在TCP/IP協議簇中,使用UDP實現數據報套接字。
原始套接字(SOCK_RAW):該套接字允許對較低層協議(如IP或ICMP)進行直接訪問。一般用於對TCP/IP核心協議的網絡編程。
二:SOCKET相關概念
1、端口
在Internet上有很多這樣的主機,這些主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不同的端口對應於不同的服務(應用程序),因此,在網絡協議中使用端口號識別主機上不同的進程。
例如:http使用80端口,FTP使用21端口。
2、協議
2.1 TCP:
TCP是一種面向連接的、可靠的,基於字節流的傳輸層通信協議。為兩臺主機提供高可靠性的數據通信服務。它可以將源主機的數據無差錯地傳輸到目標主機。當有數據要發送時,對應用進程送來的數據進行分片,以適合於在網絡層中傳輸;當接收到網絡層傳來的分組時,它要對收到的分組進行確認,還要對丟失的分組設置超時重發等。為此TCP需要增加額外的許多開銷,以便在數據傳輸過程中進行一些必要的控制,確保數據的可靠傳輸。因此,TCP傳輸的效率比較低。
2.1.1 TCP的工作過程
TCP是面向連接的協議,TCP協議通過三個報文段完成類似電話呼叫的連接建立過程,這個過程稱為三次握手,如圖所示:
第一次握手:建立連接時,客戶端發送SYN包(SEQ=x)到服務器,並進入SYN_SEND狀態,等待服務器確認。
第二次握手:服務器收到SYN包,必須確認客戶的SYN(ACK=x+1),同時自己也發送一個SYN包(SEQ=y),即SYN+ACK包,此時服務器進入SYN_RECV狀態。
第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ACK=y+1),此包發送完畢,客戶端和服務器進入Established狀態,完成三次握手。
2.1.2 傳輸數據
一旦通信雙方建立了TCP連接,連接中的任何一方都能向對方發送數據和接收對方發來的數據。TCP協議負責把用戶數據(字節流)按一定的格式和長度組成多個數據報進行發送,並在接收到數據報之後按分解順序重新組裝和恢復用戶數據。
利用TCP傳輸數據時,數據是以字節流的形式進行傳輸的。
2.1.3 連接的終止
建立一個連接需要三次握手,而終止一個連接要經過四次握手,這是由TCP的半關閉(half-close)造成的。具體過程如圖所示:
2.1.4 TCP的主要特點
TCP最主要的特點如下。
(1) 是面向連接的協議。
(2) 端到端的通信。每個TCP連接只能有兩個端點,而且只能一對一通信,不能一點對多點直接通信。
(3) 高可靠性。通過TCP連接傳送的數據,能保證數據無差錯、不丟失、不重復地準確到達接收方,並且保證各數據到達的順序與其發出的順序相同。
(4) 全雙工方式傳輸。
(5) 數據以字節流的方式傳輸。
(6) 傳輸的數據無消息邊界。
2.1.5 同步與異步
同步工作方式是指利用TCP編寫的程序執行到監聽或接收語句時,在未完成工作(偵聽到連接請求或收到對方發來的數據)前不再繼續往下執行,線程處於阻塞狀態,直到該語句完成相應的工作後才繼續執行下一條語句。
異步工作方式是指程序執行到監聽或接收語句時,不論工作是否完成,都會繼續往下執行。
2.2 UDP
UDP是一種簡單的、面向數據報的無連接的協議,提供的是不一定可靠的傳輸服務。所謂“無連接”是指在正式通信前不必與對方先建立連接,不管對方狀態如何都直接發送過去。這與發手機短信非常相似,只要知道對方的手機號就可以了,不要考慮對方手機處於什麽狀態。UDP雖然不能保證數據傳輸的可靠性,但數據傳輸的效率較高。
2.1.1 UDP與TCP的區別
(1) UDP可靠性不如TCP
TCP包含了專門的傳遞保證機制,當數據接收方收到發送方傳來的信息時,會自動向發送方發出確認消息;發送方只有在接收到該確認消息之後才繼續傳送其他信息,否則將一直等待直到收到確認信息為止。與TCP不同,UDP並不提供數據傳送的保證機制。如果在從發送方到接收方的傳遞過程中出現數據報的丟失,協議本身並不能做出任何檢測或提示。因此,通常人們把UDP稱為不可靠的傳輸協議。
(2) UDP不能保證有序傳輸
UDP不能確保數據的發送和接收順序。對於突發性的數據報,有可能會亂序。
2.1.2 UDP的優勢
(1) UDP速度比TCP快
由於UDP不需要先與對方建立連接,也不需要傳輸確認,因此其數據傳輸速度比TCP快得多。對於強調傳輸性能而不是傳輸完整性的應用(比如網絡音頻播放、視頻點播和網絡會議等),使用UDP比較合適,因為它的傳輸速度快,使通過網絡播放的視頻音質好、畫面清晰。
(2) UDP有消息邊界
發送方UDP對應用程序交下來的報文,在添加首部後就向下直接交付給IP層。既不拆分,也不合並,而是保留這些報文的邊界。使用UDP不需要考慮消息邊界問題,這樣使得UDP編程相比TCP,在對接收到的數據的處理方面要方便的多。在程序員看來,UDP套接字使用比TCP簡單。UDP的這一特征也說明了它是一種面向報文的傳輸協議。
(3) UDP可以一對多傳輸
由於傳輸數據不建立連接,也就不需要維護連接狀態(包括收發狀態等),因此一臺服務器可以同時向多個客戶端傳輸相同的消息。利用UDP可以使用廣播或組播的方式同時向子網上的所有客戶進程發送消息,這一點也比TCP方便。
其中,速度快是UDP的首要優勢
由於TCP協議中植入了各種安全保障功能,在實際執行的過程中會占用大量的系統開銷,無疑使速度受到嚴重影響。反觀UDP,由於拋棄了信息可靠傳輸機制,將安全和排序等功能移交給上層應用完成,極大地降低了執行時間,使速度得到了保證。簡而言之,UDP的“理念”就是“不顧一切,只為更快地發送數據”。
三:socket一般應用模式:
四:SOCKET通信基本流程圖:
根據socket通信基本流程圖,總結通信的基本步驟:
服務器端:
第一步:創建一個用於監聽連接的Socket對像;
第二步:用指定的端口號和服務器的ip建立一個EndPoint對像;
第三步:用socket對像的Bind()方法綁定EndPoint;
第四步:用socket對像的Listen()方法開始監聽;
第五步:接收到客戶端的連接,用socket對像的Accept()方法創建一個新的用於和客戶端進行通信的socket對像;
第六步:通信結束後一定記得關閉socket;
客戶端:
第一步:建立一個Socket對像;
第二步:用指定的端口號和服務器的ip建立一個EndPoint對像;
第三步:用socket對像的Connect()方法以上面建立的EndPoint對像做為參數,向服務器發出連接請求;
第四步:如果連接成功,就用socket對像的Send()方法向服務器發送信息;
第五步:用socket對像的Receive()方法接受服務器發來的信息 ;
第六步:通信結束後一定記得關閉socket;
五:示例程序
服務端界面:
代碼實現如下:
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Net;
8 using System.Net.Sockets;
9 using System.Text;
10 using System.Threading.Tasks;
11 using System.Windows.Forms;
12 using System.Threading;
13 using System.IO;
14
15 namespace SocketServer
16 {
17 public partial class FrmServer : Form
18 {
19 public FrmServer()
20 {
21 InitializeComponent();
22 }
23
24 //定義回調:解決跨線程訪問問題
25 private delegate void SetTextValueCallBack(string strValue);
26 //定義接收客戶端發送消息的回調
27 private delegate void ReceiveMsgCallBack(string strReceive);
28 //聲明回調
29 private SetTextValueCallBack setCallBack;
30 //聲明
31 private ReceiveMsgCallBack receiveCallBack;
32 //定義回調:給ComboBox控件添加元素
33 private delegate void SetCmbCallBack(string strItem);
34 //聲明
35 private SetCmbCallBack setCmbCallBack;
36 //定義發送文件的回調
37 private delegate void SendFileCallBack(byte[] bf);
38 //聲明
39 private SendFileCallBack sendCallBack;
40
41 //用於通信的Socket
42 Socket socketSend;
43 //用於監聽的SOCKET
44 Socket socketWatch;
45
46 //將遠程連接的客戶端的IP地址和Socket存入集合中
47 Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();
48
49 //創建監聽連接的線程
50 Thread AcceptSocketThread;
51 //接收客戶端發送消息的線程
52 Thread threadReceive;
53
54 /// <summary>
55 /// 開始監聽
56 /// </summary>
57 /// <param name="sender"></param>
58 /// <param name="e"></param>
59 private void btn_Start_Click(object sender, EventArgs e)
60 {
61 //當點擊開始監聽的時候 在服務器端創建一個負責監聽IP地址和端口號的Socket
62 socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
63 //獲取ip地址
64 IPAddress ip=IPAddress.Parse(this.txt_IP.Text.Trim());
65 //創建端口號
66 IPEndPoint point=new IPEndPoint(ip,Convert.ToInt32(this.txt_Port.Text.Trim()));
67 //綁定IP地址和端口號
68 socketWatch.Bind(point);
69 this.txt_Log.AppendText("監聽成功"+" \r \n");
70 //開始監聽:設置最大可以同時連接多少個請求
71 socketWatch.Listen(10);
72
73 //實例化回調
74 setCallBack = new SetTextValueCallBack(SetTextValue);
75 receiveCallBack = new ReceiveMsgCallBack(ReceiveMsg);
76 setCmbCallBack = new SetCmbCallBack(AddCmbItem);
77 sendCallBack = new SendFileCallBack(SendFile);
78
79 //創建線程
80 AcceptSocketThread = new Thread(new ParameterizedThreadStart(StartListen));
81 AcceptSocketThread.IsBackground = true;
82 AcceptSocketThread.Start(socketWatch);
83 }
84
85 /// <summary>
86 /// 等待客戶端的連接,並且創建與之通信用的Socket
87 /// </summary>
88 /// <param name="obj"></param>
89 private void StartListen(object obj)
90 {
91 Socket socketWatch = obj as Socket;
92 while (true)
93 {
94 //等待客戶端的連接,並且創建一個用於通信的Socket
95 socketSend = socketWatch.Accept();
96 //獲取遠程主機的ip地址和端口號
97 string strIp=socketSend.RemoteEndPoint.ToString();
98 dicSocket.Add(strIp, socketSend);
99 this.cmb_Socket.Invoke(setCmbCallBack, strIp);
100 string strMsg = "遠程主機:" + socketSend.RemoteEndPoint + "連接成功";
101 //使用回調
102 txt_Log.Invoke(setCallBack, strMsg);
103
104 //定義接收客戶端消息的線程
105 Thread threadReceive = new Thread(new ParameterizedThreadStart(Receive));
106 threadReceive.IsBackground = true;
107 threadReceive.Start(socketSend);
108
109 }
110 }
111
112
113
114 /// <summary>
115 /// 服務器端不停的接收客戶端發送的消息
116 /// </summary>
117 /// <param name="obj"></param>
118 private void Receive(object obj)
119 {
120 Socket socketSend = obj as Socket;
121 while (true)
122 {
123 //客戶端連接成功後,服務器接收客戶端發送的消息
124 byte[] buffer = new byte[2048];
125 //實際接收到的有效字節數
126 int count = socketSend.Receive(buffer);
127 if (count == 0)//count 表示客戶端關閉,要退出循環
128 {
129 break;
130 }
131 else
132 {
133 string str = Encoding.Default.GetString(buffer, 0, count);
134 string strReceiveMsg = "接收:" + socketSend.RemoteEndPoint + "發送的消息:" + str;
135 txt_Log.Invoke(receiveCallBack, strReceiveMsg);
136 }
137 }
138 }
139
140 /// <summary>
141 /// 回調委托需要執行的方法
142 /// </summary>
143 /// <param name="strValue"></param>
144 private void SetTextValue(string strValue)
145 {
146 this.txt_Log.AppendText(strValue + " \r \n");
147 }
148
149
150 private void ReceiveMsg(string strMsg)
151 {
152 this.txt_Log.AppendText(strMsg + " \r \n");
153 }
154
155 private void AddCmbItem(string strItem)
156 {
157 this.cmb_Socket.Items.Add(strItem);
158 }
159
160 /// <summary>
161 /// 服務器給客戶端發送消息
162 /// </summary>
163 /// <param name="sender"></param>
164 /// <param name="e"></param>
165 private void btn_Send_Click(object sender, EventArgs e)
166 {
167 try
168 {
169 string strMsg = this.txt_Msg.Text.Trim();
170 byte[] buffer = Encoding.Default.GetBytes(strMsg);
171 List<byte> list = new List<byte>();
172 list.Add(0);
173 list.AddRange(buffer);
174 //將泛型集合轉換為數組
175 byte[] newBuffer = list.ToArray();
176 //獲得用戶選擇的IP地址
177 string ip = this.cmb_Socket.SelectedItem.ToString();
178 dicSocket[ip].Send(newBuffer);
179 }
180 catch (Exception ex)
181 {
182 MessageBox.Show("給客戶端發送消息出錯:"+ex.Message);
183 }
184 //socketSend.Send(buffer);
185 }
186
187 /// <summary>
188 /// 選擇要發送的文件
189 /// </summary>
190 /// <param name="sender"></param>
191 /// <param name="e"></param>
192 private void btn_Select_Click(object sender, EventArgs e)
193 {
194 OpenFileDialog dia = new OpenFileDialog();
195 //設置初始目錄
196 dia.InitialDirectory = @"";
197 dia.Title = "請選擇要發送的文件";
198 //過濾文件類型
199 dia.Filter = "所有文件|*.*";
200 dia.ShowDialog();
201 //將選擇的文件的全路徑賦值給文本框
202 this.txt_FilePath.Text = dia.FileName;
203 }
204
205 /// <summary>
206 /// 發送文件
207 /// </summary>
208 /// <param name="sender"></param>
209 /// <param name="e"></param>
210 private void btn_SendFile_Click(object sender, EventArgs e)
211 {
212 List<byte> list = new List<byte>();
213 //獲取要發送的文件的路徑
214 string strPath = this.txt_FilePath.Text.Trim();
215 using (FileStream sw = new FileStream(strPath,FileMode.Open,FileAccess.Read))
216 {
217 byte[] buffer = new byte[2048];
218 int r = sw.Read(buffer, 0, buffer.Length);
219 list.Add(1);
220 list.AddRange(buffer);
221
222 byte[] newBuffer = list.ToArray();
223 //發送
224 //dicSocket[cmb_Socket.SelectedItem.ToString()].Send(newBuffer, 0, r+1, SocketFlags.None);
225 btn_SendFile.Invoke(sendCallBack, newBuffer);
226
227
228 }
229
230 }
231
232 private void SendFile(byte[] sendBuffer)
233 {
234
235 try
236 {
237 dicSocket[cmb_Socket.SelectedItem.ToString()].Send(sendBuffer, SocketFlags.None);
238 }
239 catch (Exception ex)
240 {
241 MessageBox.Show("發送文件出錯:"+ex.Message);
242 }
243 }
244
245 private void btn_Shock_Click(object sender, EventArgs e)
246 {
247 byte[] buffer = new byte[1] { 2};
248 dicSocket[cmb_Socket.SelectedItem.ToString()].Send(buffer);
249 }
250
251 /// <summary>
252 /// 停止監聽
253 /// </summary>
254 /// <param name="sender"></param>
255 /// <param name="e"></param>
256 private void btn_StopListen_Click(object sender, EventArgs e)
257 {
258 socketWatch.Close();
259 socketSend.Close();
260 //終止線程
261 AcceptSocketThread.Abort();
262 threadReceive.Abort();
263 }
264 }
265 }
客戶端界面
代碼實現如下:
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Linq;
7 using System.Text;
8 using System.Threading.Tasks;
9 using System.Windows.Forms;
10 using System.Net.Sockets;
11 using System.Net;
12 using System.Threading;
13 using System.IO;
14
15 namespace SocketClient
16 {
17 public partial class FrmClient : Form
18 {
19 public FrmClient()
20 {
21 InitializeComponent();
22 }
23
24 //定義回調
25 private delegate void SetTextCallBack(string strValue);
26 //聲明
27 private SetTextCallBack setCallBack;
28
29 //定義接收服務端發送消息的回調
30 private delegate void ReceiveMsgCallBack(string strMsg);
31 //聲明
32 private ReceiveMsgCallBack receiveCallBack;
33
34 //創建連接的Socket
35 Socket socketSend;
36 //創建接收客戶端發送消息的線程
37 Thread threadReceive;
38
39 /// <summary>
40 /// 連接
41 /// </summary>
42 /// <param name="sender"></param>
43 /// <param name="e"></param>
44 private void btn_Connect_Click(object sender, EventArgs e)
45 {
46 try
47 {
48 socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
49 IPAddress ip = IPAddress.Parse(this.txt_IP.Text.Trim());
50 socketSend.Connect(ip, Convert.ToInt32(this.txt_Port.Text.Trim()));
51 //實例化回調
52 setCallBack = new SetTextCallBack(SetValue);
53 receiveCallBack = new ReceiveMsgCallBack(SetValue);
54 this.txt_Log.Invoke(setCallBack, "連接成功");
55
56 //開啟一個新的線程不停的接收服務器發送消息的線程
57 threadReceive = new Thread(new ThreadStart(Receive));
58 //設置為後臺線程
59 threadReceive.IsBackground = true;
60 threadReceive.Start();
61 }
62 catch (Exception ex)
63 {
64 MessageBox.Show("連接服務端出錯:" + ex.ToString());
65 }
66 }
67
68 /// <summary>
69 /// 接口服務器發送的消息
70 /// </summary>
71 private void Receive()
72 {
73 try
74 {
75 while (true)
76 {
77 byte[] buffer = new byte[2048];
78 //實際接收到的字節數
79 int r = socketSend.Receive(buffer);
80 if (r == 0)
81 {
82 break;
83 }
84 else
85 {
86 //判斷發送的數據的類型
87 if (buffer[0] == 0)//表示發送的是文字消息
88 {
89 string str = Encoding.Default.GetString(buffer, 1, r - 1);
90 this.txt_Log.Invoke(receiveCallBack, "接收遠程服務器:" + socketSend.RemoteEndPoint + "發送的消息:" + str);
91 }
92 //表示發送的是文件
93 if (buffer[0] == 1)
94 {
95 SaveFileDialog sfd = new SaveFileDialog();
96 sfd.InitialDirectory = @"";
97 sfd.Title = "請選擇要保存的文件";
98 sfd.Filter = "所有文件|*.*";
99 sfd.ShowDialog(this);
100
101 string strPath = sfd.FileName;
102 using (FileStream fsWrite = new FileStream(strPath, FileMode.OpenOrCreate, FileAccess.Write))
103 {
104 fsWrite.Write(buffer, 1, r - 1);
105 }
106
107 MessageBox.Show("保存文件成功");
108 }
109 }
110
111
112 }
113 }
114 catch (Exception ex)
115 {
116 MessageBox.Show("接收服務端發送的消息出錯:" + ex.ToString());
117 }
118 }
119
120
121 private void SetValue(string strValue)
122 {
123 this.txt_Log.AppendText(strValue + "\r \n");
124 }
125
126 /// <summary>
127 /// 客戶端給服務器發送消息
128 /// </summary>
129 /// <param name="sender"></param>
130 /// <param name="e"></param>
131 private void btn_Send_Click(object sender, EventArgs e)
132 {
133 try
134 {
135 string strMsg = this.txt_Msg.Text.Trim();
136 byte[] buffer = new byte[2048];
137 buffer = Encoding.Default.GetBytes(strMsg);
138 int receive = socketSend.Send(buffer);
139 }
140 catch (Exception ex)
141 {
142 MessageBox.Show("發送消息出錯:" + ex.Message);
143 }
144 }
145
146 private void FrmClient_Load(object sender, EventArgs e)
147 {
148 Control.CheckForIllegalCrossThreadCalls = false;
149 }
150
151 /// <summary>
152 /// 斷開連接
153 /// </summary>
154 /// <param name="sender"></param>
155 /// <param name="e"></param>
156 private void btn_CloseConnect_Click(object sender, EventArgs e)
157 {
158 //關閉socket
159 socketSend.Close();
160 //終止線程
161 threadReceive.Abort();
162 }
163 }
164 }
C# Socket編程