Socket通信
關於Socket
Socket作為進程通信的機制,是處於網絡層中的應用層,說白了就是兩個程序間通信用的。
它的形式與電話插座類似,電話的通話雙方相當於兩個互相通信的程序,電話號相當於IP。
網絡通信三要素
- IP地址(網絡上主機設備的唯一標識,識別一台唯一的主機)
- 端口號(定位程序,確定兩個通信的程序)
有效端口:0~65535,其中0~1023由系統使用,稱為公認端口,他們緊密綁定與一些服務。從1024~49151是一些松散的綁定於一些服務,需要注冊的一些端口,稱為注冊端口,剩下的49152~65535為動態端口、私有端口,我們一般開發都是使用這一頻段的端口.
- 傳輸協議(用什么樣的方式進行交互)
常見協議:TCP(面向連接,提供可靠的服務),UDP(無連接,傳輸速度快),一般使用TCP。
服務端於客戶端Socket通信流程
重點記憶兩個端的步驟:
服務端: 客戶端:
1、創建Socket對象(負責偵聽) 1、創建Socket對象
2、綁定端口 2、連接服務器端
3、開啟偵聽 3、發送消息、接受消息
4、開始接受客戶端連接(不斷接收,涉及多線程) 4、停止連接
5、創建一個代理Socket對象(負責通信) 5、關閉Socket對象
6、發送、接收消息
7、關閉Socket對象
實現代碼
服務端XAML代碼(客戶端類似):

