项目要求:实现看门狗软件与外部调试软件在不知道对方信息的状态下建立可靠的通信。
思路:双方定义一个共同的通信端口号,调试软件定义为发送端,通过广播的方式发送指令。
看门狗定义为接收端,获取需要监控的网口,对每个网口定义UDP通信服务器,接收发送端发来的命令,
调试软件定义为发送端,定义UDP通信客户端,绑定已知的网口。
为保证数据的完整性我们需要定义一下报文格式,我使用的是CRC16_IBM效验。外部调试软件主动发送请求另一端电脑的网络信息,监控回复,解析回复信息得到另一端电脑的IP信息,然后修改本地IP信息与另一端电脑在同一网段,这样就可以建立TCP通信了。
看门狗作为接收端,实时监控来自调试软件的(需要注意的是我对本地的电脑以太网进行了遍历,创建UDP客户端数组,便于确认是哪个网口连接网线收到了广播信息,可以定位到想要修改的网口),单例模式实现如下:
public class UdpClient_Cmd
{
static UdpClient_Cmd() { }
private UdpClient_Cmd() { }
static readonly UdpClient_Cmd instance = new UdpClient_Cmd();
public static UdpClient_Cmd Instance
{
get
{
return instance;
}
}
public string broadcastMsg = "This is UDP broadcast";
Thread sendThread = null;
bool sendThreadFlag = false;
int BroadcastPort = 40001;
private void BroadcastLocalIp()
{
//定义广播内容
byte[] sendBytes = Encoding.Default.GetBytes(broadcastMsg);
while (sendThreadFlag)
{
using (UdpClient sendClient = new UdpClient())
{
sendClient.Send(sendBytes, sendBytes.Length, new IPEndPoint(IPAddress.Broadcast, BroadcastPort));
}
Thread.Sleep(1000);
}
}
/// <summary>
/// 开始向外部广播信息
/// </summary>
public void StartSendBroadcast(int broadcastport)
{
BroadcastPort = broadcastport;
sendThreadFlag = true;
sendThread = new Thread(BroadcastLocalIp);
sendThread.IsBackground = true;
sendThread.Start();
LogHelper.Info("UDP通信广播服务线程启动完成!");
}
/// <summary>
/// 停止向外部广播信息
/// </summary>
public void StopSendBroadcast()
{
sendThreadFlag = false;
if (sendThread != null)
sendThread.Abort();
if (sendThread != null)
{
while ((sendThread.ThreadState != System.Threading.ThreadState.Stopped) && (sendThread.ThreadState != System.Threading.ThreadState.Aborted))
{
Thread.Sleep(10);
}
}
sendThread = null;
LogHelper.Info("UDP通信广播服务线程停止!");
}
Thread receThread = null;
bool recvThreadflag = false;
int BindPort = 40000;
private async void RecvThread()
{
// 接收广播消息
try
{
while (recvThreadflag)
{
// 定义监听的网口IP地址和端口号
List<string> listenInterfaces = IPProvider.getLocalNetworks();
if (listenInterfaces.Count > 0)
{
// 创建 UDP 客户端套接字数组
UdpClient[] udpClients = new UdpClient[listenInterfaces.Count];
// 创建并绑定 UDP 客户端套接字到每个网口
for (int i = 0; i < listenInterfaces.Count; i++)
{
IPAddress listenIP = IPAddress.Parse(listenInterfaces[i]);
udpClients[i] = new UdpClient();
udpClients[i].Client.Bind(new IPEndPoint(listenIP, BindPort));
}
// 异步接收数据报文
Task<UdpReceiveResult>[] receiveTasks = new Task<UdpReceiveResult>[udpClients.Length];
for (int i = 0; i < udpClients.Length; i++)
{
receiveTasks[i] = udpClients[i].ReceiveAsync();
}
// 等待任意一个UDP套接字接收到数据报文
Task<UdpReceiveResult> completedTask = await Task.WhenAny(receiveTasks);
// 获取完成的任务所对应的接收结果
UdpReceiveResult receiveResult = completedTask.Result;
//IPAddress localIPAddress = ((IPEndPoint)receiveResult.RemoteEndPoint).Address;
if (CommunicateHelper.Check_ReceData(receiveResult.Buffer))
{
byte addressYype = receiveResult.Buffer[2];
byte funType = receiveResult.Buffer[3];
if (addressYype == (byte)AddressCode.WatchDog)
{
// 获取主设备的IP地址和端口号
IPAddress mainDeviceIPAddress = ((IPEndPoint)receiveResult.RemoteEndPoint).Address;
int mainDevicePort = ((IPEndPoint)receiveResult.RemoteEndPoint).Port;
IPAddress localIPAddress = ((IPEndPoint)udpClients[Array.IndexOf(receiveTasks, completedTask)].Client.LocalEndPoint).Address;
byte[] receivedData = receiveResult.Buffer.Skip(8).Take(receiveResult.Buffer.Length - 12).ToArray();
byte[] sendData = null;
switch (funType)
{
case (byte)FunctionCode.Broadcast:
sendData = CommunicateHelper.Gen_SendData_Cmd(AddressCode.Debuger, FunctionCode.Broadcast, Encoding.Default.GetBytes(localIPAddress.ToString()));
// 使用UDP单播向主设备发送回复消息
using (UdpClient replyClient = new UdpClient())
{
replyClient.Send(sendData, sendData.Length, mainDeviceIPAddress.ToString(), mainDevicePort);
}
break;
case (byte)FunctionCode.getIPAddress:
var networkInterface = IPProvider.GetNetworkInterface(localIPAddress);
string ret1 = IPProvider.GetIpv4Adress(networkInterface);
string ret2 = IPProvider.GetSubnetMask(networkInterface);
string ret3 = IPProvider.GetDefaultGateway(networkInterface);
string replayString = String.Format("{0},{1},{2}", ret1, ret2, ret3);
sendData = CommunicateHelper.Gen_SendData_Cmd(AddressCode.Debuger, FunctionCode.getIPAddress, Encoding.Default.GetBytes(replayString));
using (UdpClient replyClient = new UdpClient())// 使用UDP单播向主设备发送回复消息
{
replyClient.Send(sendData, sendData.Length, mainDeviceIPAddress.ToString(), mainDevicePort);
}
break;
case (byte)FunctionCode.setIPAddress:
sendData = CommunicateHelper.Gen_SendData_Cmd(AddressCode.Debuger, FunctionCode.setIPAddress, receivedData);
using (UdpClient replyClient = new UdpClient())// 使用UDP单播向主设备发送回复消息
{
replyClient.Send(sendData, sendData.Length, mainDeviceIPAddress.ToString(), mainDevicePort);
}
var ipsArr = Encoding.Default.GetString(receivedData).Split(',');
IPProvider.ModifyIPAddress(localIPAddress.ToString(), ipsArr[0], ipsArr[1], ipsArr[2]);
Thread.Sleep(6000);
break;
default:
sendData = CommunicateHelper.Gen_SendData_Cmd(AddressCode.Debuger, FunctionCode.setIPAddress, Encoding.Default.GetBytes("FunctionCode指令不存在!"));
using (UdpClient replyClient = new UdpClient())// 使用UDP单播向主设备发送回复消息
{
replyClient.Send(sendData, sendData.Length, mainDeviceIPAddress.ToString(), mainDevicePort);
}
LogHelper.Info("未定义的UDP通信格式,功能码不存在或不支持!");
break;
}
}
}
// 关闭 UDP 客户端套接字
foreach (var udpClient in udpClients)
{
udpClient.Close();
}
}
else
{
Thread.Sleep(1000);
}
}
}
catch (Exception ex)
{
LogHelper.Info($"UDPClient Exception: {ex.Message}");
}
}
/// <summary>
/// 开始监听外部广播信息
/// </summary>
public void StartReceiveBroadcast(int bindport)
{
BindPort = bindport;
recvThreadflag = true;
receThread = new Thread(new ThreadStart(RecvThread));
receThread.IsBackground = true;
receThread.Start();
LogHelper.Info("UDP通信服务器线程启动完成!");
}
/// <summary>
/// 停止监听外部广播信息
/// </summary>
public void StopReceiveBroadcast()
{
recvThreadflag = false;
if (receThread != null)
receThread.Abort();
Thread.Sleep(50);
if (receThread != null)
{
while ((receThread.ThreadState != System.Threading.ThreadState.Stopped) && (receThread.ThreadState != System.Threading.ThreadState.Aborted))
{
Thread.Sleep(10);
}
}
receThread = null;
LogHelper.Info("UDP通信服务器线程停止!");
}
}
调试软作为发送端,这样可以控制广播的使用,模式实现看门狗软件与外部调试软件在不知道对方信息的状态下建立可靠的通信,单例模式实现代码如下:
public class UdpClientHelper
{
static UdpClientHelper() { }
private UdpClientHelper() { }
static readonly UdpClientHelper instance = new UdpClientHelper();
public static UdpClientHelper Instance
{
get
{
return instance;
}
}
public void SendBroadcast(string LocalIp,int LocalPort, byte[] send)
{
try
{
using (UdpClient udpClient = new UdpClient())
{
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
udpClient.Client.Bind(new IPEndPoint(IPAddress.Parse(LocalIp), LocalPort));
udpClient.Send(send, send.Length, new IPEndPoint(IPAddress.Broadcast, LocalPort));
}
}
catch (SocketException ex)
{
// 捕获超时异常
if (ex.SocketErrorCode == SocketError.TimedOut)
{
Console.WriteLine("Receive timeout.");
}
else
{
// 其他 SocketException 异常处理
Console.WriteLine($"SocketException: {ex.Message}");
}
}
}
public string ReceiveBroadcast(string LocalIp, int LocalPort)
{
string receiveMsg = null;
try
{
using (UdpClient udpClient = new UdpClient())
{
//udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 600);// 设置接收超时为 0.6 秒
udpClient.Client.Bind(new IPEndPoint(IPAddress.Parse(LocalIp), LocalPort));
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);
byte[] receiveBytes = udpClient.Receive(ref remoteEP);
if ( CommunicateHelper.Check_ReceData(receiveBytes))
{
byte addressYype = receiveBytes[2];
byte funType = receiveBytes[3];
if (addressYype == (byte)AddressCode.Debuger)
{
byte[] receivedData = receiveBytes.Skip(8).Take(receiveBytes.Length - 12).ToArray();
byte[] sendData = null;
switch (funType)
{
case (byte)FunctionCode.getIPAddress:
receiveMsg = Encoding.Default.GetString(receivedData);
break;
case (byte)FunctionCode.setIPAddress:
receiveMsg = Encoding.Default.GetString(receivedData);
break;
default:
receiveMsg = null;
break;
}
}
}
}
}
catch (SocketException ex)
{
// 捕获超时异常
if (ex.SocketErrorCode == SocketError.TimedOut)
{
Console.WriteLine("Receive timeout.");
}
else
{
// 其他 SocketException 异常处理
Console.WriteLine($"SocketException: {ex.Message}");
}
}
return receiveMsg;
}
}
调用方法:
private void btn_getIpAddressInfo_Click(object sender, EventArgs e)
{
if(CurrentIp==null)
{
MessageBox.Show("请先选择网卡!!");return;
}
txt_ContolBox_IPAddress.Text = "";
txt_ContolBox_IPSubnet.Text = "";
txt_ContolBox_DefaultIPGateway.Text = "";
byte[] SendBytes = CommunicateClass.Gen_SendDataWithCRC(AddressCode.WatchDog, FunctionCode.getIPAddress, Encoding.Default.GetBytes("getIPAddress"));
UdpClientHelper.Instance.SendBroadcast(CurrentIp, 12000, SendBytes);
string ips= UdpClientHelper.Instance.ReceiveBroadcast(CurrentIp, 12000);
if (ips!=null)
{
txt_ContolBox_IPAddress.Text = ips.Split(',')?[0];
txt_ContolBox_IPSubnet.Text = ips.Split(',')?[1];
txt_ContolBox_DefaultIPGateway.Text = ips.Split(',')?[2];
}
}
报文格式如下:
//
//报文格式:地址码+功能码+内容+校验码
//
//功能码 定义 发送内容备注
//0x09 Broadcast 无要求
//0x10 getIPAddress 无要求
//0x11 setIPAddress IP地址, 子网掩码, 默认网关
//
//地址码 定义
//0x01 调试软件
//0x02 看门狗
//
public enum FunctionCode : byte
{
Broadcast = 0x09,
getIPAddress = 0x10,
setIPAddress = 0x11,
}
public enum AddressCode : byte
{
Debuger = 0x01,
WatchDog = 0x02,
}
CRC效验静态库方法(nuget安装包,搜索“ApeFree.CodePlus.Algorithm”并安装。):
public static class CommunicateClass
{
/// <summary>
/// 将发送数据整理成设定的报文格式
/// </summary>
/// <param name="func">功能码</param>
/// <param name="byteSource">发送内容</param>
/// <returns>返回的完整报文数据</returns>
public static byte[] Gen_SendDataWithCRC(AddressCode addr, FunctionCode func, byte[] byteSource)
{
List<byte> tmp = new List<byte>();
byte[] addrByte = new byte[] { (byte)addr };
byte[] functionByte = new byte[] { (byte)func };
tmp.AddRange(addrByte);
tmp.AddRange(functionByte);
tmp.AddRange(byteSource);
byte[] sendData = tmp.ToArray();
var crc = new Crc(CrcModel.CRC16_IBM);
var calculatedCRC = crc.Calculate(sendData);
byte[] dataToSend = sendData.Concat(calculatedCRC).ToArray(); // 将CRC校验码拼接到发送数据后面
return dataToSend;
}
/// <summary>
/// 接受报文并解析
/// </summary>
/// <param name="sourceData">收到的信息</param>
/// <returns></returns>
public static bool Check_ReceDataWithCRC(byte[] sourceData)
{
// 接收数据并效验
byte[] receivedCRC = sourceData.Skip(sourceData.Length - 2).Take(2).ToArray(); // 从接收数据中取出CRC校验码
byte[] receivedDataWithoutCRC = sourceData.Take(sourceData.Length - 2).ToArray();
// // 从接收数据中删除CRC校验码
var crc = new Crc(CrcModel.CRC16_IBM);
var calculatedCRC = crc.Calculate(receivedDataWithoutCRC);
if (receivedCRC.SequenceEqual(calculatedCRC)) // 判断接收到的CRC校验码是否与计算得到的CRC校验码相等
{
return true;
}
else
{
return false;
}
}
}