在其中,要注意的是:
1.關于服務端在客戶端連接之前
如果沒有客戶端連接時,在調用accept()時程序將會出現freeze,即阻塞,而一旦有客戶端連接過來,accept將會新建一Socket與客戶端的Socket相通,原先Socket繼續進入監聽狀態,等待他人的連接要求。
該函數調用成功返回一個新產生的Socket對象,否則返回INVALID_SOCKET,該新socket就是一個和一個客戶端對話的套接字。有點類似孫悟空對陣天兵天將時,當遇到某個具體敵手來,拔出一根毫毛變出一個孫行者去應付,再來再拔毫毛一樣。accept()定義如下:
SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
s:Socket的識別碼;
addr:存放來連接的客戶端的地址;
addrlen:addr的長度
當客戶端連接上后一直沒有斷開情況下,如果連接越來越多時,則創建的Socket也越多,其最大上限在listen中已經設置。
2.關于服務端的accept()使用
accept過后才是真正的和客戶端進行交互,在accept時,由于程序會freeze,在調用accept時有多種方法,其中方法有:
*事件處理模式:
通過WSAAsyncSelect()函數,其異步通知有accept信號來,然后在一個窗體自定義事件中處理accept信號。
如下在listen()之后調用:
WSAAsyncSelect(m_hSocket, m_hWnd, WM_CLIENT_ACCEPT,FD_ACCEPT); //wm_xxx_xxz自定義消息。
這樣在構建的自定義消息中處理accept()連接請求。如下,OnAccept()單元
LRESULT CPublicNetSoftDlg::OnAccept(WPARAM wParam,LPARAM lParam)
{
。。。。
if(WSAGETSELECTEVENT(lParam) == FD_ACCEPT)//如果
{
Client = accept(ServerSocket,(LPSOCKADDR)&m_sockServerAddr,0);
if (Client == INVALID_SOCKET)
return 0L;
}
。。。。。
}
*線程處理模式:將accept放在線程中讓其freeze,一旦來了連接,則自然從freeze中出來進行處理下一步。下面就是直接把accetp放在線程中處于等待狀態。
//連接請求隊列長度為1,即只允許有一個請求,若有多個請求, 則出現錯誤,給出錯誤代碼WSAECONNREFUSED。
listen(sock,1);
//開啟線程避免主程序的阻塞
AfxBeginThread(Server,NULL);
……
//處理線程,等待客戶端連接
UINT Server(LPVOID lpVoid)
{
……
int nLen = sizeof(SOCKADDR);
connSocket = accept(ListSocket,(LPSOCKADDR)& sockin,(LPINT)& nLen);
……
WSAAsyncSelect(connSocket,
m_hWnd,
WM_SOCKET_MSG,
FD_READ|FD_CLOSE);
return 1;
}
把accept()放到線程中去是因為在執行到該函數時如沒有客戶連接請求到來,服務器就會停在accept語句上處于等待阻塞,這勢必會引起進程的阻塞,雖然也可以通過設置套接字為非阻塞方式使在沒有客戶等待時可以使accept()函數調用立即返回,但這種輪詢套接字的方式會使CPU處于忙等待方式,從而降低程序的運行效率大大浪費系統資源(我覺得做法很多,暫不考慮非阻塞情況)。
在阻塞工作方式,為其單獨開辟一個子線程,將其阻塞控制在子線程范圍內而不會造成整個應用程序的阻塞。對于網絡事件的響應顯然要采取異步選擇機制,只有采取這種方式才可以在由網絡對方所引起的不可預知的網絡事件發生時能馬上在進程中做出及時的響應處理,而在沒有網絡事件到達時則可以處理其他事件,這種效率是很高的。前面那段代碼中的WSAAsyncSelect()函數便是實現網絡事件異步選擇的核心函數。
第4個參數注冊應用程序關心的網絡事件,在這里通過FD_READ|FD_CLOSE指定了網絡讀和網絡斷開兩種事件,當這種事件發生時變會發出由第三個參數指定的自定義消息 WM_SOCKET_MSG,接收該消息的窗口通過第二個參數指定其句柄。
其響應函數如下:
void CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam)
{
int iReadLen=0;
int message=lParam & 0x0000FFFF;
switch(message)
{
case FD_READ: //讀事件發生。此時有字符到達,需要進行接收處理
char cDataBuffer[MTU*10];
//通過套接字接收信息
iReadLen = recv(newskt,cDataBuffer,MTU*10,0);
//將信息保存到文件
if(!file.Open("ServerFile.txt",CFile::modeReadWrite))
file.Open("E:ServerFile.txt",
CFile::modeCreate|CFile::modeReadWrite);
file.SeekToEnd();
file.Write(cDataBuffer,iReadLen);
file.Close();
break;
case FD_CLOSE://網絡斷開事件發生。此時客戶機關閉或退出。
……//進行相應的處理
break;
default:
break;
}
}
對于recv和send的處理一般就是在事件中處理,通過WSAAsySelect()來傳遞信號,這種方式形成一種固定寫socket方式,比如有的人喜歡把recv和send各自放入一個線程中通過輪詢+阻塞模式,或者采用事件通知模式,一般來說采用I/O模型是較為專業的做法。
3.客戶端的connect()
客戶端連接時存在阻塞現象,就是程序在connect會出現freeze,一般可以容忍。但若想通過超時設置來解決這個問題,可采用在vckbase中,對于connect()超時的處理辦法。不過覺得有時調用封裝好的socket,直接是指connectionTimeout屬性倒是簡單的方法。
WSADATA wsd;
SOCKET cClient;
int ret;
struct sockaddr_in server;
hostent *host=NULL;
if(WSAStartup(MAKEWORD(2,0),&wsd))
{
return 0;
}
cClient = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(cClient == INVALID_SOCKET){return 0;}
//set Recv and Send time out
int TimeOut=6000; //設置發送超時6秒
if(::setsockopt(cClient,SOL_SOCKET,SO_SNDTIMEO,(char *)&TimeOut,sizeof(TimeOut))==SOCKET_ERROR){
return 0;
}
TimeOut = 6000;//設置接收超時6秒
if(::setsockopt(cClient,SOL_SOCKET,SO_RCVTIMEO,(char *)&TimeOut,sizeof(TimeOut))==SOCKET_ERROR){
return 0;
}
//設置非阻塞方式連接
unsigned long ul = 1;
ret = ioctlsocket(cClient, FIONBIO, (unsigned long*)&ul);
if(ret==SOCKET_ERROR) return 0;
//連接
server.sin_family = AF_INET;
server.sin_port = htons(25);
server.sin_addr .s_addr = inet_addr((LPCSTR)pSmtp);
if(server.sin_addr.s_addr == INADDR_NONE){return 0;}
//運行這里將不會阻塞,而是直接運行下去,通過select中設置的 timeval結構參數設定連接超時處理。
connect(cClient,(const struct sockaddr *)&server,sizeof(server));
//select 模型,即設置超時
struct timeval timeout ;
fd_set r;
FD_ZERO(&r);
FD_SET(cClient, &r);
timeout.tv_sec = 15; //連接超時15秒
timeout.tv_usec =0;
ret = select(0, 0, &r, 0, &timeout); //超時socket將關閉
if ( ret <= 0 )
{
::closesocket(cClient);
return 0;
}
//一般非鎖定模式套接比較難控制,可以根據實際情況考慮 再設回阻塞模式,又把狀態設置為 阻塞狀態。
unsigned long ul1= 0 ;
ret = ioctlsocket(cClient, FIONBIO, (unsigned long*)&ul1);
if(ret==SOCKET_ERROR){
::closesocket (cClient);
return 0;
}
4.select()
a. 當你希望服務器監聽連接服務請求,而又不想通過輪詢的方式,則理想的方式是調用select().它運行你把程序本身掛起來,而同時使系統內核監聽所要求的一組文件描述符的任何活動,只要確認在任何被監控的文件描述符上出現了活動,select()調用將返回指示該文件描述符已準備好的信息,從而實現了程序的選出是隨機變化的,而不必由程序本身對輸入進行測試而浪費cpu開銷,
在socket編程中,select函數一般在非阻塞的socket中,用來檢查socket緩沖區中是否有數據可讀,或是否可以寫數據到socket緩沖區。
有時,select()也被用來當作延時函數使用。sleep()延時會釋放cpu,用select的話,可以在占用cpu的情況下,延時。
select()是用來進行多路轉接的函數。它可以同時等待n(n大于等于1)個文件描述字或者socket套接口。只要它等待的任意描述字準備好或者等待時間超過了設定時間程序就往下執行。可以防止進程長時間阻塞,占用資源。
b.簡單說法:
如果你要發數據用select(sock+1,&s,NULL,NULL,NULL);
if(FD_ISSET(sock,&s) ,你可以發了。send it
如果你要收數據用select(sock+1,NULL,&s,NULL,NULL);
if(FD_ISSET(sock,&s) ,你可以收了。recv it
socket默認情況下是阻塞的,除非你用WSAAsyncSelect OR select 就變成NOBBLOCKING,
將阻塞設為非阻塞如下:
int opt=1;
ioctlsocket(sock,FIONBIO,&opt)
若opt=0就是阻塞的了
c.用法一
fd_set m_readfds;
fd_set m_exceptfds;
timeval m_tmOut;
m_tmOut.tv_sec = 120; //接收時間如果超過120秒,即認為網絡連接已經中斷,
m_tmOut.tv_usec = 0; //客戶端應該定時每40秒發送一次空閑信號,以防止被誤認為是網絡連接中斷。
FD_ZERO( &m_readfds );
FD_ZERO( &m_exceptfds );
FD_SET( m_scSocket, &m_exceptfds );
FD_SET( m_scSocket, &m_readfds );
int CNet::Receive( char * szBuff, int iSize )
{
int iRet;
if( m_ntType == _NET_SERVER_ )
{
iRet = select( m_scSocket + 1, &m_readfds, NULL, &m_exceptfds, &m_tmOut );
if( iRet == 0 )
{
m_iError = 13; //超時
return -2;
}
if( iRet == SOCKET_ERROR )
{
GetLastError( );
return -1;
}
if( FD_ISSET( m_scSocket, &m_exceptfds ) )
{
m_iError = 14; //連接被終止
return -1;
}
}
iRet = recv( m_scSocket, szBuff, iSize, 0 );
if( iRet == 0 )
{
m_iError = 14; //連接被終止
return -1;
}
if( iRet == SOCKET_ERROR )
{
GetLastError( );
return -1;
}
return iRet;
}
用法二
int recvex_sock(SOCKET sock, void* buf, int len, int sec)
{
int rs;
fd_set fd;
struct timeval tv;
memset(&tv, 0, sizeof(tv));
if ( sec > 0 )
tv.tv_sec = sec;
FD_ZERO(&fd);
FD_SET(sock, &fd);
rs = select(sock + 1, &fd, 0, 0, sec >= 0 ? &tv : NULL);
if ( rs == 0 )
return SOCKET_TIMEOUT;
if ( rs < 0 )
return SOCKET_ERROR;
if ( !FD_ISSET(sock, &fd) )
return SOCKET_ERROR;
return (recv_sock(sock, buf, len));
}