即时通讯软件系统

2015/07/07 个人项目

本项目主要是基于QT界面开发库、SQL server 2005和winSock开发组件完成的即时通讯软件系统。系统包括两大块,分别是客户端和服务器端。主要实现的功能是用户登录注册,用户与某个联系人进行互相发送消息,互相发送文件。通过服务器端转发消息,某两个客户端实现点对点通信。

程序流程图:

系统运行的主要流程是:首先启动服务器端进行监听。然后用户通过客户端的注册界面进行注册,此时客户端会请求连接服务器,连接成功以后发送注册信息,服务器收到注册信息写入数据库。然后用户回到登陆界面,此时客户端也会请求连接服务器,连接成功以后发送登陆信息,服务器收到登陆信息以后读取数据库中的注册信息进行核对比较,成功则返回登陆成功信息,否则断开连接。每个客户端成功连接上服务器以后,服务器都会记录在特定的数据结构中,同时有用户登录或者离线断开,服务器都会把所有的客户在线信息发送给在线客户,提醒用户哪些联系人可以进行聊天。每个客户端可以选择特定的联系人进行互相聊天和发送文件。

保存源id、目的id、对应socket,结构体数组中

多线程实现

登陆与注册

在客户端作一切操作之前,首先要启动服务器。启动聊天软件以后第一个界面是登陆,在登陆界面上有用户名框和密码框以及注册按钮。如果用户第一次使用软件,得首先注册一个账号。点击注册按钮进入注册框的同时,会同时根据服务器的IP地址和监听端口发送连接请求。这个过程就是客户端请求连接服务器端一般过程。

网络协议主要是传输层的TCP协议,流程是客户端使用winSock中的socket函数立一个套接字,设定服务器的IP和端口。调用connect函数连接远程计算机指定的端口。服务器监听以后进行连接并发送连接成功消息给客户端。

//填写服务器地址
//char IP[20]="127.0.0.1";
char IP[20]="118.202.16.198";
memset((void *)&server_addr,0,addr_len);             //把地址结构体清0
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(PORT);
server_addr.sin_addr.s_addr=inet_addr(IP);
//套接字与服务器连接
if(connect(sock_client,(struct sockaddr *)&server_addr,addr_len)!=0){
	cout<<"地址绑定失败!错误代码:"<<WSAGetLastError()<<endl;
	closesocket(sock_client);
	WSACleanup();
	return;
}

客户端收到连接成功的信息后,从注册窗口获取的注册信息进行打包,整合成一个结构体数据:

typedef struct{
	short int infoType;   //消息类型
	short int source_Id;  //来源Id号
	short int desty_Id;   //目的Id号
	char buf[1000];       //消息内容
}message

约定infoType编号成不同数字来表示不同的消息类型,包括注册信息、登陆信息、登陆成功信息、所有的客户在线信息、聊天信息、文件信息

发送注册信息时,声明message结构体,把infoType标定为注册信息,然后把注册用户名放在buf前10个char中(假设用户名长度不超过10),把用户密码放在buf的11字节开始的区域。然后客户端把这个结构体通过已连接的soket和send函数发送服务器,服务器开辟一个内存区,以message结构体类型接收数据。接收完成以后服务器首先会判断消息类型infoType,发现是注册消息,就会到buf的开头读取用户名,在11字节处读取密码,以‘\0’符结束,然后把用户名和密码以及分配的ID号写入数据库。至此注册完成。

回到登陆窗口,过程和注册过程一样,首先和服务器连接成功后,发送登陆信息给服务器,把消息类型标记为登陆信息,用户名放在buf前10个char中,把用户密码放在buf的11字节开始的区域。然后把结构体发送给服务器。服务器接收数据以后,发现是登陆信息,立即把用户名和密码和数据库中的所有行进行比较,如果不匹配,服务器就会断开网络连接;如果匹配成功,服务器就会给客户端发送登陆成功信息,在登陆成功信息结构中,服务器把客户端的Id号保存在source_Id中。客户端接收到登陆成功信息以后,把source_Id中自身Id保存起来。同时会从登陆窗口切换到客户端窗口。至此登陆完成。