1 <Window x:Class="SocketDemo.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 5 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 6 xmlns:local="clr-namespace:SocketDemo" 7 mc:Ignorable="d" 8 Title="MainWindow" Height="472.5" Width="605"> 9 <StackPanel> 10 <Canvas Margin="10,20" Height="30"> 11 <Label Content="IP:" Height="30" Width="30" FontSize="18" HorizontalContentAlignment="Center" Canvas.Left="8"/> 12 <TextBox x:Name="txtIp" Text="192.168.0.4" Height="30" Width="150" FontSize="20" HorizontalContentAlignment="Center" Canvas.Left="41" /> 13 <Label Content="Port:" Height="30" Width="50" FontSize="18" HorizontalContentAlignment="Center" Canvas.Left="210"/> 14 <TextBox x:Name="txtPort" Text="45000" Height="30" Width="150" FontSize="20" HorizontalContentAlignment="Center" Canvas.Left="263" /> 15 <Button x:Name="btnStartServer" Content="開啟服務" Height="30" Width="100" Canvas.Left="460"/> 16 </Canvas> 17 <TextBox Name="txtLog" Height="300" AcceptsReturn="True" TextWrapping="Wrap"></TextBox> 18 <Canvas Margin="0,20" Height="30"> 19 <TextBox x:Name="txtMsg" Height="30" Width="450" FontSize="20" HorizontalContentAlignment="Center" Canvas.Left="0" /> 20 <Button x:Name="btnSendMsg" Content="發送消息" Height="30" Width="100" Canvas.Left="470" /> 21 </Canvas> 22 </StackPanel> 23 </Window>
具體服務端實現:

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Net; 5 using System.Net.Sockets; 6 using System.Text; 7 using System.Threading; 8 using System.Threading.Tasks; 9 using System.Windows; 10 using System.Windows.Controls; 11 using System.Windows.Data; 12 using System.Windows.Documents; 13 using System.Windows.Input; 14 using System.Windows.Media; 15 using System.Windows.Media.Imaging; 16 using System.Windows.Navigation; 17 using System.Windows.Shapes; 18 19 namespace SocketDemo 20 { 21 /// <summary> 22 /// MainWindow.xaml 的交互邏輯 23 /// </summary> 24 public partial class MainWindow : Window 25 { 26 List<Socket> clientScoketLis = new List<Socket>();//存儲連接服務器端的客戶端的Socket 27 public MainWindow() 28 { 29 InitializeComponent(); 30 Loaded += MainWindow_Loaded; 31 btnStartServer.Click += BtnStartServer_Click;//事件注冊 32 btnSendMsg.Click += BtnSendMsg_Click; 33 Closing += MainWindow_Closing; 34 } 35 36 37 private void MainWindow_Loaded(object sender, RoutedEventArgs e) 38 { 39 ClientWindows clientWindows = new ClientWindows(); 40 clientWindows.Show(); 41 } 42 /// <summary> 43 /// 關閉事件 44 /// </summary> 45 private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) 46 { 47 //使用foreach出現 “集合已修改;可能無法執行枚舉操作”,ClientExit源於方法中對list集合進行了Remove,所造成的異常。 48 //msdn的解釋:foreach 語句是對枚舉數的包裝,它只允許從集合中讀取,不允許寫入集合。也就是,不能在foreach里遍歷的時侯把它的元素進行刪除或增加的操作的 49 //foreach (var socket in clientScoketLis) 50 //{ 51 // ClientExit(null , socket); 52 //} 53 //改成for循環即可 54 for (int i = 0; i < clientScoketLis.Count; i++)//向每個客戶端說我下線了 55 { 56 ClientExit(null, clientScoketLis[i]); 57 } 58 } 59 60 /// <summary> 61 /// 開啟服務事件 62 /// </summary> 63 private void BtnStartServer_Click(object sender, RoutedEventArgs e) 64 { 65 //1、創建Socket對象 66 //參數:尋址方式,當前為Ivp4 指定套接字類型 指定傳輸協議Tcp; 67 Socket socket = new Socket(AddressFamily.InterNetwork , SocketType.Stream , ProtocolType.Tcp); 68 //2、綁定端口、IP 69 IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(this.txtIp.Text) , int.Parse(txtPort.Text)); 70 socket.Bind(iPEndPoint); 71 //3、開啟偵聽 10為隊列最多接收的數量 72 socket.Listen(10);//如果同時來了100個連接請求,只能處理一個,隊列中10個在等待連接的客戶端,其他的則返回錯誤消息。 73 74 //4、開始接受客戶端的連接 ,連接會阻塞主線程,故使用線程池。 75 ThreadPool.QueueUserWorkItem(new WaitCallback(AcceptClientConnect),socket); 76 77 78 } 79 /// <summary> 80 /// 線程池線程執行的接受客戶端連接方法 81 /// </summary> 82 /// <param name="obj">傳入的Socket</param> 83 private void AcceptClientConnect(object obj) 84 { 85 //轉換Socket 86 var serverSocket = obj as Socket; 87 88 AppendTxtLogText("服務端開始接收客戶端連接!"); 89 90 //不斷接受客戶端的連接 91 while (true) 92 { 93 //5、創建一個負責通信的Socket 94 Socket proxSocket = serverSocket.Accept(); 95 AppendTxtLogText(string.Format("客戶端:{0}連接上了!", proxSocket.RemoteEndPoint.ToString())); 96 //將連接的Socket存入集合 97 clientScoketLis.Add(proxSocket); 98 //6、不斷接收客戶端發送來的消息 99 ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveClientMsg) , proxSocket); 100 } 101 102 } 103 /// <summary> 104 /// 不斷接收客戶端信息子線程方法 105 /// </summary> 106 /// <param name="obj">參數Socke對象</param> 107 private void ReceiveClientMsg(object obj) 108 { 109 var proxSocket = obj as Socket; 110 //創建緩存內存,存儲接收的信息 ,不能放到while中,這塊內存可以循環利用 111 byte[] data = new byte[1020*1024]; 112 while (true) 113 { 114 int len; 115 try 116 { 117 //接收消息,返回字節長度 118 len = proxSocket.Receive(data, 0, data.Length, SocketFlags.None); 119 } 120 catch (Exception ex) 121 { 122 //7、關閉Socket 123 //異常退出 124 try 125 { 126 ClientExit(string.Format("客戶端:{0}非正常退出", proxSocket.RemoteEndPoint.ToString()), proxSocket); 127 } 128 catch (Exception) 129 { 130 } 131 return;//讓方法結束,終結當前客戶端數據的異步線程,方法退出,即線程結束 132 } 133 134 if (len <= 0)//判斷接收的字節數 135 { 136 //7、關閉Socket 137 //小於0表示正常退出 138 try 139 { 140 ClientExit(string.Format("客戶端:{0}正常退出", proxSocket.RemoteEndPoint.ToString()), proxSocket); 141 } 142 catch (Exception) 143 { 144 } 145 return;//讓方法結束,終結當前客戶端數據的異步線程,方法退出,即線程結束 146 } 147 //將消息顯示到TxtLog 148 string msgStr = Encoding.Default.GetString(data , 0 , len); 149 //拼接字符串 150 AppendTxtLogText(string.Format("接收到客戶端:{0}的消息:{1}" , proxSocket.RemoteEndPoint.ToString() , msgStr)); 151 } 152 } 153 154 /// <summary> 155 /// 消息發送事件 156 /// </summary> 157 private void BtnSendMsg_Click(object sender, RoutedEventArgs e) 158 { 159 foreach (Socket proxSocket in clientScoketLis) 160 { 161 if (proxSocket.Connected)//判斷客戶端是否還在連接 162 { 163 byte[] data = Encoding.Default.GetBytes(this.txtMsg.Text); 164 //6、發送消息 165 proxSocket.Send(data , 0 , data.Length , SocketFlags.None); //指定套接字的發送行為 166 this.txtMsg.Text = null; 167 } 168 } 169 } 170 /// <summary> 171 /// 向文本框中追加信息 172 /// </summary> 173 /// <param name="str"></param> 174 private void AppendTxtLogText( string str) 175 { 176 if (!(txtLog.Dispatcher.CheckAccess()))//判斷跨線程訪問 177 { 178 ////同步方法 179 //this.Dispatcher.Invoke(new Action<string>( s => 180 //{ 181 // this.txtLog.Text = string.Format("{0}\r\n{1}" , s , txtLog.Text); 182 //}) ,str); 183 //異步方法 184 this.Dispatcher.BeginInvoke(new Action<string>(s => 185 { 186 this.txtLog.Text = string.Format("{0}\r\n{1}", s, txtLog.Text); 187 }), str); 188 } 189 else 190 { 191 this.txtLog.Text = string.Format("{0}\r\n{1}", str, txtLog.Text); 192 } 193 } 194 /// <summary> 195 /// 客戶端退出調用 196 /// </summary> 197 /// <param name="msg"></param> 198 private void ClientExit(string msg , Socket proxSocket) 199 { 200 AppendTxtLogText(msg); 201 clientScoketLis.Remove(proxSocket);//移除集合中的連接Socket 202 203 try 204 { 205 if (proxSocket.Connected)//如果是連接狀態 206 { 207 proxSocket.Shutdown(SocketShutdown.Both);//關閉連接 208 proxSocket.Close(100);//100秒超時間 209 } 210 } 211 catch (Exception ex) 212 { 213 } 214 } 215 } 216 }
具體客戶端實現:

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Net; 5 using System.Net.Sockets; 6 using System.Text; 7 using System.Threading; 8 using System.Threading.Tasks; 9 using System.Windows; 10 using System.Windows.Controls; 11 using System.Windows.Data; 12 using System.Windows.Documents; 13 using System.Windows.Input; 14 using System.Windows.Media; 15 using System.Windows.Media.Imaging; 16 using System.Windows.Shapes; 17 18 namespace SocketDemo 19 { 20 /// <summary> 21 /// ClientWindows.xaml 的交互邏輯 22 /// </summary> 23 public partial class ClientWindows : Window 24 { 25 private Socket _socket; 26 public ClientWindows() 27 { 28 InitializeComponent(); 29 btnSendMsg.Click += BtnSendMsg_Click;//注冊事件 30 btnConnect.Click += BtnConnect_Click; 31 Closing += ClientWindows_Closing; 32 } 33 /// <summary> 34 /// 窗口關閉事件 35 /// </summary> 36 private void ClientWindows_Closing(object sender, System.ComponentModel.CancelEventArgs e) 37 { 38 ServerExit(null,_socket);//向服務端說我下線了。 39 } 40 41 /// <summary> 42 /// 連接按鈕事件 43 /// </summary> 44 private void BtnConnect_Click(object sender, RoutedEventArgs e) 45 { 46 //1、創建Socket對象 47 Socket socket = new Socket(AddressFamily.InterNetwork , SocketType.Stream , ProtocolType.Tcp); 48 _socket = socket; 49 //2、連接服務器,綁定IP 與 端口 50 IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(txtIp.Text) , int.Parse(txtPort.Text)); 51 try 52 { 53 socket.Connect(iPEndPoint); 54 } 55 catch (Exception) 56 { 57 MessageBox.Show("連接失敗,請重新連接!","提示"); 58 return; 59 } 60 //3、接收消息 61 ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveServerMsg),socket); 62 } 63 64 /// <summary> 65 /// 不斷接收客戶端信息子線程方法 66 /// </summary> 67 /// <param name="obj">參數Socke對象</param> 68 private void ReceiveServerMsg(object obj) 69 { 70 var proxSocket = obj as Socket; 71 //創建緩存內存,存儲接收的信息 ,不能放到while中,這塊內存可以循環利用 72 byte[] data = new byte[1020 * 1024]; 73 while (true) 74 { 75 int len; 76 try 77 { 78 //接收消息,返回字節長度 79 len = proxSocket.Receive(data, 0, data.Length, SocketFlags.None); 80 } 81 catch (Exception ex) 82 { 83 //7、關閉Socket 84 //異常退出 85 try 86 { 87 ServerExit(string.Format("服務端:{0}非正常退出", proxSocket.RemoteEndPoint.ToString()), proxSocket); 88 } 89 catch (Exception) 90 { 91 92 } 93 return;//讓方法結束,終結當前客戶端數據的異步線程,方法退出,即線程結束 94 } 95 96 if (len <= 0)//判斷接收的字節數 97 { 98 //7、關閉Socket 99 //小於0表示正常退出 100 try 101 { 102 ServerExit(string.Format("服務端:{0}正常退出", proxSocket.RemoteEndPoint.ToString()), proxSocket); 103 } 104 catch (Exception) 105 { 106 107 } 108 return;//讓方法結束,終結當前客戶端數據的異步線程,方法退出,即線程結束 109 } 110 //將消息顯示到TxtLog 111 string msgStr = Encoding.Default.GetString(data, 0, len); 112 //拼接字符串 113 AppendTxtLogText(string.Format("接收到服務端:{0}的消息:{1}", proxSocket.RemoteEndPoint.ToString(), msgStr)); 114 } 115 } 116 117 /// <summary> 118 /// 客戶端退出調用 119 /// </summary> 120 /// <param name="msg"></param> 121 private void ServerExit(string msg, Socket proxSocket) 122 { 123 AppendTxtLogText(msg); 124 try 125 { 126 if (proxSocket.Connected)//如果是連接狀態 127 { 128 proxSocket.Shutdown(SocketShutdown.Both);//關閉連接 129 proxSocket.Close(100);//100秒超時間 130 } 131 } 132 catch (Exception ex) 133 { 134 } 135 } 136 137 /// <summary> 138 /// 發送信息按鈕事件 139 /// </summary> 140 private void BtnSendMsg_Click(object sender, RoutedEventArgs e) 141 { 142 byte[] data = Encoding.Default.GetBytes(this.txtMsg.Text); 143 //6、發送消息 144 _socket.Send(data, 0, data.Length, SocketFlags.None); //指定套接字的發送行為 145 this.txtMsg.Text = null; 146 } 147 148 /// <summary> 149 /// 向文本框中追加信息 150 /// </summary> 151 /// <param name="str"></param> 152 private void AppendTxtLogText(string str) 153 { 154 if (!(txtLog.Dispatcher.CheckAccess()))//判斷跨線程訪問 155 { 156 ////同步方法 157 //this.Dispatcher.Invoke(new Action<string>( s => 158 //{ 159 // this.txtLog.Text = string.Format("{0}\r\n{1}" , s , txtLog.Text); 160 //}) ,str); 161 //異步方法 162 this.Dispatcher.BeginInvoke(new Action<string>(s => 163 { 164 this.txtLog.Text = string.Format("{0}\r\n{1}", s, txtLog.Text); 165 }), str); 166 } 167 else 168 { 169 this.txtLog.Text = string.Format("{0}\r\n{1}", str, txtLog.Text); 170 } 171 } 172 } 173 }
運行展示: