18.2 Socket及其基本处理介绍

让我们直接开始一个基于事件的socket客户机和服务器的例子,作为对wxWidgets中socket编程的介绍.代码是相当直观的,只需要你有一点最基础的socket编程的背景.为了简洁起见,所有GUI操作的部分将被省略,我们只关注那些Socket有关的函数.完整的代码可以在光盘的 examples/chap18目录中找到.例子中用到的socket API都附有详细的使用手册.

这个例子程序的功能是很简单的,服务器倾听连接请求,当有客户端建立连接的时候,服务器首先从socket上接收10个字符,然后再把这10个字符发送回去.相应的,客户端在建立连接以后先发送10个字符,然后等待接收10个响应字符.在例子中,这10个字符写死为 "0123456789".服务器端和客户端的程序运行的样子如下图所示:

客户端的代码

下面列出了客户端的关键代码

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
    EVT_MENU(CLIENT_CONNECT, MyFrame::OnConnectToServer)
    EVT_SOCKET(SOCKET_ID,    MyFrame::OnSocketEvent)
END_EVENT_TABLE()
void MyFrame::OnConnectToServer(wxCommandEvent& WXUNUSED(event))
{
    wxIPV4address addr;
    addr.Hostname(wxT("localhost"));
    addr.Service(3000);
    // 创建Socket
    wxSocketClient* Socket = new wxSocketClient();
    // 设置要监视的Socket事件
    Socket->SetEventHandler(*this, SOCKET_ID);
    Socket->SetNotify(wxSOCKET_CONNECTION_FLAG |
                                        wxSOCKET_INPUT_FLAG |
                                        wxSOCKET_LOST_FLAG);
    Socket->Notify(true);
    // 等待连接事件
    Socket->Connect(addr, false);
}
void MyFrame::OnSocketEvent(wxSocketEvent& event)
{
    // 从事件获取socket
    wxSocketBase* sock = event.GetSocket();
    // 所有事件共享的一块缓冲(Common buffer shared by the events)
    char buf[10];
    switch(event.GetSocketEvent())
    {
        case wxSOCKET_CONNECTION:
        {
            // 填充'0'-'9'的ASCII码
            char mychar = '0';
            for (int i = 0; i < 10; i++)
            {
                buf[i] = mychar++;
            }
            // 发送10个字符到对端
            sock->Write(buf, sizeof(buf));
            break;
        }
        case wxSOCKET_INPUT:
        {
            sock->Read(buf, sizeof(buf));
            break;
        }
        // 服务器在发送10个字节以后关闭了连接
        case wxSOCKET_LOST:
        {
            sock->Destroy();
            break;
        }
    }
}

服务器端代码

下面列出了服务器端的代码

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
    EVT_MENU(SERVER_START, MyFrame::OnServerStart)
    EVT_SOCKET(SERVER_ID,  MyFrame::OnServerEvent)
    EVT_SOCKET(SOCKET_ID,  MyFrame::OnSocketEvent)
END_EVENT_TABLE()
void MyFrame::OnServerStart(wxCommandEvent& WXUNUSED(event))
{
    // 创建地址,默认为localhost:0
    wxIPV4address addr;
    addr.Service(3000);
    // 创建一个Socket,保留其地址以便我们可以在需要的时候关闭它.
    m_server = new wxSocketServer(addr);
    // 检查Ok函数以判断服务器是否正常启动
    if (! m_server->Ok())
    {
        return;
    }
    // 设置我们需要监视的事件
    m_server->SetEventHandler(*this, SERVER_ID);
    m_server->SetNotify(wxSOCKET_CONNECTION_FLAG);
    m_server->Notify(true);
}
void MyFrame::OnServerEvent(wxSocketEvent& WXUNUSED(event))
{
    // 接受连接请求,并创建Socket
    wxSocketBase* sock = m_server->Accept(false);
    // 告诉这个新的Socket它的事件应该被谁处理
    sock->SetEventHandler(*this, SOCKET_ID);
    sock->SetNotify(wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG);
    sock->Notify(true);
}
void MyFrame::OnSocketEvent(wxSocketEvent& event)
{
    wxSocketBase *sock = event.GetSocket();
    // 处理事件
    switch(event.GetSocketEvent())
    {
        case wxSOCKET_INPUT:
        {
            char buf[10];
            // 读数据
            sock->Read(buf, sizeof(buf));
            // 写回数据
            sock->Write(buf, sizeof(buf));
            // 服务器接受的这个socket已经完成任务,释放它
            sock->Destroy();
            break;
        }
        case wxSOCKET_LOST:
        {
            sock->Destroy();
            break;
        }
    }
}

连接服务器

这一小段我们来解释一下怎样创建一个客户端Socket并且用它连接某个Server.

Socket地址

所有的socket地址相关的类都是基于虚类wxSockAddress,它提供了基于socket标准的所有地址相关的参数和操作.而 wxIPV4address类则具体实现了当前应用最广泛的标准国际地址方案IPV4.wxIPV6address类是用来提供IPv6支持的,不过它的功能实现的并不完整,等到IPv6在全世界范围内广泛使用的那天,这个类当然会相应的变得完整.

