服务器和客户端的全部功在数据处理模块中实 现,这也是整个程序的核心之处 由 于 数 据 包 的 接 受 在 单 独 的 线 程 中 完 成 , 而 界 面 采 用 Qt 实 现 。 因 此 在 接 受 线 程 中 采 用 QCoreApplication::postEvent()方法向界面线程发送通知事件,在界面窗口中通过重载 customEvent() 方法响应该通知,然后从 Server 中获取数据并显示。整个过程中涉及到线程同步和多线程安全问题,觉采 用信号量和互斥量解决。 3.2 服务端的实现 3.2.1 服务总体实现与概述 服务端的全部功能在类 Server 中实现, 由于在整个系统中有且只有一个服务端出现, 应此该类采用单例模 式实现, 通过 Server::Instance()方法获得 Server 的一个实例, 通过调用 Init()函数完成服务端的初始化 (包 括信号量的初始化、数据库的连接、设置要发送事件通知的界面对象 Object*) ,最后通过 Start()方法创建 socket,绑定端口,创建数据包接受线程,至此,完成服务器的启动 部分代码: class Server { friend class ServerPacketProcessor; public: virtual ~Server(); static Server* Instance(); bool Init(QObject *mainWindow); bool Start(); void WaitForServerStop(); protected: Server(); void* ThreadStartRoutine(void* arg); private: static void* ThreadRoutineTransponder(void* arg); void postEvent(QEvent::Type eventType); static Server* m_server; sockaddr_in m_localaddr; int m_sockfd; pthread_t m_thread; bool bCancel; pthread_mutex_t m_mutex_clients; pthread_mutex_t m_mutex_msgs; QObject* m_ui; 。 。 。 。 。 。 2 / 33 }; Server.cpp #include "server.h" Server* Server::m_server=0; bool Server::Init(QObject* mainWindow) { m_ui=mainWindow; bCancel=false; pthread_mutex_init(&m_mutex_clients,0); pthread_mutex_init(&m_mutex_msgs,0); return true; } Server* Server::Instance() { if(m_server==0) m_server=new Server(); return m_server; } bool Server::Start() { m_sockfd=socket(AF_INET,SOCK_DGRAM,0); if(m_sockfd==-1) { cerr<<"create udp socket failed!"<ThreadStartRoutine(arg); } 。 。 。 。 。 。 。 3.2.2 服务器发送数据模块的显示 由于所有的数据包均以 udp_packet 结构包装,应此发送模块较简单 void Server::SendMsgTo(sockaddr_in clientaddr,const udp_packet* packet) { sendto(m_sockfd,packet,packet->size,0,(struct sockaddr*)&clientaddr,sizeof(clientaddr)); } 3.2.3 服务器接受线程的实现 服务端的接受线程负责接受客户端发来的数据,并且解析数据包,采取合适的动作。 由于数据包的种类较多(随着功能的增加而增加) ,处理过程函数较为复杂,因此将数据包的处理过程封装 在类 ServerPacketProcessor 的子类中, GetPacketProcessor() 方法根据数据包的类型会创建合适的 ServerPacketProcessor 对象并返回 线程函数和 GetPacketProcessor 方法的实现 void* Server::ThreadStartRoutine(void* arg) { while(!bCancel) { udp_packet packet; //定义缓冲区 memset(&packet,0,sizeof(packet)); sockaddr_in clientaddr; socklen_t len=sizeof(clientaddr); size_t sockaddr*)&clientaddr,&len); if(res==-1) { cerr<<"recvfrom error..."<ProcessPacket(&packet,clientaddr); delete processor; } return 0; } ServerPacketProcessor* Server::GetPacketProcessor(int packet_type) { switch(packet_type) { case CLIENT_ONLINE_REQUST: { return new CltOnlinePacketProcessor(this); } case GROUP_MSG_SEND: { return new GroupMsgPacketProcessor(this); } //客户端发送群聊消息 //客户端上线请求 case QUERY_USER_INFO: { //客户端查询其它用户信息 return new QueryUserInfoPacketProcessor(this); } case QUERY_CHAT_RECORD: { return new QueryRecordPacketProcessor(this); } case CLIENT_OFF_LINE: { return new CltOfflinePacketProcessor(this); } default: return 0; } } //客户端下线 //客户端查询聊天记录 5 / 33 3.2.4 服务器数据包处理模块的设计 当接受线程收到数据包后要根据数据包的类型作出正确的处理和响应 对应的消息类型和 content 字段的数据结构 1. 客户端上线请求 #define CLIENT_ONLINE_REQUST 1 struct client_info{ char username[20]; char hostname[30]; short client_port; }; //客户端用户昵称 //客户端的主机名 //客户端使用的通信端口 2. 客户端发送群聊信息 #define GROUP_MSG_SEND 4 struct group_msg_send { char content[MAX_MSG_SIZE]; }; //消息内容 3. 客户端查询其它用户信息 #define QUERY_USER_INFO 6 struct query_user { int userId; }; //可以查询指定的 ID 的用户,0 则表示查询所有的用户信息 4. 客户端查询聊天记录 #define QUERY_CHAT_RECORD 10 struct query_chat_record { int by_begin_time; 点最近的 count 条记录 time_t begintime; time_t endtime; int count; }; //开始时间点 //结束时间点 //查询记录的条数 //查询方式,1 表示离开始时间点最近的 count 条记录,0 表示离结束时间 5. 客户端下线 #define CLIENT_OFF_LINE 12 (content 内容无含义) 6 / 33 1. 2. CLIENT_ONLINE_REQUST GROUP_MSG_SEND 收到数据包 具体过程的实现见附录源码 3. QUERY_USER_INFO 4. QUERY_CHAT_RECORD CLIENT_OFF_LINE 判断数据包类型 5. 1 2 3 4 5 对需要访问的多线程数据加锁 生成唯一的客 户端 ID 保存聊天信息 到数据库 获得服务器上 保存的所有客 户端信息 根据指定条件 从数据库中查 询聊天记录 删除该客户端 信息 向客户端返回 该 ID 转发该信息到 向其它所有客 其它所有的客 保存该客户端 信息 户端 构造数据包,发 送所有的客户 端信息 构造数据包,发 送聊天记录 户端报告该客 户端已下线 向其它用户发 送该客户端信 息 释放锁 处理结束 7 / 33 3.3 客户端设计概要 本系统在群聊的基础上实现私聊功能,从服务器的设计可看出,群聊消息是由服务器转发的。 私聊消息的实现是通过客户端之间直接点到点的通信完成的。因此每个客户端必须知道其它客户端的地址 和端口号。 客户端的启动流程如下 初始化:创建 socket,绑定端口, 创建接受线程 向服务器发送上线请求 否 8 / 33 等待 收到服务器响应? 是 由于客户端和服务端总体结构上相似,应此不再详细叙述 3.4 聊天记录存取模块的实现 服务端采用 mysql 数据库保存群聊记录 数据库表设计 字段 Username Content time ip 数据类型 Varchar(20) Varchar(200) timestamp Varchar(15) 含义 发送者昵称 消息内容 该条消息的发送时间 发送者的 ip 地址 聊天记录读写的实现由 ChatRecordManager 类实现. 通过接口函数 bool InsertRecord(const char *username,const char *content,time_t time,const char *ip);完成插入聊天记录的功能 通过 Records QueryRecordsByBeginTime(time_t begintime,time_t endtime,int count); Records QueryRecordsByEndTime(time_t begintime,time_t endtime,int count); 两种方式查询聊天记录 查询聊天记录时提供开始时间和结束时间、和要查询的记录的数量。