[声明:本文章来源于网络,原作者不详!]
利用Socket编程是一类典型的网络通信程序,特别是在实时性要求比较高的项目中,Winsock编程方法是非常实用的。
下面介绍在VC 6.0环境下开发Winsock程序的方法。
这里并没有直接应用MFC 提供的CSocket类,这是因为考虑到对于类而言,其成员函数调用必然是完全阻塞方式的,因此只能用于人工线程中。
基于这种思想,可以在CObject类基础上派生一个套接字类,其使用方式为阻塞方式,
虽然增加了使用的条件,但可以保证其正常工作,而不会出现不加控制地使用CSocket对象带来的冲突现象。
下面首先将具体介绍有关的套接字类的定义,新创建的套接字功能主要通过调用CSocket的相关操作实现。
1. 套接字类CBlockingSocket
首先需要定义此套接字类,在类中设置了一个属性变量:SOCKET m_hSocket;
m_hSocket 表示套接字的句柄。
另外还构造了一组方法,其功能与CSocket类是对应的,下面以创建、监听、连接建立和消息的接收和发送为例,介绍其实现方法,。
创建
创建套接字即要求创建相应的连接,缺省类型为面向连接的流,具体实现为:
void CBlockingSocket::Create(int nType)
{
ASSERT(m_hSocket == NULL);
if((m_hSocket = socket(AF_INET, nType, 0)) == INVALID_SOCKET)
{
throw new CBlockingSocketException("创建套接字");
}
}
监听
Listen函数完成监听连接的任务,在实现时要求最多有10个连接请求排队,这在一般的应用中是完全足够的。
void CBlockingSocket::Listen()
{
ASSERT(m_hSocket != NULL);
if(listen(m_hSocket, 10) == SOCKET_ERROR)
{
throw new CBlockingSocketException("Listen");
}
}
建立连接
连接的实际建立可以由Connect实现,同样地,缺省的建立方式为面向连接的流。
void CBlockingSocket::Create(int nType )
{
ASSERT(m_hSocket == NULL);
if((m_hSocket = socket(AF_INET, nType, 0)) == INVALID_SOCKET)
{
throw new CBlockingSocketException("创建套接字");
}
}
发送消息
Send函数的作用是将数据块按一个消息发送,参数pch即为发送的消息,nSize为消息长度,nSecs可以限制操作时间。如果客户方取消读操作,则返回值将小于指定消息长度。
int CBlockingSocket::Send(const char* pch, const int nSize, const int nSecs)
{
ASSERT(m_hSocket != NULL);
FD_SET fd = {1, m_hSocket};
TIMEVAL tv = {nSecs, 0};
if(select(0, NULL, &fd, NULL, &tv) == 0)
{
throw new CBlockingSocketException("发送超时");
}
if((int nBytesSent = send(m_hSocket, pch, nSize, 0)) == SOCKET_ERROR)
{
throw new CBlockingSocketException("发送");
}
return nBytesSent;
}
此外,如果数据块比较大,可以将数据块分成多个消息发送,此工作由函数Write完成。具体实现时将通过循环调用Send函数来实现部分消息发送,通过对局部量nBytesThisTime 和nBytesSent的维护,保证整个数据块的正常发送。
int CBlockingSocket::Write(const char* pch, const int nSize, const int nSecs)
{
int nBytesSent = 0,nBytesThisTime;
const char* pch1 = pch;
do
{
nBytesThisTime = Send(pch1, nSize - nBytesSent, nSecs);
nBytesSent += nBytesThisTime;
pch1 += nBytesThisTime;
} while(nBytesSent < nSize);
return nBytesSent;
}
接收消息
Receive函数的作用是与发送消息对应的,可以将接收到的消息重组为数据块。
int CBlockingSocket::Receive(char* pch, const int nSize, const int nSecs)
{
ASSERT(m_hSocket != NULL);
FD_SET fd = {1, m_hSocket};
TIMEVAL tv = {nSecs, 0};
if(select(0, &fd, NULL, NULL, &tv) == 0)
{
throw new CBlockingSocketException("接收超时");
}
if((int nBytesReceived = recv(m_hSocket, pch, nSize, 0)) == SOCKET_ERROR)
{
throw new CBlockingSocketException("接收");
}
return nBytesReceived;
}
2.地址类CSockAddr
在应用中还实现了一个Winsock地址类,它继承了sockaddr_in结构的主要属性:
属性 |
类型 |
含义 |
Sin_family |
short |
协议类别 |
Sin_port |
u_short |
端口 |
Sin_addr |
in_addr |
地址 |
CSockAddr类有多种构造函数,如协议类别默认为AF_INET,端口号和地址均初始化为0,比较常用的构造函数实现如下,其中IP地址串参数已经是网络地址顺序形式。
CSockAddr(const char* pchIP, const USHORT ushPort = 0)
{
sin_family = AF_INET;
sin_port = htons(ushPort);
sin_addr.s_addr = inet_addr(pchIP);
}
CSockAddr还包括一组成员函数,这些成员函数主要是对属性的存取。如按点分十进制方式返回地址、获得端口、获得地址等等。
3 套接字异常类CBlockingSocketException
CBlockingSocketException用于处理套接字阻塞异常,可由CException类派生实现。
类中定义了两个属性变量:
m_nError 表示错误代码,
m_strMessage 表示错误信息。
在截获到异常时,需要利用这两个属性设置提示信息。异常消息的获得由GetErrorMessage函数完成,实现为:
BOOL CBlockingSocketException::GetErrorMessage(LPTSTR lpstrError, UINT nMaxError,PUINT pnHelpContext)
{
char text[200];
if(m_nError == 0)
wsprintf(text, "%s 错误", (const char*) m_strMessage);
else
wsprintf(text, "%s 错误 #%d", (const char*) m_strMessage, m_nError);
strncpy(lpstrError, text, nMaxError - 1);
return TRUE;
}
4. 服务器应用程序
为了利用套接字实现服务器应用程序,需要在应用程序类的初始化实例函数中增加以下代码,进行套接字的初始化工作:
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
我们的目的是当客户发出请求后,在服务器应用程序端显示相应的信息,同时向客户返回确认信息。
首先创建一个全局的套接字对象:CBlockingSocket g_sListen;
另外需要重新实现视类的初始化函数,先建立服务器的地址,设置通令端口为5858,这并不是必要的,只要保证客户方的通信端口与服务器的端口一致就可以了。
然后创建g_sListen,并将其绑定到服务器上,调用套接字的监听操作,若监听到客户请求,将创建一个新的线程,并在此线程中处理客户请求。
void CTestsockView::OnInitialUpdate()
{
CEditView::OnInitialUpdate();
try
{
CSockAddr saServer;
saServer = CSockAddr(INADDR_ANY, (USHORT) 5858);
g_sListen.Create();
g_sListen.Bind(saServer);
g_sListen.Listen();// start listening
g_bListening = TRUE;
g_nConnection = 0;
AfxBeginThread(ServerThreadProc,GetSafeHwnd(), HREAD_PRIORITY_NORMAL);
}
catch(CBlockingSocketException* e)
{
g_sListen.Cleanup();
LogBlockingSocketException(GetSafeHwnd(), "VIEW:", e);
e->Delete();
}
}
下面,再来看线程函数ServerThreadProc是如何处理客户请求的:
UINT ServerThreadProc(LPVOID pParam)
{
CSockAddr saClient;
CBlockingSocket sConnect;
try
{
if(!g_sListen.Accept(sConnect, saClient))
{
g_bListening = FALSE;
return 0;
}
g_nConnection++;
AfxBeginThread(ServerThreadProc, pParam, THREAD_PRIORITY_NORMAL);
DoRequest(pParam, sConnect, saClient);
sConnect.Close();
}// 析构函数不能关闭它
catch(CBlockingSocketException* pe)
{
LogBlockingSocketException(pParam, "服务器:", pe);
pe->Delete();
}
return 0;
}
如果g_sListen调用Accept失败,说明视或应用程序关闭了连接的套接字,此时将调整当前状态,在具体处理客户请求之前,为了不影响继续接收其它客户的请求,可以再创建新的线程,
即构造多线程,每个线程处理自已的具体事务。
具体处理工作由DoRequest函数完成,由于析构函数不能关闭临时建立的套接字,所以在处理完之后,需要主动关闭。
DoRequest的工作包括在服务器应用视中显示相应的提示语句,这是通过向其发送消息完成的,
另外还将显示客户发送的信息,信息由套接字的Receive()函数获得,同样通过向窗口发送消息完成,这里的消息发送窗口由参数pParam确定。
最后,调用套接字的.Send()函数向客户端发送一条简短的确认信息。
void DoRequest(LPVOID pParam, CBlockingSocket& sockCon, LPCSOCKADDR psa)
{
char inbuff[500],text1[200];
wsprintf(text1, "建立连接\r\n");
::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM) 500, 65535);
::SendMessage((HWND) pParam, EM_REPLACESEL, (WPARAM) 0, (LPARAM) text1);
try
{
int len=sockCon.Receive(inbuff,500,20);
inbuff[len]='\0';
::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM) 500, 65535);
::SendMessage((HWND)pParam,EM_REPLACESEL,(WPARAM)0,(LPARAM)inbuff);
sockCon.Send("ok!\r\nBye!",9,5);
}catch(CBlockingSocketException* pe)
{
LogBlockingSocketException(pParam, "服务器:", pe);
pe->Delete();
}
}
5. 客户端应用程序
在客户端应用程序中同样使用了前面介绍的套接字类CBlockingSocket,Winsock地址类CSockAddr和套接字异常类CBlockingSocketException。
这里将主要介绍客户如何建立与服务器的连接,如何向服务器发送消息,以及如何接收和处理服务器返回的消息。
为了实现与服务器的连接,需要在菜单上增加一个连接项,资源标识为ID_BEGIN_LINK,消息映射设置为ON_COMMAND(ID_BEGIN_LINK, OnBeginLink),
下面再来分析消息处理函数OnBeginLink的实现:
void CTestclientsockView::OnBeginLink()
{
CBlockingSocket sClient;
char inbuff[200];
try
{
CSockAddr saClient("146.127.35.70",5858);
sClient.Create();
sClient.Connect(saClient);
sClient.Send("hello ,你好!\n",5,5);
int len=sClient.Receive(inbuff,200,20);
inbuff[len]='\0';
SendMessage(EM_SETSEL, (WPARAM) 500, 65535);
SendMessage(EM_REPLACESEL, (WPARAM) 0, (LPARAM) inbuff);
sClient.Close();
}
catch(CBlockingSocketException* e)
{
sClient.Cleanup();
LogBlockingSocketException(GetSafeHwnd(), "VIEW:", e);
e->Delete();
}
}
首先需要建立与服务器的连接,这里创建了一个地址对象saClient,在创建时提供了两个参数,其中“146.127.35.70”为服务器的IP地址,另外将端口号设置为5858,
即与服务器应用程序保持一致。接下来创建一个客户端的套接字对象,并通过调用函数Connect建立与服务器的连接。
连接建立后,可以向服务器发送信息了,这里用套接字的函数Send()发送了一条简短的欢迎信息,同时调用Receive()函数接收服务器返回的信息。
最后向视发送显示消息,将服务器所返回的确认信息显示在主窗口中。
这里还需要说明的是,如果在连接或通信过程中出现异常,无论是客户应用程序还是服务器应用程序都将做出相应的响应。
具体的处理工作由函数LogBlockingSocketException实现,这里的参数pParam 为拥有目的窗口的 HWND ,这是由另一个线程提供的。
void LogBlockingSocketException(LPVOID pParam, char* pch, CBlockingSocketException* pe)
{
CString strGmt = CTime::GetCurrentTime().FormatGmt("%m/%d/%y %H:%M:%S GMT");
char text1[200], text2[50];
pe->GetErrorMessage(text2, 49);
wsprintf(text1, "WINSOCK 错误--%s %s -- %s\r\n", pch, text2, (const char*) strGmt);
::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM) 65534, 65535);
::SendMessage((HWND) pParam, EM_REPLACESEL, (WPARAM) 0, (LPARAM) text1);
}
通过调用套接字异常的GetErrorMessage函数可以得到具体的错误信息,同样通过向窗口发送消息的方法,可以将错误信息显示在主窗口中。