注意:如果地址使用的是一个长整型,那么它期待的是网络序排列方式,它返回的长整型地址也总是网络序排列方式.网络序是对应的是Big endian(Intel和AMD的x86体系使用的是little endian,而Apple的系统使用的是big endian).你可以使用字节序转换宏wxINT32_SWAP_ON_LE来进行平台无关的字节续转换,这个宏只在使用little endian的平台上才进行相应的转换工作.如下所示:

IPV4addr.Hostname(wxINT32_SWAP_ON_LE(longAddress));

Hostname可以采用的参数包括一个wxString类型的字符串(比如 www.widgets.org)或者一个长整型的IP地址(前面已经提到过,采用big endian),如果没有任何参数,则Hostname返回当前主机的主机名.

Service用来设置远端端口,你可以指定一个wxString类型的已知服务名或者直接指定一个short类型的整数.如果不带任何参数,Service返回当前指定的远端端口.

IPAddress函数返回一个十进制的以点分割的wxString类型的远端ip地址.

AnyAddress将地址设置为本机的任何IP地址,相当于将地址设置为INADDR_ANY.

Socket客户端

wxSocketClient继承自wxSocketBase并且同时继承了所有的通用Socket操作函数.新增的少数几个函数主要用来发起和建立远端连接.

Connect函数采用一个wxSockAddress参数以便知道要连接的远端地址和端口.正如前面提到的那样,你应该使用类似 wxIPV4address这样的地址而不能直接使用wxSockAddress.第二个参数是一个bool类型,默认为true,指示是否应该等连接建立再返回.如果这个函数在主线程中运行,所有的GUI都将冻结直至这个函数返回.

WaitOnConnect用来在Connect被以false作为第二个参数调用以后(不阻塞)调用.第一个参数指示要等待的秒数, 第二个参数则用来指示毫秒数.无论连接函数成功还是失败,这个函数都将返回成功.只有当连接函数返回超时的时候,这个函数才会返回失败.如果第一个参数是 -1,则代表使用默认的超时时长,通常是10分钟.也可以使用SetTimeout函数修改默认的超时时长.

Socket事件

所有的Socket事件都是使用同一个事件映射宏EVT_SOCKET指定的.

EVT_SOCKET(identifier, function)宏将标识符为identifier的事件发送给指定的函数处理.处理函数的参数类型为wxSocketEvent.

wxSocketEvent事件非常简单,内部存储了事件的标识符和对应的wxSocket对象指针,这可以避免自己保存socket指针的麻烦.

Socket事件类型

下表列出了GetSocketEvent函数可能返回的事件类型.

wxSOCKET_INPUT 指示socket上有数据可以接收.无论是socket数据缓存原本没有数据,新收到了数据,还是说原本就有数据,只是用户还没有读完,都将产生这个事件.
wxSOCKET_OUTPUT 这个事件通常在socket的Connect函数第一次连接成功或者说Accept刚刚接受了一个新的Socket的时候产生,并且通常是产生在socket的写操作失败,缓冲区的数据又从无到有的时候.
wxSOCKET_CONNECTION 对于客户端来说,用来只是Connect动作已经成功了,对于服务端来说,指示新接受了一个Socket.
wxSOCKET_LOST 用来指示接收数据时针对socket的关闭操作.这通常意味着对端已经关闭了socket.这个事件在连接失败的时候也有可能产生.

wxSocketEvent的主要成员函数

GetSocket返回指向产生这个事件的wxSocketBase对象的指针.

GetSocketEvent返回对应的上表列出的事件类型.

使用Socket事件

要处理socket事件,你需要首先指定一个事件处理器并且指定你想要处理的事件类型.wxSocketBase支持的各种事件宏,你可以在上面的服务器端例子中监听socket创建以后的代码中看到.需要注意的事,对Socket事件的设置仅对当前的socket起作用,如果你希望监听别的socket的相关事件,你需要对那个socket再次设置监听事件.

SetEventHandler函数将某个事件标识符和相应的事件处理器关联起来. 事件标识符必须和事件处理器对应的事件表中指定的标识符相对应.

SetNotify用来设置想要监听的事件,它的参数是一个bit为列表,比如wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG将监听有数据到来以及socket被关闭事件.

Notify使用一个bool类型的参数,来指示你是否想或者不想收到当前指定的事件.它的作用是让你在SetNotify之后可以不带事件指示来打开或者关闭事件监听.

Socket状态和错误提醒

在讨论数据发送和接收之前,我们先来描述一下socket状态和socket的错误提醒,以便我们在讨论数据接收的时候可以引用他们.

Close函数关闭socket,禁止随后的任何数据传输并且会通知对端socket已经被关闭.注意可能在关闭之前已经缓存了一些socket事件,因此在socket被关闭之后你可能还要准备好处理可能缓存的socket事件.

