« 菜鸟打桥牌 | 返回首页 | 三维空间直线方程是什么? »

使用 WSAAsyncSelect 的 Winsock 编程模型

前段时间思考了 Windows 下应用程序最合适的实现模型。写了这么一篇 blog 在 Windows 下使用 Timer 驱动游戏

我想,Windows 有 Windows 的哲学,Windows 平台下的应用程序,也有他的理念。关于 Windows 编程的书,我比较喜欢 Charles Petzold 的那本:《Windows 程序设计》(还有另一本是《WIndows 核心编程》,在第 5 版的 3.2 节中就提到 Windows 编程的难点在于“别调用我,我会调用您”以及“行动迅速”。

以前,我不十分理解 Windows 为什么把大量的任务放在消息循环中被动的调用,慢慢的我有点理解了。这就是 Windows 。

网络编程我一直是顺着 BSD socket 来学习和使用的,所以 Windows 下写 winsock 程序也一直没有改变习惯。这两天突然对这个做了下反思,按照 Windows 的理念来写 socket 程序应该是怎样的形式?查了下 msdn 后,发现了一个以前被我忽略掉的 api —— WSAAsyncSelect 。

以前粗读 mfc 的源码时,仿佛见过 CSocket 用这个来实现。当时没有太在意;也听不同的几个朋友跟我简单介绍过它,同样没有放在心上。直到今天,自己突然有兴趣了,才仔细研究了一下。

从今天的眼光来看,winsock 并不是一个很好的设计。在 tcp/ip 已经一统天下来看,winsock 的许多设计是蹩脚且多余的。不过,当我们把自己代入 winsock 设计的那个年代,再结合 windows 自己的理念来看。就会发现许多合理之处。

WSAAsyncSelect 就是提供了一个最适合 Windows 自己运作模型的工作方式。它可以把 socket 的消息映射到线程的消息循环中。这符合:“别调用我,我会调用您”的 Windows 哲学。

具体的用法是多说无益,msdn 已经讲的很清楚了

通过 WSAAsyncSelect 设置,线程消息循环将在指定的事件发生后,得到相应的消息。WSAGETSELECTEVENT(lParam) 可以用来得到网络事件本身,而 wParam 则被用来传递 socket 的 handle 。然后,就可以主动调用 socket 函数来处理这些事件了。我觉得这比 select 的模型更适合 Windows 应用程序。

而 windows 的应用程序的主体永远只需要一个简单的循环来处理和分发消息就够了。

今天我本想 google 一下,看有没有专门讲解 windows 网络编程的书。发现只有一本《Windows网络编程技术》,读了一下 china-pub 上的书评后,倒了胃口,便不想买了,还是读 msdn 吧。

Comments

WSAEventSelect
WSAAsyncSelect
这两个异步方式
WINSOCK是用KERNEL32.DLL线程池实现的,
跟重叠IO不一样。
猜测是用线程池对重叠IO的包装,弄到数据后,再通知主线程窗口过程(WSAAsyncSelect)或WaitObject阻塞过程(WSAEventSelect)。
真正核心态异步的,只有重叠IO与完成端口。

事后诸葛亮, 真的是人如其名

不知道这个到底是什么网站,觉得您写的很好,以后还会来看看的,有点收获

《Windows网络编程技术》
英文名应该是 Network Programming for Microsoft Windows

http://www.douban.com/subject/1229925/
这个是第一版,是 京京工作室 翻译的。

这本书的英文版出了第二版,第二版的中文版也有了
http://www.douban.com/subject/1231968/

中文版没看,英文版看了部分,还不错。在第二版加入了一整章来讨论“scalable server applications”。不过对于 scalable server 还是没有讲的很彻底,比如在关于 non-paged pool 的讲解上,不够深入。

关于这些部分,估计最终还是要找这本来看
Microsoft.Windows.Internals.Fourth.Edition

"说得没错。不得不使用多线程的时候还是得用多线程。多线程之间用消息进行异步通讯是对的,Windows的窗口消息有SendMessage和PostMessage,但是线程消息就只有PostThreadMessage。"
如果你在线程里面创建一个窗口,那“SendMessage和PostMessage”就又回来了,毕竟“SendMessage和PostMessage”还是与窗口相关的API!

“btw ,我对 OO 已经越来越没有兴趣了。现在已经极少使用 C++ ,如果一个任务用 C 实现感觉困难的话,多半 C++ 到了最后还是一锅粥。OO 是个好东西,但就在那么些合适的场合用用就可以了。”

说得好。完全同意。

“那取决于对OO的定义是什么。”

黑盒子。什么风格都可以,只要便于维护,便于理解,又有效率。。。见招拆招。。。
少提socket吧,那东西跟编程模型没什么关系,一个具体细节而已。
以后软件架构基于目标代码的拼装,比如:可跨语言继承,跨语言聚合。。。当然里可以是相同语言,只不过在目标代码级,已经分不出当初来自什么语言了。。。

那取决于对OO的定义是什么

我觉得微软从始至终还是很能贯彻“信任程序员”这个原则的。今天看来“信任程序员”未必是好事,但是对于 C 出身的程序员却能感到非常舒服。

Windows 下不用 Windows 的推荐机制来写程序的人也不在少数,尤其在 windows 下开发游戏的,大多不遵循 Windows 的指导原则。这一点上,Windows 还是非常宽容的,微软能做的就是越来越详尽的 msdn 。

btw ,我对 OO 已经越来越没有兴趣了。现在已经极少使用 C++ ,如果一个任务用 C 实现感觉困难的话,多半 C++ 到了最后还是一锅粥。

OO 是个好东西,但就在那么些合适的场合用用就可以了。

不用多线程,也不用异步IO,代码是好写了,可是真的,真的执行效率相差好几倍啊。

理论上说,我们当然要做低耦合,还要线程做什么?分成两个模块放到两个进程,互相之间全都用socket通讯好了。

但是现实中,大多数现有的程序需要放到同一个进程中来做,比如磁盘数据的读写,缓存。

做一个多线程的库,我刻意的避免加锁解锁,然而,如果要避免做这些的话,就必须得对用户的使用加以限制,比如多个类之间的初始化顺序,析构顺序,互相间谁能调用谁谁不能调用谁,这些线程问题不是程序员的错,而是线程本身就不是一个安全的东西。

虽然我不得不用,但是心里却非常不情愿

说得没错。不得不使用多线程的时候还是得用多线程。多线程之间用消息进行异步通讯是对的,Windows的窗口消息有SendMessage和PostMessage,但是线程消息就只有PostThreadMessage。

“我当然也知道把所有东西都放到一个线程是极不方便的。不过问题在于Windows的进程间通讯极不方便,多线程又是极其危险的。这也没办法啊。”

应该把UI或IO等不同的任务交给多线程来完成。各个线程各司其职,减少重叠执行的代码。一般多线程重叠的部分只是“enqueue和dequeue”,还是比较容易证明其正确性的。

“windows下所有的一切还是你自己的掌握,并非操作系统左右。”你说这话也对也不对,好比“皇上是好皇上,都是太监惹的祸。”编程的时候,除了“自己”和“Windows”之外,还有许多其他“服务”帮忙。你自己很小心,微软的Windows内核也久经考验,但是在Windows编程模型里,任何其他模块的WinProc()挂掉,你的模块跟着挂。

编程模式里面有一个很基本的原则:上层程序必须相信底层程序,而底层程序决不相信上层程序。具体到POSIX设计:用户调用Kernel的时候,要等待Kernel工作完成;Kernel回调的时候通过Singnal机制,Kernel安排用户线程,但决不等待用户线程。具体到Windows设计:用户调用Windows API,要等待Windows工作完成(这没问题);Windows回调的时候通过Send机制,Windows安排用户线程,但回调方必须等待用户线程。结果就是容易造成死锁。无论你自己多小心,你对Send()对方的行为无法控制。“Don't call me; I'll call you.”原则虽然很精辟(绝对是功大于过)。由于286/386的限制及当时人们对编程理解的局限性,Windows采用了同步回调机制,来节省参数marshaling的时间和空间。无奈,这种编程模式构成了双向依赖。

“单线程的模型并非错误,windows 设计的今天这样不伦不类,多为照顾各种开发人员和不同的编程模型所致。(IMHO)”我非常认同“单线程的模型并非错误”的说法,其实这是Windows编程的精髓,也就是制定“别调用我,我调用你”原则的初衷。世界上多数编程人员高估自己的实力,多线程编程导致了太多的程序无法按期完成,比如Access或者Outlook。同步回调(i.e., send)的设计初用起来很简单,可是后患无穷。这不完全是“多为照顾各种开发人员和不同的编程模型所致”。

“合适的时候采用合适的方法是个很浅显的道理。但合适的方法的使用需要对方法的足够理解。”大道理当然是这样。Windows API和Windows的消息轮询太基本了,二者是构成Windows编程的“阴和阳”。用户几乎无法逾越这个模式,否则只有自己重新设计一套OS API和编程模式。然而,Windows对象模型里面的WinProc()来对应C++的v-table,用SetUserLong()来指定用户数据,显然是OO史前的“非经典”做法。无论用户怎么理解,Windows编程模式都太陈旧了,以至于难于“足够理解”。这也不完全是程序员的错。

顺便说一句:中国软件老前辈唐稚松院士,30多年前在世界上首次提出“向前goto的原则”。goto的标签应该在goto语句的前面,这样便于理解,也不会产生“面条”代码。现在大家一定觉得“太显然”了。可当时关于goto的论战持续了将近十年。其实今天关于Windows里面send的讨论,大家觉得显然吗?Windows Vista(.NET)编程模型已经没有(明显的)消息机制了吧。对Windows编程模型的改革势在必行。十年以后再来看,大家也会“太显然”了的感受。

我当然也知道把所有东西都放到一个线程是极不方便的。不过问题在于Windows的进程间通讯极不方便,多线程又是极其危险的。这也没办法啊。

这也是为什么COM会存在的原因,被TAOUP书中说成“这究竟有多糟”的COM,却是Windows中能够安全使用模块的唯一办法。

提早优化带来了性能优势

吗?

ps. “别调用我,我会调用您”的 Windows哲学我认为不能理解为:“别打电话给我,我打电话给你。但你要等着我。等到海枯石烂,无怨无悔。”

究竟等到对久,取决于你自己在干什么,windows 下所有的一切还是你自己的掌握,并非操作系统左右。如果真的等到了海枯石烂,怨恨和后悔都还是归咎到自己。

单线程的模型并非错误,windows 设计的今天这样不伦不类,多为照顾各种开发人员和不同的编程模型所致。(IMHO)

合适的时候采用合适的方法是个很浅显的道理。但合适的方法的使用需要对方法的足够理解。所以我想,多理解一些编程模型是修炼之道。

“别调用我,我会调用您”的 Windows哲学是否也可以理解为:“别打电话给我,我打电话给你。但你要等着我。等到海枯石烂,无怨无悔。”(Don't call me. I will call you. 新译本)

封建社会里,立贞节牌坊。可以理解。今天看来“Windows编程原则”显然有失公平。

(Windows基于消息编程模型)“这种架构是对的,只有这种办法能够单线程同时处理UI和IO。”唉,后生啊!智者千虑,必有一失。利用同一线程处理UI及IO,那是当时没办法的办法。搞民运那年,DOS不支持多线程,才出此下策呀。

微软曾经亲生的Access,做完Beta了两年还不能RTM,只好再花钱买了FoxBase。Outlook至今卖到街上也有十多年了吧?天天挂,啥原因?“单线程同时处理UI和IO”呗。

Win2000之后微软硬加了30秒time-out,问题有所缓解,但依然存在。

“网络编程我一直是顺着 BSD socket 来学习和使用的...”

从云风的Blog来看,功夫已然“炉火纯青”,有“万夫不挡之勇”了。下一步应该是研读“岳武穆”的阵法啦。

记得《英雄》里的台词吗?“手中无剑,心中也无剑。”

TCP/IP就是“网”吗?Socket就是网络编程吗?啥时候见过用“南桥、北桥”编程的?也没见过用“IDE、SCSI”编程的吧?!

朋友指点,偶然路过此地。老夫决没有贬低的意思,是高兴啊。

瞎说、瞎说。见笑了、见笑了。

WSAEventSelect效率更高比WSAAsyncSelect吧。

还好我买的是 《Windows网络编程》这本...

MFC的那个SOCKET封装,不建议在service里使用,也就是说,如果写service的socket程序,自己去封winsocket,别用MFC的类,会有很多莫名的问题,而且MS也不会去修的。

.net的system.timer.timer也有bug,有时候事件会不触发,推荐使用system.threading.timer,不过很不幸,推荐使用的这个也有类似的问题,但是MS有补丁修复这个问题。以上仅限于.net 1.1

另,.net 1.1的socket,也有一个死结,就是handle一多,就会有很多memory pin,没有什么补丁;对于那些需要把很多连接永远保持的程序,这是一个问题。一个办法是先为这些socket预留一个memory buffer,保证Memory分配的连续性,不影响到其它部分,另一个办法就是用.net 2.0赫赫,当然这也根本不算一个方法。

相比之下,完成端口就更接近select,完成端口的处理函数可不是回调函数,通常完成端口是阻塞的去获取处理的事件,在.net框架中,微软鼓励程序员把后台运行的事件用System.Timers.Timer来定时,而System.Timers.Timer不同于SetWindowTimer那个Timer,它是把事件丢到ThreadPool上去的,而这个ThreadPool正是基于完成端口的。

这种架构是对的,只有这种办法能够单线程同时处理UI和IO

在Unix下,理论上也是可以单线程做一切工作的。他是一切皆文件,从X-Window到磁盘系统到Socket的所有东西都是fd,通通丢给select,也是可以一个线程来处理。

之所以没有人这样做只是因为把他们放到不同的进程中处理更好罢了。

但是Windows却不鼓励进程间的合作,比如Windows下的管道表面上和Unix差不多,但用用就发现很多必要的功能都没有。所以这就强迫程序员必须把从前台到后台的所有东西放到一个进程。

而在Windows下,不论是否使用多线程,每一个线程都不要阻塞,而将需要等待的事件丢到消息队列中来处理是理所当然的。

另一个角度说,多线程共享内存本质上是危险的,Windows鼓励线程间用消息来通信,同样的,Windows相比起管道而言的完善得多的另一种进程间的通信Socket当然也应该派发到消息队列上。

WSAAsyncSelect MS很简单,用好可真不容易了,异步要注意的细节非常多。仔细分析MFC的代码就知道了。

Post a comment

非这个主题相关的留言请到:留言本