Socket开发是属于通信底层的开发,.NET也提供了非常丰富的类来实现Socket的开发工作,本篇不是介绍这些基础类的操作,而是从一个大的架构方面阐述Socket的快速开发工作,本篇以TCP模式进行程序的开发介绍,以期达到抛砖引玉的目的。
要掌握或者了解Socket开发,必须了解下面所述的场景及知识。
1、TCP客户端,连接服务器端,进行数据通信
2、TCP服务器端,负责侦听客户端连接
3、连接客户端的管理,如登陆,注销等,使用独立线程处理
4、数据接收管理,负责数据的接受,并处理队列的分发,使用独立线程处理,简单处理后叫给“数据处理线程”
5、数据处理线程,对特定的数据,采用独立的线程进行数据处理
6、数据的封包和解包,按照一定的协议进行数据的封装和解包
针对以上内容,可以封装以下功能的操作类作为共用基类:
1、BaseSocketClient,客户端基类
2、BaseSocketServer,TCP服务器管理基类
3、BaseClientManager,连接客户端管理类
4、BaseReceiver,数据接收处理类
5、ThreadHandler,数据独立线程处理类
6、PreData、DataTypeKey、Sign分别是定义数据的基础格式、协议标识、分隔符号等,另外我们定义需要发送的实体类信息,发送和接收通过实体类进行数据转换和解析。
以上类是基类,不能直接使用,在服务器端和客户端都要继承相应的类来完成所需要的工作。
BaseSocketClient只要负责客户端的链接、断开、发送、接收等操作,大致的定义如下:
{
public BaseSocketClient()
{
_Name = this .GetType().Name;
}
public BaseSocketClient(Socket socket) : this ()
{
_socket = socket;
IPEndPoint ipAndPort = (IPEndPoint)socket.RemoteEndPoint;
_IP = ipAndPort.Address.ToString();
_port = ipAndPort.Port;
}
///
/// 断开连接
///
public virtual void DisConnect()
{
.........
}
///
/// 主动连接
///
public virtual void Connect( string ip, int port)
{
........
}
///
/// 开始异步接收
///
public void BeginReceive()
{
.........
}
///
/// 开始同步接收
///
public void StartReceive()
{
.........
}
///
/// 异步发送
///
public void BeginSend(SendStateObject sendState)
{
........
}
///
/// 同步发送。直接返回成功失败状态
///
public bool SendTo( string data)
{
.........
}
///
/// 主动检查连接
///
public virtual void CheckConnect()
{
.............
}
protected virtual void OnRead(PreData data)
{
}
}
2、BaseSocketServer,TCP服务器管理基类
该类负责在独立的线程中侦听指定的端口,如果有客户端连接进来,则进行相应的处理,重载处理函数可以实现独立的处理。大致的定义如下。
{
public BaseSocketServer()
{
this ._SocketName = this .GetType().Name;
}
///
/// 启动监听线程
///
public void StartListen( string ip, int port)
{
_IP = ip;
_port = port;
if (_listenThread == null )
{
_listenThread = new Thread(Listen);
_listenThread.IsBackground = true ;
_listenThread.Start();
}
}
///
/// 检查监听线程
///
public void CheckListen()
{
if (_listenThread == null || ( ! _listenThread.IsAlive))
{
_listenThread = new Thread(Listen);
_listenThread.IsBackground = true ;
_listenThread.Start();
}
}
///
/// 监听线程
///
protected virtual void Listen()
{
IPEndPoint ipAndPort = new IPEndPoint(IPAddress.Parse(IP), Port);
TcpListener tcpListener = new TcpListener(ipAndPort);
tcpListener.Start( 50 ); // 配置
while ( true )
{
Socket socket = tcpListener.AcceptSocket();
AcceptClient(socket);
}
}
///
/// 接收一个Client
///
protected virtual void AcceptClient(Socket socket)
{
}
3、BaseClientManager,连接客户端管理类
由于考虑性能的影响,客户端对象的管理交给一个独立的线程进行处理,一则处理思路清晰,二则充分利用线程的性能。该类主要负责客户端登录超时处理,连接上来的客户端维护,经过登陆验证的客户端维护,客户端登陆验证接口,客户端发送数据处理等功能。
{
#region 登陆管理
protected string _Name = " BaseClientManager " ;
private int _SessionId = 0 ;
private object _LockSession = new object ();
private System.Threading.Timer _CheckInvalidClientTimer = null ; // 检查客户端连接timer
private System.Threading.Timer _SendTimer = null ; // 发送数据调用timer
///
/// 已经注册的客户端 关键字userid
///
protected SortedList < string , T > _loginClientList = new SortedList < string , T > ();
///
/// 连上来的客户端 未注册 关键字Session
///
protected SortedList < string , T > _tempClientList = new SortedList < string , T > ();
///
/// 构造函数
///
public BaseClientManager()
{
this ._Name = this .GetType().Name;
}
///
/// 已经注册的客户端 关键字userid
///
public SortedList < string , T > LoginClientList
{
get { return _loginClientList; }
set { _loginClientList = value; }
}
///
/// 增加一个连上来(未注册)的客户端
///
///
public void AddClient(T client)
{
......
}
///
/// 增加一个已登录的客户端
///
public void AddLoginClient(T client)
{
......
}
///
/// 当客户端登陆,加入列表后的操作
///
///
protected virtual void OnAfterClientSignIn(T client)
{
}
///
/// 验证登录
///
public virtual bool CheckClientLogin( string userId, string psw, ref string memo)
{
return false ;
}
///
/// 电召客户端登出
///
///
public void ClientLogout( string userId)
{
if (_loginClientList.ContainsKey(userId))
{
RadioCallClientLogout(_loginClientList[userId]);
}
}
///
/// 电召客户端登出
///
///
private void RadioCallClientLogout(T client)
{
client.DisConnect();
}
///
/// 移除注册的客户端
///
///
private void RemoveLoginClient(T client)
{
......
}
///
/// 移除客户端后的操作
///
///
protected virtual void OnAfterClientLogout(T client)
{
}
///
/// 在连接的列表中移除客户端对象
///
///
public virtual void RemoveClient(T client)
{
RemoveLoginClient(client);
RemoveTempClient(client);
}
#endregion
///
/// 开始客户端连接处理
///
public void Start()
{
StartSendTimer();
StartCheckInvalidClientTimer();
}
///
/// 启动客户端发送数据线程
///
public void StartSendTimer()
{
......
}
///
/// 启动检查客户端连接timer
///
public void StartCheckInvalidClientTimer()
{
......
}
///
/// 检查客户端连接
///
///
private void CheckInvalidClient(Object stateInfo)
{
......
}
public virtual void RemoveInvalidClient()
{
......
}
///
/// 增加一条客户端发送数据
///
public void AddSend( string userid, string send, bool isFirst)
{
......
}
}
4、BaseReceiver,数据接收处理类
该基类是所有接受数据的处理类,负责维护数据的队列关系,并进一步进行处理。
{
protected string _Name = " BaseReceiver " ;
protected Thread _PreDataHandlehread = null ; // 处理数据线程
protected Fifo < PreData > _preDataFifo = new Fifo < PreData > ( 50000 );
public BaseReceiver()
{
_Name = this .GetType().Name;
}
///
/// 接收处理数据
///
public void AppendPreData(PreData data)
{
_preDataFifo.Append(data);
}
///
/// 数据处理
///
protected virtual void PreDataHandle()
{
......
}
///
/// 数据处理
///
///
public virtual void PreDataHandle(PreData data)
{
}
///
/// 开始数据处理线程
///
public virtual void Start()
{
if (_PreDataHandlehread == null )
{
_PreDataHandlehread = new Thread( new ThreadStart(PreDataHandle));
_PreDataHandlehread.IsBackground = true ;
_PreDataHandlehread.Start();
}
}
}
5、ThreadHandler,数据独立线程处理类
对每个不同类型的数据(不同的协议类型),可以用独立的线程进行处理,这里封装了一个基类,用于进行数据独立线程的处理。
{
Thread _Handlehread = null ; // 处理数据线程
private string _ThreadName = "" ;
private Fifo < T > _DataFifo = new Fifo < T > ();
///
/// 接收处理数据
///
public virtual void AppendData(T data)
{
if (data != null )
_DataFifo.Append(data);
}
///
/// 数据处理
///
protected virtual void DataThreadHandle()
{
while ( true )
{
T data = _DataFifo.Pop();
DataHandle(data);
}
}
///
/// 数据处理
///
///
public virtual void DataHandle(T data)
{
}
///
/// 检查数据处理线程
///
public virtual void Check()
{
......
}
///
/// 开始数据处理线程
///
public virtual void StartHandleThread()
{
......
}
}
6、PreData、DataTypeKey、Sign
PreData是定义了一个标准的协议数据格式,包含了协议关键字、协议内容、用户标识的内容,代码如下。
/// 预处理的数据
///
public class PreData
{
private string _key;
private string _content;
private string _userId;
public PreData()
{
}
public PreData( string key, string data)
{
_key = key;
_content = data;
}
///
/// 协议关键字
///
public string Key
{
get { return _key; }
set { _key = value; }
}
///
/// 数据内容
///
public string Content
{
get { return _content; }
set { _content = value; }
}
///
/// 客户端过来为用户帐号,或者指定的名称
///
public string UserId
{
get { return _userId; }
set { _userId = value; }
}
}
其中的DataTypeKey和Sign定义了一系列的协议头关键字和数据分隔符等信息。
{
///
/// 认证请求 AUTHR C->S
///
public const string AuthenticationRequest = " AUTHR " ;
///
/// 认证请求应答AUTHA S->C
///
public const string AuthenticationAnswer = " AUTHA " ;
///
/// 测试数据TESTR C->S
///
public const string TestDataRequest = " TESTR " ;
///
/// 测试数据TESTA S->C
///
public const string TestDataAnswer = " TESTA " ;
.........
}
下面是数据分割符号,定义了数据包的开始符号、结束符号,分隔符号和数据分隔符等。
{
///
/// 开始符
///
public const string Start = " ~ " ;
///
/// 开始符比特
///
public const byte StartByte = 0x7E ;
///
/// 结束符
///
public const string End = " # " ;
///
/// 结束符比特
///
public const byte EndByte = 0x23 ;
///
/// 分隔符
///
public const string Separator = " & " ;
///
/// 分隔符比特
///
public const byte SeparatorByte = 0x26 ;
///
/// 数据分隔符
///
public const string DataSeparator = " | " ;
///
/// 数据分隔符比特
///
public const byte DataSeparatorByte = 0x7C ;
}
另外,前面说了,我们数据是通过实体类作为载体的,我们知道,收到的Socket数据经过粗略的解析后,就是PreData类型的数据,这个是通用的数据格式,我们需要进一步处理才能转化为所能认识的数据对象(实体类对象),同样,我们发送数据的时候,内容部分肯定是按照一定协议规则串联起来的数据,那么我们就需要把实体转化为发送的数据格式。综上所述,我们通过实体类,必须实现数据的发送和读取的转换。
/// 测试数据的实体类信息
///
public class TestDataRequest
{
#region MyRegion
///
/// 请求序列
///
public string seq;
///
/// 用户帐号
///
public string userid;
///
/// 用户密码
///
public string psw;
#endregion
public TestDataRequest( string seq, string userid, string psw)
{
this .seq = seq;
this .userid = userid;
this .psw = psw;
}
public TestDataRequest()
{
}
///
/// 转换Socket接收到的信息为对象信息
///
/// Socket接收到的信息
public TestDataRequest( string data)
{
string [] dataArray = null ;
dataArray = NetStringUtil.UnPack(data);
if (dataArray != null && dataArray.Length > 0 )
{
TestDataRequest newAnswerData = new TestDataRequest();
int i = 0 ;
this .seq = dataArray[i ++ ];
this .userid = dataArray[i ++ ];
this .psw = dataArray[i ++ ];
}
}
///
/// 转换对象为Socket发送格式的字符串
///
///
public override string ToString()
{
string data = "" ;
data = this .seq + " | " + this .userid + " | " + this .psw.ToString();
data = NetStringUtil.PackSend(DataTypeKey.TestDataRequest, data);
return data;
}
在接下来的工作中,就需要继承以上的基类,完成相关的对象和数据的处理了。
本人是实际中,编写了一个测试的例子,大致的基类使用情况如下所示。