客户端(客户端与客户端聊天实现)

在某一个客户端中要想实现和另一个客户端中进行聊天,首先必须知道那些客户端可以聊天。当每个客户端登陆成功以后服务器会使用一个结构体数组,在这简称在线用户结构体数组,来存储每个在线客户端的Id号。如果某个客户端下线,服务器会把该客户端Id号从在线用户结构体数组中剔除。因此要使每个客户端要想知道其他哪些客户端是在线的,必须通过服务器获取这些信息。

在每个客户端登陆成功以后收到登陆成功信息以后,会再次收到所有的客户在线信息,该信息结构体的buf中包含所有在线用户的用户名和id号。buf中的分配格式是首字节代表所有在线用户的个数,然后后面每11个字节代表一个用户,前10个字节存储用户名,后一个字节用户的Id号。当服务器把这个结构体发送个客户端以后,客户端接收同时会发现这是所有的客户在线信息,在读取buf中的数据,因此客户端也要生成一个在线用户结构体数组来存储所有在线用户的用户名和用户Id号。在客户端窗口上有一个tableview窗体部件,把所有在线用户名显示出来。这样客户端就通过tableview就可以知道其他哪些用户是在线的,并选择要聊天的用户。

在tableview中,用户可以选择任意一个客户端进行聊天,只要双击对应的用户名,就会弹出一个用户聊天对话框,如下图:

只要把相应的聊天内容填写好,点击发送即可。客户端会把聊天信息进行打包,建立message结构体,设定消息类型为聊天信息infoType,source_Id填写自己的Id号。当用户选择完聊天的目的客户端时,也把目的客户端对应的Id号从在线用户结构体数组中取出,在desty_Id中填写目的客户端的Id号,把聊天内容存储中buf中,然后把整个结构体发送给服务器,服务器发现是聊天信息,再把参看结构体中的目的Id,然后把聊天信息转发给目的Id的客户端,从而实现了点对点的聊天功能。

每个客户端既要发送信息,又要接收信息。要同时实现这两个功能,必须建立一个线程来进行处理接收信息:

_beginthreadex(NULL,0,(unsigned int (__stdcall *)(void *))Mythread1,this,0,NULL);  //开启接收信息线程

服务器端(实现数据库的连接、信息的聊天信息转发以及发送其他各种信息)

服务器与SQL server 2005数据库管理系统连接,主要是在客户端登陆和注册时进行访问数据库。

在客户端开始操作之前,必须启动进行监听。主要完成以下工作:

初始化winsock2.DLL:

WSADATA wsaData;
WORD wVerionRequested=MAKEWORD(2,2);
if(WSAStartup(wVerionRequested,&wsaData)!=0){
	cout<<"加载winsock.dll失败!";
	return;
}

创建监听套接字:

if((sock_server=socket(AF_INET,SOCK_STREAM,0))==SOCKET_ERROR){
	cout<<"创建套接字失败!错误代码:"<<WSAGetLastError()<<endl;
	WSACleanup();
	return;
}

填写绑定服务器地址:

addr_len=sizeof(struct sockaddr_in);
memset((void *)&addr,0,addr_len);             //把地址结构体清0
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);
addr.sin_addr.s_addr=htonl(INADDR_ANY);       //使用本机IP地址作为服务器地址

监听套接字绑定服务器地址:

if(bind(sock_server,(struct sockaddr *)&addr,sizeof(addr))!=0){
	cout<<"地址绑定失败!错误代码:"<<WSAGetLastError()<<endl;
	closesocket(sock_server);
	WSACleanup();
	return;
}

如上图,点击启动按钮以后服务器将启动监听。同时可以查看各个客户端发送过来的信息。

服务器启动监听以后,每一个客户端发送连接请求给服务器,当登陆成功以后,就会产生一个新的套接字,该套接字和监听套接字不同,是服务器用来和该对应的客户端通信的套接字,以后服务器每次和该客户端通信都会通过该套接字来完成,因此必需保存该套接字。在项目中的做法是把套接字和Id号,用户名存储在在线用户结构体数组,服务器的在线用户结构体类型为:

