CCESocket:适用于 WinCE 的通用 TCP/UDP 套接字类






4.91/5 (23投票s)
本文介绍了一个专门为 WinCE (Pocket PC) 平台设计的全新套接字包装类,该类解决了异步通知等问题。
目录
简介
该类旨在取代 WinCE (Pocket PC) 平台上存在 bug 的 MFC CCeSocket
类。但是,它在 Win32 系统(在 Win2K 和 WinXP 上进行了测试)上也能正常工作。
它允许创建 TCP 或 UDP 套接字类型,可配置为客户端或服务器模式,集成了动态缓冲区来存储所有传入数据以便延迟访问,并提供异步事件通知。
以下情况会触发异步通知:
- 传入数据
- 连接关闭
- 已接受的客户端(仅限 TCP)
要读取传入数据,有四种选择:
- 直接读取:数据到达时即读取。这样,数据就不会存储在内部缓冲区中。
- 二进制读取。
- 文本读取:可以读取单个字符串。
- 数据包读取:您可以读取单个数据包,就像它们被接收时一样。
发送数据时,您可以选择:
- 二进制模式。
- 字符串模式。
- 带有自动 EOL 终止的字符串模式。
其他函数允许您指定字符串 EOL 格式,查询套接字状态,更改 UDP 或 TCP 接收缓冲区,以及查询内部数据缓冲区状态。
背景
当我第一次为 PDA 编写网络应用程序时,我意识到 MFC 附带的 CCeSocket
完全无用。它不仅缺乏异步通知,而且消耗的资源比实际需要的多,并且必须将其包装在 CSocketFile
/CArchive
框架中才能使用高级加载/存储操作。因此,我决定为 WinCE OS 编写一个全新的套接字包装器,它体积小巧、高效、通用,并具有高级读/写函数。我在 RoboCup Robot@Home 联赛的人形机器人接口中广泛使用它。
代码使用
在详细解释类的每个函数的用法之前,我认为您应该知道 CCESocket
使用两个独立的线程:一个用于接收数据,另一个用于接受连接(如果套接字正在接受连接)。这两个线程都使用阻塞调用 Winsock API 函数来最小化 CPU 使用率。您应该意识到这些线程的存在,因为它们会触发事件(虚拟函数,请参阅 通知),因此当您收到事件时,您所在的线程与主线程不同。
创建套接字
要使用 CCESocket
,您首先需要调用 Create
函数。
bool Create(int socketType, int bufferSize = 0); //Examples: mySocket->Create(SOCK_STREAM); mySocket->Create(SOCK_DGRAM); mySocket->Create(SOCK_STREAM, 4096);
socketType
:可以是SOCK_STREAM
(TCP),或SOCK_DGRAM
(UDP)。bufferSize
:您可以指定传入数据包的缓冲区大小。否则,将使用默认值:TCP 为 1024 字节,UDP 为 2048 字节。
返回值是 TRUE
,表示套接字创建成功;FALSE
表示发生错误。您始终可以使用以下函数查询最后一个错误代码:
int GetLastError() //returned error is obtained from a WSAGetLastError() call
现在,您可以决定创建客户端套接字还是服务器套接字。
创建客户端套接字
要创建客户端套接字,请调用:
bool Connect(CString &addr, UINT remotePort); //Examples: mySocket->Connect("192.168.0.20", 3000); mySocket->Connect("www.someServer.com", 5003);
addr
:是远程主机地址。它可以是 IP 地址或主机名。remotePort
:是要连接到的远程主机端口。
返回值是 TRUE
,表示连接成功建立;FALSE
表示发生错误。
- TCP 套接字:此函数实际连接两个计算机。在这种情况下,如果您想建立新连接,则必须先 断开 套接字连接。
- UDP 套接字:
CCESocket
不建立实际连接,它仅保存已保存的地址以发送数据。在这种情况下,您可以在不先断开连接的情况下重新连接到另一个地址。
现在,您可以使用套接字 发送 和 读取 数据。但是,您应该先了解 通知(事件)的工作原理,否则您将不知道何时有新数据可供读取,或者您的套接字是否因某种原因断开连接。
创建服务器套接字
要创建服务器套接字,请调用:
bool Accept(UINT localPort, int maxConn = SOMAXCONN); //Examples: mySocket->Accept(3000); mySocket->Accept(5003, 1);
localPort
:是用于传入请求的本地端口。maxConn
:是挂起连接队列的最大长度。
返回值是 TRUE
,表示 Accept
成功;FALSE
表示发生错误。此函数将套接字绑定到本地端口以等待连接。
- UDP 套接字:当从客户端接收到 UDP 数据包时,
CCESocket
将触发正常的OnReceive
事件(请参阅 通知)。接收到此数据后,您可以使用同一套接字 发送 数据到远程客户端。UDP 是盲目的,您永远不知道是否真的有人在监听,也不需要像 TCP 那样需要服务套接字。 - TCP 套接字:当接受新连接时,
CCESocket
将触发OnAccept
事件。virtual bool OnAccept(SOCKET serviceSocket)
它提供了一个服务套接字,可用于接受连接。
您会注意到这是一个虚拟函数。所有事件都是虚拟函数。事实上,您不能直接使用 CCESocket
,必须对其进行子类化并为要捕获的事件重新定义虚拟函数。
因此,当 CCESocket
调用您的 OnAccept
函数时,您可以拒绝连接(返回 FALSE
)或接受连接(返回 TRUE
)。如果您接受连接,则必须创建一个新的(子类化的)CCESocket
并将其服务套接字传递给它的 AcceptServiceSocket
函数(您不必先调用 Create
函数)。
void AcceptServiceSocket(SOCKET serviceSocket);
一个快速示例(用于 TCP)
这是一个快速示例,用于说明 OnAccept
/AcceptServiceSocket
函数。
//This is a subclassed CCESocket class CMySocket : public CCESocket { public: CMySocket(); CMySocket(CWnd* parent); virtual ~CMySocket(); void SetParent(CWnd* parent) {m_parent = parent;} virtual bool OnAccept(SOCKET serviceSocket); virtual void OnReceive(); virtual void OnClose(int closeEvent); protected: CWnd* m_parent; }; CMySocket::CMySocket() : CCESocket() { m_parent = NULL; } CMySocket::CMySocket(CWnd* parent) : CCESocket() { m_parent = parent; } CMySocket::~CMySocket() { } bool CMySocket::OnAccept(SOCKET serviceSocket) { if(m_parent) { ::PostMessage(m_parent->m_hWnd, ON_ACCEPT, (WPARAM) serviceSocket, (LPARAM) this); return TRUE; } return FALSE; } void CMySocket::OnReceive() { if(m_parent) ::PostMessage(m_parent->m_hWnd, ON_RECEIVE, NULL, (LPARAM) this); } void CMySocket::OnClose(int closeEvent) { if(m_parent) ::PostMessage(m_parent->m_hWnd, ON_CLOSE, (WPARAM) closeEvent, (LPARAM) this); }
ON_ACCEPT
、ON_RECEIVE
和 ON_CLOSE
是传递给主应用程序的窗口消息。后者可能看起来像这样(注意:代码很粗糙,仅用于展示设置服务器所需的步骤):
class MyApp : public CDialog { public: ... afx_msg LRESULT OnAccept(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnReceiveData(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnDisconnected(WPARAM wParam, LPARAM lParam); ... protected: CMySocket *m_server; CMySocket *m_serviceSocket; ... }; BOOL MyApp::OnInitDialog() { bool serverStarted; ... m_server = new CMySocket(this); if(serverStarted = m_server->Create(SOCK_STREAM)) serverStarted = m_server->Accept(somePortNumber); if(!serverStarted) //Recovery code ... } LRESULT MyApp::OnAccept(WPARAM wParam, LPARAM lParam) { m_serviceSocket = new CMySocket(this); m_serviceSocket->AcceptServiceSocket((SOCKET) wParam); m_serviceSocket->SendLine("Hello!"); return 0; }
OnAccept
、OnReceiveData
和 OnDisconnect
由 CMySocket
发布的 ON_ACCEPT
、ON_RECEIVE
和 ON_CLOSE
事件触发。但是,在此示例中,我仅定义了 OnAccept
函数。我认为代码非常简单,不需要任何注释 :-)
断开连接
要断开套接字连接,您可以调用:
void Disconnect(); //Example: mySocket->Disconnect();
断开连接后,如果您想再次使用该套接字,则必须再次调用 Create
。
通知
我们已经看到了 OnAccept
事件。现在让我们分析 OnReceive
和 OnClose
。要接收这些事件,您必须对 CCESocket
进行子类化并提供新的虚拟函数,如 CMySocket
示例类中所示。
一旦收到新数据,就会调用以下事件(如果您重新定义了它的虚拟函数):
virtual bool OnReceive(char* buf, int len);
buf
:是接收到的数据包。len
:是数据包的长度。
此通知直接从接收线程调用,以通知队列中有新数据。这是此类通知的第一个,并提供了在不缓冲数据的情况下获取数据的可能性。如果您接受数据包,则返回 TRUE
。在这种情况下,您负责在使用后删除缓冲区。如果您拒绝数据,则返回 FALSE
。在这种情况下,数据包将被存储在内部缓冲区中,您将收到新的通知。
virtual void OnReceive();
此事件仅通知您缓冲区中有数据。稍后我将展示如何从缓冲区 读取 数据。
如果连接因任何原因关闭,您将收到:
virtual void OnClose(int closeEvent);
closeEvent
:这是一个枚举,可以是:CCESocket::EVN_CONNCLOSED
(=0)CCESocket::EVN_CONNLOST
(=1)CCESocket::EVN_SERVERDOWN
(=2)
- TCP 套接字:如果对方断开连接,您会收到
EVN_CONNCLOSED
。如果您是客户端并希望建立新连接,请调用Create
然后调用Connect
。如果您是服务器,则只需释放(删除)服务套接字。 - UDP 套接字:如果对方“断开连接”,您会收到
EVN_CONNLOST
。据我所知,这不应该发生,但这是我使用 Winsock API 实验出的结果。如果您是客户端并想建立新连接,请调用Connect
。如果您是服务器,您可以处理或忽略此事件,它不会干扰服务器操作。请注意,如果您继续向已断开连接的对端发送数据,CCESocket
将忽略您的尝试,并且 发送 函数将返回 0 个已发送字节。
如果您的套接字是服务器,并且由于某种原因,读取(UDP)或接受(TCP)线程失败,TCP 和 UDP 套接字都将返回 EVN_SERVERDOWN
事件。在这种情况下,要重新启动服务器,您必须调用 Create
(仅限 TCP)然后调用 Accept
。
//Respawning a TCP server: (OnDisconnected //implementation of MyApp example class) LRESULT MyApp::OnDisconnected(WPARAM wParam, LPARAM lParam) { CCESocket *disconnectedSocket = (CCESocket*) lParam; if(disconnectedSocket == m_server) { Sleep(100); if(m_server->Create(SOCK_STREAM)) if(m_server->Accept(somePortNumber)) return 0; //Recovery code } return 0; }
如开头所述,请记住,这些函数是从另一个线程调用的,而不是从主应用程序线程调用的。如果您需要在窗口线程中执行某些操作,则应使用 PostMessage
将消息发送到该线程(如 CMySocket
示例所示)。MFC 对象需要这样做。它们在线程之间传递时不起作用,您应该在定义它们的同一线程中使用这些对象。
读取数据
您已经知道四种读取数据的方式之一:通过第一个 OnReceive
事件进行直接读取。其余函数直接访问内部缓冲区中的数据。通常,在收到 OnReceive
事件后,您会调用其中一个。即使套接字断开连接,数据仍然可用。
您可以使用以下方法读取二进制数据:
int Read(char* buf, int len); //Example: char *buf = NULL; int count; int len = mySocket->GetDataSize(); if(len > 0) { buf = new char[len]; count = mySocket->Read(buf, len); }
buf
:将接收数据的缓冲区。它必须已分配。len
:缓冲区大小。
返回值是实际读取的字节数。
要读取字符串,请使用以下函数:
bool ReadString(CString &str); //Example: CString str; while(mySocket->GetDataSize() > 0 && mySocket->ReadString(str)) doSomethingWithTheString(str);
str
:是要读取的字符串。
返回值是 TRUE
,表示字符串已读取;FALSE
表示没有字符串可读取。字符串是字节数组,从缓冲区当前位置开始,并以 EOL 终止。此值可以使用 SetEolFormat
函数(稍后查看)进行设置。
要读取缓冲区中存储的第一个数据包,请使用:
bool GetPacket(char*& buf, int* len); //Example: char *buf = NULL; int len; while(mySocket->GetNumPackets() > 0) { if(mySocket->GetPacket(buf, &len)) useTheData(buf, len); if(buf != NULL) delete[] buf; }
buf
:指向将包含数据包数据的缓冲区的指针。缓冲区将根据数据包大小进行内部分配。您必须在使用后删除它。len
:将包含缓冲区大小。
返回值是 TRUE
,表示数据包已成功检索;FALSE
表示没有数据包可检索。
发送数据
您有三个函数可以发送数据。
二进制发送
int Send(const char* buf, int len); //Example: int count; count = mySocket->Send(myBuffer, myBufferSize);
buf
:包含要发送数据的缓冲区。len
:缓冲区大小。
返回值是发送的字节数,如果发生错误则返回 SOCKET_ERROR
。注意:您不能使用正在侦听的 TCP 套接字发送数据。
要发送字符串,请使用以下函数之一:
int Send(CString& str); int SendLine(CString &str); //Examples: CString msg = "My beautiful string"; mySocket->Send(msg); mySocket->Send("Hello!"); mySocket->SendLine(msg);
两者都发送字符串 str
。但是,后者会在字符串后添加一个 EOL。EOL 使用 SetEolFormat
函数(稍后查看)设置。
其他功能
int GetDataSize();
返回缓冲区中排队的总数据大小。
int GetNumPackets();
返回缓冲区中排队的数据包数量。
int GetPacketSize();
返回队列中第一个数据包的大小(下一个要检索的数据包)。
int GetLastError(); //returned error is obtained from a WSAGetLastError() call
返回上一个错误的错误代码。
int GetSocketType();
返回 SOCK_STREAM
(TCP 套接字)或 SOCK_DGRAM
(UDP 套接字)。
int GetSocketState();
返回以下状态之一:
CCESocket::NONE
(=0)CCESocket::DISCONNECTED
(=1)CCESocket::CREATED
(=2)CCESocket::CONNECTED
(=3)CCESocket::ACCEPTING
(=4)
void SetBufferSize(int bufSize);
bufSize
:新的缓冲区大小。
调用此函数以修改缓冲区大小。您可以随时调用它,即使套接字已连接。如果您想在调用 AcceptServiceSocket
后更改缓冲区大小,这很有用。
void SetEolFormat(eolFormat eol);
eolFormat
:可以是以下之一:CCESocket::EOL_NULL
(=0)CCESocket::EOL_LFCR
(=1)CCESocket::EOL_CR
(=2)
为 SendLine
和 ReadString
函数设置新的 EOL 格式。