Socket开发框架之数据采集客户端
虽然目前.NET的主流的开发基本上是基于Web方式(传统的Web方式和Silvelight方式)、基于Winform方式(传统的Winform模式和WPF方式等)、基于服务应用方式(传统的WebService和WCF服务方式)等主要几种开发,另外,还有一种就是基于Socket协议的开发方式,不同于高级服务层的WebService和WCF服务,基于Socket协议开发是较为底层的开发方式,它往往具有更加灵活,可控性更高的优点,不过相对来说,开发难度也会大一些。
我由于工作需要,需要开发一个数据采集客户端,监控指定目录的文件变化,登陆服务器端并传送数据内容,本人将其重构优化,逐步提炼把它升级为一种Socket框架的方式来对待,其中涉及一些有意思的技术应用,在此分享讨论下。首先我们看来看数据采集客户端的主界面。
一般的Socket客户端需要涉及到服务启动、服务停止、状态自动检查、用户登录、用户注销、日志记录、协议数据组装拆包、协议数据收发、多线程数据处理、数据展示通知、参数配置等等功能。本文命名为Socket开发框架之数据采集客户端,就是想站在一个较为通用、完好封装的角度上介绍这些功能点的实现。
1、 服务启动及服务停止
根据不同的业务需要,我创建几个业务管理类,分别是CommonManager、DispatchManager、FileWatcherManger、LoginManager、ReqAnsManager。
其中CommonManager是其他业务管理类的总管,并且包含和Socket服务器连接的控制管理,DispatchManager则是负责解析收到的数据并对不同数据进行分发处理,FileWatcherManger是实现对指定目录的文件进行监控并对其数据进行管理,LoginManager主要是实现登录控制管理、ReqAnsManager是对发送数据包需要后续验证处理的管理业务类。他们的关系如图所示:
其中重要的总管CommonManager类负责管理各类的协调,其中启动、停止及退出实现如下所示:
bool_isStart=false;
publicDateTimeStartTime;
publicDateTimeStopTime;
publicboolIsStart
{
get{return_isStart;}
set{_isStart=value;}
}
publicvoidStart()
{
if(!IsStart)
{
StartTime=DateTime.Now;
DispatchManager.Instance.Start();
LoginManager.Instance.Login();//使用默认账号密码登录
this.CheckTimer();
TimeSpants=(TimeSpan)(DateTime.Now-this.StartTime);
Log.WriteInfo(string.Format("[{0}]结束启动服务.|cost:{1}ms",this._Name,ts.TotalMilliseconds));
IsStart=true;
}
}
publicvoidStop()
{
if(pcClient.Connected)
{
pcClient.DisConnect();
}
this.StopBaseDataRefresh();
FileWatcherManger.Instance.StopFileWatcher();
this.StopCheckTimer();
StopTime=DateTime.Now;
Portal.gc.MainDialog.SetNotifyStatus(NotifyIconHelper.Status.Offline);
Log.WriteInfo(string.Format("操作者主动停止服务"));
IsStart=false;
}
publicvoidExit()
{
if(IsProcess)
{
Application.ExitThread();
System.Diagnostics.Process.GetCurrentProcess().Kill();
}
else
{
Stop();
}
}
#endregion
#region终端通信
PCDataClientpcClient=newPCDataClient();
publicboolConnected
{
get{returnpcClient.Connected;}
}
///<summary>
///连接服务器
///</summary>
publicvoidConnect(stringip,intport)
{
pcClient.Connect(ip,port);
}
///<summary>
///不指定服务器ip和port时,使用默认值
///</summary>
publicvoidConnect()
{
this.Connect(PCDataCollector_Config.Default.ServerIP,PCDataCollector_Config.Default.ServerPort);
}
publicboolSend(stringsend)
{
if(!this.Connected)
{
Connect();//确保连接上
}
returnpcClient.SendTo(send);
}
2、 状态自动检查
另外,CommonManager类需要启用一个定时器,来定时检测Socket客户端的连接情况,数据接收线程的正常情况,数据处理分派业务类的正常情况,把它放到一个Timer处理定时检测的实现。
///服务检查
///</summary>
publicvoidCheckTimer()
{
if(_CheckTimer==null)
{
_CheckTimer=newSystem.Threading.Timer(newTimerCallback(Check));
_CheckTimer.Change(PCDataCollector_Config.Default.CheckTimerSpan,PCDataCollector_Config.Default.CheckTimerSpan);
}
Log.WriteInfo(string.Format("[{0}]服务检查线程启动.....",this._Name));
}
///<summary>
///停止服务器检查
///</summary>
publicvoidStopCheckTimer()
{
if(_CheckTimer!=null)
{
_CheckTimer.Change(Timeout.Infinite,Timeout.Infinite);
}
}
///<summary>
///检查
///</summary>
///<paramname="stateInfo"></param>
privatevoidCheck(ObjectstateInfo)
{
if(_CheckTimer!=null)
{
_CheckTimer.Change(Timeout.Infinite,Timeout.Infinite);
}
Check();
if(_CheckTimer!=null)
{
_CheckTimer.Change(PCDataCollector_Config.Default.CheckTimerSpan,PCDataCollector_Config.Default.CheckTimerSpan);
}
}
publicvoidCheck()
{
pcClient.CheckConnect();
ReceiverForServer.Instance.Check();
DispatchManager.Instance.Check();
PostServerInfo();
}
这样终端就能定时的检测和通讯服务器之间的连接以及数据处理线程的正常运行。
3、 用户登录及用户注销
用户登录,首先通过发送登录协议指令并在内存中记录发送的协议包,等待服务器的响应,当收到服务器的登录响应后 ,执行相关的操作,如启动文件目录监控,为发送数据做好准备。
{
Login(PCDataCollector_Config.Default.UserName,PCDataCollector_Config.Default.Password);
}
///<summary>
///使用账号密码登录
///</summary>
publicvoidLogin(stringuserNo,stringpassword)
{
if(string.IsNullOrEmpty(userNo)&&string.IsNullOrEmpty(password))
{
thrownewException("用户名或密码不能为空!");
}
this.userNo=userNo;
this.password=password;
SendLogin();
}
///<summary>
///重新登录
///</summary>
publicvoidReLogin()
{
SendLogin();
}
privatevoidSendLogin()
{
stringseqNo=DateTime.Now.ToString("yyyyMMdd")+newRandom().Next(99999).ToString().PadLeft(5,'0');//发送请求
AuthenticationRequestrequestData=newAuthenticationRequest(seqNo,userNo,password);
CommonManager.Instance.Send(requestData.ToString());
//记录请求
ReqAnsManager.Instance.Add(newRequestRecord(DataTypeKey.AuthenticationRequest,seqNo,DateTime.Now.AddSeconds(10)));
Log.WriteInfo(string.Format("正在登录。。。。{0}",tryCount));
Interlocked.Increment(reftryCount);//计数
}
///<summary>
///服务器响应处理
///</summary>
publicvoidHandleLoginResult(AuthenticationAnswerDatadata)
{
try
{
RequestRecordrecord=ReqAnsManager.Instance.Find(data.SeqNo,DataTypeKey.AuthenticationRequest);
if(record!=null)
{
ReqAnsManager.Instance.Remove(record);
if(data.ValidateResult==0)
{
tryCount=0;//重置失败次数为0
lastLoginTime=DateTime.Now;
isLogined=true;
Portal.gc.MainDialog.SetNotifyStatus(NotifyIconHelper.Status.Online);
Log.WriteInfo("登录成功!");
//ThreadPool.QueueUserWorkItem(DataAccess.Instance.LoadBaseData);
//CommonManager.Instance.StarBaseDataRefresh();
//登录成功后,对指定文件夹进行监控,自动发送数据
FileWatcherManger.Instance.StartFileWatcher();
}
else
{
lastLoginTime=DateTime.Now;
isLogined=false;
Portal.gc.MainDialog.SetNotifyStatus(NotifyIconHelper.Status.Offline);
Log.WriteError("登录失败:"+data.Message);
if(tryCount<PCDataCollector_Config.Default.TryLoginCount)
{
Thread.Sleep(100);
LoginManager.Instance.ReLogin();
}
else
{
Log.WriteInfo(string.Format("尝试登录失败超过【{0}】次,等待【{1}】秒后再进行连接!",
PCDataCollector_Config.Default.TryLoginCount,PCDataCollector_Config.Default.ReConnectSecconds));
tryCount=0;//重置失败次数为0
Thread.Sleep(PCDataCollector_Config.Default.ReConnectSecconds*1000);
LoginManager.Instance.ReLogin();
}
}
}
}
catch(Exceptionex)
{
Log.WriteError("初始化异常:"+ex.Message);
CommonManager.Instance.Exit();
}
}
用户的注销及断开,是通过客户端连接类来处理,该类命名为PCDataClient,其继承自Socket客户端处理类BaseSocketClient类,基类封装了对一般Socket类的连接、接收、发送等操作。该类只需要把解析好的数据传送给ReceiverForServer类进行处理,而该类会把数据分派给处理类DispatchManager类进行进一步处理。
{
protectedoverridevoidOnRead(PreDatadata)
{
ReceiverForServer.Instance.AppendPreData(data);
ReceiverForServer.Instance.Check();
}
///<summary>
///断开连接
///</summary>
publicoverridevoidDisConnect()
{
base.DisConnect();
LoginManager.Instance.isLogined=false;
LoginManager.Instance.tryCount=0;//重置失败次数为0
//客户端和服务器连接中断
Log.WriteError(string.Format("客户端和服务器连接中断"));
}
///<summary>
///由于基类只是在调用CheckConnect()函数时确保连接,并没有重新登录,
///因此需要重载基类,如果断网了,连接后重新执行登录,否则收发数据不成功
///</summary>
publicoverridevoidCheckConnect()
{
if(!this.Connected)
{
Connect(IP,Port);
LoginManager.Instance.Login();
}
Log.WriteInfo(string.Format("[{0}]服务连接|IP:{1}|Port:{2}|receive:{3}|send:{4}",
_Name,IP,Port,ReceivePackCount,SendPackCount));
}
}
对接收到的数据进行统一处理,只需要继承基类BaseReceiver即可,相关基类的实现可以参考随笔《Socket开发探秘--基类及公共类的定义》。
{
#region单件
///<summary>
///Receiver实例
///</summary>
privatestaticReceiverForServer_manager;
///<summary>
///锁定实例
///</summary>
privatestaticobjectoClassLock=typeof(ReceiverForServer);
///<summary>
///得到该实例
///</summary>
///<returns></returns>
publicstaticReceiverForServerInstance
{
get
{
lock(oClassLock)//加锁只生成一个实例
{
if(_manager==null)
{
_manager=newReceiverForServer();
}
}
return_manager;
}
}
///<summary>
///私有的构造函数,防止从外部实例化
///</summary>
privateReceiverForServer()
{
}
#endregion
publicoverridevoidPreDataHandle(PreDatadata)
{
DispatchManager.Instance.Dispatch(data);
}
4、 协议数据组装拆包
数据的组包和拆包,在较早的随笔《Socket开发探秘--数据封包和拆包》有详细的介绍,数据的组装和拆包主要通过反射原理,把字符串数据转换为对应的实体类,或者把实体类组装成字符串。
5、 数据分派处理
{
#region单例
privatestaticDispatchManager_manager=newDispatchManager();
publicstaticDispatchManagerInstance
{
get
{
return_manager;
}
}
privateDispatchManager(){}
#endregion
#region委托、事件
//认证应答事件
publicdelegatevoidAuthenticationAnswerDelegate(AuthenticationAnswerDatadata);
publiceventAuthenticationAnswerDelegateSignalAuthenticationAnswer;
//空车位上传数据应答事件
publicdelegatevoidPCParkingDataAnswerDelegate(PCParkingInfoAnswerdata);
publiceventPCParkingDataAnswerDelegateparkingDataAnswer;
//明细信息上传的应答事件
publicdelegatevoidPCTicketDataAnswerDelegate(PCTicketDataAnswerdata);
publiceventPCTicketDataAnswerDelegateticketDataAnswer;
#endregion
///<summary>
///启动,关联所有socket接收过来分发的事件
///</summary>
publicvoidStart()
{
//注册登录结果处理方法
SignalAuthenticationAnswer+=newDispatchManager.AuthenticationAnswerDelegate(LoginManager.Instance.HandleLoginResult);
parkingDataAnswer+=newPCParkingDataAnswerDelegate(FileWatcherManger.Instance.HandleAnswerResult);
ticketDataAnswer+=newPCTicketDataAnswerDelegate(FileWatcherManger.Instance.HandleAnswerResult);
}
#region方法
///<summary>
///分发应答数据
///对于部分类型的应答,需要进行内部广播
///</summary>
///<paramname="predata"></param>
publicvoidDispatch(PreDatapredata)
{
if(predata==null)
return;
switch(predata.Key)
{
//身份认证应答
caseDataTypeKey.AuthenticationAnswer:
AuthenticationAnswerDataauthData=newAuthenticationAnswerData(predata.Content);
SignalAuthenticationAnswer(authData);
break;
caseDataTypeKey.PCParkingInfoAnswer:
PCParkingInfoAnswerspaceData=newPCParkingInfoAnswer(predata.Content);
parkingDataAnswer(spaceData);
break;
caseDataTypeKey.PCTicketDataAnswer:
PCTicketDataAnswerpickBaseData=newPCTicketDataAnswer(predata.Content);
ticketDataAnswer(pickBaseData);
break;
default:
break;
}
}
///<summary>
///系统检查
///</summary>
publicvoidCheck()
{
}
#endregion
}