C#中Socket通信编程的同步实现
目錄(?)[+]
本文通過分析和總結C#中Socket通信編程的關鍵技術,按照同步實現的方式實現了一個簡單的Socket聊天程序,目的是通過這個程序來掌握Socket編程,為進一步開發Unity3D網絡游戲打下一個堅實的基礎。
Socket編程基礎
關于Socket編程基礎部分的內容,主要是了解和掌握.NET框架下為Socket編程提供的相關類和接口方法。.NET中常見的網絡相關的API都集中在System.NET和System.Net.Socket這兩個命名空間下,大家可以通過MSDN去了解這兩個命名空間下相關的類和方法。這里援引一位朋友總結的一篇文章http://www.cnblogs.com/sunev/archive/2012/08/05/2604189.html,大家可以從這里獲得更為直觀的認識。
什么是Socket編程的同步實現
本文的目的是按照同步實現的方式來實現一個簡單的Socket聊天程序,因此在解決這個問題前,我們首先來看看什么是Socket編程的同步實現。所謂Socket編程的同步實現就是指按照同步過程的方法來實現Socket通信。從編程來說,我們常用的方法或者函數都是同步過程。因為當我們調用一個方法或者函數的時候我們能夠立即得到它的返回值。可是我們知道在Socket通信中,我們不能保證時時刻刻連接都通暢、更不能夠保證時時刻刻都有數據收發,因為我們就需要不斷去讀取相應的值來確定整個過程的狀態。這就是Socket編程的同步實現了,下面我們來看具體的實現過程。
如何實現Socket同步通信
服務端
服務端的主要職責是處理各個客戶端發送來的數據,因此在客戶端的Socket編程中需要使用兩個線程來循環處理客戶端的請求,一個線程用于監聽客戶端的連接情況,一個線程用于監聽客戶端的消息發送,當服務端接收到客戶端的消息后需要將消息處理后再分發給各個客戶端。
基本流程
- 創建套接字
 - 綁定套接字的IP和端口號——Bind()
 - 將套接字處于監聽狀態等待客戶端的連接請求——Listen()
 - 當請求到來后,接受請求并返回本次會話的套接字——Accept()
 - 使用返回的套接字和客戶端通信——Send()/Receive()
 - 返回,再次等待新的連接請求
 - 關閉套接字
 
