1 流程
2 示例
看下面一个服务器端的代码:
namespace MyScoketTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 开始监听 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Start_Click(object sender, EventArgs e) { //(1)创建套接字 Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //(2)绑定IP和端口 int port = 2018; string host = "127.0.0.1"; //指示服务器应侦听所有网络接口上的客户端活动。此字段为只读。 IPAddress iP = IPAddress.Any; //创建端口号对象 IPEndPoint point = new IPEndPoint(iP, port); //绑定 socketWatch.Bind(point); //(3)监听 socketWatch.Listen(10); ShowMsg($"监听成功"); //(4)等待客户端连接,并且创建一个负责通信的Socket Socket socket = socketWatch.Accept(); //RemoteEndPoint是当前客户端连接成功的IP ShowMsg($"{socket.RemoteEndPoint.ToString()}:连接成功"); } private void ShowMsg(string str) { this.textBox1.AppendText(str + "\r\n"); } private void textBox1_TextChanged(object sender, EventArgs e) { } } }
运行成功之后点击开始监听按钮,结果如下:
现在有2个问题,而且问题都是出现在代码 Socket socket = socketWatch.Accept()这里:
一是界面会卡死:在这个地方,会一直等待连接,如果客户端一直不连接,winform界面就会卡主,因为主线程阻塞了,界面的操作无法响应。所以我们可以开启一个新线程来执行。
二是一次只能连接一个客户端:这个问题可以通过写在循环里面来解决,这样的话就能一直接收别的客户端出来的申请连接。
还有在窗体加载的加一段代码,防止客户端链接的时候出现跨线程的错误:
private void Form1_Load(object sender, EventArgs e) { //当某个线程不是控件的创建线程尝试访问该控件的一个或多个属性时,这通常会导致不可预知的结果。
常见的无效线程活动是对访问控件属性的错误线程的调用 Handle 。 设置 CheckForIllegalCrossThreadCalls 为 true 可以在调试时更轻松地查找和诊断此线程活动。 Control.CheckForIllegalCrossThreadCalls = false; }
更改后的代码:
namespace MyScoketTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 开始监听 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Start_Click(object sender, EventArgs e) { //(1)创建套接字 Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //(2)绑定IP和端口 int port = 2018; string host = "127.0.0.1"; //IPAddress.Any 指示服务器应侦听所有网络接口上的客户端活动。此字段为只读。 // IPAddress iP = IPAddress.Any; IPAddress ip = IPAddress.Parse(host); //创建端口号对象 IPEndPoint point = new IPEndPoint(ip, port); //绑定 socketWatch.Bind(point); //(3)监听 socketWatch.Listen(10); ShowMsg($"监听成功"); //使用委托异步的方式界面界面卡死 Action<Socket> action = Listen; action.BeginInvoke(socketWatch, null, null); //Thread thread = new Thread(new ThreadStart(Listen)); } private void ShowMsg(string str) { this.textBox1.AppendText(str + "\r\n"); } void Listen(Socket socketWatch) { //(4)等待客户端连接,并且创建一个负责通信的Socket while (true) { Socket socket = socketWatch.Accept(); //RemoteEndPoint是当前客户端连接成功的IP ShowMsg($"{socket.RemoteEndPoint.ToString()}:连接成功"); } } private void textBox1_TextChanged(object sender, EventArgs e) { } private void Form1_Load(object sender, EventArgs e) { //当某个线程不是控件的创建线程尝试访问该控件的一个或多个属性时,这通常会导致不可预知的结果。
常见的无效线程活动是对访问控件属性的错误线程的调用 Handle 。 设置 CheckForIllegalCrossThreadCalls 为 true 可以在调试时更轻松地查找和诊断此线程活动。 Control.CheckForIllegalCrossThreadCalls = false; } } }
上面是服务端的代码,我们可以暂时通过telnet来进行客户端的测试,执行cmd执行,打开命令提示符界面,输入telnet 127.0.0.1 2018,
点击回车,结果:
上面的代码成功实现了连接,下面就需要我们进行通信的处理,Listen方法给为如下:
void Listen(Socket socketWatch) { //(4)等待客户端连接,并且创建一个负责通信的Socket while (true) { //通信的Socket对象 Socket socket = socketWatch.Accept(); //RemoteEndPoint是当前客户端连接成功的IP ShowMsg($"{socket.RemoteEndPoint.ToString()}:连接成功"); //连接成功之后,服务器应该接收来自于客户端的消息,这是由负责通信的socket来实现 byte[] buffer = new byte[1024 * 1024 * 2];//2M //realLength实际接收的有效字节数 int realLength = socket.Receive(buffer); string str = Encoding.UTF8.GetString(buffer, 0, realLength); ShowMsg($"{socket.RemoteEndPoint.ToString()}:{str}"); } }
运行起来,然后发送数据,但是却发现只能接收一次数据,后面无论怎么发送都不过去了,如下,只发送了一个字母a:
这是因为下面这行代码写在了循环里面:
//通信的Socket对象 Socket socket = socketWatch.Accept();
比如说第一次循环的时候连接上这个客户端,这个客户端发了消息之后再次循环的时候又调了一次上面的代码,相当于创建了一个新的Socket连接, 上一次通信的连接没有了,所以就接收不到后面的信息了。所以我们需要拿到循环外面,不止如此,还要将下面Receive方法再开辟一个线程来执行,防止卡死。这样的话既能接收多个客户端的连接信息,来一个客户端信息,就给这个客户端分配一个Socket对象,专门负责这个客户端和服务端的通信,也能为每一个客户端的信息开辟一个子线程来专门去处理信息:
如下,Listen方法:
void Listen(Socket socketWatch) { //(4)等待客户端连接,并且创建一个负责通信的Socket while (true) { //通信的Socket对象,在循环中,每来一个客户端信息,就给这个客户端分配一个Socket对象,专门负责这个客户端和服务端的通信 Socket socket = socketWatch.Accept(); //RemoteEndPoint是当前客户端连接成功的IP ShowMsg($"{socket.RemoteEndPoint.ToString()}:连接成功"); Action<Socket> action = Receive; action.BeginInvoke(socket, null, null); } } /// <summary> /// 接收信息--在这里也是专门弄成每一个客户端连接信息都有专门的线程来负责。 /// </summary> /// <param name="socket"></param> private void Receive(Socket socket) { while (true) { //连接成功之后,服务器应该接收来自于客户端的消息,这是由负责通信的socket来实现 byte[] buffer = new byte[1024 * 1024 * 2];//2M //realLength实际接收的有效字节数 int realLength = socket.Receive(buffer); string str = Encoding.UTF8.GetString(buffer, 0, realLength); ShowMsg($"{socket.RemoteEndPoint.ToString()}:{str}"); } }
此时运行之后上述问题已经解决了。但是又出现新问题了,关闭客户端之后,服务端一直在接收空信息,陷入死循环中了。
我们现在需要知道一件事:int realLength = socket.Receive(buffer);看这行代码,如果客户端A和服务器端已经连接上了,子线程A运行到这行代码之后就会阻塞,只有客户端发来信息之后才会继续向下执行,还有就是客户端关闭连接之后子线程A也会取消在这个地方的阻塞,继续向下执行,所以我们可以判断一下接收到信息的长度,如果为0,则表示客户端已经关闭了。凡是在网络中传输数据,或者说操纵网络数据,肯定会引发各种异常,不管是断电,断网。所以我们需要在有可能出异常的地方尽量都加上try/catch,比如下面的代码直接在循环中价格try/catch,catch中不用加其它处理,万一客户端出现问题了,只要重新连接上,我们这边照样能接收发送数据。这样用户也不会看出来出问题了。
/// <summary> /// 接收信息 /// </summary> /// <param name="socket"></param> private void Receive(Socket socket) { while (true) { try { //连接成功之后,服务器应该接收来自于客户端的消息,这是由负责通信的socket来实现 byte[] buffer = new byte[1024 * 1024 * 2];//2M //realLength实际接收的有效字节数 int realLength = socket.Receive(buffer); if (realLength == 0) { break; } string str = Encoding.UTF8.GetString(buffer, 0, realLength); ShowMsg($"{socket.RemoteEndPoint.ToString()}:{str}"); } catch{ } } }
其实现在还是有一个问题,那就是Listen方法中,每一次循环的时候都会调用accept方法,如果存在多个客户端和服务器连接之后,服务器端想向客户端发消息的话,就不知道到底会给谁发送消息,因为循环的原因,总是默认给最近一次连接服务器的那个客户端发消息。所以我们可以加一个下拉框,因为每次服务器接收客户端申请的时候都能知道申请的客户端的IP和端口信息,我们将其存起来,发送消息的时候选择对应的客户端就可以了。
所以最终服务器端的代码更改为:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 开始监听 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Start_Click(object sender, EventArgs e) { try { //(1)创建套接字 Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //(2)绑定IP和端口 int port = 2018; string host = "127.0.0.1"; //IPAddress.Any 指示服务器应侦听所有网络接口上的客户端活动。此字段为只读。 // IPAddress iP = IPAddress.Any; IPAddress ip = IPAddress.Parse(host); //创建端口号对象 IPEndPoint point = new IPEndPoint(ip, port); //绑定 socketWatch.Bind(point); //(3)监听 socketWatch.Listen(10); ShowMsg($"监听成功"); //使用委托异步的方式界面界面卡死 Action<Socket> action = Listen; action.BeginInvoke(socketWatch, null, null); //Thread thread = new Thread(new ThreadStart(Listen)); } catch { } } private void ShowMsg(string str) { this.textBox1.AppendText(str + "\r\n"); } //放到方法外是因为点击发送消息按钮的时候需要通过当前的socket对象给客户端发送消息。 Socket socket; Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>(); /// <summary> /// 监听 /// </summary> /// <param name="socketWatch"></param> void Listen(Socket socketWatch) { //(4)等待客户端连接,并且创建一个负责通信的Socket while (true) { try { //通信的Socket对象 socket = socketWatch.Accept(); #region 这是服务器给多个客户端中的某一个客户发送信息处理的一部分代码 //存储客户端信息以及处理这个客户端的socket对象 dicSocket[socket.RemoteEndPoint.ToString()] = socket; //向下拉框添加值 this.comboBox1.Items.Add(socket.RemoteEndPoint.ToString()); #endregion //RemoteEndPoint是当前客户端连接成功的IP ShowMsg($"{socket.RemoteEndPoint.ToString()}:连接成功"); Action<Socket> action = Receive; action.BeginInvoke(socket, null, null); } catch { } } } /// <summary> /// 接收信息 /// </summary> /// <param name="socket"></param> private void Receive(Socket socket) { while (true) { try { //连接成功之后,服务器应该接收来自于客户端的消息,这是由负责通信的socket来实现 byte[] buffer = new byte[1024 * 1024 * 2];//2M //realLength实际接收的有效字节数 int realLength = socket.Receive(buffer); if (realLength == 0) { //向下拉框移除关闭的客户端信息 this.comboBox1.Items.Remove(socket.RemoteEndPoint.ToString()); break; } string str = Encoding.UTF8.GetString(buffer, 0, realLength); ShowMsg($"{socket.RemoteEndPoint.ToString()}:{str}"); } catch { } } } private void textBox1_TextChanged(object sender, EventArgs e) { } private void Form1_Load(object sender, EventArgs e) { //当某个线程不是控件的创建线程尝试访问该控件的一个或多个属性时,这通常会导致不可预知的结果。
常见的无效线程活动是对访问控件属性的错误线程的调用 Handle 。 设置 CheckForIllegalCrossThreadCalls 为 true 可以在调试时更轻松地查找和诊断此线程活动。 Control.CheckForIllegalCrossThreadCalls = false; } /// <summary> /// 服务器给客户端发送消息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSend_Click(object sender, EventArgs e) { try { string str = this.textBox2.Text.Trim(); byte[] buffer = Encoding.UTF8.GetBytes(str); //获取选中的客户端socket对象,给对应的客户端发送信息 string ip = this.comboBox1.SelectedItem.ToString(); dicSocket[ip].Send(buffer); } catch (Exception ex) { } } }
客户端代码:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } //客户端发送信息的Socket Socket socketSend; /// <summary> /// 客户端连接 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnConnection_Click(object sender, EventArgs e) { try { //(1)创建套接字 socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //(2)连接 这是服务器应用程序的IP和端口号 int port = 2018; string host = "127.0.0.1"; IPAddress ip = IPAddress.Parse(host); IPEndPoint point = new IPEndPoint(ip, port); socketSend.Connect(point); ShowMsg("连接成功"); //开启子线程,不停的接收服务器消息 Action action = Receive; Task.Run(action); } catch { } } /// <summary> /// 接收服务端信息 /// 记住,要想接收到服务端的消息,必须拿到当前客户端和服务端 /// 通信的Socket对象:socketSend,通过它来获取消息 /// </summary> private void Receive() { while (true) { try { //连接成功之后,服务器应该接收来自于客户端的消息,这是由负责通信的socket来实现 byte[] buffer = new byte[1024 * 1024 * 2];//2M //realLength实际接收的有效字节数 int realLength = socketSend.Receive(buffer); if (realLength == 0) { break; } string str = Encoding.UTF8.GetString(buffer, 0, realLength); ShowMsg($"{socketSend.RemoteEndPoint.ToString()}:{str}"); } catch { } } } /// <summary> /// 展示传来的消息 /// </summary> /// <param name="str"></param> private void ShowMsg(string str) { this.textBox1.AppendText(str + "\r\n"); } /// <summary> /// 发送消息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSend_Click(object sender, EventArgs e) { byte[] buffer = Encoding.UTF8.GetBytes(textBox2.Text.Trim()); //发送信息 socketSend.Send(buffer); } private void Form1_Load(object sender, EventArgs e) { //取消 Control.CheckForIllegalCrossThreadCalls = false; } }
先启动服务端,开启监听,然后启动客户端,连接,发送消息,结果:
关于上面代码为什么会使用过IPAddress.Any的补充:
IPAddress.Any 解决本地ip和服务器ip切换问题IPAddress.Any表示本机ip,换言之,如果服务器绑定此地址,则表示侦听本机所有ip对应的那个端口(本机可能有多个ip或只有一个ip)
IPAddress.Any微软给出的解释是:Provides an IP address that indicates that the server must listen for client activity on all network interfaces. This field is read-only.翻译过来就是:提供一个iP地址来指示服务器必须监听所有网卡上的客户端活动。此字段为只读的。也就是说,对双卡网或者多网卡的机器,每个网卡都会有一个独立的ip,如果使用了IPAddress.Any就表示服务器必须监听本机所有网卡上的指定端口。
比如双网卡机器,内网ip为192.168.0.1,外网ip为120.210.1.1,服务器可以同时监听192.168.0.1:80和120.210.1.1:80。
localipAddress = Dns.Resolve(IPAddress.Any.ToString()).AddressList[0];
m_RecSocket = new TcpListener(localipAddress, m_localPort);
的写法可以改成
m_RecSocket = new TcpListener(IPAddress.Any, m_localPort);
参考:https://www.bilibili.com/video/BV1FW411v7as?p=9&spm_id_from=pageDriver
IPAddress.Any表示本机ip,换言之,如果服务器绑定此地址,则表示侦听本机所有ip对应的那个端口(本机可能有多个ip或只有一个ip)
IPAddress.Any微软给出的解释是:Provides an IP address that indicates that the server must listen for client activity on all network interfaces. This field is read-only.翻译过来就是:提供一个iP地址来指示服务器必须监听所有网卡上的客户端活动。此字段为只读的。也就是说,对双卡网或者多网卡的机器,每个网卡都会有一个独立的ip,如果使用了IPAddress.Any就表示服务器必须监听本机所有网卡上的指定端口。
比如双网卡机器,内网ip为192.168.0.1,外网ip为120.210.1.1,服务器可以同时监听192.168.0.1:80和120.210.1.1:80。
localipAddress = Dns.Resolve(IPAddress.Any.ToString()).AddressList[0];
m_RecSocket = new TcpListener(localipAddress, m_localPort);
的写法可以改成
m_RecSocket = new TcpListener(IPAddress.Any, m_localPort);