刚写这个程序的时候,无从下手之中又夹杂着一点兴奋,整天脑子里想的都是该怎么设计服务器,怎么设计数据库,数据库中该有哪些字段,怎么去设计客户端,怎么才能让服务器保存用户的好友信息等等,太多太多了,不断地想到方法,又不断地推翻自己,虽然我当时什么都不懂,但也没上网查,我觉得我喜欢这种设计的感觉(当然,后来事实证明知道的太少了,还是从网上知道了数据库该怎么设计)。最开始纠结我的还是客户端的消息到底经不经过服务器,问了好多人,各有各的说法,最后黄学长给我说马学长做过做个程序,就去问了问他,他告诉我登陆的时候和服务器通信,然后和好友聊天的时候就直接用udp模式给好友发信息了,不经过服务器(虽然这样说,但是我还是不明白,我们有时候在QQ上不能发敏感词汇,如果不经过服务器,那他是怎么检测到的呢?)
写这个程序之前,网络编程方面的程序,我就写过书上那几个,那种最最最基本的“你点send,我点receive”的程序,甚至连c/s模式的都没写过,更别说这个程序的异步套接字了。开始那会儿都是特别天真的,以为把书上的那种通信模式写成c/s模式就可以了,现实是残酷的,我再次遭遇了阻塞函数的困扰,当时在看书的时候学长就给我说过accept和receive会阻塞,说实话当时并没有深刻的理解,后来自己看到线程那章的时候,就想过创一个线程来接收用户发过来的消息,当时就只是想了想,这次想付诸于行动,正好手边的一本,也是唯一带回来的一本vc的书——孙鑫的《VC++深入详解》,这是我从学长那儿借来的书,这一个月多亏了这本书,好多好多东西都是从上面借鉴的,包括后面的异步套接字;言归正传,在面临阻塞函数的困扰时,我想用多线程来解决这个问题,在这个过程中,遇到了好多问题,比如:
因为基础不好,所以当时看到这个之后,想的就是是不是自己函数定义或者声明出了问题,不断地找啊找,都找不到那出错了,后来是因为在定义成员函数的时候,
必须手动输入,不能复制!!!
在调试最开始的通信代码中,发现客户端那边线程并没有阻塞,后来上网问了才知道是因为没绑定套接字(当时连这种错误都要犯,一些概念性的东西真是没掌握)
中间还遇到了套接字相互赋值的问题,因为CSocket类没有重载=,所以不能通过socket1=socket2这样简单地把socket2赋给socket1,上网查到可以用detach和attach来实现套接字的传递,但是detach之后,原来的套接字就被关闭了,不是我想要的。
有天在网上了篇文章,上面说到:
“曾经看到有人自己创建线程,在线程中创建CSocket对象进行Listen、Accept,若Accept成功则再起一个线程继续Listen、Accept。。。可以说他完全不理解CSocket,实际上CSocket已经内置了多线程机制,你只需要从CSocket派生,然后重载OnAccept:”
我一直就是那个不理解CSocket的人,我一直都是Accept成功则再起一个线程继续Listen、Accept。。。因为我当时甚至不知道监听套接字仅仅是用于监听的(因为这个问题,还在csdn上被人吼了一顿)........后来弄了两天,发现创了线程之后有好多细节的地方,比如入口函数必须是全局函数或者静态函数,而静态函数又有很多要注意的细节,说实话,自己基础太差了,在网络编程这块儿都还没掌握好的前提下,再涉及线程这些东西,真的有点困难。后来再次翻阅孙鑫的《VC++深入详解》,在线程同步这章的后面,发现了一个基于消息的异步套接字的网络聊天室程序,不断地研究那个程序,当时还没用过socket类,因为前一本书上网络编程这块都是用的csocket类,所以在看到加载套接字,以及SOCKETADD_IN类型的时候都会特别迷糊,不过后来多看几遍也就习惯了。在这个程序中,首先要用WSAAsyncselect()函数为指定的套接字注册一个网络事件,这个地方犯了一个到最后才发现的问题,就是在服务器端我居然就为监听套接字注册了网络事件,不光注册了FD_Connect,还注册了FD_Read这个事件,真是因为这方面知识太少了,我当时还以为只要注册了网络事件,不管是哪个套接字,只要有消息来,就会触发事件,后来就导致了一些问题,这些后面再说。照着书上的葫芦,我也画了一个初步的瓢,一个简单的基于消息的异步套接字。第一阶段算是结束了。
第二阶段是数据库的操作。特别不幸的是,孙鑫的《VC++深入详解》中,关于数据库的操作基本没讲,又找到那本《21天学通vc++》,上面主要讲了用ODBC对数据库进行操作,但是讲得也很浅,跑到我们这边的书店转了转,居然没有数据库编程相关的书,没办法只有用ODBC尝试,而这时困扰我的还有另一个问题,就是数据库的设计,主要是好友这块,我当时还没想到用两张表,想的就是在一张表内存用户的基本信息,以及他的好友,后来发现好友不只一个,那岂不是要建很多字段(好友1,好友2,好友3......),当然这一点都不实际,又想过数据库中字段有没有类似于数组的类型,最后实在没办法,就问问了学长,学长说建两个表,一个存用户信息,一个存好友关系。虽然这样说,但是还是有迷惑,用户多了之后,那好友关系那张表数据不是会很大吗?而且服务器要遍历一次多费劲呀。当时两张表的关键字都是设的QQ,后来又发现一个问题,就是我好友关系表的QQ字段里的记录可能会有重复的,但是关键字是不允许重复的,而且此时我试了试遍历一张表(当时表里只有两条数据),奇怪的是,出现结果却是4条,就是这两条数据的不同组合,上网查了查,发现是笛卡尔积,两张表连接的时候,会遵循笛卡尔积的规则。当时的一个想法就是怎么只在一张表中操作,网上一问,好心的网友回答了很多,但是odbc书上讲得太少了,他们讲那些我真是一点都看不明白,没办法,身边能看的书都看了,只剩下《vc++深入详解》中那一丁儿点关于ADO操作数据库的了,开始还想,就这么几页,看了肯定也没用,还是回学校之后再去借专门数据库的书来看吧。但是抱着死马当活马医的想法,还是看了一点,谁知道那天晚上这么一看,充分地明白了为什么现在ADO这么盛行了,果然比起ODBC太方便了,看完之后立马从床上下来打开电脑开始写书上的程序,当看到ADO通过 rst1->Open( "select *from QQ账号信息表", _variant_t ((IDispatch*)con,true),adOpenStatic,adLockOptimistic,-1 );直接获得某个指定的表的指针时,真有一种绝境逢生的感觉。
第三阶段就是自己设计协议了(不知道能不能算是协议)。当时遇到的问题就是,用户要发给服务器很多消息,比如登陆时的账号和密码消息,比如注册时的消息,或者添加好友的信息。其实这个问题从最开始就困扰我,直到我有次尝试着发送结构体,因为send和receive函数buf都是char *型的,所以当时不知道怎么发结构体,网上说强制转换就行,但是好多细节还是不明白,最后还是csdn上的一位网友教会了我,有了这次发送结构体的经验,加上看到网上的说可以自定义包头来区分这些消息,包头里是标示符。说实话我对包头没什么概念,但是当时我的第一个想法就是,用结构体,定义一个成员flag,用来标示这些消息。比如客户端发送的消息中flag为10,就是登陆信息,20就是注册信息,30就是添加好友的信息,40就是请求刷新好友的信息。当时我突然觉得有点明白协议是什么意思了。身在北邮,但是却没学过通信方面的课程,整天听其他院的同学说着什么协议协议,我记得我有次还问过一个信通的同学,协议到底是什么,他给我的答复就是双方定好的一种规则。我当时就在想,字面意思就是这样,但是对于实际中的协议到底是什么,我还真是一点概念都没有,这次没想到自己居然就这样定义了一个“协议”。但是我还是觉得不妥,这个程序简单,所以就几种类型的消息,要是以后功能多了,还能这样约定吗?我想也许会有更好的方法吧。这段时间遇到的几个大的问题上面已经说了,还有一个就是用户如何实时获取自己好友消息,想了很久,最后还是用了定时器来实现,每隔一段时间(程序里是1分钟,方便调试点)就向服务器发送一个请求,请求刷新自己的好友,服务器就去数据库中查找他好友里面在线的人的信息,然后发给用户。
程序还有几个带完善的问题,首先就是服务器如何判断用户是否在线的问题,其次是客户端处于好友界面时,怎么提醒用户来了一条消息,因为我chat的套接字是在chat窗口中创建的,所以如果chat窗口没打开,套接字也就不会创建,也就不会收到好友发的消息;
最后在网上找了一个托盘程序,实现了程序能够最小化到右下角的托盘区域;