代碼示例
using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading;namespace TCPLib {public class TCPServer{private byte[] result = new byte[1024];/// <summary>/// 最大的監聽數量/// </summary>private int maxClientCount;public int MaxClientCount{get { return maxClientCount; }set { maxClientCount = value; }}/// <summary>/// IP地址/// </summary>private string ip;public string IP{get { return ip; }set { ip = value; }}/// <summary>/// 端口號/// </summary>private int port;public int Port{get { return port; }set { port = value; }}/// <summary>/// 客戶端列表/// </summary>private List<Socket> mClientSockets;public List<Socket> ClientSockets{get { return mClientSockets; }}/// <summary>/// IP終端/// </summary>private IPEndPoint ipEndPoint;/// <summary>/// 服務端Socket/// </summary>private Socket mServerSocket;/// <summary>/// 當前客戶端Socket/// </summary>private Socket mClientSocket;public Socket ClientSocket {get { return mClientSocket; }set { mClientSocket = value; }}/// <summary>/// 構造函數/// </summary>/// <param name="port">端口號</param>/// <param name="count">監聽的最大樹目</param>public TCPServer(int port, int count){this.ip = IPAddress.Any.ToString();this.port = port;this.maxClientCount=count;this.mClientSockets = new List<Socket>();//初始化IP終端this.ipEndPoint = new IPEndPoint(IPAddress.Any, port);//初始化服務端Socketthis.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//端口綁定this.mServerSocket.Bind(this.ipEndPoint);//設置監聽數目this.mServerSocket.Listen(maxClientCount);}/// <summary>/// 構造函數/// </summary>/// <param name="ip">ip地址</param>/// <param name="port">端口號</param>/// <param name="count">監聽的最大數目</param>public TCPServer(string ip,int port,int count){this.ip = ip;this.port = port;this.maxClientCount = count;this.mClientSockets = new List<Socket>();//初始化IP終端this.ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);//初始化服務端Socketthis.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//端口綁定this.mServerSocket.Bind(this.ipEndPoint);//設置監聽數目this.mServerSocket.Listen(maxClientCount);}/// <summary>/// 定義一個Start方法將構造函數中的方法分離出來/// </summary>public void Start(){//創建服務端線程,實現客戶端連接請求的循環監聽var mServerThread = new Thread(this.ListenClientConnect);//服務端線程開啟mServerThread.Start();}/// <summary>/// 監聽客戶端鏈接/// </summary>private void ListenClientConnect(){//設置循環標志位bool flag = true;while (flag){//獲取連接到服務端的客戶端this.ClientSocket = this.mServerSocket.Accept();//將獲取到的客戶端添加到客戶端列表this.mClientSockets.Add(this.ClientSocket);//向客戶端發送一條消息this.SendMessage(string.Format("客戶端{0}已成功連接到服務器", this.ClientSocket.RemoteEndPoint));//創建客戶端消息線程,實現客戶端消息的循環監聽var mReveiveThread = new Thread(this.ReceiveClient);//注意到ReceiveClient方法傳入了一個參數//實際上這個參數就是此時連接到服務器的客戶端//即ClientSocketmReveiveThread.Start(this.ClientSocket);}}/// <summary>/// 接收客戶端消息的方法/// </summary>private void ReceiveClient(object obj){//獲取當前客戶端//因為每次發送消息的可能并不是同一個客戶端,所以需要使用var來實例化一個新的對象//可是我感覺這里用局部變量更好一點var mClientSocket = (Socket)obj;// 循環標志位bool flag = true;while (flag){try{//獲取數據長度int receiveLength = mClientSocket.Receive(result);//獲取客戶端消息string clientMessage = Encoding.UTF8.GetString(result, 0, receiveLength);//服務端負責將客戶端的消息分發給各個客戶端this.SendMessage(string.Format("客戶端{0}發來消息:{1}",mClientSocket.RemoteEndPoint,clientMessage));}catch (Exception e){//從客戶端列表中移除該客戶端this.mClientSockets.Remove(mClientSocket);//向其它客戶端告知該客戶端下線this.SendMessage(string.Format("服務器發來消息:客戶端{0}從服務器斷開,斷開原因:{1}",mClientSocket.RemoteEndPoint,e.Message));//斷開連接mClientSocket.Shutdown(SocketShutdown.Both);mClientSocket.Close();break;}}}/// <summary>/// 向所有的客戶端群發消息/// </summary>/// <param name="msg">message</param>public void SendMessage(string msg){//確保消息非空以及客戶端列表非空if (msg == string.Empty || this.mClientSockets.Count <= 0) return;//向每一個客戶端發送消息foreach (Socket s in this.mClientSockets){(s as Socket).Send(Encoding.UTF8.GetBytes(msg));}}/// <summary>/// 向指定的客戶端發送消息/// </summary>/// <param name="ip">ip</param>/// <param name="port">port</param>/// <param name="msg">message</param>public void SendMessage(string ip,int port,string msg){//構造出一個終端地址IPEndPoint _IPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);//遍歷所有客戶端foreach (Socket s in mClientSockets){if (_IPEndPoint == (IPEndPoint)s.RemoteEndPoint){s.Send(Encoding.UTF8.GetBytes(msg));}}}} }- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 - 30
 - 31
 - 32
 - 33
 - 34
 - 35
 - 36
 - 37
 - 38
 - 39
 - 40
 - 41
 - 42
 - 43
 - 44
 - 45
 - 46
 - 47
 - 48
 - 49
 - 50
 - 51
 - 52
 - 53
 - 54
 - 55
 - 56
 - 57
 - 58
 - 59
 - 60
 - 61
 - 62
 - 63
 - 64
 - 65
 - 66
 - 67
 - 68
 - 69
 - 70
 - 71
 - 72
 - 73
 - 74
 - 75
 - 76
 - 77
 - 78
 - 79
 - 80
 - 81
 - 82
 - 83
 - 84
 - 85
 - 86
 - 87
 - 88
 - 89
 - 90
 - 91
 - 92
 - 93
 - 94
 - 95
 - 96
 - 97
 - 98
 - 99
 - 100
 - 101
 - 102
 - 103
 - 104
 - 105
 - 106
 - 107
 - 108
 - 109
 - 110
 - 111
 - 112
 - 113
 - 114
 - 115
 - 116
 - 117
 - 118
 - 119
 - 120
 - 121
 - 122
 - 123
 - 124
 - 125
 - 126
 - 127
 - 128
 - 129
 - 130
 - 131
 - 132
 - 133
 - 134
 - 135
 - 136
 - 137
 - 138
 - 139
 - 140
 - 141
 - 142
 - 143
 - 144
 - 145
 - 146
 - 147
 - 148
 - 149
 - 150
 - 151
 - 152
 - 153
 - 154
 - 155
 - 156
 - 157
 - 158
 - 159
 - 160
 - 161
 - 162
 - 163
 - 164
 - 165
 - 166
 - 167
 - 168
 - 169
 - 170
 - 171
 - 172
 - 173
 - 174
 - 175
 - 176
 - 177
 - 178
 - 179
 - 180
 - 181
 - 182
 - 183
 - 184
 - 185
 - 186
 - 187
 - 188
 - 189
 - 190
 - 191
 - 192
 - 193
 - 194
 - 195
 - 196
 - 197
 - 198
 - 199
 - 200
 - 201
 - 202
 - 203
 - 204
 - 205
 - 206
 - 207
 - 208
 - 209
 - 210
 - 211
 - 212
 - 213
 - 214
 - 215
 - 216
 - 217
 - 218
 - 219
 - 220
 - 221
 - 222
 - 223
 - 224
 - 225
 - 226
 - 227
 - 228
 
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 - 30
 - 31
 - 32
 - 33
 - 34
 - 35
 - 36
 - 37
 - 38
 - 39
 - 40
 - 41
 - 42
 - 43
 - 44
 - 45
 - 46
 - 47
 - 48
 - 49
 - 50
 - 51
 - 52
 - 53
 - 54
 - 55
 - 56
 - 57
 - 58
 - 59
 - 60
 - 61
 - 62
 - 63
 - 64
 - 65
 - 66
 - 67
 - 68
 - 69
 - 70
 - 71
 - 72
 - 73
 - 74
 - 75
 - 76
 - 77
 - 78
 - 79
 - 80
 - 81
 - 82
 - 83
 - 84
 - 85
 - 86
 - 87
 - 88
 - 89
 - 90
 - 91
 - 92
 - 93
 - 94
 - 95
 - 96
 - 97
 - 98
 - 99
 - 100
 - 101
 - 102
 - 103
 - 104
 - 105
 - 106
 - 107
 - 108
 - 109
 - 110
 - 111
 - 112
 - 113
 - 114
 - 115
 - 116
 - 117
 - 118
 - 119
 - 120
 - 121
 - 122
 - 123
 - 124
 - 125
 - 126
 - 127
 - 128
 - 129
 - 130
 - 131
 - 132
 - 133
 - 134
 - 135
 - 136
 - 137
 - 138
 - 139
 - 140
 - 141
 - 142
 - 143
 - 144
 - 145
 - 146
 - 147
 - 148
 - 149
 - 150
 - 151
 - 152
 - 153
 - 154
 - 155
 - 156
 - 157
 - 158
 - 159
 - 160
 - 161
 - 162
 - 163
 - 164
 - 165
 - 166
 - 167
 - 168
 - 169
 - 170
 - 171
 - 172
 - 173
 - 174
 - 175
 - 176
 - 177
 - 178
 - 179
 - 180
 - 181
 - 182
 - 183
 - 184
 - 185
 - 186
 - 187
 - 188
 - 189
 - 190
 - 191
 - 192
 - 193
 - 194
 - 195
 - 196
 - 197
 - 198
 - 199
 - 200
 - 201
 - 202
 - 203
 - 204
 - 205
 - 206
 - 207
 - 208
 - 209
 - 210
 - 211
 - 212
 - 213
 - 214
 - 215
 - 216
 - 217
 - 218
 - 219
 - 220
 - 221
 - 222
 - 223
 - 224
 - 225
 - 226
 - 227
 - 228
 
好了,現在我們已經編寫好了一個具備接收和發送數據能力的服務端程序。現在我們來嘗試讓服務端運行起來:
using System; using System.Collections.Generic; using System.Text; using TCPLib; using System.Net; using System.Net.Sockets;namespace TCPLib.Test {class Program{static void Main(string[] args){//指定IP和端口號及最大監聽數目的方式TCPLib.TCPServer s1 = new TCPServer("127.0.0.1", 6001, 10);//指定端口號及最大監聽數目的方式TCPLib.TCPServer s2 = new TCPServer(6001, 10);//執行Start方法s1.Start();}}}- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 
現在我們來看看編寫客戶端Socket程序的基本流程
客戶端
客戶端相對于服務端來說任務要輕許多,因為客戶端僅僅需要和服務端通信即可,可是因為在和服務器通信的過程中,需要時刻保持連接通暢,因此同樣需要兩個線程來分別處理連接情況的監聽和消息發送的監聽。
基本流程
- 創建套接字保證與服務器的端口一致
 - 向服務器發出連接請求——Connect()
 - 和服務器端進行通信——Send()/Receive()
 - 關閉套接字
 
代碼示例
using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading;namespace TCPLib {public class TCPClient{/// <summary>/// 定義數據/// </summary>private byte[] result = new byte[1024];/// <summary>/// 客戶端IP/// </summary>private string ip;public string IP{get { return ip; }set { ip = value; }}/// <summary>/// 客戶端端口號/// </summary>private int port;public int Port{get { return port; }set { port = value; }}/// <summary>/// IP終端/// </summary>private IPEndPoint ipEndPoint;/// <summary>/// 客戶端Socket/// </summary>private Socket mClientSocket;/// <summary>/// 是否連接到了服務器/// 默認為flase/// </summary>private bool isConnected = false;/// <summary>/// 構造函數/// </summary>/// <param name="ip">IP地址</param>/// <param name="port">端口號</param>public TCPClient(string ip, int port){this.ip=ip;this.port=port;//初始化IP終端this.ipEndPoint = new IPEndPoint(IPAddress.Parse(this.ip), this.port);//初始化客戶端SocketmClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);}public void Start(){//創建一個線程以不斷連接服務器var mConnectThread = new Thread(this.ConnectToServer);//開啟線程mConnectThread.Start();}/// <summary>/// 連接到服務器/// </summary>private void ConnectToServer(){//當沒有連接到服務器時開始連接while (!isConnected){try{//開始連接mClientSocket.Connect(this.ipEndPoint);this.isConnected = true;}catch (Exception e){//輸出Debug信息Console.WriteLine(string.Format("因為一個錯誤的發生,暫時無法連接到服務器,錯誤信息為:{0}",e.Message));this.isConnected = false;}//等待5秒鐘后嘗試再次連接Thread.Sleep(5000);Console.WriteLine("正在嘗試重新連接...");}//連接成功后Console.WriteLine("連接服務器成功,現在可以和服務器進行會話了");//創建一個線程以監聽數據接收var mReceiveThread = new Thread(this.ReceiveMessage);//開啟線程mReceiveThread.Start();}/// <summary>/// 因為客戶端只接受來自服務器的數據/// 因此這個方法中不需要參數/// </summary>private void ReceiveMessage(){//設置循環標志位bool flag = true;while (flag){try{//獲取數據長度int receiveLength = this.mClientSocket.Receive(result);//獲取服務器消息string serverMessage = Encoding.UTF8.GetString(result, 0, receiveLength);//輸出服務器消息Console.WriteLine(serverMessage);}catch (Exception e){//停止消息接收flag = false;//斷開服務器this.mClientSocket.Shutdown(SocketShutdown.Both);//關閉套接字this.mClientSocket.Close();//重新嘗試連接服務器this.isConnected = false;ConnectToServer();}}}/// <summary>/// 發送消息/// </summary>/// <param name="msg">消息文本</param>public void SendMessage(string msg){if(msg==string.Empty || this.mClientSocket==null) return;mClientSocket.Send(Encoding.UTF8.GetBytes(msg));}} }- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 - 30
 - 31
 - 32
 - 33
 - 34
 - 35
 - 36
 - 37
 - 38
 - 39
 - 40
 - 41
 - 42
 - 43
 - 44
 - 45
 - 46
 - 47
 - 48
 - 49
 - 50
 - 51
 - 52
 - 53
 - 54
 - 55
 - 56
 - 57
 - 58
 - 59
 - 60
 - 61
 - 62
 - 63
 - 64
 - 65
 - 66
 - 67
 - 68
 - 69
 - 70
 - 71
 - 72
 - 73
 - 74
 - 75
 - 76
 - 77
 - 78
 - 79
 - 80
 - 81
 - 82
 - 83
 - 84
 - 85
 - 86
 - 87
 - 88
 - 89
 - 90
 - 91
 - 92
 - 93
 - 94
 - 95
 - 96
 - 97
 - 98
 - 99
 - 100
 - 101
 - 102
 - 103
 - 104
 - 105
 - 106
 - 107
 - 108
 - 109
 - 110
 - 111
 - 112
 - 113
 - 114
 - 115
 - 116
 - 117
 - 118
 - 119
 - 120
 - 121
 - 122
 - 123
 - 124
 - 125
 - 126
 - 127
 - 128
 - 129
 - 130
 - 131
 - 132
 - 133
 - 134
 - 135
 - 136
 - 137
 - 138
 - 139
 - 140
 - 141
 - 142
 - 143
 - 144
 - 145
 - 146
 - 147
 - 148
 - 149
 - 150
 - 151
 - 152
 - 153
 - 154
 - 155
 - 156
 - 157
 - 158
 
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 - 30
 - 31
 - 32
 - 33
 - 34
 - 35
 - 36
 - 37
 - 38
 - 39
 - 40
 - 41
 - 42
 - 43
 - 44
 - 45
 - 46
 - 47
 - 48
 - 49
 - 50
 - 51
 - 52
 - 53
 - 54
 - 55
 - 56
 - 57
 - 58
 - 59
 - 60
 - 61
 - 62
 - 63
 - 64
 - 65
 - 66
 - 67
 - 68
 - 69
 - 70
 - 71
 - 72
 - 73
 - 74
 - 75
 - 76
 - 77
 - 78
 - 79
 - 80
 - 81
 - 82
 - 83
 - 84
 - 85
 - 86
 - 87
 - 88
 - 89
 - 90
 - 91
 - 92
 - 93
 - 94
 - 95
 - 96
 - 97
 - 98
 - 99
 - 100
 - 101
 - 102
 - 103
 - 104
 - 105
 - 106
 - 107
 - 108
 - 109
 - 110
 - 111
 - 112
 - 113
 - 114
 - 115
 - 116
 - 117
 - 118
 - 119
 - 120
 - 121
 - 122
 - 123
 - 124
 - 125
 - 126
 - 127
 - 128
 - 129
 - 130
 - 131
 - 132
 - 133
 - 134
 - 135
 - 136
 - 137
 - 138
 - 139
 - 140
 - 141
 - 142
 - 143
 - 144
 - 145
 - 146
 - 147
 - 148
 - 149
 - 150
 - 151
 - 152
 - 153
 - 154
 - 155
 - 156
 - 157
 - 158
 
同樣地,我們現在來運行客戶端程序,這樣客戶端就可以和服務端進行通信了:
using System; using System.Collections.Generic; using System.Text; using TCPLib; using System.Net; using System.Net.Sockets;namespace TCPLib.Test {class Program{static void Main(string[] args){//保證端口號和服務端一致TCPLib.TCPClient c = new TCPClient("127.0.0.1",6001);//執行Start方法c.Start();while(true){//讀取客戶端輸入的消息string msg = Console.ReadLine();//發送消息到服務端c.SendMessage(msg);}}}}- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 
注意要先運行服務端的程序、再運行客戶端的程序,不然程序會報錯,嘿嘿!好了,下面是今天的效果演示圖:
總結
今天我們基本上寫出了一個可以使用的用例,不過這個例子目前還存在以下問題: 
 * 這里僅僅實現了發送字符串的功能,如何讓這個程序支持更多的類型,從基礎的int、float、double、string、single等類型到structure、class甚至是二進制文件的類型? 
 * 如何讓這個用例更具有擴展性,我們發現所有的Socket編程流程都是一樣的,唯一不同就是在接收到數據以后該如何去處理,因為能不能將核心功能和自定義功能分離開來? 
 * 在今天的這個用例中,數據傳輸的緩沖區大小我們人為設定為1024,那么如果碰到比這個設定更大的數據類型,這個用例該怎么來寫?
好了,這就是今天的內容了,希望大家喜歡,同時希望大家關注我的博客!
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的C#中Socket通信编程的同步实现的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 对C语言进行调试的最好方法是什么?
 - 下一篇: Visual C++利用Intel C+