通讯编程上位机软件实现(SOCKET)——第二回
这篇废话不多说,直接上代码。
首先说明,通讯过程中的异常均不进行处理(连接异常除外),由超时重发控制。
一、获取SOCKET连接类TimeOutSocket
public class TimeOutSocket
{
private static bool IsConnectionSuccessful = false;//连接是否成功
private static Exception socketexception;
private static ManualResetEvent TimeoutObject = new ManualResetEvent(false);
public static Socket Connect(IPAddress ipAddress, int port, int timeoutMSec)
{
TimeoutObject.Reset();//将事件设为非终止状态,阻止线程
socketexception = null;
Socket temp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//异步连接
temp.BeginConnect(new IPEndPoint(ipAddress, port), new AsyncCallback(CallBackMethod), temp);
if (TimeoutObject.WaitOne(timeoutMSec, false))
{
//在连接超时时间范围内等待信号量为真则返回SOCKET
if (IsConnectionSuccessful)
{
return temp;
}
else
{
throw new TimeoutException("连接远程主机失败!请检查网络连接是否通畅。");
}
}
else
{
//关闭连接,并释放所有相关资源
temp.Close();
throw new TimeoutException("连接超时!请检查网络连接是否通畅。");
}
}
/// <summary>
/// 异步连接成功后回调函数
/// </summary>
/// <param></param>
private static void CallBackMethod(IAsyncResult asyncresult)
{
try
{
IsConnectionSuccessful = false;
Socket client = asyncresult.AsyncState as Socket;
//结束挂起的异步连接请求
client.EndConnect(asyncresult);
IsConnectionSuccessful = true;
}
catch (Exception ex)
{
IsConnectionSuccessful = false;
socketexception = ex;
}
finally
{
//允许其它线程继续
TimeoutObject.Set();
}
}
}
二、通讯变量(常量)
private const int ConnectTimeOut = 6000;//连接超时时间,以ms为单位
private byte[] data = new byte[1024];//接收字节数组
private int recvSize = 1024;//接收数据个数
private static int XH = 1;//发送的命令序号
//下面两个类库ConvertLibrary和DealProtocolData是我封装的类库,在后续中将逐一给出
ConvertLibrary convertData = new ConvertLibrary();//对数据提供转换支持
DealProtocolData dealProtocolData = new DealProtocolData();//按要求处理协议结果
三、委托
委托,实质上就是指向函数的指针,也是一个类。
delegate void DealRecvMsgHandler(string str);//用于处理接收到的数据
上面一句用ILDASM反编译后如下:
可以看出,系统默认生成了其构造函数(.ctor是构造函数),以及成员函数BeginInvoke、EndInvoke以及Invoke。自己定义的委托继承于System.MulticastDelegate(反编译中extends看出)。
四、异步接收数据回调函数ReceivData()
/// <summary>
///异步接收数据回调
/// </summary>
/// <param></param>
private void ReceivData(IAsyncResult iar)
{
Socket remote = (Socket)iar.AsyncState;
int recv = 0;
try
{
recv = remote.EndReceive(iar);//异常:远程主机强迫关闭了一个现有的连接
}
catch
{
}
string tempStr = convertData.ByteToHex(data, recv);//字节数组转化成16进制
if (tempStr.Length > 0)
{
// DealRecvMsg数据处理函数
DealRecvMsgHandler handler = new DealRecvMsgHandler(DealRecvMsg);
handler.Invoke(tempStr);//同步数据处理
}
else
{
//由于用了第三方的串口转网口模块,所以有时候会收到空的情况,这时继续接收
try
{
remote.BeginReceive(data, 0, recvSize, SocketFlags.None, new AsyncCallback(ReceivData), PublicCommunication.client);
}
catch { }
}
}
五、异步发送数据回调函数(SendData)
/// <summary>
///异步发送数据回调
/// </summary>
/// <param></param>
private void SendData(IAsyncResult iar)
{
Socket remote = (Socket)iar.AsyncState;
try
{
int send = remote.EndSend(iar);
currentSendedOrderDate = DateTime.Now;//记录下发送时间,用于重发时使用
if (listenerTimer.Enabled == false)// listenerTimer用于超时重发
listenerTimer.Enabled = true;
currentSendedOrderEnum = GetSendedOrderType(sendMsg);//获取已发送命令类型
currentSendedOrderXH = GetSendedOrderXH(sendMsg);//获取已发送命令序号
//发送后,异步接收
remote.BeginReceive(data, 0, recvSize, SocketFlags.None, new AsyncCallback(ReceivData), PublicCommunication.client);
}
catch
{
}
}
六、接收数据处理函数(只判断命令的合法性,决定下一个命令。具体的数据需要另需委托进行处理。)
private void DealRecvMsg(string orgTempStr)
{
//这里面是一个while循环,循环的条件是命令长度大于某个最小指,每次循环按命令包中命令长度取出命令,并将剩余的接收到的命令用于循环。
//由于通讯追循一定的流程,所以在这里由接收到的命令可以决定下一个命令
//大致给一下这里面的程序
while (orgTempStr.Length >= 12)
{
try
{
string tempStr = orgTempStr.Substring(0, int.Parse(convertData.ConvertString(orgTempStr.Substring(2, 2), 16, 10)) * 2);//取出一条命令
orgTempStr = orgTempStr.Substring(int.Parse(convertData.ConvertString(orgTempStr.Substring(2, 2), 16, 10)) * 2);// orgTempStr保留剩余的命令
//判断数据合法性:根据协议接收到的数据长度至少为12,而且保证接收到的数据无误(校验码), GetStrXOR()函数是获取校验码,后面给出
if (tempStr.Length >= 12
&& dealProtocolData.GetStrXOR(tempStr.Substring(0, tempStr.Length - 2)).Equals(tempStr.Substring(tempStr.Length - 2), StringComparison.OrdinalIgnoreCase))
{
string workState = tempStr.Substring(8, 2);//获取仪器工作状态
string order = tempStr.Substring(4, 2);//获取命令字
//如果不是上次发送的命令则丢弃当前数据
if (!JudgeReceiveOrderWithSendOrder(order, currentSendedOrderEnum))
continue;
//命令序号不同则丢弃
if (!(int.Parse(convertData.ConvertString(tempStr.Substring(6, 2), 16, 10)) == currentSendedOrderXH))
continue;
currentSendedOrderDate = DateTime.MinValue;//收到命令后,设置命令的发送时间为DateTime的最小值
ReSendTimeCount = -1;//超时重发次数
//"00"应该定义成常量,方便使用时如果"00"改为”ff”时不用满程序中修改
if (workState.Equals("00", StringComparison.OrdinalIgnoreCase))//仪器正常工作
{
if (order.Equals(ProtocolContent.SetParamsCommandWord, StringComparison.OrdinalIgnoreCase))//设置参数命令的返回
{
//决定下一个要发送的命令
sendMsg =……;
}
………..
………..
byte[] msg = convertData.HexToByte(sendMsg);
try
{
//异步发送数据
PublicCommunication.client.BeginSend(msg, 0, msg.Length, SocketFlags.None, new AsyncCallback(SendData), PublicCommunication.client);
//处理发送序号,保证发送序号在1-255之间
XH = (XH + 1) % 256;
if (XH == 0)
XH = 1;
}
catch
{
}
}
七、超时重发
/// <summary>
/// 命令的枚举值
/// </summary>
public enum OrderEnum
{
UnKnown = 0
}
private static string sendMsg = String.Empty;//记录每次发送的命令
private static OrderEnum currentSendedOrderEnum = OrderEnum.UnKnown;//记录本次发送的命令的类型
private static int currentSendedOrderXH = -1;//当前发送命令的序号
private static DateTime currentSendedOrderDate = DateTime.MinValue;//最近一次发送命令的时间
private System.Timers.Timer listenerTimer;//用于监控命令发送的定时器
private static int ReSendTimeCount = -1;//重新发送命令的次数
listenerTimer = new System.Timers.Timer(25);
listenerTimer.Elapsed += new System.Timers.ElapsedEventHandler(listenerTimer_Elapsed);
listenerTimer.Enabled = false;
private void listenerTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
if (currentSendedOrderDate != DateTime.MinValue)//当没有收到数据时
{
double timeInterval = (DateTime.Now - currentSendedOrderDate).TotalMilliseconds;
if (timeInterval >= 1000)//1S重发
{
ReSendTimeCount++;
if (ReSendTimeCount >= 3)
{
//超时处理
……..
return;
}
//未超时三次,继续重发
SendMsgWhenTimeOut(sendMsg);
}
}
}
八、通讯结束或者超时后的处理
当无通讯时,客户端关闭,服务器是检测不到的,所以我在做的时候,一次通讯结束或者超时后都会将连接关闭并置为空。
private void WhenCommunicateOver()
{
listenerTimer.Enabled = false;
try
{
PublicCommunication.client.Close();
}
catch { }
finally
{
PublicCommunication.client = null;
}
}
九、尝试三次连接
private void startCommunication()
{
int connectTime = 0;
do
{
try
{
PublicCommunication.client = TimeOutSocket.Connect(IPAddress.Parse(PublicModel.InstrumentIP), PublicModel.InstrumentPort, ConnectTimeOut);
break;
}
catch (Exception ec)
{
connectTime++;
PublicCommunication.client = null;
if (connectTime >= 3)
{
WhenCommunicateOver();
lock (whetherShowErrorMsg)
{
if (!((bool)whetherShowErrorMsg))
{
MessageBox.Show(this, ec.Message, "系统提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
whetherShowErrorMsg = true;
}
}
Application.DoEvents();
return;
}
}
}
while (connectTime < 4);
}