1.概念理解ios
在進行網絡編程時,咱們經常見到同步(Sync)/異步(Async),阻塞(Block)/非阻塞(Unblock)編程
四種調用模式:windows
同步:所謂同步,就是在發出一個功能調用時,在沒有獲得結果前,該調用就不返回。也就是必須一件數組
一件作事,等前一件作完了才能作另外一件。安全
例如在C/S模式的某個流程中,你服務器提交了某個請求,在服務器處理完畢返回結果期間客戶端什麼服務器
也不能作。網絡
異步:異步概念和同步相對。當一個異步過程調用發出後,調用者不會馬上獲得結果。調用者在發出併發
調用後能夠繼續作本身的事,被調用者經過狀態、通知來通知調用者,或者經過回調函數處理這個調用。異步
阻塞:阻塞調用是指調用結果返回前,當前線程會被掛起(當前線程處於非可執行狀態,在這個狀態下,socket
CPU不會給線程分配時間片,即線程暫停運行),函數只有在獲得結果後纔回返回。
非阻塞:非阻塞和阻塞的概念相對,是指不能馬上獲得借過前,該函數不會阻塞當前進程,而回馬上返回。
區別:有人會把同步和阻塞調用等同起來,實際上他們是不一樣的,對於同步調用來講,不少時候當前調用
仍是激活的,只是從邏輯上當前函數沒有返回而已。阻塞的話當前線程會被掛起。
2.Select模型的原理和使用步驟
select(選擇)模型是Winsock中最多見的I/O模型。之因此稱其爲「 select模型」,是因爲它的「中心思想」
即是利用select函數,實現對 I/O的管理!利用select函數,咱們判斷套接字上是否存在數據,或者可否向一
個套接字寫入數據。之因此要設計這個函數,惟一的目的即是防止應用程序在套接字處於鎖定模式中時,在
一次I/O綁定調用(如send或recv)過程當中,被迫進入「鎖定」狀態;同時防止在套接字處於非鎖定模式中時,
產生WSAEWOULDBLOCK錯誤。除非知足事先用參數規定的條件,不然select函數會在進行I/O操做時鎖定。
select的函數原型以下:
int select ( int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout );
其中,第一個參數nfds會被忽略。之因此仍然要提供這個參數,只是爲了保持與早期的Berkeley套接字應用程
序的兼容。你們可注意到三個 fd_set參數:一個用於檢查可讀性(readfds),一個用於檢查可寫性(writefds),
另外一個用於例外數據( excepfds)。從根本上說,fdset數據類型表明着一系列特定套接字的集合。其中,
readfds集合包括符合下述任何一個條件的套接字:
■ 有數據能夠讀入。
■ 鏈接已經關閉、重設或停止。
■ 假如已調用了listen,並且一個鏈接正在創建,那麼accept函數調用會成功。
writefds集合包括符合下述任何一個條件的套接字:
■ 有數據能夠發出。
■ 若是已完成了對一個非鎖定鏈接調用的處理,鏈接就會成功。
最後,exceptfds集合包括符合下述任何一個條件的套接字:
■ 假如已完成了對一個非鎖定鏈接調用的處理,鏈接嘗試就會失敗。
■ 有帶外(out-of-band,OOB)數據可供讀取。
例如,假定咱們想測試一個套接字是否「可讀」,必須將本身的套接字增添到readfds集合,再等待select函數
完成。select完成以後,必須判斷本身的套接字是否仍爲readfds集合的一部分。若答案是確定的,便代表該套
接字「可讀」,可當即着手從它上面讀取數據。在三個參數中(readfds、writedfss和exceptfds),任何兩個都
能夠是空值(NULL);可是,至少有一個不能爲空值!在任何不爲空的集合中,必須包含至少一個套接字句柄;
不然, select函數便沒有任何東西能夠等待。最後一個參數timeout對應的是一個指針,它指向一個timeval結構,
用於決定select最多等待 I / O操做完成多久的時間。如 timeout是一個空指針,那麼select調用會無限期地「鎖定」
或停頓下去,直到至少有一個描述符符合指定的條件後結束。對timeval結構的定義以下:
struct timeval {
long tv_sec;
long tv_usec;
} ;
若將超時值設置爲(0,0),代表select會當即返回,容許應用程序對 select操做進行「輪詢」。出於對性能方面
的考慮,應避免這樣的設置。select成功完成後,會在 fd_set結構中,返回恰好有未完成的I/O操做的全部套接字
句柄的總量。若超過timeval設定的時間,便會返回0。無論因爲什麼緣由,假如select調用失敗,都會返回SOCKET_ERROR。
用select對套接字進行監視以前,在本身的應用程序中,必須將套接字句柄分配給一個集合,設置好一個或所有
讀、寫以及例外 fd_set結構。將一個套接字分配給任何一個集合後,再來調用select,即可知道一個套接字上是
否正在發生上述的I/O活動。Winsock提供了下列宏操做,可用來針對I/O活動,對 fd_set進行處理與檢查:
■ FD_CLR(s, *set):從s e t中刪除套接字 s。
■ FD_ISSET(s, *set):檢查 s是否s e t集合的一名成員;如答案是確定的是,則返回 T R U E。
■ FD_SET(s, *set):將套接字 s加入集合 s e t。
■ F D _ Z E R O ( * s e t ):將s e t初始化成空集合。
例如,假定咱們想知道是否可從一個套接字中安全地讀取數據,同時不會陷於無休止的「鎖定」狀態,即可使用
FD_SET宏,將本身的套接字分配給fd_set集合,再來調用select。要想檢測本身的套接字是否仍屬 fd_read集合
的一部分,可以使用FD_ISSET宏。採用下述步驟,即可完成用select操做一個或多個套接字句柄的全過程:
1) 使用FD_ZERO宏,初始化本身感興趣的每個fd_set。
2) 使用FD_SET宏,將套接字句柄分配給本身感興趣的每一個fd_set。
3) 調用select函數,而後等待在指定的fd_set集合中,I/O活動設置好一個或多個套接字句柄。
select完成後,會返回在全部fd_set集合中設置的套接字句柄總數,並對每一個集合進行相應的更新。
4) 根據select的返回值,咱們的應用程序即可判斷出哪些套接字存在着還沒有完成(待決)
的I/O操做—具體的方法是使用FD_ISSET宏,對每一個fd_set集合進行檢查。
5) 知道了每一個集合中「待決」的I/O操做以後,對I/O進行處理,而後返回步驟 1 ),繼續進
行
select返回後,它會修改每一個fd_set結構,刪除那些不存在待決 I / O操做的套接字句柄。這正是咱們在上述的步
驟 ( 4 )中,爲什麼要使用FD_ISSET宏來判斷一個特定的套接字是否仍在集合中的緣由。
3.參考代碼
// Select.cpp : 定義控制檯應用程序的入口點。
//
#include "stdafx.h"
#include <WinSock2.h>
#include <iostream>
using namespace std;
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define PORT 8000
#define MSGSIZE 255
#define SRV_IP "127.0.0.1"
int g_nSockConn = 0;//請求鏈接的數目
//FD_SETSIZE是在winsocket2.h頭文件裏定義的,這裏windows默認最大爲64
//在包含winsocket2.h頭文件前使用宏定義能夠修改這個值
struct ClientInfo
{
SOCKET sockClient;
SOCKADDR_IN addrClient;
};
ClientInfo g_Client[FD_SETSIZE];
DWORD WINAPI WorkThread(LPVOID lpParameter);
int _tmain(int argc, _TCHAR* argv[])
{//基本步驟就不解釋了,網絡編程基礎那篇博客裏講的很詳細了
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2),&wsaData);
SOCKET sockListen = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr(SRV_IP);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(PORT);
bind(sockListen,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
listen(sockListen,64);
DWORD dwThreadIDRecv = 0;
DWORD dwThreadIDWrite = 0;
HANDLE hand = CreateThread(NULL,0, WorkThread,NULL,0,&dwThreadIDRecv);//用來處理手法消息的進程
if (hand == NULL)
{
cout<<"Create work thread failed\n";
getchar();
return -1;
}
SOCKET sockClient;
SOCKADDR_IN addrClient;
int nLenAddrClient = sizeof(SOCKADDR);//這裏用0初試化找了半天才找出錯誤
while (true)
{
sockClient = accept(sockListen,(SOCKADDR*)&addrClient,&nLenAddrClient);//第三個參數必定要按照addrClient大小初始化
//輸出鏈接者的地址信息
//cout<<inet_ntoa(addrClient.sin_addr)<<":"<<ntohs(addrClient.sin_port)<<"has connect !"<<endl;
if (sockClient != INVALID_SOCKET)
{
g_Client[g_nSockConn].addrClient = addrClient;//保存鏈接端地址信息
g_Client[g_nSockConn].sockClient = sockClient;//加入鏈接者隊列
g_nSockConn++;
}
}
closesocket(sockListen);
WSACleanup();
return 0;
}
DWORD WINAPI WorkThread(LPVOID lpParameter)
{
FD_SET fdRead;
int nRet = 0;//記錄發送或者接受的字節數
TIMEVAL tv;//設置超時等待時間
tv.tv_sec = 1;
tv.tv_usec = 0;
char buf[MSGSIZE] = "";
while (true)
{
FD_ZERO(&fdRead);
for (int i = 0;i < g_nSockConn;i++)
{
FD_SET(g_Client[i].sockClient,&fdRead);
}
//只處理read事件,不事後面仍是會有讀寫消息發送的
nRet = select(0,&fdRead,NULL,NULL,&tv);
if (nRet == 0)
{//沒有鏈接或者沒有讀事件
continue;
}
for (int i = 0;i < g_nSockConn;i++)
{
if (FD_ISSET(g_Client[i].sockClient,&fdRead))
{
nRet = recv(g_Client[i].sockClient,buf,sizeof(buf),0);
if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
cout<<"Client "<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<"closed"<<endl;
closesocket(g_Client[i].sockClient);
if (i < g_nSockConn-1)
{
//將失效的sockClient剔除,用數組的最後一個補上去
g_Client[i--].sockClient = g_Client[--g_nSockConn].sockClient;
}
}
else
{
cout<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<": "<<endl;
cout<<buf<<endl;
cout<<"Server:"<<endl;
//gets(buf);
strcpy(buf,"Hello!");
nRet = send(g_Client[i].sockClient,buf,strlen(buf)+1,0);
}
}
}
}
return 0;
}
服務器的主要步驟:
1.建立監聽套接字,綁定,監聽
2.建立工做者線程
3.建立一個套接字組,用來存放當前全部活動的客戶端套接字,沒accept一個鏈接就更新一次數組
4.接收客戶端的鏈接,由於沒有從新定義FD_SIZE宏,服務器最多支持64個併發鏈接。最好是記錄下鏈接數,不要無條件的接受鏈接
工做線程
工做線程是一個死循環,依次循環完成的動做是:
1.將當前客戶端套接字加入到fd_read集中
2.調用select函數
3.用FD_ISSET查看時候套接字還在讀集中,若是是就接收數據。若是接收的數據長度爲0,或者發生WSAECONNRESET錯誤,,則
表示客戶端套接字主動關閉,咱們要釋放這個套接字資源,調整咱們的套接字數組(讓下一個補上)。上面還有個nRet==0的判斷,
就是由於select函數會當即返回,鏈接數爲0會陷入死循環。