由于公司需要做一个网络数据服务器,接收各个客户机发过来的数据,对于什么协议的由于需求方面还没有太明确,考虑可能TCP、UDP都有可能用到;不管怎么样先把TCP的服务器做出来再说,之前也曾搜集一些相关的资料,像什么完成端口模型、重叠IO等之类的,不过发现好像都不太理想,可能是我技术没到那一块 ;后来参考了网上的一个架构《可扩展多线程异步Socket服务器框架》做了类似的一个服务器,以界面的形式运行后没什么问题,不过给做成服务的形式后死活不能运行,具体原因有待探究;于是就结合这个框架的思路,自己重新写一个比较通用的类库。
具体思路是这样的,分出三大线程,用一个线程用来监听客户端请求,称为数据监听线程,用一个线程组来处理数据,称为数据处理线程,最后用一个线程来管理服务器与客户端的会话,如超时,中断之类的,称为会话处理线程。由于数据处理占用的时间是最多的,所以这一块的处理对整个服务器的性能影响最大,因此根据不同的机器,分出不同的线程数处理数据以达到最好的效果;像单核机器你可以分出两三个线程,双核的分出8-10个都可以,这样就具有比较大的灵活性。下面是部分代码:
1 private Thread threadListen;//监听客户端连接线程
2 private Thread[] threadDataHandler;//数据处理线程
3 private Thread threadSessionHandler;//终端Session维护线程
4
5 private int threadDataHandlerNum = 1;//数据处理线程数
6
7 /// <summary>
8 /// 启动服务器
9 /// </summary>
10 public void Start()
11 {
12 if (!m_serverClosed)
13 {
14 return;
15 }
16 this.m_serverClosed = true;
17 try
18 {
19 if (this.dataOperater == null)
20 {
21 //给出数据接口未初始化的通知
22 if (this.ServerException != null) this.ServerException(null, new EventArgsException(new Exception("服务器启动失败,数据接口未初始化!")));
23 return;
24 }
25 if (this.isNeedStore)
26 {
27 if (!this.dataOperater.Open())
28 {
29 //给出数据库打开失败的通知
30 if(this.DatabaseException !=null) this.DatabaseException(null, new EventArgsException(new Exception("服务器启动失败,数据库打开失败,请检查!")));
31 return;
32 }
33 }
34 if (!this.CreateServerSocket()) return;
35 //启动线程
36 this.threadListen = new Thread(new ThreadStart(this.StartServerListen));//终端监听线程
37 this.threadListen.Start();
38 this.threadDataHandler = new Thread[this.threadDataHandlerNum];
39 for (int i = 0; i < this.threadDataHandlerNum;i++ )
40 {
41 this.threadDataHandler[i] = new Thread(new ThreadStart(this.DataHandler));//数据处理线程
42 this.threadDataHandler[i].Start();
43 }
44 this.threadSessionHandler = new Thread(new ThreadStart(this.SessionHandler));//终端Session维护线程
45 this.threadSessionHandler.Start();</p><p> m_serverClosed = false;
46 if (this.ServerException != null) this.ServerException(null, new EventArgsException(new Exception("服务器启动成功")));
47 }
48 catch (Exception err)
49 {
50 if (this.ServerException != null) this.ServerException(null, new EventArgsException(new Exception("服务器启动失败!详细信息:" + err.Message)));
51 }
52 }
对于数据处理线程,最关键还有一点就是加入数据缓冲区,对于接收到的数据进行简单的处理后放入数据缓冲区,那么数据处理线程都从缓冲区中取数据进行处理。
在整个系统中还具有以下功能:
预留数据接口,可进行数据的存储、分析、显示、终端相关参数设置(包括接收和发送缓冲区、起始结束标识字符串等)。
代码如下:
/// <summary>
/// 数据操作接口,供外部程序使用,实现存储
/// </summary>
public interface IDataOperater
{
/// <summary>
/// 打开数据库
/// </summary>
/// <returns></returns>
bool Open();
/// <summary>
/// 执行数据库存储
/// </summary>
/// <returns></returns>
bool Store(DataHandlerEventArgs e);
/// <summary>
/// 关闭数据库
/// </summary>
void Close();
/// <summary>
/// 分析接收到的数据并显示
/// </summary>
/// <param name="e"></param>
void Display(DataHandlerEventArgs e);
/// <summary>
/// 设置客户端的相关参数
/// </summary>
/// <param name="Ip"></param>
/// <returns></returns>
ClientArgs ClientArgsSet(string ip);
}
预留数据回复接口,服务器接收到数据时,可设置是否向客户回复及回复内容。
public interface IReplyToClient
{
/// <summary>
/// 根据接收到的数据给出向终端回复信息
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
string GetReplyData(string ip, byte[] data);
}
对数据包进行接包处理,基本上能保证接收数据的完整性,实现不丢包。
代码如下:
/// <summary>
/// 将有些中断的包前后合并组成完整的包
/// </summary>
private void ResolveSessionBuffer(byte[] receiveData)
{
if (receiveData == null) return;
try
{
//搜索第一个起始字符和最后一个结束字符,其中间数据认为是正常数据,放入缓冲区中,起始字符前的数据丢掉
//尾数据保留,下次分析时可用
int headIndex = -1;
int tailIndex = -1;
string receiveStr = Encoding.ASCII.GetString(receiveData);
if (lastTailData != null)//如果有尾包数据,则将其合并
{
string tailStr = Encoding.ASCII.GetString(this.lastTailData);
string str = Encoding.ASCII.GetString(receiveData);
receiveStr = tailStr + str;//合并数据
}
//查找数据包的头尾
if (receiveStr.Contains(this.clientArgs.M_PackageBeginStr))
{
headIndex = receiveStr.IndexOf(this.clientArgs.M_PackageBeginStr);//找到头的位置
}
if (receiveStr.Contains(this.clientArgs.M_PackageEndStr))
{
tailIndex = receiveStr.LastIndexOf(this.clientArgs.M_PackageEndStr);//找到尾的位置
}
//查找完整的数据包,放入缓冲区
if (headIndex >-1 && tailIndex > -1 && headIndex < tailIndex)
{
//取出头尾之间的数据
string str = receiveStr.Substring(headIndex, tailIndex - headIndex + this.clientArgs.M_PackageEndStr.Length);
//把尾标识用头标识替换
str = str.Replace(this.clientArgs.M_PackageEndStr, this.clientArgs.M_PackageBeginStr);
//两个头标识用一个头标识替换,全部用头标识好分割
str = str.Replace(this.clientArgs.M_PackageBeginStr + this.clientArgs.M_PackageBeginStr, this.clientArgs.M_PackageBeginStr);
//分割数据出完整的数据包
string[] token = str.Split(this.clientArgs.M_PackageBeginStr.ToCharArray());
foreach (string s in token)
{
if (s.Trim() != string.Empty)
{
byte[] tempData = Encoding.ASCII.GetBytes(s.Trim());
lock (this.receiveBuffer)
{
this.receiveBuffer.Enqueue(new BufferData(DateTime.Now, tempData));//将完整的数据包放入缓冲区
}
}
}
if (tailIndex < receiveStr.Length - 1)//找到尾包
{
string tailStr = receiveStr.Substring(tailIndex + this.clientArgs.M_PackageEndStr.Length, receiveStr.Length - tailIndex - this.clientArgs.M_PackageEndStr.Length);
this.lastTailData = Encoding.ASCII.GetBytes(tailStr);
}
else//没有尾包数据,说明数据包是完整数据包
{
this.lastTailData = null;
}
}
else//合并后的数据包还不是完整数据包,则当作尾包数据处理
{
this.lastTailData = Encoding.ASCII.GetBytes(receiveStr);
}
}
catch(Exception ex)//程序异常
{
if(this.ProgramException !=null) this.ProgramException(null, new EventArgsProgram("ClientSession", "ResolveSessionBuffer", ex));
}
}
设置服务器异常处理事件、数据异常处理事件、会话异常处理事件、程序异常处理事件。
//事件成员
public event EventHandler<EventArgsException> ServerException;//服务器异常信息
public event EventHandler<EventArgsException> DatabaseException;//数据库异常信息
public event EventHandler<EventArgsException> DataHandlerException;//数据异常信息
public event EventHandler<EventArgsException> SessionException;//终端Session异常信息
public event EventHandler<EventArgsClientLink> AcceptedClient;//已经连接上的客户端
public event EventHandler<EventArgsClientLink> ClosedClient;//已经断开连接的客户端
public event EventHandler<EventArgsProgram> ProgramException;//程序异常信息
可以向指定客户端发送数据,也可向全体客户端发送数据。
代码如下:
/// <summary>
/// 向所有的终端发送数据
/// </summary>
/// <param name="data"></param>
public void SendDataToAllClient(string data)
{
lock (this.m_sessions)
{
foreach (ClientSession session in this.m_sessions)
{
session.SendDatagram(data);
}
}
}
/// <summary>
/// 向指定终端发送数据
/// </summary>
/// <param name="ip">客户端IP</param>
/// <param name="data">数据</param>
public void SendDataToClient(string ip, string data)
{
lock (this.m_sessions)
{
foreach (ClientSession session in this.m_sessions)
{
if (session.IP == ip)
{
session.SendDatagram(data);
return;
}
}
}
}
本人C#这一块也是初学,网络编程也正在努力学习中,呵呵,希望这些思路能给一些和我一样的初学者提供一点点的帮助,那我也就很高兴了,呵呵。
源代码在下面给出,希望有更好想法的朋友也能一起来交流学习,呵呵!
代码下载:http://files.cnblogs.com/rookey/%E5%9F%BA%E4%BA%8ETCP%E5%8D%8F%E8%AE%AE%E7%9A%84socket%E6%9C%8D%E5%8A%A1%E5%99%A8.rar