Socket编程中,如何高效地接收和处理数据,这里介绍一个简单的编程模型。

Socket索引 - SocketId


在给出编程模型之前,先提这样一个问题,程序中如何描述Socket连接?

为什么这么问呢,大家可以翻看我之前在项目总结(一)中给出的一个简单的基本架构,其中的网络层用来管理Socket的连接,并负责接收发送Socket数据,这个模块中可以直接使用建立的Socket连接对象。但如果上层需要给某个Socket发送数据怎么办,如果直接把Socket对象传送给上层,就破坏了面向对象中封装原则,上层甚至可以直接绕过网络层操作Socket数据收发,显然不是我们希望看到的。

既然这样不能直接传递Socket对象,那么就要给上层传递一个能够标识这个对象的一个标识,这就是我要说的这个SocketId。

SocketId实际上就是一个无符号整形数据,在网络层维护一个SocketId与Socket对象的映射表,上层通过SocketId通知网络层向对应的Socket发送数据。

Socket索引 - 如何建立SocketId


SocketId并不是说简单的从1开始,然后来一个Socket连接就直接加1作为对应的SocketId,我希望能够标识更多的东西。

如图所示,我在网络层建立了一个SocketMark数组,长度是经过配置的允许Socket连接的最大个数。其中每个SocketMark包含两个主要的成员,连接的Socket对象,和对应的索引SocketId,如上所示。对于SocketId我不仅希望能够标识出在SocketMark数组中的的位置index(最终找到Socket发出数据),还希望标识出这个SocketMark被使用了多少次(在项目中有特殊用处,在这不做过多说明)。

那么,怎么用index和usetimes表示SocketId呢?具体来说,SocketId是一个无符号整形数据,也就是有4个字节,我使用2个高字节来表示index,两个低字节来表示usetimes。那么,SocketId就是 index * 65536 + usetimes % 65536,相应的index = socketId / 65536, usetimes = socketId % 65536。

SocketMark代码如下所示:

  1. public sealed class SocketMark
  2. {
    //用于线程同步
  3. public readonly object m_SyncLock = new object();
  4.  
  5. public uint SocketId = ;
  6. public Socket WorkSocket = null;
    //用于接收Socket数据
  7. public byte[] Buffer;
    //当前WorkSocket是否连接
  8. public bool Connected = false;
  9.  
  10. public SocketMark(int index, int bufferSize)
  11. {
    //默认情况下usetimes为0
  12. SocketId = Convert.ToUInt32(index * );
  13. Buffer = new byte[bufferSize];
  14. }
  15.  
  16. public void IncReuseTimes()
  17. {
  18. int reuseTimes = GetReuseTimes(SocketId) + ;
  19. SocketId = Convert.ToUInt32(GetIndex(SocketId) * + reuseTimes % );
  20. }
  21.  
  22. public static int GetIndex(uint socketId)
  23. {
  24. return Convert.ToInt32(socketId / );
  25. }
  26.  
  27. public static int GetReuseTimes(uint socketId)
  28. {
  29. return Convert.ToInt32(socketId % );
  30. }
  31. }

当有Socket连接建立时,通过查询SocketMark数组中Connected字段值为false的元素(可以直接遍历查找,也可以采取其他方式,我使用的是建立一个对应SOcketMark的栈,保存index,有新连接index就出栈,然后设置SocketMark[index]的WorkSocket为这个连接;Socket断开后index再入栈),设置相应的WorkSocket后,同时要调用一次IncReuseTimes()函数,使用次数加1,并更新SocketId。

在这里,网络层就可以使用Socket连接对象接收数据存储在Buffer中,并把数据连同SocketId传送给数据协议层。

数据模型


