# 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的行為.
- 第一章 介紹
- 1.1 為什么要使用wxWidgets?
- 1.2 wxWidgets的歷史
- 1.3 wxWidgets社區
- 1.4 wxWidgets和面向對象編程
- 1.5 wxWidgets的體系結構
- 1.6 許可協議
- 第一章小結
- 第二章 開始使用
- 2.1 一個小例子
- 2.2 應用程序類
- 2.3 Frame窗口類
- 2.4 事件處理函數
- 2.5 Frame窗口的構造函數
- 2.6 完整的例子
- 2.7 wxWidgets程序一般執行過程
- 2.8 編譯和運行程序
- 第二章小結
- 第三章 事件處理
- 3.1 事件驅動編程
- 3.2 事件表和事件處理過程
- 3.3 過濾某個事件
- 3.4 掛載事件表
- 3.5 動態事件處理方法
- 3.6 窗口標識符
- 3.7 自定義事件
- 第三章小結
- 第四章 窗口的基礎知識
- 4.1 窗口解析
- 4.2 窗口類概覽
- 4.3 基礎窗口類
- 4.4 頂層窗口
- 4.5 容器窗口
- 4.6 非靜態控件
- 4.7 靜態控件
- 4.8 菜單
- 4.9 控制條
- 第四章小結
- 第五章繪畫和打印
- 5.1 理解設備上下文
- 5.2 繪畫工具
- 5.3 設備上下文中的繪畫函數
- 5.4 使用打印框架
- 5.5 使用wxGLCanvas繪制三維圖形
- 第五章小節
- 第六章處理用戶輸入
- 6.1 鼠標輸入
- 6.2 處理鍵盤事件
- 6.3 處理游戲手柄事件
- 第六章小結
- 第七章使用布局控件進行窗口布局
- 7.1 窗口布局基礎
- 7.2 窗口布局控件
- 7.3 使用布局控件進行編程
- 7.4 更多關于布局的話題
- 第七章小結
- 第八章使用標準對話框
- 8.1信息對話框
- 8.2 文件和目錄對話框
- 8.3 選擇和選項對話框
- 8.4 輸入對話框
- 8.5 打印對話框
- 第八章小結
- 第九章創建定制的對話框
- 9.1 創建定制對話框的步驟
- 9.2 一個例子:PersonalRecordDialog
- 9.3 在小型設備上調整你的對話框
- 9.4 一些更深入的話題
- 9.5 使用wxWidgets資源文件
- 第九章小結
- 第十章使用圖像編程
- 10.1 wxWidgets中圖片相關的類
- 10.2 使用wxBitmap編程
- 10.3 使用wxIcon編程
- 10.4 使用wxCursor編程
- 10.5 使用wxImage編程
- 10.6 圖片列表和圖標集
- 10.7 自定義wxWidgets提供的小圖片
- 第十章小結
- 第十一章剪貼板和拖放操作
- 11.1 數據對象
- 11.2 使用剪貼板
- 11.3 實現拖放操作
- 第十一章小結
- 第十二章高級窗口控件
- 12.1 wxTreeCtrl
- 12.2 wxListCtrl
- 12.3 wxWizard
- 12.4 wxHtmlWindow
- 12.5 wxGrid
- 12.6 wxTaskBarIcon
- 12.7 編寫自定義的控件
- 第十二章小結
- 第十三章數據結構類
- 13.1 為什么沒有使用STL?
- 13.2 字符串類型
- 13.3 wxArray
- 13.4 wxList和wxNode
- 13.5 wxHashMap
- 13.6 存儲和使用日期和時間
- 13.7 其它常用的數據類型
- 第十三章小結
- 第十四章文件和流操作
- 14.1 文件類和函數
- 14.2 流操作相關類
- 第十四章小結
- 第十五章內存管理,調試和錯誤處理
- 15.1 內存管理基礎
- 15.2 檢測內存泄漏和其它錯誤
- 15.3 構建自防御的程序
- 15.4 錯誤報告
- 15.5 提供運行期類型信息
- 15.6 使用wxModule
- 15.7 加載動態鏈接庫
- 15.8 異常處理
- 15.9 調試提示
- 第十五章小結
- 第十六章編寫國際化程序
- 16.1 國際化介紹
- 16.2 從翻譯說起
- 16.3 字符編碼和Unicode
- 16.4 數字和日期
- 16.5 其它媒介
- 16.6 一個小例子
- 第十六章小結
- 第十七章編寫多線程程序
- 17.1 什么時候使用多線程,什么時候不要使用
- 17.2 使用wxThread
- 17.3 用于線程同步的對象
- 17.4 多線程的替代方案
- 第十七章小結
- 第十八章使用wxSocket編程
- 18.1 Socket類和功能概覽
- 18.2 Socket及其基本處理介紹
- 18.3 Socket標記
- 18.4 使用Socket流
- 18.5 替代wxSocket
- 第十八章小結
- 第十九章使用文檔/視圖框架
- 19.1 文檔/視圖基礎
- 19.2 文檔/視圖框架的其它能力
- 19.3 實現Undo/Redo的策略
- 第十九章小結
- 第二十章完善你的應用程序
- 20.1 單個實例和多個實例
- 20.2 更改事件處理機制
- 20.3 降低閃爍
- 20.4 實現聯機幫助
- 20.5 解析命令行參數
- 20.6 存儲應用程序資源
- 20.7 調用別的應用程序
- 20.8 管理應用程序設置
- 20.9 應用程序安裝
- 20.10 遵循用戶界面設計規范
- 20.11 全書小結