tydef struct{
	SOCKET sock;        //保存套接字
	short int Id      //对应的Id   
	string username   //用户名
}Inline

如果有某个客户端登陆服务器成功或者掉线,服务器都会把与客户端连接的套接字和Id信息加入在线用户结构体数组或者从在线用户结构体数组删除。同时还要把所有的客户在线信息给每个在线客户端都发送一遍,动态更新每个客户端的在线客户信息。

当服务器接收到聊天信息以后,首先会读取发送目的的Id号desty_Id,确定要把该信息发送给那个客户端,然后更加目的Id号从在线用户结构体中找到其对应的套接字,通过该套接字,服务器再把聊天信息转发给特定的客户端,从而实现了点对点聊天功能。通过这个机制,不仅可以点对点聊天,而且每一个客户端可以和任意一个客户端进行对话

服务器能同时和多个客户端进行连接,会不定时收到某个客户端发送过来的信息。服务器只能通过多线程机制来监听每个客户端对应的套接字是否有消息。具体实现过程是:每当服务器与客户端连接,会创建一个新的套接字与该客户端通信。在创建新的套接字的同时,也创建了一个线程来监听新套接字,记录相应的线程Id号

_beginthreadex(NULL,0,(unsigned int (__stdcall *)(void *))Mythread2,&structdlgaddr,0,NULL);  //开启监听接收消息线程

有消息则接收消息,根据消息类型进行判断和处理:

swich(infoType){
case 注册信息:{
	处理....
} break;

case 登陆信息:{
	处理....
} break;

case 聊天信息:{
	处理....
} break;
.
.
.
case 文件信息:{
	处理....
} break;
}

每当一个客户端下线都会通过Id号找到对应的线程Id号,通过线程Id号来把下线客户端对应的线程杀死,减少系统的运行开销。

文件发送

在本项目中主要是实现对txt类型的文件的传送。

文件是一种数据存储的形式,因此文件传输的实质就是数据的传输。在文件传输程序中,发送方首先打开文件将文件数据读入应用程序的发送缓冲区,然后调用send()函数将数据发送给接收端,接收方则调用recv()函数接收数据,并将收到的数据写入文件。

C++中的文件处理功能是由输入文件流ifstream和输出文件流ofstream提供,这两个流在头文件fstream中定义。

点击打开发送给文件按钮,弹出选择文件对话框选择要发送的文件:

ifstream inFile(filename,inmode);
ofstream outFile(filename,inmode);

//以读写方式打开二进制文件
fstream f("d:\\12.dat",ios::in | ios::out | ios::binary);

传输一个文件需要传输两部分内容:一是文件的名字,二是文件的内容,收发双方必须约定何时发送文件以及何时发送文件内容。一般的做法是,发送方发送文件名给接收方,接收方收到文件名以后以输出方式(ios::out)打开文件,然后通知发送方“可以发送文件内容了”,发送方收到允许发送文件内容的通知后,就开始从文件中读取文件内容并发送给接收方。其中发送方已事先打开要发送的文件,并且收发双方约定用字符串“OK”表示接收方允许发送方发送文件内容的通知。

然后在另一个对话中弹出保存对话框来保存文件:

发送方会提示发送完成:

接收方判断文件传输结束的方法:比较简单的方法利用recv()函数返回值,如果用于传送数据的套接字已关闭,recv()函数将返回0,其他情况要么返回实际接收到的数据量,要么返回SOCKET_ERROR。因此,文件发送方在文件数据传输完成后将所用的套接字直接关闭,接收方接收完数据后在此调用recv(),函数的返回值将为0,此时就可以知道文件传输已结束。

总结整个过程:

文件传输前,首先要打开文件,获取文件长度并将文件名和文件长度发送给客户端,客户端收到后则打开文件为写入数据做好准备,并向服务器端发送“准备好接收文件内容”的确认信息。服务器端在收到客户端发来的确认信息后,就通过循环多次读取文件内容并发送给客户端,直到读取文件结束。接收方则循环调用recv函数接收数据并将数据写入文件,直到文件传输完成。文件传输完成后客户端和服务器端各自关闭打开的文件。

Search

    Post Directory