接下来是我要说的重点,在数据协议层这里,我需要定义一个新的结构,用来接收Socket数据,并尽可能地使处理高效。

  1. public sealed class ConnCache
  2. {
  3. public uint SocketId; // 连接标识
  4.  
  5. public byte[] RecvBuffer; // 接收数据缓存,传输层抵达的数据,首先进入此缓存
  6. public int RecvLen; // 接收数据缓存中的有效数据长度
  7. public readonly object RecvLock; // 数据接收锁
  8.  
  9. public byte[] WaitBuffer; // 待处理数据缓存,首先要将数据从RecvBuffer转移到该缓存,数据处理线程才能进行处理
  10. public int WaitLen; // 待处理数据缓存中的有效数据长度
  11. public readonly object AnalyzeLock; // 数据分析锁
  12.  
  13. public ConnCache(int recvBuffSize, int waitBuffSize)
  14. {
  15. SocketId = ;
  16. RecvBuffer = new byte[recvBuffSize];
  17. RecvLen = ;
  18. RecvLock = new object();
  19. WaitBuffer = new byte[waitBuffSize];
  20. WaitLen = ;
  21. AnalyzeLock = new object();
  22. }
  23. }

解释一下:ConnCache用于管理从网络层接收的数据,并维护一个SocketId用来标识数据的归属。

在这其中,包括上面SocketMark都定义了一个公共的只读对象,用来提供多线程时数据同步,但你应该注意到这几个锁的对象全都是public类型的,实际上这样并不好。因为这样,对象就无法控制程序对锁的使用,一旦锁的使用不符合预期,就很有可能造成程序出现死锁,所以建议大家在使用的时候还是考虑使用private修饰符,尽量由对象来完成资源的同步。

但是,很不幸,我在项目中发现这样做有点不现实,使用private可能破坏了整个系统的结构。。而使用public只要能完全掌控代码,对不产生死锁有信心,还是非常方便的,基于这个理由,最终放弃了使用private的想法。

继续回来说这个结构,在这里,我把从网络层接收的数据存储在RecvBuffer中,但我的解析线程并不直接访问这个数组,而是另外建立一个新的数据WaitBuffer,这个WaitBuffer的用处就是从RecvBuffer Copy一定的数据,然后提供给解析线程处理。这样做有两个好处,第一,避免了接收线程和处理线程直接争抢Buffer资源,能够提高处理性能。第二,额。。我看着挺清晰的,一个用来接收,一个用来处理,不是么

注:当初在设计这个模型的时候,还不知道专门有个ReaderWriterLockSlim,我想如果能够代替上面的接收锁和分析锁,效果应该更好一点。

模型使用


介绍了上面两个主要的结构后,我们来看下如何写代码简单使用上述模型

首先,实现客户端,参考项目总结 - 异步中的客户端代码,将代码修改为定时发送数据到服务器,并删除一些无关的代码

  1. class Program
  2. {
  3. static Socket socket;
  4. static void Main(string[] args)
  5. {
  6. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  7. socket.Connect(IPAddress.Parse("127.0.0.1"), );
  8.  
  9. //启动一个线程定时向服务器发送数据
  10. ThreadPool.QueueUserWorkItem(state => {
  11. int index = ;
  12.  
  13. while (true)
  14. {
  15. byte[] senddata = Encoding.Default.GetBytes("我们都有" + index++ + "个家,名字叫中国");
  16. socket.BeginSend(senddata, , senddata.Length, SocketFlags.None, new AsyncCallback(Send), null);
  17.  
  18. Thread.Sleep();
  19. }
  20. });
  21.  
  22. Console.ReadKey();
  23. }
  24.  
  25. static void Send(IAsyncResult ar)
  26. {
  27. socket.EndSend(ar);
  28. }
  29. }

