头说做个很简单的转发就行了(不是项目计划的一部分),当时,我也以为,很简单就行,不就是把收到的数据,按照配置文件的信息,找到目标机器,在发送出去吗?于是,我做。
1. BCB下面的过程
A. 在主窗体里面,拖一个TServerSocket,TClientSocket。定义两个结构,一个是用来保存收到的信息(变量里面只定义一个这个结构的变量),一个用来保存找目标机器的配置信息列表。分别为
TADDR{ //地址信息
AnsiString strID;
AnsiString strip;
Int strPort;
}
TMSG{ //发送内容假设是这样
AnsiString strID;
AnsiString strTitle;
AnsiString strMemo;
}
红色部分是用来做标记的,收到的内容有strID这个标记的时候,就发往有strID这个标记的ADDR;
当时思路是这样的:定义一个变量 TMSG Gmsg;每次ServerSocket收到东西的时候都往这个地方写,从AddrList里面找到对应的ADDR然后用ClientSocket发送出去。
是很简单,还真的很简单。后来发现有个缺点(网络基本没做过,有问题,非常正常,况且没有认真研究过BCB,工作需要,况且思想总是不断深入的嘛!今天就来个总结):
当数据读下来,还没等ClientSocket发送出去,新的数据来了,于是,有写MSG的冲突了。这冲突还不是一两次的事情。(当然这里还有其他问题,包括后面的几个方案,但是等后面考虑到的时候再说,如果最后还有什么问题,各位看客[有吗?]再留言提出来,后来也发现这个冲突也是有办法解决的,用TCriticalSection,顾名思义,临界区域,或者TEvent事件触发也可以,但是不彻底解决问题)。
B. 头又说了,A方案不行,就用B方案啊!呵呵,就是做一个链表,或者用队列更适合VC中用的是CList的,BCB中自然就用TList了,呵呵!真是天才,@_@.
每次发送的时候都从链表的最前面取,收到的时候,就从后面添加进去。我还真不是高手,做了出来,还是有冲突,怎么办。这时候,我想到了多线程,犹如一缕阳光,从漆黑的夜空里投射下来,我看到了希望(晕,这么烂的东西也写得出来)。下面是C方案了,呵呵
C. 当时还没有高明到想到服务多线程的程度,于是想到了,收到以后,从地址列表中找到地址信息。然后创建一个发送数据的有ClientSocket的线程,这时候,当然,窗体上的ClientSocket就没有用了,每个线程必须用一个ClientSocket。这下,发送数据的时候,想发送多久就发送多久吧!对其他的数据分发没有影响。冲突明显减少了,可还是有。终于被我发现,TCriticalSection这个类,(刘欢的声音响起:千万里我追寻着你。。。)。冲突的地方主要就是从地址列表中找地址的时候,于是把找地址的单独分出一个函数来。然后
GcritSect->Acquire();
…….操作
CcritSect->Release();
对应的在VC中有
CCriticalSection m_critSect;
M_critSet->Lock();
…操作
m_critSect->Unlock();
当然上面应该是全局函数,要不然对别的线程就不会有影响了。
于是测试,做下面环境配置。
机器1
机器2
机器3
机器N
模拟一个发送数据的触发一下
a->b箭头表示a转发数据到b。这只要在配置文件设置一下就行了。没错,就是要的一个循环。多发几条过去,还行。偷偷,说一下,不起眼的时候,还是会死一次,呵呵。
这里有个发现,把线程中 FreeOnTerminate 设为true。然后
OnTerminate = TaskThreadTerminate;
就可以在函数TaskThreadTerminate函数中清除资源了。
D.最后,突然发现。那个ServerSocket不是Blocking的。Server Type :stNonBlocking 。狂晕。另外还有一个stThreadBlocking的是怎么回事呢?应该就是所谓多线程服务器吧。
经过对原程序的改造,觉得它的流程应该如下:
1. 在OnGetThread的事件中,给它new一个TServerClientThread派生出来的对象假设类为TDisSvrCltThread。
看帮助文档中的,TServerClientThread的帮助的范例,是
while (!Terminated && ClientSocket->Connected)
{//省略了一些代码
Stream = new TWinSocketStream(ClientSocket, 60000);
if (pStream->WaitForData(60000))
if pStream->Read(Buffer, 10) == 0)
ClientSocket->Close();
只有当收到数据为0的时候才关闭连接。
2. TDisSvrCltThread的ClientExecute函数中,执行等待连接,读取数据。并且发送数据。
上面有三个误区:
1. 上面的多线程并不是一般想的,客户端每发送一条数据来,就新建一个线程,然后Terminate这个线程。而是,ClientExecute结束后,线程并不结束,挂在那里,有新的连接近来,继续运行ClientExecute。只有当有新的连接进来,但是前一个线程还在忙的时候,才新建一个线程。反正,总保持最少线程数量。这样的设计的确好。
2. 我用了一下上面的代码while(…)
发现,上面,要是跑批量的话,有一个连接进来后,就会一直在跑这个线程,虽然有新的线程产生。但是实际上,端口却抢不到。于是,客户端连不上是常有的事(或许只是我的片面之词)。后来,我是不管怎么样,每次连接,接收数据以后,都把连接关掉(当然这里对要保持连接的无用)。当然,同时也把while去掉了,每次都从ClientExecute运行起来。这样做的结果,是多服务线程和单个差不多:ServerSocket->ActiveThreads可以看出来。
3. pStream->WaitForData(60000)好像没什么效果,常常会出现收到数据为0的时候,也就是说,常常因此ClientSocket->Close();而浪费时间。到CSDN的BCB板块,找人要了两个stThreadBlocking是用的范例。发现有个范例用了两次WaitForData,我也试了一下,还真有效。这下很少出现收到0字节的情况了。
至于,这种情况下的数据发送,刚开始看TServerClientThread里面有ClientSocket的属性。我还以为直接可以用来发送数据的呢!后来大概知道了点。TServerClientThread里面,当有连接进来的时候,采取的是主动连接的客户端去取数据(应该没错吧!)。具体的技术细节,就不清楚了。呵呵。我还是用前面用来发数据的线程来做,改的东西也不用很多。
当然也考虑多线程访问冲突的问题:
n 服务线程之间(虽然大部分时间只有一个服务线程,但还是可能的)。地址配置信息查找的函数作为临界资源的问题,用TCriticalSection。有个决定全区是否分发数据的变量,可以用volatile关键字。
n 服务线程把参数传递进发送线程的时候,用TEvent,这样就不会发送线程还没有把数据保存起来,服务线程就把资源给清除掉了。
现在的方案看起来完美多了,不是吗?
E. 用VC做的解决方案。
CSocket是现在用VC做网络常用的类,也用过,用的大概比较久了,都忘了怎么用。还是用它做起了可以转发的东西。虽然问题多多。刚做的时候就发现,用CString放进结构里,和上面AnsiString一样。Delete结构的时候会出现的错误。用char strID[6]这样替换,那个错误竟然没了。怀疑是CString,AnsiString放进结构里就会出现问题(BCB中也出现了)。于是,说到莫名其妙的错误时,发表了关于AnsiString放进结构里有问题的意见,遭到一片痛骂。回来赶紧一试,真的,简简单单做一个,竟然没有问题。郁闷!
CSocket做出来的也不是阻塞的,而且看起来好几个文件,没多大用处。还是自己学学用API写得了。于是,有了两个线程函数(应该差不多了)。一个SvrSocketThread,CltSocketThread。
该说的就是SvrSocketThread了。常常见到的SvrThread的都是
while(true)
{
if(bWantToExit)
Exit;
SocketAccept();
Do SomeThing…..
}
后来又看到这种写法
if(bWantTExit)
Exit;
SocketAccept();
AfxBeginThread(…);
DoSomeThing();
ExitThread;
也有种终于被我发现的感觉,不用说什么,应该就是后一种写法好得多了。也许是程序员的天性,见到好的代码就爽。后一种代码,把后面DoSomeThing的时间也使用起来了。也许不用我说,第一种写法,就是单线程服务的写法。第二中,毫无疑问,是多线程的了。或许,我真的是我太孤陋寡闻了。这么好的代码现在才发现,也许,早就见过,只是没用到就没注意。对于VC中事件触发,可以用SetEvent,CreateEvent,WaitForSingleObject,WaitForMultiObject等api。
用VC做的这个程序也完成了。还是有几个问题:暂且记下来,以备后查。
1. Socket重新绑定端口的问题。第一次绑定一个端口后,想个这个socket重新绑定,虽然,closesocket,WSACleanup也调用过了。可就是无法绑定另外一个端口。现在是要绑定另外的端口,就把端口close掉,重新用socket函数建一个。难道是close以后,一定要重新建一个socket才能绑定。也没有看到其他清除socket的函数。另外,如果重新新建一个socket的时候绑定的端口是被占用的,后面就再也起不来了,即使重新给了一个空的端口。
2. 在ListView中,建了一个CEdit,设了Tabstop属性,ListView设了ShowSelectAlways,可是使用CEdit的时候,ListView选定的地方总是难看的灰色,总不会是要我为这点小问题重载CListCtrl吧。
3. 状态栏左下角都会有一个“就绪”的状态显示,资源中也能找到这个字串。现在想在程序中动态设定,却又不知道怎么动手,现在能做的就是添加一个Pane,“就绪”不是Pane。
F. 终于到了高潮部分了,这就是今天在做的:提供多服务端口,且可以动态申请服务的服务程序。和上面的不同描述如下:
1. 用服务制作,看了好些关于服务程序编写的代码,都是自己完整写出来的。当然最完整的还是那个包括安装、启动、控制、关闭等的HelloWorld程序。今天发现ATL Com向导里面有个Service Exe的选项,看里面用的是CComModule为基类。这不是挺好的嘛!这就简单多了。看代码注册部分分为Server和Service,哎,失败,竟然分不出他们的区别。注册一下。看起来好像Server不在控制面板组件里面注册,而Service有。想知道dll的那种服务是怎么做的,有人告诉我么?我想应该都有ServiceMain导出的,可是我发现有个dll(系统的),用depends看,只有一个函数导出,却还能正常在组件里面控制,不是要有个Handler的吗?
2. 线程结构是这样的:有一个专门处理申请开通端口的服务线程(当然要对应一个端口),并通过这个服务线程对其他线程进行管理。其他线程,分别管理一个分发端口。
3. 比上面的多了一层,用ini做配置文件似乎就有点不够了,这里用xml作为配置文件,也算是学习一下MSXML.DLL的用法。于是,一个关于XML的专门类又诞生了,谓之CXmlFile。
4. 考虑写日志的方法分两种,系统日志和业务日志,系统日志自然是记到控制面板的日志里,业务日志则,由另外一个服务程序来做,可以考虑是假冒的服务程序——有界面,但是隐藏了界面,为的是有窗体,可以通过SendMessage、PostMessage来发送日志。内容的传输,大概也可以通过内存映射来做,只是就不能做到实时记录了(可能吗?)。不知道有没有办法通过知道进程ID,来发送消息的方法呢?考虑用另外的程序来做日志,一是学习、二是直接记日志到硬盘,输入输出的操作影响性能,业务日志实在太多了。当然,在前面的几个阶段中已经考虑到了这一点,并不是每次记日志都保存到文件里,而是达到一定大小的时候才写文件。
5. 服务都是要求比较小的,默认生成的项目也不支持大部分MFC,呵呵。CComModule应该也算是MFC里面的吧!SDK的东西用得太少,用起来实在不顺手。比如说CString::Format、CArray这些要用到,自己编,可就觉得烦了。只好加入头文件。不过,大部分来说,用的还是SDK的东西,至少CXmlFile是。
2004年7月11日补:
重新绑定端口已经可以了,也做得差不多了,做成服务,还是没完成,比较失败,或许别人说得对,学习高级语言让人变得浮躁。如果有人需要,我可以发送VC源代码。
上面说到每次都启动新线程的方法,其实也有缺点,就是启动新线程的时候,必然会多耗一点资源,再就是后来又看到用完成端口进行网络编程的,应该是Windows网络编程中最好的一种方法了。下面页面参考
理解I/O Completion Port nonocast(原作)
http://community.csdn.net/Expert/topic/3056/3056877.xml?temp=.3150751