Destroy函数应该代替针对socket的delete操作,原因和Window对象类似,有可能队列中仍然有针对这个socket的事件,因此,在系统事件队列处理完以后再释放这个socket是一个安全的作法,Destroy函数正是提供了这个功能.

Error函数返回True如果上次的socket操作遇到某种错误.

GetPeer返回一个wxSockAddress引用,它包含当前socket的对端信息比如IP地址和端口号.

IsConnected返回是否这个socket已经成功连接.

LastCount返回最近一次读写操作成功进行的字节数.

LastError返回最近一次的错误码.注意如果操作成功并不会更新最近一次的错误码,因此你需要使用Error函数来判断最近一次操作是否成功.socket所支持的错误码如下表所示:

wxSOCKET_INVOP 非法操作,比如使用了非法的地址类型.
wxSOCKET_IOERR I/O错误,比如无法创建和初始化socket.
wxSOCKET_INVADDR 不正确的地址, 比如试图连接空地址或者不完整的地址.
wxSOCKET_INVSOCK socket使用方法不正确或者尚未初始化.
wxSOCKET_NOHOST 指定的地址不存在.
wxSOCKET_INVPORT 无效端口.
wxSOCKET_WOULDBLOCK socket被指示为非阻塞socket,但是操作将导致阻塞 (参见socket模式的讨论).
wxSOCKET_TIMEDOUT socket操作超时.
wxSOCKET_MEMERR socket操作时内存分配失败.

Ok返回True的条件是: 客户端Socket必须已经和Server建立连接或者服务端Socket已经成功绑定了本地地址并且开始监听客户端连接

SetTimeout指定阻塞式访问的超时时长.默认为10分钟.

发送和接收Socket数据

wxSocketBase提供了各种基本的或高级的读写socket操作.所有操作都将保存相关的数据并且支持使用LastCount返回成功操作的字节个数,LastError返回最近一次遇到的操作错误码.

接收

Discard函数删除所有的socket接收缓冲区数据.

Peek函数让你可以读取缓冲区的数据但是不将socket缓冲区清除.你必须指定要Peek的数据的大小并且自己提供Peek目的地的缓冲区.

Read函数和Peek一样,只是它在成功获取数据以后会清除相应的Socket接收缓冲区.

ReadMsg函数对应于WriteMsg函数,将会完整的接收WriteMsg发送的数据,除非需要系统错误.注意如果ReadMsg开辟的缓冲区比WriteMsg发送的数据少,则多出的数据将被直接删除.

Unread将数据放回接收缓冲区,你需要指定希望放回去的数据的字节数.

发送

Write函数以参数中数据指针指向的缓冲作为开始位置,向socket写入参数中指定的数据大小.

WriteMsg和Write的区别在于,wxWidgets会增加一个消息头,以便接收端可以准确的知道消息的大小,WriteMsg发送的数据必须由ReadMsg函数接收.

创建一个Server

wxSocketServer也只对其基类wxSocketBase增加了少数几个函数用来创建和监听连接请求.要创建一个 Server,你必须指定要监听的端口.wxSocketServer使用和wxSocketClient一样的wxIPV4address类型,只是前者不需要指定远端地址.在大多数情况下,你需要调用Ok函数来判断是否绑定和监听动作已经成功.

wxSocketServer的主要成员函数

wxSocketServer构造函数使用一个地址对象用来指定监听端口,以及一个可选的Socket标记(参见下一节"Socket Flags").

Accept函数返回一个新的socket连接或者立即返回NULL,如果没有连接请求.你可以设置可选的等待标记,如果你这样做,Accept将导致程序阻塞.

AcceptWith和Accept的功能相近,只是它提供一个额外的已存在的wxSocketBase对象(引用),并且其返回值为bool型,用来指示是否接受了一个新的连接.

WaitForAccept采用一个秒参数和一个毫秒参数以指定在某个事件范围内等待新的连接请求,如果请求发生则返回True,否则超时返回False.

处理新的连接请求事件

当监听socket检测到一个新的连接请求的时候,将产生一个相应的事件.在其事件处理函数中,你可以接受这个请求并且执行任何必要的即时处理.你需要保证连接在其生命周期内不被立即关闭,你还需要为新接受的socket指定事件处理器.注意监听的socket在被关闭之前将一直在监听, 而每一个新的连接请求都会创建一个新的socket.在server的整个生命周期内,同一个监听socket可以接受成千上万个新的socket.

Socket事件概述

从程序员的观点来说,基于事件的socket处理简化了socket编程,使得他们不需要关心线程的创建和释放.这个例子没有使用线程, 但是GUI界面同样不会阻塞,因为所有的数据读取都是在确信有数据到来的时候才进行的,因此会立即返回.如果有很大量的数据需要读取,你可以将它们分为多个小部分,然后一次读一部分并将其放入你自己的缓冲区.或者你可以使用Peek函数检查当前缓冲区的数据的数量,如果没有达到需要处理的范围,你可以什么也不做,静静等待下一次数据事件通知的到来.

在下一节,我们来看看怎样使用不同的socket标记来改变socket的行为.