对于服务器,新建一个类NetLayer,用来模拟网络层,网络层建立Socket连接后,启动异步接收,并把接收到的数据通过委托传给处理层,传送时发送SocketId,代码如下:

  1. public delegate void ArrivedData(uint socketId, byte[] buffer);
  2.  
  3. class NetLayer
  4. {
  5. Socket socket;
  6. SocketMark[] socketMarks;
  7.  
  8. public event ArrivedData arrivedData;
  9.  
  10. public NetLayer(int maxConnNum)
  11. {
  12. //初始化SocketMark
  13. //最大允许连接数
  14. socketMarks = new SocketMark[maxConnNum];
  15. for (int i = ; i < socketMarks.Length; i++)
  16. socketMarks[i] = new SocketMark(i, );
  17. }
  18.  
  19. public void Start()
  20. {
  21. //新建Socket,并开始监听连接
  22. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  23. socket.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), ));
  24. socket.Listen();
  25. socket.BeginAccept(new AsyncCallback(Accept), "new socket connect");
  26. }
  27.  
  28. public void Accept(IAsyncResult ar)
  29. {
  30. Console.WriteLine(ar.AsyncState.ToString());
  31.  
  32. //结束监听
  33. Socket _socket = socket.EndAccept(ar);
  34.  
  35. //这里为了方便直接通过循环的方式查找可用的SocketMark
  36. SocketMark socketMark = null;
  37. for (int i = ; i < socketMarks.Length; i++)
  38. {
  39. if (!socketMarks[i].Connected)
  40. {
  41. socketMark = socketMarks[i];
  42. break;
  43. }
  44. }
  45. //如果没有找到可用的SocketMark,说明达到最大连接数,关闭该连接
  46. if (socketMark == null)
  47. {
  48. _socket.Close();
  49. return;
  50. }
  51.  
  52. socketMark.WorkSocket = _socket;
  53. socketMark.Connected = true;
  54. socketMark.IncReuseTimes();
  55.  
  56. _socket.BeginReceive(socketMark.Buffer, , socketMark.Buffer.Length, SocketFlags.None, new AsyncCallback(Receive), socketMark);
  57. }
  58.  
  59. public void Receive(IAsyncResult ar)
  60. {
  61. SocketMark mark = (SocketMark)ar.AsyncState;
  62.  
  63. int length = mark.WorkSocket.EndReceive(ar);
  64. if (length > )
  65. {
  66. //多线程下资源同步
  67. lock (mark.m_SyncLock)
  68. {
  69. byte[] data = new byte[length];
  70. Buffer.BlockCopy(mark.Buffer, , data, , length);
  71.  
  72. if (arrivedData != null)
  73. arrivedData(mark.SocketId, data);
  74.  
  75. //再次投递接收申请
  76. mark.WorkSocket.BeginReceive(mark.Buffer, , mark.Buffer.Length, SocketFlags.None, new AsyncCallback(Receive), mark);
  77. }
  78. }
  79. }
  80. }

数据处理层收到数据后,先把数据存到对应ConnCache中的RecvBuffer中,并向队列Queue<ConnCache>写入一个标记,告诉处理线程应该处理哪个ConnCache的数据,在这里大家会看到,我在之前的文章中讨论的lock和Monitor是如何使用的。

  1. class Program
  2. {
  3. static ConnCache[] connCaches;
  4.  
  5. //处理线程通过这个队列知道有数据需要处理
  6. static Queue<ConnCache> tokenQueue;
  7. //接收到数据后,同时通知处理线程处理数据
  8. static AutoResetEvent tokenEvent;
  9.  
  10. static void Main(string[] args)
  11. {
  12. //最大允许连接数
  13. int maxConnNum = ;
  14.  
  15. //要和底层SocketMark数组的个数相同
  16. connCaches = new ConnCache[maxConnNum];
  17. for (int i = ; i < maxConnNum; i++)
  18. connCaches[i] = new ConnCache(, );
  19.  
  20. tokenQueue = new Queue<ConnCache>();
  21. tokenEvent = new AutoResetEvent(false);
  22.  
  23. NetLayer netLayer = new NetLayer(maxConnNum);
  24. netLayer.arrivedData += new ArrivedData(netLayer_arrivedData);
  25. netLayer.Start();
  26.  
  27. //处理线程
  28. ThreadPool.QueueUserWorkItem(new WaitCallback(AnalyzeThrd), null);
  29.  
  30. Console.ReadKey();
  31. }
  32.  
  33. static void netLayer_arrivedData(uint socketId, byte[] buffer)
  34. {
  35. int index = (int)(socketId / );
  36. int reusetimes = (int)(socketId % );
  37.  
  38. Console.WriteLine("recv data from - index = {0}, reusetimes = {1}", index, reusetimes);
  39.  
  40. int dataLen = buffer.Length;
  41. //仅使用了RecvLock,不影响WaitBuffer中的数据处理
  42. lock (connCaches[index].RecvLock)
  43. {
  44. //说明已经是一个新的Socket连接了,需要清理之前的数据
  45. if (connCaches[index].SocketId != socketId)
  46. {
  47. connCaches[index].SocketId = socketId;
  48. connCaches[index].RecvLen = ;
  49. connCaches[index].WaitLen = ;
  50. }
  51.  
  52. //如果收到的数据超过了可以接收的长度,截断
  53. if (dataLen > connCaches[index].RecvBuffer.Length - connCaches[index].RecvLen)
  54. dataLen = connCaches[index].RecvBuffer.Length - connCaches[index].RecvLen;
  55. if (dataLen > )
  56. {
  57. //接收数据到RecvBuffer中,并更新已接收的长度值
  58. Buffer.BlockCopy(buffer, , connCaches[index].RecvBuffer, connCaches[index].RecvLen, dataLen);
  59. connCaches[index].RecvLen += dataLen;
  60. }
  61. }
  62.  
  63. lock (((ICollection)tokenQueue).SyncRoot)
  64. {
  65. tokenQueue.Enqueue(connCaches[index]);
  66. }
  67. tokenEvent.Set();
  68. }
  69.  
  70. static void AnalyzeThrd(object state)
  71. {
  72. ConnCache connCache;
  73.  
  74. while (true)
  75. {
  76. Monitor.Enter(((ICollection)tokenQueue).SyncRoot);
  77. if (tokenQueue.Count > )
  78. {
  79. connCache = tokenQueue.Dequeue();
  80. Monitor.Exit(((ICollection)tokenQueue).SyncRoot);
  81. }
  82. else
  83. {
  84. Monitor.Exit(((ICollection)tokenQueue).SyncRoot);
  85. //如果没有需要处理的数据,等待15秒后再运行
  86. tokenEvent.WaitOne(, false);
  87. continue;
  88. }
  89.  
  90. //这里就需要使用两个锁,只要保证使用这两个锁的顺序不变,就不会出现死锁问题
  91. lock (connCache.AnalyzeLock)
  92. {
  93. while (connCache.RecvLen > )
  94. {
  95. lock (connCache.RecvBuffer)
  96. {
  97. //这里把接收到的数据COPY到待处理数组
  98. int copyLen = connCache.WaitBuffer.Length - connCache.WaitLen;
  99. if (copyLen > connCache.RecvLen)
  100. copyLen = connCache.RecvLen;
  101. Buffer.BlockCopy(connCache.RecvBuffer, , connCache.WaitBuffer, connCache.WaitLen, copyLen);
  102. connCache.WaitLen += copyLen;
  103. connCache.RecvLen -= copyLen;
  104. //如果RecvBuffer中还有数据没有COPY完,把它们提到数组开始位置
  105. if (connCache.RecvLen > )
  106. Buffer.BlockCopy(connCache.RecvBuffer, copyLen, connCache.RecvBuffer, , connCache.RecvLen);
  107. }
  108. }
  109.  
  110. //这里就是解析数据的地方,在这我直接把收到的数据打印出来(注意:如果客户端数据发送很快,有可能打印出乱码)
  111. //还在AnalyzeLock锁中
  112. {
  113. string data = Encoding.Default.GetString(connCache.WaitBuffer, , connCache.WaitLen);
  114. Console.WriteLine("analyzed: " + data);
  115.  
  116. //WaitLen置0,相当于清理了WaitBuffer中的数据
  117. connCache.WaitLen = ;
  118. }
  119. }
  120.  
  121. }
  122. }
  123. }

至此,整个模型的使用就完成了。代码图省事就直接放上去了,见谅!

结果如下:

大家可以试着修改下代码使发送更快,一次发送数据更多,再来多个客户端试一下效果。

注:本文中的代码是用来进行演示的简化后的代码,并不保证没有缺陷,仅为了阐述这一模型。

