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类负责管理各类的协调,其中启动、停止及退出实现如下所示:

         #region  启动和退出

        
bool  _isStart  =   false ;
        
public  DateTime StartTime;
        
public  DateTime StopTime;

        
public   bool  IsStart
        {
            
get  {  return  _isStart; }
            
set  { _isStart  =  value; }
        }

        
public   void  Start()
        {
            
if  ( ! IsStart)
            {
                StartTime 
=  DateTime.Now;
                DispatchManager.Instance.Start();
                LoginManager.Instance.Login();
// 使用默认账号密码登录

                
this .CheckTimer();

                TimeSpan ts 
=  (TimeSpan)(DateTime.Now  -   this .StartTime);
                Log.WriteInfo(
string .Format( " [{0}] 结束启动服务.  | cost: {1} ms " this ._Name, ts.TotalMilliseconds));

                IsStart 
=   true ;
            }
        }

        
public   void  Stop()
        {
            
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 ;
        }

        
public   void  Exit()
        {
            
if  (IsProcess)
            {
                Application.ExitThread();
                System.Diagnostics.Process.GetCurrentProcess().Kill();
            }
            
else
            {
                Stop();
            }
        }

        
#endregion

        
#region  终端通信

        PCDataClient pcClient 
=   new  PCDataClient();
        
public   bool  Connected
        {
            
get  {  return  pcClient.Connected; }
        }

        
///   <summary>
        
///  连接服务器
        
///   </summary>
         public   void  Connect( string  ip,  int  port)
        {
            pcClient.Connect(ip, port);
        }

        
///   <summary>
        
///  不指定服务器ip和port时,使用默认值
        
///   </summary>
         public   void  Connect()
        {
            
this .Connect(PCDataCollector_Config.Default.ServerIP, PCDataCollector_Config.Default.ServerPort);
        }

        
public   bool  Send( string  send)
        {
            
if  ( ! this .Connected)
            {
                Connect();
// 确保连接上
            }

            
return  pcClient.SendTo(send);
        }
       #endregion

2、 状态自动检查

 另外,CommonManager类需要启用一个定时器,来定时检测Socket客户端的连接情况,数据接收线程的正常情况,数据处理分派业务类的正常情况,把它放到一个Timer处理定时检测的实现。

         ///  <summary>
        
///  服务检查
        
///  </summary>
         public  void CheckTimer()
        {
             if (_CheckTimer ==  null)
            {
                _CheckTimer =  new System.Threading.Timer( new TimerCallback(Check));
                _CheckTimer.Change(PCDataCollector_Config.Default.CheckTimerSpan, PCDataCollector_Config.Default.CheckTimerSpan);
            }
            Log.WriteInfo( string.Format( " [{0}] 服务检查线程启动.....  "this._Name));
        }

         ///  <summary>
        
///  停止服务器检查
        
///  </summary>
         public  void StopCheckTimer()
        {
             if (_CheckTimer !=  null)
            {
                _CheckTimer.Change(Timeout.Infinite, Timeout.Infinite);
            }
        }

         ///  <summary>
        
///  检查
        
///  </summary>
        
///  <param name="stateInfo"></param>
         private  void Check(Object stateInfo)
        {
             if (_CheckTimer !=  null)
            {
                _CheckTimer.Change(Timeout.Infinite, Timeout.Infinite);
            }

            Check();

             if (_CheckTimer !=  null)
            {
                _CheckTimer.Change(PCDataCollector_Config.Default.CheckTimerSpan, PCDataCollector_Config.Default.CheckTimerSpan);
            }
        }

         public  void Check()
        {
            pcClient.CheckConnect();
            ReceiverForServer.Instance.Check();
            DispatchManager.Instance.Check();
            PostServerInfo();

        }  

这样终端就能定时的检测和通讯服务器之间的连接以及数据处理线程的正常运行。  

3、 用户登录及用户注销

