一、题目分析
要实现一个简单的聊天工具,实现即时聊天、文件传输功能。根据我们所学的知识,可以采用socket套接字编程。
自己采用的是MFC中CSocket结合TCP传输协议来实现。使用了多套接字分别来实现消息传递和文件传输。
使用的知识点有:
1.使用CSocket类来实现建立套接字、监听、接受连接、发送数据、接收数据;
2.使用加速键来实现对键盘输入的控制,实现回车及发送消息;
二、概要设计
(一)服务器端
如图所示:
class CServSocket继承于CSocket,用于等待客户端申请连接;
class CRecvSocket继承于CSocket,用于和客户端连接;
clas CNetChatDlg是主对话框;
class CSendFileDlg主要用来显示传输文件时显示信息;
(二)客户端
class CClientSocket继承于CSocket用于和服务器进行通讯;
CNetClientDlg是主对话框类;
三、详细设计
首先先介绍聊天信息的实现:
(一)服务器端
1)界面
列表框控件IDC_Log属性“sort”值为false,属性“Horizontal Scroll”值为true。
按钮IDC_Send属性Disable值为true。
2)定义CSocket类的派生类CServSocket和CRecvSocket。
从CSocket编程模型知道,服务器端需要两种套接字,一个用来侦听连接请求,一个用来与请求连接的套接字建立连接。因此,为程序添加两个CSocket派生类:SServSocket和CRecvSocket,它们与对话框类密切配合,共同完成程序所要求实现的功能。
3)建立套接字与对话框类的关联
在程序中,对话框类要用到套接字类,而套接字类在响应某些消息,如在函数OnAccept、OnReceive中进行处理时,也要改变对话框的某些控件状态,以反映给用户这些事情的发生。
这里存在着两个类相互使用的情况,把套接字类对象定义成对话框类的成员变量,同时在套接字类中也把对话框类定义为成员变量。如何实现这样的用法呢?在对话框类头文件中加入套接字头文件的声明,然后在套接字类头文件中加入对话框类头文件的声明,这样的做法显然行不通。
具体做法应该如下:
首先,在ServerDemoDlg.h中加入套接字类头文件的声明,语句#pragma once的后面加入如下语句:
#include "ServSocket.h"
#include "RecvSocket.h"
然后在该文件中为CServerDemoDlg类增加两个公有成员变量,语句如下:
CServSocket *ServSock;
CRecvSocket *RecvSock;
这样在对话框类中就可以使用套接字类了。
继续在套接字类中加入对话框类信息。
首先,在ServSocket.h文件的开头,语句#pragma once的后面加入如下语句:
class CServerDemoDlg;
然后,在该文件中为CServSocket类添加一个公有成员变量和一个构造函数:
CServSocket(CServerDemoDlg *Dlg);
CServerDemoDlg *m_Dlg;
接着在ServSocket.cpp文件中添加新的构造函数的实现,并添加一条关于ServerDemoDlg.h文件的预编译声明,代码如下:
#include “ServerDemoDlg.h”
CServSocket::CServSocket(CServerDemoDlg *Dlg)
{
m_Dlg=Dlg;
}
这样,在套接字类中也可以通过成员变量使用对话框了。
使用同样的方法对CRecvSocket类进行设置,使其也可以通过成员变量使用对话框。
4)为套接字添加串行化读写信息的功能。在服务器端的两个套接字中,只有CRecvSocket套接字是真正与客户端套接字建立连接,发送与接收数据的,因此,我们只为该类添加串行化读写信息功能。在RecvSocket.h文件中为类CRecvSocket添加三个公有成员变量。
CSocketFile *m_File;
CArchive *m_ArIn;
CArchive *m_ArOut;
5)在对话框中初始化套接字并侦听连接请求。在OnInitDialog函数中添加如下代码:
// TODO: 在此添加额外的初始化代码
if(ServSock=new CServSocket(this))
{
if(ServSock->Create (9547))
{
m_LogCtrl.AddString ("等待连接......");
ServSock->Listen ();
}
else
{
m_LogCtrl.AddString ("初始化失败,请重新启动程序!");
delete ServSock;
}
}
else
{
m_LogCtrl.AddString ("初始化失败,请重新启动程序!");
}
上述代码主要是创建并初始化ServSock套接字,并开始侦听连接请求。
6)接受连接请求。由于是CServSocket类的ServSock对象在侦听连接请求,因此由该类来接受连接请求。
首先,在ServSocket.h文件中加入如下语句:
#iinclude “RecvSocket.h”
然后,重载该类的OnAccept函数,在该函数中添加如下代码:
CRecvSocket *tempSock;
if(tempSock=new CRecvSocket(this->m_Dlg ))
{
if(Accept(*tempSock))
{
tempSock->m_File =new CSocketFile(tempSock);
tempSock->m_ArIn =new CArchive(tempSock->m_File ,CArchive::load );
tempSock->m_ArOut =new CArchive(tempSock->m_File ,CArchive::store );
m_Dlg->RecvSock =tempSock;
tempSock=NULL;
m_Dlg->m_LogCtrl .AddString ("连接成功,可以开始传递消息");
m_Dlg->m_SendCtrl.EnableWindow (true);
}
else
{
m_Dlg->m_LogCtrl .AddString ("客户端当前的连接尝试失败");
delete tempSock;
}
}
else
{
m_Dlg->m_LogCtrl .AddString ("连接套接字初始化失败");
}
上述代码首先调用Accept函数接受连接请求,然后为该连接创建一个CRecvSocket类型的套接字,并为该套接字关联CArchive对象,使其能实现串行化传输信息的功能。最后把关联好的套接字传回给对话框对象供其使用。这样,对话框对象的成员变量RecvSock套接字便与客户端套接字之间建立了一条信息通道,信息将在两个套接字之间传递。
7)接收信息,连接建立成功后,当有信息到达服务器端时,就会引发RecvSock套接字对象的OnReceive函数,因此需要重载CRecvSocket类的OnReceive函数。添加代码如下:
CString str;
(*m_ArIn)>>str;
m_Dlg->m_LogCtrl .AddString ("对方发来的信息如下:");
m_Dlg->m_LogCtrl .AddString (str);
m_Dlg->m_LogCtrl .SetCurSel (m_Dlg->m_LogCtrl .GetCount() - 1);
8)发送信息。为对话框“发送”按钮添加事件处理函数OnBnClickedSend(),代码如下:
void CServerDemoDlg::OnBnClickedSend()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
m_MsgCtrl.GetWindowText (str);
if(str.GetLength ()==0)
AfxMessageBox("空信息,所以不发出");
else
{
m_LogCtrl.AddString ("你发出的信息如下:");
m_LogCtrl.AddString (str);
m_LogCtrl.SetCurSel (m_LogCtrl.GetCount ()-1);
*(RecvSock->m_ArOut )<
RecvSock->m_ArOut ->Flush ();
}
}
(二)客户端
1)界面
列表框控件IDC_Log属性“sort”值为false,属性“Horizontal Scroll”值为true。
按钮IDC_Send属性Disable值为true。
2)创建套接字类(从CSocket类派生)。客户端只需要一个套接字,命名为CClientSocket。
3)建立对话框类与套接字类的关联。
首先,在ClientDemoDlg.h文件的开头,语句#pragma once后面加入如下语句:
#include “ClientSocket.h”
然后,在该文件中为CClientDemoDlg类添加一个公有成员变量,语句如下:
CClientSocket *ClientSock;
接着,在ClientSocket.h文件的开头,语句#pragma once后面加入如下语句:
class CClientDemoDlg;
然后,在该文件中为CClientSocket类添加一公有成员变量和一个构造函数,语句如下:
CClientSocket(CClientDemoDlg *Dlg);
CClientDemoDlg *m_Dlg;
接着,在ClientSocket.cpp文件中添加新的构造函数的实现代码,并添加一条关于CClientDemoDlg.h文件的预编译声明,代码如下:
#include "ClientDemoDlg.h"
CClientSocket::CClientSocket(CClientDemoDlg *Dlg)
{
m_Dlg=Dlg;
}
这样,便完成了对话框和套接字之间的连接了。
4)为套接字添加串行化读写信息的功能。在ClientSocket.h文件中,为类CClientSocket添加三个公有成员变量,代码如下:
CSocketFile *m_File;
CArchive *m_ArIn;
CArchive *m_ArOut;
5)在对话框中初始化套接字并建立连接
在对话框类的OnInitDialog函数中添加如下代码
// TODO: 在此添加额外的初始化代码
m_LogCtrl.AddString ("正在连接......");
if(ClientSock=new CClientSocket(this))
{
if(ClientSock->Create())
{
if(ClientSock->Connect ("localhost",9547))
{
ClientSock->m_File =new CSocketFile(ClientSock);
ClientSock->m_ArIn =new CArchive(ClientSock->m_File ,CArchive::load );
ClientSock->m_ArOut =new CArchive(ClientSock->m_File,CArchive::store );
m_LogCtrl.AddString ("连接成功,可以开始传递消息");
m_SendCtrl.EnableWindow (true);
}
else
{
m_LogCtrl.AddString ("连接不成功");
delete ClientSock;
}
}
else
{
m_LogCtrl.AddString ("初始化失败,请重新启动程序");
delete ClientSock;
}
}
else
{
m_LogCtrl.AddString ("初始化失败,请重新启动程序");
}
6)接收消息。消息到来时,会引发套接字的OnReceive消息,因此要重载CClientSocket类的OnReceive函数,在其中添加代码如下
// TODO: 在此添加专用代码和/或调用基类
CString str;
m_Dlg->m_LogCtrl .AddString ("对方发来消息如下:");
*m_ArIn>>str;
m_Dlg->m_LogCtrl .AddString (str);
m_Dlg->m_LogCtrl .SetCurSel (m_Dlg->m_LogCtrl .GetCount ()-1);
7)发送信息。为对话框“发送”按钮添加事件处理函数OnBnClickedSend(),代码如下:
void CClientDemoDlg::OnBnClickedSend()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
m_MsgCtrl.GetWindowText (str);
if(str.GetLength ()==0)
AfxMessageBox("空信息,所以不发出");
else
{
m_LogCtrl.AddString ("你发的信息如下:");
m_LogCtrl.AddString (str);
m_LogCtrl.SetCurSel (m_LogCtrl.GetCount ()-1);
*(ClientSock->m_ArOut )<
ClientSock->m_ArOut ->Flush ();
}
}
然后,介绍文件传输的放式:
对于传输文件来说容易出错的地方就是无法完整的接收文件,所以在实现的过程中使用了多个变量来控制每一个文件块能完全接收。
第一先传送文件的路径和信息:
fileLength = sourceFile.GetLength();
fileLength = htonl( fileLength );
cbLeftToSend = sizeof( fileLength );
do
{
int cbBytesSent;
BYTE* bp = (BYTE*)(&fileLength) + sizeof(fileLength) - cbLeftToSend;
cbBytesSent = sockConnection.Send( bp, cbLeftToSend );
// test for errors and get out if they occurred
if ( cbBytesSent == SOCKET_ERROR )
{
int iErr = ::GetLastError();
TRACE( "SendFileToRemoteRecipient returned a socket error while sending file length/n"
"/tNumber of Bytes sent = %d/n"
"/tGetLastError = %d/n", cbBytesSent, iErr );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
// data was successfully sent, so account for it with already-sent data
cbLeftToSend -= cbBytesSent;
}
while ( cbLeftToSend>0 );
第二传送文件内容:
sendData = new BYTE[SEND_BUFFER_SIZE];
cbLeftToSend = sourceFile.GetLength();
do
{
// read next chunk of SEND_BUFFER_SIZE bytes from file
int sendThisTime, doneSoFar, buffOffset;
sendThisTime = sourceFile.Read( sendData, SEND_BUFFER_SIZE );
buffOffset = 0;
do
{
doneSoFar = sockConnection.Send( sendData + buffOffset, sendThisTime );
// test for errors and get out if they occurred
if ( doneSoFar == SOCKET_ERROR )
{
int iErr = ::GetLastError();
TRACE( "SendFileToRemoteRecipient returned a socket error while sending chunked file data/n"
"/tNumber of Bytes sent = %d/n"
"/tGetLastError = %d/n", doneSoFar, iErr );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
buffOffset += doneSoFar;
sendThisTime -= doneSoFar;
cbLeftToSend -= doneSoFar;
}
while ( sendThisTime > 0 );
}
while ( cbLeftToSend > 0 );
PreReturnCleanup: // labelled goto destination
// free allocated memory
// if we got here from a goto that skipped allocation, delete of NULL pointer
// is permissible under C++ standard and is harmless
delete[] sendData;
if ( bFileIsOpen )
sourceFile.Close(); // only close file if it's open (open might have failed above)
sockConnection.Close();
MessageBox(L"文件传送完毕");
四、运行结果
五、课程设计体会
通过对该课程的设计的逐步理解,加深了自己对于socket编程的熟悉和理解,大致清楚的了解使用socket编程的过程,也使自己掌握了一些
额外的知识比如使用加速键来实现对键盘输入的控制等。
对于老师的努力我们可以感受到,在这里表示感谢。