一个Socket数据处理模型的更多相关文章

  1. socket select模型

    由于socket recv()方法是堵塞式的,当多个客户端连接服务器时,其中一个socket的recv调用时,会产生堵塞,使其他连接不能继续. 如果想改变这种一直等下去的焦急状态,可以多线程来实现(不 ...

  2. socket select()模型

    转载:http://www.cnblogs.com/xiangshancuizhu/archive/2012/10/05/2711882.html 由于socket recv()方法是阻塞式的,当有多 ...

  3. 很幽默的讲解六种Socket IO模型

    很幽默的讲解六种Socket IO模型 本文简单介绍了当前Windows支持的各种Socket I/O模型,如果你发现其中存在什么错误请务必赐教. 一:select模型二:WSAAsyncSelect ...

  4. Linux 的 Socket IO 模型

    前言 之前有看到用很幽默的方式讲解Windows的socket IO模型,借用这个故事,讲解下linux的socket IO模型: 老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系. 他 ...

  5. Socket编程模型之完毕port模型

    转载请注明来源:viewmode=contents">http://blog.csdn.net/caoshiying?viewmode=contents 一.回想重叠IO模型 用完毕例 ...

  6. 很幽默的讲解六种Socket IO模型 Delphi版本(自己Select查看,WM_SOCKET消息通知,WSAEventSelect自动收取,Overlapped I/O 事件通知模型,Overlapped I/O 完成例程模型,IOCP模型机器人)

    很幽默的讲解六种Socket IO模型(转)本文简单介绍了当前Windows支持的各种Socket I/O模型,如果你发现其中存在什么错误请务必赐教. 一:select模型 二:WSAAsyncSel ...

  7. ZeroMQ接口函数之 :zmq_bind - 绑定一个socket

    ZeroMQ 官方地址 : http://api.zeromq.org/4-0:zmq-bind zmq_bind(3) ZMQ Manual - ZMQ/3.2.5 Name zmq_bind -  ...

  8. ZeroMQ接口函数之 :zmq_connect - 由一个socket创建一个对外连接

    ZeroMQ 官方地址 :http://api.zeromq.org/4-0:zmq_connect zmq_connect(3)  ØMQ Manual - ØMQ/3.2.5 Name zmq_c ...

  9. ZeroMQ接口函数之 :zmq_disconnect - 断开一个socket的连接

    ZeroMQ 官方地址 :http://api.zeromq.org/4-0:zmq_disconnect zmq_disconnect(3) ØMQ Manual - ØMQ/3.2.5 Name ...

随机推荐

  1. AVOIR发票的三种作用

    1. 开错了发票,应收多写了,应该抵消掉一部分应收2. 客户临时有变化,比如只买一部分产品,取消了另一部分,那么也是开AVOIR抵消了一部分应收3. 退钱给客户的时候,也要开一张AVOIR发票 注意, ...

  2. Android开源项目发现---TextView,Button篇(持续更新)

    android-flowtextview 文字自动环绕其他View的Layout 项目地址:https://code.google.com/p/android-flowtextview/ 效果图:ht ...

  3. 经典的单例模式c3p0来控制数据库连接池

    package com.c3p0.datapools; //数据库连接池  单例模式 import java.sql.Connection; import java.sql.SQLException; ...

  4. vijosP1902学姐的清晨问候

    题目:https://vijos.org/p/1902 题解:sb题...扫一遍每个字母出现的次数即可 代码: #include<cstdio> #include<cstdlib&g ...

  5. bootstrap真是个好东西

    之前就知道有bootstrap这么个东东,但是因为本身不做web,也就没有仔细了解.这次一个项目合作方使用django和bootstrap做的,有机会接触了一些,感觉确实非常好! 今天下午利用一个下午 ...

  6. Unity 的 unitypackage 的存放路径

    Windows,C:\Users\<username>\AppData\Roaming\Unity\Asset Store Mac OS X,~/Library/Unity/Asset S ...

  7. HDOJ/HDU 2551 竹青遍野(打表~)

    Problem Description "临流揽镜曳双魂 落红逐青裙 依稀往梦幻如真 泪湿千里云" 在MCA山上,除了住着众多武林豪侠之外,还生活着一个低调的世外高人,他本名逐青裙 ...

  8. Sicily1020-大数求余算法及优化

    Github最终优化代码: https://github.com/laiy/Datastructure-Algorithm/blob/master/sicily/1020.c 题目如下: 1020. ...

  9. hdu 4405概率dp

    #include <cstdio> #include <cstring> #include <iostream> #include <cmath> #i ...

  10. Java GC 专家系列5:Java应用性能优化的原则

    本文是GC专家系列中的第五篇.在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别.所以,你应该已经了解了JDK 7中的5种GC类型,以及每种GC ...