65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (23投票s)

2006 年 6 月 27 日

CPOL

9分钟阅读

viewsIcon

164539

downloadIcon

6802

本文介绍了一个专门为 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_ACCEPTON_RECEIVEON_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;
}

OnAcceptOnReceiveDataOnDisconnectCMySocket 发布的 ON_ACCEPTON_RECEIVEON_CLOSE 事件触发。但是,在此示例中,我仅定义了 OnAccept 函数。我认为代码非常简单,不需要任何注释 :-)

断开连接

要断开套接字连接,您可以调用:

void Disconnect();

//Example:
mySocket->Disconnect();

断开连接后,如果您想再次使用该套接字,则必须再次调用 Create

通知

我们已经看到了 OnAccept 事件。现在让我们分析 OnReceiveOnClose。要接收这些事件,您必须对 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)

SendLineReadString 函数设置新的 EOL 格式。

© . All rights reserved.