.NET下WPF学习之Socket通信
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 }
运行展示: