WinSock2程式設計之打造完整的SOCKET池
WinSock2程式設計之打造完整的SOCKET池
部落格分類:
• IOCP
程式設計SocketWindows應用伺服器網路應用
WinSock2程式設計之打造完整的SOCKET池
IOCP程式設計 2010-02-15 22:46:34 閱讀592 評論5 字號:大中小 訂閱
在Winodows平臺上,網路程式設計的主要介面就是WinSock,目前大多數的Windows平臺上的WinSock平臺已經升級到2.0版, 簡稱為WinSock2。在WinSock2中擴充套件了很多很有用的Windows味很濃的SOCKET專用API,為Windows平臺使用者提供高效能的 網路程式設計支援。這些函式中的大多數已經不再是標準的“Berkeley”套接字模型的API了。使用這些函式的代價就是你不能再將你的網路程式輕鬆的移植 到“尤里平臺”(我給Unix +Linux平臺的簡稱)下,反過來因為Windows平臺支援標準的“Berkeley”套接字模型,所以你可以將大多數尤里平臺下的網路應用移植到 Windows平臺下。
如果不考慮可移植性(或者所謂的跨平臺性),而是著重於應用的效能時,尤其是注重伺服器效能時,對於Windows的程式,都鼓勵使用 WinSock2擴充套件的一些API,更鼓勵使用IOCP模型,因為這個模型是目前Windows平臺上比較完美的一個高效能IO程式設計模型,它不但適用於 SOCKET程式設計,還適用於讀寫硬碟檔案,讀寫和管理命名管道、郵槽等等。如果再結合Windows執行緒池,IOCP幾乎可以利用當今硬體所有可能的新特 性(比如多核,DMA,高速匯流排等等),本身具有先天的擴充套件性和可用性。
今天討論的重點就是SOCKET池。很多VC程式設計師也許對SOCKET池很陌生,也有些可能很熟悉,那麼這裡就先討論下這個概念。
在Windows平臺上SOCKET實際上被視作一個核心物件的控制程式碼,很多Windows API在支援傳統的HANDLE引數的同時也支援SOCKET,比如有名的CreateIoCompletionPort就支援將SOCKET控制程式碼代替 HANDLE引數傳入並呼叫。熟悉Windows核心原理的讀者,立刻就會發現,這樣的話,我們建立和銷燬一個SOCKET控制程式碼,實際就是在系統內部建立 了一個核心物件,對於Windows來說這牽扯到從Ring3層到Ring0層的耗時操作,再加上覆雜的安全稽核機制,實際建立和銷燬一個SOCKET內 核物件的成本還是蠻高的。尤其對於一些面向連線的SOCKET應用,服務端往往要管理n多個代表客戶端通訊的SOCKET物件,而且因為客戶的變動性,主 要面臨的大量操作除了一般的收發資料,剩下的就是不斷建立和銷燬SOCKET控制程式碼,對於一個頻繁接入和斷開的伺服器應用來說,建立和銷燬SOCKET的性 能代價立刻就會體現出來,典型的例如WEB伺服器程式,就是一個需要頻繁建立和銷燬SOCKET控制程式碼的SOCKET應用。這種情況下我們通常都希望對於斷 開的SOCKET物件,不是簡單的“銷燬”了之(很多時候“斷開”的含義不一定就等價於“銷燬”,可以仔細思考一下),更多時候希望能夠重用這個 SOCKET物件,這樣我們甚至可以事先建立一批SOCKET物件組成一個“池”,在需要的時候“重用”其中的SOCKET物件,不需要的時候將 SOCKET物件重新丟入池中即可,這樣就省去了頻繁建立銷燬SOCKET物件的效能損失。在原始的“Berkeley”套接字模型中,想做到這點是沒有 什麼辦法的。而幸運的是在Windows平臺上,尤其是支援WinSock2的平臺上,已經提供了一套完整的API介面用於支援SOCKET池。
對於符合以上要求的SOCKET池,首先需要做到的就是對SOCKET控制程式碼的“回收”,因為建立函式無論在那個平臺上都是現成的,而最早能夠實現 這個功能的WinSock函式就是TransmitFile,如果代替closesocket函式像下面這樣呼叫就可以“回收”一個SOCKET控制程式碼,而 不是銷燬:(注意“回收”這個功能對於TransmitFile函式來說只是個“副業”。)
TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );
注意上面函式的最後一個引數,使用了標誌TF_DISCONNECT和TF_REUSE_SOCKET,第一個值表示斷開,第二個值則明確的表示 “重用”實際上也就是回收這個SOCKET,經過這個處理的SOCKET控制程式碼,就可以直接再用於connect等操作,但是此時我們會發現,這個回收來的 SOCKET似乎沒什麼用,因為其他套接字函式沒法直接利用這個回收來的SOCKET控制程式碼。
這時就要WinSock2的一組專用API上場了。我將它們按傳統意義上的服務端和客戶端分為兩組:
一、 服務端:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
BOOL AcceptEx(
__in SOCKET sListenSocket,
__in SOCKET sAcceptSocket,
__in PVOID lpOutputBuffer,
__in DWORD dwReceiveDataLength,
__in DWORD dwLocalAddressLength,
__in DWORD dwRemoteAddressLength,
__out LPDWORD lpdwBytesReceived,
__in LPOVERLAPPED lpOverlapped
);
BOOL DisconnectEx(
__in SOCKET hSocket,
__in LPOVERLAPPED lpOverlapped,
__in DWORD dwFlags,
__in DWORD reserved
);
二、 客戶端:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
BOOL PASCAL ConnectEx(
__in SOCKET s,
__in const struct sockaddr* name,
__in int namelen,
__in_opt PVOID lpSendBuffer,
__in DWORD dwSendDataLength,
__out LPDWORD lpdwBytesSent,
__in LPOVERLAPPED lpOverlapped
);
BOOL DisconnectEx(
__in SOCKET hSocket,
__in LPOVERLAPPED lpOverlapped,
__in DWORD dwFlags,
__in DWORD reserved
);
注意觀察這些函式,似乎和傳統的“Berkeley”套接字模型中的一些函式“大同小異”,其實仔細觀察他們的引數,就已經可以發現一些呼叫他們的“玄機”了。
首先我們來看AcceptEx函式,與accept函式不同,它需要兩個SOCKET控制程式碼作為引數,頭一個引數的含義與accept函式的相同, 而第二個引數的意思就是accept函式返回的那個代表與客戶端通訊的SOCKET控制程式碼,在傳統的accept內部,實際在返回那個代表客戶端的 SOCKET時,是在內部呼叫了一個SOCKET的建立動作,先建立這個SOCKET然後再“accept”讓它變成代表客戶端連線的SOCKET,而 AcceptEx函式就在這裡“擴充套件”(實際上是“閹割”才對)accept函式,省去了內部那個明顯的建立SOCKET的動作,而將這個建立動作交給最 終的呼叫者自己來實現。AcceptEx要求呼叫者建立好那個sAcceptSocket控制程式碼然後傳進去,這時我們立刻發現,我們回收的那個SOCKET 是不是也可以傳入呢?答案是肯定的,我們就是可以利用這個函式傳入那個“回收”來的SOCKET控制程式碼,最終實現服務端的SOCKET重用。
這裡需要注意的就是,AcceptEx函式必須工作在非阻塞的IOCP模型下,同時即使AcceptEx函式返回了,也不代表客戶端連線進來或者 連線成功了,我們必須依靠它的“完成通知”才能知道這個事實,這也是AcceptEx函式區別於accept這個阻塞方式函式的最大之處。通常可以利用 AcceptEx的非阻塞特性和IOCP模型的優點,一次可以“預先”發出成千上萬個AcceptEx呼叫,“等待”客戶端的連線。對於習慣了 accept阻塞方式的程式設計師來說,理解AcceptEx的工作方式還是需要費一些周折的。下面的例子就演示瞭如何一次呼叫多個AcceptEx:
//批量建立SOCKET,並呼叫對應的AcceptEx
for(UINT i = 0; i < 1000; i++)
{//呼叫1000次
//建立與客戶端通訊的SOCKET,注意SOCKET的建立方式
skAccept = ::WSASocket(AF_INET,
SOCK_STREAM,
IPPROTO_TCP,
NULL,
0,
WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == skAccept)
{
throw CGRSException((DWORD)WSAGetLastError());
}
//建立一個自定義的OVERLAPPED擴充套件結構,使用IOCP方式呼叫
pAcceptOL = new CGRSOverlappedData(GRS_OP_ACCEPT
,this,skAccept,NULL);
pAddrBuf = pAcceptOL->GetAddrBuf();
//4、發出AcceptEx呼叫
//注意將AcceptEx函式接收連線資料緩衝的大小設定成了0,這將導致此函式立即返回,雖然與
//不設定成0的方式而言,這導致了一個較低下的效率,但是這樣提高了安全性,所以這種效率
//犧牲是必須的
if(!AcceptEx(m_skServer,
skAccept,
pAddrBuf->m_pBuf,
0,//將接收緩衝置為0,令AcceptEx直接返回,防止拒絕服務攻擊
GRS_ADDRBUF_SIZE,
GRS_ADDRBUF_SIZE,
NULL,
(LPOVERLAPPED)pAcceptOL))
{
int iError = WSAGetLastError();
if( ERROR_IO_PENDING != iError
&& WSAECONNRESET != iError )
{
if(INVALID_SOCKET != skAccept)
{
::closesocket(skAccept);
skAccept = INVALID_SOCKET;
}
if( NULL != pAcceptOL)
{
GRS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData));
delete pAcceptOL;
pAcceptOL = NULL;
}
}
}
}
以上的例子只是簡單的演示了AcceptEx的呼叫,還沒有涉及到真正的“回收重用”這個主題,那麼下面的例子就演示瞭如何重用一個SOCKET控制程式碼:
if(INVALID_SOCKET == skClient)
{
throw CGRSException(_T("SOCKET控制程式碼是無效的!"));
}
OnPreDisconnected(skClient,pUseData,0);
CGRSOverlappedData*pData
= new GRSOverlappedData(GRS_OP_DISCONNECTEX
,this,skClient,pUseData);
//回收而不是關閉後再建立大大提高了伺服器的效能
DisconnectEx(skClient,&pData->m_ol,TF_REUSE_SOCKET,0);
......
//在接收到DisconnectEx函式的完成通知之後,我們就可以重用這個SOCKET了
CGRSAddrbuf*pBuf = NULL;
pNewOL = new CGRSOverlappedData(GRS_OP_ACCEPT
,this,skClient,pUseData);
pBuf = pNewOL->GetAddrBuf();
//把這個回收的SOCKET重新丟進連線池
if(!AcceptEx(m_skServer,skClient,pBuf->m_pBuf,
0,//將接收緩衝置為0,令AcceptEx直接返回,防止拒絕服務攻擊
GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE,
NULL,(LPOVERLAPPED)pNewOL))
{
int iError = WSAGetLastError();
if( ERROR_IO_PENDING != iError
&& WSAECONNRESET != iError )
{
throw CGRSException((DWORD)iError);
}
}
//注意在這個SOCKET被重新利用後,重新與IOCP繫結一下,該操作會返回一個已設定的錯誤,這個錯誤直接被忽略即可
::BindIoCompletionCallback((HANDLE)skClient
,Server_IOCPThread, 0);
至此回收重用SOCKET的工作也就結束了,以上的過程實際理解了IOCP之後就比較好理解了,例子的最後我們使用了 BindIoCompletionCallback函式重新將SOCKET丟進了IOCP執行緒池中,實際還可以再次使用 CreateIoCompletionPort函式達到同樣的效果,這裡列出這一步就是告訴大家,不要忘了再次繫結一下完成埠和SOCKET。
對於客戶端來說,可以使用ConnectEx函式來代替connect函式,與AcceptEx函式相同,ConnectEx函式也是以非阻塞的IOCP 方式工作的,唯一要注意的就是在WSASocket呼叫之後,在ConnectEx之前要呼叫一下bind函式,將SOCKET提前繫結到一個本地地址端 口上,當然回收重用之後,就無需再次繫結了,這也是ConnectEx較之connect函式高效的地方之一。
與AcceptEx函式類似,也可以一次發出成千上萬個ConnectEx函式的呼叫,可以連線到不同的伺服器,也可以連線到相同的伺服器,連線到不同的伺服器時,只需提供不同的sockaddr即可。
通過上面的例子和講解,大家應該對SOCKET池概念以及實際的應用有個大概的瞭解了,當然核心仍然是理解了IOCP模型,否則還是寸步難行。
在上面的例子中,回收SOCKET控制程式碼主要使用了DisconnectEx函式,而不是之前介紹的TransmitFile函式,為什麼呢?因為 TransmitFile函式在一些情況下會造成死鎖,無法正常回收SOCKET,畢竟不是專業的回收重用SOCKET函式,我就遇到過好幾次死鎖,最後 偶然的發現了DisconnectEx函式這個專用的回收函式,呼叫之後發現比TransmitFile專業多了,而且不管怎樣都不會死鎖。
最後需要補充的就是這幾個函式的呼叫方式,不能像傳統的SOCKET API那樣直接呼叫它們,而需要使用一種間接的方式來呼叫,尤其是AcceptEx和DisconnectEx函式,下面給出了一個例子類,用於演示如何動態載入這些函式並呼叫之:
class CGRSMsSockFun
{
public:
CGRSMsSockFun(SOCKET skTemp = INVALID_SOCKET)
{
if( INVALID_SOCKET != skTemp )
{
LoadAllFun(skTemp);
}
}
public:
virtual ~CGRSMsSockFun(void)
{
}
protected:
BOOL LoadWSAFun(SOCKET& skTemp,GUID&funGuid,void*&pFun)
{
DWORD dwBytes = 0;
BOOL bRet = TRUE;
pFun = NULL;
BOOL bCreateSocket = FALSE;
try
{
if(INVALID_SOCKET == skTemp)
{
skTemp = ::WSASocket(AF_INET,SOCK_STREAM,
IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
bCreateSocket = (skTemp != INVALID_SOCKET);
}
if(INVALID_SOCKET == skTemp)
{
throw CGRSException((DWORD)WSAGetLastError());
}
if(SOCKET_ERROR == ::WSAIoctl(skTemp,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&funGuid,sizeof(funGuid),
&pFun,sizeof(pFun),&dwBytes,NULL,
NULL))
{
pFun = NULL;
throw CGRSException((DWORD)WSAGetLastError());
}
}
catch(CGRSException& e)
{
if(bCreateSocket)
{
::closesocket(skTemp);
}
}
return NULL != pFun;
}
protected:
LPFN_ACCEPTEX m_pfnAcceptEx;
LPFN_CONNECTEX m_pfnConnectEx;
LPFN_DISCONNECTEX m_pfnDisconnectEx;
LPFN_GETACCEPTEXSOCKADDRS m_pfnGetAcceptExSockaddrs;
LPFN_TRANSMITFILE m_pfnTransmitfile;
LPFN_TRANSMITPACKETS m_pfnTransmitPackets;
LPFN_WSARECVMSG m_pfnWSARecvMsg;
protected:
BOOL LoadAcceptExFun(SOCKET &skTemp)
{
GUID GuidAcceptEx = WSAID_ACCEPTEX;
return LoadWSAFun(skTemp,GuidAcceptEx
,(void*&)m_pfnAcceptEx);
}
BOOL LoadConnectExFun(SOCKET &skTemp)
{
GUID GuidAcceptEx = WSAID_CONNECTEX;
return LoadWSAFun(skTemp,GuidAcceptEx
,(void*&)m_pfnConnectEx);
}
BOOL LoadDisconnectExFun(SOCKET&skTemp)
{
GUID GuidDisconnectEx = WSAID_DISCONNECTEX;
return LoadWSAFun(skTemp,GuidDisconnectEx
,(void*&)m_pfnDisconnectEx);
}
BOOL LoadGetAcceptExSockaddrsFun(SOCKET &skTemp)
{
GUID GuidGetAcceptExSockaddrs
= WSAID_GETACCEPTEXSOCKADDRS;
return LoadWSAFun(skTemp,GuidGetAcceptExSockaddrs
,(void*&)m_pfnGetAcceptExSockaddrs);
}
BOOL LoadTransmitFileFun(SOCKET&skTemp)
{
GUID GuidTransmitFile = WSAID_TRANSMITFILE;
return LoadWSAFun(skTemp,GuidTransmitFile
,(void*&)m_pfnTransmitfile);
}
BOOL LoadTransmitPacketsFun(SOCKET&skTemp)
{
GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;
return LoadWSAFun(skTemp,GuidTransmitPackets
,(void*&)m_pfnTransmitPackets);
}
BOOL LoadWSARecvMsgFun(SOCKET&skTemp)
{
GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;
return LoadWSAFun(skTemp,GuidTransmitPackets
,(void*&)m_pfnWSARecvMsg);
}
public:
BOOL LoadAllFun(SOCKET skTemp)
{//注意這個地方的呼叫順序,是根據伺服器的需要,並結合了表示式副作用
//而特意安排的呼叫順序
return (LoadAcceptExFun(skTemp) &&
LoadGetAcceptExSockaddrsFun(skTemp) &&
LoadTransmitFileFun(skTemp) &&
LoadTransmitPacketsFun(skTemp) &&
LoadDisconnectExFun(skTemp) &&
LoadConnectExFun(skTemp) &&
LoadWSARecvMsgFun(skTemp));
}
public:
GRS_FORCEINLINE BOOL AcceptEx (
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
)
{
GRS_ASSERT(NULL != m_pfnAcceptEx);
return m_pfnAcceptEx(sListenSocket,
sAcceptSocket,lpOutputBuffer,
dwReceiveDataLength,dwLocalAddressLength,
dwRemoteAddressLength,lpdwBytesReceived,
lpOverlapped);
}
GRS_FORCEINLINE BOOL ConnectEx(
SOCKET s,const struct sockaddr FAR *name,
int namelen,PVOID lpSendBuffer,
DWORD dwSendDataLength,LPDWORD lpdwBytesSent,
LPOVERLAPPED lpOverlapped
)
{
GRS_ASSERT(NULL != m_pfnConnectEx);
return m_pfnConnectEx(
s,name,namelen,lpSendBuffer,
dwSendDataLength,lpdwBytesSent,
lpOverlapped
);
}
GRS_FORCEINLINE BOOL DisconnectEx(
SOCKET s,LPOVERLAPPED lpOverlapped,
DWORD dwFlags,DWORD dwReserved
)
{
GRS_ASSERT(NULL != m_pfnDisconnectEx);
return m_pfnDisconnectEx(s,
lpOverlapped,dwFlags,dwReserved);
}
GRS_FORCEINLINE VOID GetAcceptExSockaddrs (
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
sockaddr **LocalSockaddr,
LPINT LocalSockaddrLength,
sockaddr **RemoteSockaddr,
LPINT RemoteSockaddrLength
)
{
GRS_ASSERT(NULL != m_pfnGetAcceptExSockaddrs);
return m_pfnGetAcceptExSockaddrs(
lpOutputBuffer,dwReceiveDataLength,
dwLocalAddressLength,dwRemoteAddressLength,
LocalSockaddr,LocalSockaddrLength,
RemoteSockaddr,RemoteSockaddrLength
);
}
GRS_FORCEINLINE BOOL TransmitFile(
SOCKET hSocket,HANDLE hFile,
DWORD nNumberOfBytesToWrite,
DWORD nNumberOfBytesPerSend,
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
DWORD dwReserved
)
{
GRS_ASSERT(NULL != m_pfnTransmitfile);
return m_pfnTransmitfile(
hSocket,hFile,nNumberOfBytesToWrite,
nNumberOfBytesPerSend,lpOverlapped,
lpTransmitBuffers,dwReserved
);
}
GRS_FORCEINLINE BOOL TransmitPackets(
SOCKET hSocket,
LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,
DWORD nElementCount,DWORD nSendSize,
LPOVERLAPPED lpOverlapped,DWORD dwFlags
)
{
GRS_ASSERT(NULL != m_pfnTransmitPackets);
return m_pfnTransmitPackets(
hSocket,lpPacketArray,nElementCount,
nSendSize,lpOverlapped,dwFlags
);
}
GRS_FORCEINLINE INT WSARecvMsg(
SOCKET s,LPWSAMSG lpMsg,
LPDWORD lpdwNumberOfBytesRecvd,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
)
{
GRS_ASSERT(NULL != m_pfnWSARecvMsg);
return m_pfnWSARecvMsg(
s,lpMsg,lpdwNumberOfBytesRecvd,
lpOverlapped,lpCompletionRoutine
);
}
/*WSAID_ACCEPTEX
WSAID_CONNECTEX
WSAID_DISCONNECTEX
WSAID_GETACCEPTEXSOCKADDRS
WSAID_TRANSMITFILE
WSAID_TRANSMITPACKETS
WSAID_WSARECVMSG
WSAID_WSASENDMSG */
};
這個類的使用非常簡單,只需要宣告一個類的物件,然後呼叫其成員AcceptEx、DisconnectEx函式等即可,引數與這些函式的 MSDN宣告方式完全相同,除了本文中介紹的這些函式外,這個類還包含了很多其他的Winsock2函式,那麼都應該按照這個類中演示的這樣來動態載入後 再行呼叫,如果無法載入通常說明你的環境中沒有Winsock2函式庫,或者是你初始化的不是2.0版的Winsock環境。
這個類是本人完整類庫的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什麼問題,可以直接跟帖說明,我會不定期回答大家的問題, 這個類可以免費使用、分發、修改,可以用於任何商業目的,但是對於使用後引起的任何問題,本人概不負責,有問題請跟帖。關於AcceptEx以及其他一些 函式,包括本文中沒有介紹到得函式,我會在後續的一些專題文章中進行詳細深入的介紹,敬請期待。如果你有什麼疑問,或者想要了解什麼也請跟帖說明,我會在 後面的文章中儘量說明。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/25897606/viewspace-704942/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 網路程式設計之socket程式設計
- PHP回顧之socket程式設計PHP程式設計
- SOCKET程式設計程式設計
- 玩轉 PHP 網路程式設計全套之 socket stream 程式設計PHP程式設計
- Python 併發程式設計之執行緒池/程式池Python程式設計執行緒
- Python併發程式設計之執行緒池/程式池Python程式設計執行緒
- socket程式設計(1)程式設計
- Java Socket程式設計Java程式設計
- Java Socket程式設計Java程式設計
- WCF、Socket程式設計程式設計
- Socket程式設計(九)程式設計
- Socket程式設計模型程式設計模型
- golang中的socket程式設計Golang程式設計
- Linux系統程式設計(37)—— socket程式設計之原始套接字Linux程式設計
- Linux系統程式設計(33)—— socket程式設計之TCP程式的錯誤處理Linux程式設計TCP
- python網路-Socket之TCP程式設計(26)PythonTCP程式設計
- socket程式設計的select模型程式設計模型
- java的socket程式設計問題Java程式設計
- Python socket程式設計Python程式設計
- Socket程式設計基礎程式設計
- socket程式設計實戰程式設計
- Socket網路程式設計程式設計
- Socket 程式設計實戰程式設計
- IO和socket程式設計程式設計
- Java Socket 程式設計指南Java程式設計
- linux Socket 程式設計Linux程式設計
- 網路程式設計-socket程式設計
- C網路程式設計socket之connect函式程式設計函式
- Python 中的 Socket 程式設計(指南)Python程式設計
- 對 Python Socket 程式設計的初探Python程式設計
- 讀懂Java中的Socket程式設計Java程式設計
- 介紹Java Socket程式設計的文章Java程式設計
- 併發程式設計之:執行緒池(一)程式設計執行緒
- 【socket程式設計基礎模板】程式設計
- Socket 程式設計 (網路篇)程式設計
- Socket 程式設計IO Multiplexing程式設計
- PHP Socket 程式設計詳解PHP程式設計
- 【Python】socket 程式設計初探Python程式設計