用户登录,首先通过发送登录协议指令并在内存中记录发送的协议包,等待服务器的响应,当收到服务器的登录响应后 ,执行相关的操作,如启动文件目录监控,为发送数据做好准备。

         public   void  Login()
        {
            Login(PCDataCollector_Config.Default.UserName, PCDataCollector_Config.Default.Password);
        }

        
///   <summary>
        
///  使用账号密码登录
        
///   </summary>
         public   void  Login( string  userNo,  string  password)
        {
            
if  ( string .IsNullOrEmpty(userNo)  &&   string .IsNullOrEmpty(password))
            {
                
throw   new  Exception( " 用户名或密码不能为空! " );
            }
            
this .userNo  =  userNo;
            
this .password  =  password;            

            SendLogin();
        }
        
        
///   <summary>
        
///  重新登录
        
///   </summary>
         public   void  ReLogin()
        {
            SendLogin();
        }

        
private   void  SendLogin()
        {
            
string  seqNo  =  DateTime.Now.ToString( " yyyyMMdd " +   new  Random().Next( 99999 ).ToString().PadLeft( 5 ' 0 ' ); //  发送请求
            AuthenticationRequest requestData  =   new  AuthenticationRequest(seqNo, userNo, password);
            CommonManager.Instance.Send(requestData.ToString());

            
//  记录请求
            ReqAnsManager.Instance.Add( new  RequestRecord(DataTypeKey.AuthenticationRequest, seqNo, DateTime.Now.AddSeconds( 10 )));

            Log.WriteInfo(
string .Format( " 正在登录。。。。{0} " , tryCount));
            Interlocked.Increment(
ref  tryCount); // 计数
        }

        
///   <summary>
        
///  服务器响应处理
        
///   </summary>
         public   void  HandleLoginResult(AuthenticationAnswerData data)
        {
            
try
            {
                RequestRecord record 
=  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  (Exception ex)
            {
                Log.WriteError(
" 初始化异常: "   +  ex.Message);
                CommonManager.Instance.Exit();
            }
        } 

用户的注销及断开,是通过客户端连接类来处理,该类命名为PCDataClient,其继承自Socket客户端处理类BaseSocketClient类,基类封装了对一般Socket类的连接、接收、发送等操作。该类只需要把解析好的数据传送给ReceiverForServer类进行处理,而该类会把数据分派给处理类DispatchManager类进行进一步处理。

     public   class  PCDataClient : BaseSocketClient
    {
        
protected   override   void  OnRead(PreData data)
        {
            ReceiverForServer.Instance.AppendPreData(data);
            ReceiverForServer.Instance.Check();
        }

        
///   <summary>
        
///  断开连接
        
///   </summary>
         public   override   void  DisConnect()
        {
            
base .DisConnect();

            LoginManager.Instance.isLogined 
=   false ;
            LoginManager.Instance.tryCount 
=   0 ; // 重置失败次数为0

            
// 客户端和服务器连接中断
            Log.WriteError( string .Format( " 客户端和服务器连接中断 " ));
        }

        
///   <summary>
        
///  由于基类只是在调用CheckConnect()函数时确保连接,并没有重新登录,
        
///  因此需要重载基类,如果断网了,连接后重新执行登录,否则收发数据不成功
        
///   </summary>
         public   override   void  CheckConnect()
        {
            
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开发探秘--基类及公共类的定义》。

     public   class  ReceiverForServer : BaseReceiver
    {          
        
#region  单件

        
///   <summary>
        
///  Receiver实例
        
///   </summary>
         private   static  ReceiverForServer _manager;
        
///   <summary>
        
///  锁定实例
        
///   </summary>
         private   static   object  oClassLock  =   typeof (ReceiverForServer);

        
///   <summary>
        
///  得到该实例
        
///   </summary>
        
///   <returns></returns>
         public   static  ReceiverForServer Instance
        {
            
get
            {
                
lock  (oClassLock)  // 加锁只生成一个实例 
                {
                    
if  (_manager  ==   null )
                    {
                        _manager 
=   new  ReceiverForServer();
                    }
                }
                
return  _manager;
            }
        }

        
///   <summary>
        
///  私有的构造函数,防止从外部实例化
        
///   </summary>
         private  ReceiverForServer()
        {
        }

        
#endregion

        
public   override   void  PreDataHandle(PreData data)
        {
            DispatchManager.Instance.Dispatch(data);
        }

4、 协议数据组装拆包

 数据的组包和拆包,在较早的随笔《Socket开发探秘--数据封包和拆包》有详细的介绍,数据的组装和拆包主要通过反射原理,把字符串数据转换为对应的实体类,或者把实体类组装成字符串。

5、 数据分派处理

     public   sealed   class  DispatchManager
    {
        
#region  单例

        
private   static  DispatchManager _manager  =   new  DispatchManager();

        
public   static  DispatchManager Instance
        {
            
get
            {
                
return  _manager;
            }
        }

        
private  DispatchManager() { }

        
#endregion

        
#region  委托、事件

        
//  认证应答事件
         public   delegate   void  AuthenticationAnswerDelegate(AuthenticationAnswerData data);
        
public   event  AuthenticationAnswerDelegate SignalAuthenticationAnswer;

        
// 空车位上传数据应答事件
         public   delegate   void  PCParkingDataAnswerDelegate(PCParkingInfoAnswer data);
        
public   event  PCParkingDataAnswerDelegate parkingDataAnswer;

        
// 明细信息上传的应答事件
         public   delegate   void  PCTicketDataAnswerDelegate(PCTicketDataAnswer data);
        
public   event  PCTicketDataAnswerDelegate ticketDataAnswer;

        
#endregion

        
///   <summary>
        
///  启动,关联所有socket接收过来分发的事件
        
///   </summary>
         public   void  Start()
        {
            
//  注册登录结果处理方法
            SignalAuthenticationAnswer  +=   new  DispatchManager.AuthenticationAnswerDelegate(LoginManager.Instance.HandleLoginResult);
            parkingDataAnswer 
+=   new  PCParkingDataAnswerDelegate(FileWatcherManger.Instance.HandleAnswerResult);
            ticketDataAnswer 
+=   new  PCTicketDataAnswerDelegate(FileWatcherManger.Instance.HandleAnswerResult);
        }

        
#region  方法
        
///   <summary>
        
///  分发应答数据
        
///  对于部分类型的应答,需要进行内部广播
        
///   </summary>
        
///   <param name="predata"></param>
         public   void  Dispatch(PreData predata)
        {
            
if  (predata  ==   null )
                
return ;

            
switch  (predata.Key)
            {
                
//  身份认证应答
                 case  DataTypeKey.AuthenticationAnswer:
                    AuthenticationAnswerData authData 
=   new  AuthenticationAnswerData(predata.Content);
                    SignalAuthenticationAnswer(authData);
                    
break ;

                
case  DataTypeKey.PCParkingInfoAnswer:
                    PCParkingInfoAnswer spaceData 
=   new  PCParkingInfoAnswer(predata.Content);
                    parkingDataAnswer(spaceData);
                    
break ;

                
case  DataTypeKey.PCTicketDataAnswer:
                    PCTicketDataAnswer pickBaseData 
=   new  PCTicketDataAnswer(predata.Content);
                    ticketDataAnswer(pickBaseData);
                    
break ;

                
default :
                    
break ;
            }
        }

        
///   <summary>
        
///  系统检查
        
///   </summary>
         public   void  Check()
        {
        }

        
#endregion
    }

由于涉及内容较多,篇幅太长,这些相关的内容将逐步介绍,期间可能会逐步对代码进行优化和提炼,以求达到通用、良好封装的目的,能促进其他项目的使用及处理。

希望本文对于从事Socket开发处理的读者有一个好的引导和启发,大家一起交流,互通有无,各取所长。 

本文转自博客园伍华聪的博客,原文链接:Socket开发框架之数据采集客户端,如需转载请自行联系原博主。



目录
相关文章
|
5月前
|
缓存 监控 Java
Java Socket编程最佳实践:优化客户端-服务器通信性能
【6月更文挑战第21天】Java Socket编程优化涉及识别性能瓶颈,如网络延迟和CPU计算。使用非阻塞I/O(NIO)和多路复用技术提升并发处理能力,减少线程上下文切换。缓存利用可减少I/O操作,异步I/O(AIO)进一步提高效率。持续监控系统性能是关键。通过实践这些策略,开发者能构建高效稳定的通信系统。
159 1
|
19天前
|
Python
Socket学习笔记(二):python通过socket实现客户端到服务器端的图片传输
使用Python的socket库实现客户端到服务器端的图片传输,包括客户端和服务器端的代码实现,以及传输结果的展示。
90 3
Socket学习笔记(二):python通过socket实现客户端到服务器端的图片传输
|
19天前
|
JSON 数据格式 Python
Socket学习笔记(一):python通过socket实现客户端到服务器端的文件传输
本文介绍了如何使用Python的socket模块实现客户端到服务器端的文件传输,包括客户端发送文件信息和内容,服务器端接收并保存文件的完整过程。
77 1
Socket学习笔记(一):python通过socket实现客户端到服务器端的文件传输
|
5月前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
95 0
|
5月前
|
安全 Java 网络安全
Java Socket编程教程:构建安全可靠的客户端-服务器通信
【6月更文挑战第21天】构建安全的Java Socket通信涉及SSL/TLS加密、异常处理和重连策略。示例中,`SecureServer`使用SSLServerSocketFactory创建加密连接,而`ReliableClient`展示异常捕获与自动重连。理解安全意识,如防数据截获和中间人攻击,是首要步骤。通过良好的编程实践,确保网络应用在复杂环境中稳定且安全。
93 0
|
3月前
|
网络协议 C# 开发者
WPF与Socket编程的完美邂逅:打造流畅网络通信体验——从客户端到服务器端,手把手教你实现基于Socket的实时数据交换
【8月更文挑战第31天】网络通信在现代应用中至关重要,Socket编程作为其实现基础,即便在主要用于桌面应用的Windows Presentation Foundation(WPF)中也发挥着重要作用。本文通过最佳实践,详细介绍如何在WPF应用中利用Socket实现网络通信,包括创建WPF项目、设计用户界面、实现Socket通信逻辑及搭建简单服务器端的全过程。具体步骤涵盖从UI设计到前后端交互的各个环节,并附有详尽示例代码,助力WPF开发者掌握这一关键技术,拓展应用程序的功能与实用性。
98 0
|
5月前
|
Java Android开发
Java Socket编程示例:服务器开启在8080端口监听,接收客户端连接并打印消息。
【6月更文挑战第23天】 Java Socket编程示例:服务器开启在8080端口监听,接收客户端连接并打印消息。客户端连接服务器,发送&quot;Hello, Server!&quot;后关闭。注意Android中需避免主线程进行网络操作。
94 4
|
4月前
|
Java 数据格式
Java面试题:简述Java Socket编程的基本流程,包括客户端和服务器的创建与通信。
Java面试题:简述Java Socket编程的基本流程,包括客户端和服务器的创建与通信。
69 0
|
5月前
|
Java
java使用ServerSocket和Socket实现客户端与服务端通讯
java使用ServerSocket和Socket实现客户端与服务端通讯
|
5月前
|
网络协议 Java Linux
探索Java Socket编程:实现跨平台客户端-服务器通信的奥秘
【6月更文挑战第21天】Java Socket编程示例展示了如何构建跨平台聊天应用。服务器端使用`ServerSocket`监听客户端连接,每个连接启动新线程处理。客户端连接服务器,发送并接收消息。Java的跨平台能力确保代码在不同操作系统上无需修改即可运行,简化开发与维护。
53 0