一、TCP/IP 體系結構與特點
1、TCP/IP體系結構
TCP/IP協議實際上就是在物理網上的一組完整的網絡協議。其中TCP是提供傳輸層服務,而IP則是提供網絡層服務。TCP/IP包括以下協議:(結構如圖1.1)

(圖1.1)
IP: 網間協議(Internet Protocol) 負責主機間數據的路由和網絡上數據的存儲。同時為ICMP,TCP, UDP提供分組發送服務。用戶進程通常不需要涉及這一層。
ARP: 地址解析協議(Address Resolution Protocol)
此協議將網絡地址映射到硬件地址。
RARP: 反向地址解析協議(Reverse Address Resolution Protocol)
此協議將硬件地址映射到網絡地址
ICMP: 網間報文控制協議(Internet Control Message Protocol)
此協議處理信關和主機的差錯和傳送控制。
TCP: 傳送控制協議(Transmission Control Protocol)
這是一種提供給用戶進程的可靠的全雙工字節流面向連接的協議。它要為用戶進程提供虛電路服務,并為數據可靠傳輸建立檢查。(注:大多數網絡用戶程序使用TCP)
UDP: 用戶數據報協議(User Datagram Protocol)
這是提供給用戶進程的無連接協議,用于傳送數據而不執行正確性檢查。
FTP: 文件傳輸協議(File Transfer Protocol)
允許用戶以文件操作的方式(文件的增、刪、改、查、傳送等)與另一主機相互通信。
SMTP: 簡單郵件傳送協議(Simple Mail Transfer Protocol)
SMTP協議為系統之間傳送電子郵件。
TELNET:終端協議(Telnet Terminal Procotol)
允許用戶以虛終端方式訪問遠程主機
HTTP: 超文本傳輸協議(Hypertext Transfer Procotol)
TFTP: 簡單文件傳輸協議(Trivial File Transfer Protocol)
2、TCP/IP特點
TCP/IP協議的核心部分是傳輸層協議(TCP、UDP),網絡層協議(IP)和物理接口層,這三層通常是在操作系統內核中實現。因此用戶一般不涉及。編程時,編程界面有兩種形式:一、是由內核心直接提供的系統調用;二、使用以庫函數方式提供的各種函數。前者為核內實現,后者為核外實現。用戶服務要通過核外的應用程序才能實現,所以要使用套接字(socket)來實現。
圖1.2是TCP/IP協議核心與應用程序關系圖。

(圖1.2)
二、專用術語
1、套接字
套接字是網絡的基本構件。它是可以被命名和尋址的通信端點,使用中的每一個套接字都有其類型和一個與之相連聽進程。套接字存在通信區域(通信區域又稱地址簇)中。套接字只與同一區域中的套接字交換數據(跨區域時,需要執行某和轉換進程才能實現)。WINDOWS 中的套接字只支持一個域——網際域。套接字具有類型。
WINDOWS SOCKET 1.1 版本支持兩種套接字:流套接字(SOCK_STREAM)和數據報套接字(SOCK_DGRAM)
2、WINDOWS SOCKETS 實現
一個WINDOWS SOCKETS 實現是指實現了WINDOWS SOCKETS規范所描述的全部功能的一套軟件。一般通過DLL文件來實現
3、阻塞處理例程
阻塞處理例程(blocking hook,阻塞鉤子)是WINDOWS SOCKETS實現為了支持阻塞套接字函數調用而提供的一種機制。
4、多址廣播(multicast,多點傳送或組播)
是一種一對多的傳輸方式,傳輸發起者通過一次傳輸就將信息傳送到一組接收者,與單點傳送
(unicast)和廣播(Broadcast)相對應。
一、客戶機/服務器模式
在TCP/IP網絡中兩個進程間的相互作用的主機模式是客戶機/服務器模式(Client/Server model)。該模式的建立基于以下兩點:1、非對等作用;2、通信完全是異步的。客戶機/服務器模式在操作過程中采取的是主動請示方式:
首先服務器方要先啟動,并根據請示提供相應服務:(過程如下)
1、打開一通信通道并告知本地主機,它愿意在某一個公認地址上接收客戶請求。
2、等待客戶請求到達該端口。
3、接收到重復服務請求,處理該請求并發送應答信號。
4、返回第二步,等待另一客戶請求
5、關閉服務器。
客戶方:
1、打開一通信通道,并連接到服務器所在主機的特定端口。
2、向服務器發送服務請求報文,等待并接收應答;繼續提出請求……
3、請求結束后關閉通信通道并終止。
二、基本套接字
為了更好說明套接字編程原理,給出幾個基本的套接字,在以后的篇幅中會給出更詳細的使用說明。
1、創建套接字——socket()
功能:使用前創建一個新的套接字
格式:SOCKET PASCAL FAR socket(int af,int type,int procotol);
參數:af: 通信發生的區域
type: 要建立的套接字類型
procotol: 使用的特定協議
2、指定本地地址——bind()
功能:將套接字地址與所創建的套接字號聯系起來。
格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen);
參數:s: 是由socket()調用返回的并且未作連接的套接字描述符(套接字號)。
其它:沒有錯誤,bind()返回0,否則SOCKET_ERROR
地址結構說明:
struct sockaddr_in
{
short sin_family;//AF_INET
u_short sin_port;//16位端口號,網絡字節順序
struct in_addr sin_addr;//32位IP地址,網絡字節順序
char sin_zero[8];//保留
}
3、建立套接字連接——connect()和accept()
功能:共同完成連接工作
格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen);
SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen);
參數:同上
4、監聽連接——listen()
功能:用于面向連接服務器,表明它愿意接收連接。
格式:int PASCAL FAR listen(SOCKET s, int backlog);
5、數據傳輸——send()與recv()
功能:數據的發送與接收
格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags);
int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags);
參數:buf:指向存有傳輸數據的緩沖區的指針。
6、多路復用——select()
功能:用來檢測一個或多個套接字狀態。
格式:int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds,
fd_set FAR * exceptfds,const struct timeval FAR * timeout);
參數:readfds:指向要做讀檢測的指針
writefds:指向要做寫檢測的指針
exceptfds:指向要檢測是否出錯的指針
timeout:最大等待時間
7、關閉套接字——closesocket()
功能:關閉套接字s
格式:BOOL PASCAL FAR closesocket(SOCKET s);
三、典型過程圖
2.1 面向連接的套接字的系統調用時序圖

2.2 無連接協議的套接字調用時序圖

2.3 面向連接的應用程序流程圖
Windows Socket1.1 程序設計
一、簡介
Windows Sockets 是從 Berkeley Sockets 擴展而來的,其在繼承 Berkeley Sockets 的基礎上,又進行了新的擴充。這些擴充主要是提供了一些異步函數,并增加了符合WINDOWS消息驅動特性的網絡事件異步選擇機制。
Windows Sockets由兩部分組成:開發組件和運行組件。
開發組件:Windows Sockets 實現文檔、應用程序接口(API)引入庫和一些頭文件。
運行組件:Windows Sockets 應用程序接口的動態鏈接庫(WINSOCK.DLL)。
二、主要擴充說明
1、異步選擇機制:
Windows Sockets 的異步選擇函數提供了消息機制的網絡事件選擇,當使用它登記網絡事件發生時,應用程序相應窗口函數將收到一個消息,消息中指示了發生的網絡事件,以及與事件相關的一些信息。
Windows Sockets 提供了一個異步選擇函數 WSAAsyncSelect(),用它來注冊應用程序感興趣的網絡事件,當這些事件發生時,應用程序相應的窗口函數將收到一個消息。
函數結構如下:
int PASCAL FAR WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent); |
參數說明:
hWnd:窗口句柄
wMsg:需要發送的消息
lEvent:事件(以下為事件的內容)
值: |
含義: |
FD_READ |
期望在套接字上收到數據(即讀準備好)時接到通知 |
FD_WRITE |
期望在套接字上可發送數據(即寫準備好)時接到通知 |
FD_OOB |
期望在套接字上有帶外數據到達時接到通知 |
FD_ACCEPT |
期望在套接字上有外來連接時接到通知 |
FD_CONNECT |
期望在套接字連接建立完成時接到通知 |
FD_CLOSE |
期望在套接字關閉時接到通知 |
例如:我們要在套接字讀準備好或寫準備好時接到通知,語句如下:
rc=WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE); |
如果我們需要注銷對套接字網絡事件的消息發送,只要將 lEvent 設置為0
2、異步請求函數
在 Berkeley Sockets 中請求服務是阻塞的,WINDOWS SICKETS 除了支持這一類函數外,還增加了相應的異步請求函數(WSAAsyncGetXByY();)。
3、阻塞處理方法
Windows Sockets 為了實現當一個應用程序的套接字調用處于阻塞時,能夠放棄CPU讓其它應用程序運行,它在調用處于阻塞時便進入一個叫“HOOK”的例程,此例程負責接收和分配WINDOWS消息,使得其它應用程序仍然能夠接收到自己的消息并取得控制權。
WINDOWS 是非搶先的多任務環境,即若一個程序不主動放棄其控制權,別的程序就不能執行。因此在設計Windows Sockets 程序時,盡管系統支持阻塞操作,但還是反對程序員使用該操作。但由于 SUN 公司下的 Berkeley Sockets 的套接字默認操作是阻塞的,WINDOWS 作為移植的 SOCKETS 也不可避免對這個操作支持。
在Windows Sockets 實現中,對于不能立即完成的阻塞操作做如下處理:DLL初始化→循環操作。在循環中,它發送任何 WINDOWS 消息,并檢查這個 Windows Sockets 調用是否完成,在必要時,它可以放棄CPU讓其它應用程序執行(當然使用超線程的CPU就不會有這個麻煩了^_^)。我們可以調用 WSACancelBlockingCall() 函數取消此阻塞操作。
在 Windows Sockets 中,有一個默認的阻塞處理例程 BlockingHook() 簡單地獲取并發送 WINDOWS 消息。如果要對復雜程序進行處理,Windows Sockets 中還有 WSASetBlockingHook() 提供用戶安裝自己的阻塞處理例程能力;與該函數相對應的則是 SWAUnhookBlockingHook(),它用于刪除先前安裝的任何阻塞處理例程,并重新安裝默認的處理例程。請注意,設計自己的阻塞處理例程時,除了函數 WSACancelBlockingHook() 之外,它不能使用其它的 Windows Sockets API 函數。在處理例程中調用 WSACancelBlockingHook()函數將取消處于阻塞的操作,它將結束阻塞循環。
4、出錯處理
Windows Sockets 為了和以后多線程環境(WINDOWS/UNIX)兼容,它提供了兩個出錯處理函數來獲取和設置當前線程的最近錯誤號。(WSAGetLastEror()和WSASetLastError())
5、啟動與終止
使用函數 WSAStartup() 和 WSACleanup() 啟動和終止套接字。
三、Windows Sockets網絡程序設計核心
我們終于可以開始真正的 Windows Sockets 網絡程序設計了。不過我們還是先看一看每個 Windows Sockets 網絡程序都要涉及的內容。讓我們一步步慢慢走。
1、啟動與終止
在所有 Windows Sockets 函數中,只有啟動函數 WSAStartup() 和終止函數 WSACleanup() 是必須使用的。
啟動函數必須是第一個使用的函數,而且它允許指定 Windows Sockets API 的版本,并獲得 SOCKETS的特定的一些技術細節。本結構如下:
int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); |
其中 wVersionRequested 保證 SOCKETS 可正常運行的 DLL 版本,如果不支持,則返回錯誤信息。
我們看一下下面這段代碼,看一下如何進行 WSAStartup() 的調用
WORD wVersionRequested;// 定義版本信息變量 WSADATA wsaData;//定義數據信息變量 int err;//定義錯誤號變量 wVersionRequested = MAKEWORD(1,1);//給版本信息賦值 err = WSAStartup(wVersionRequested, &wsaData);//給錯誤信息賦值 if(err!=0) { return;//告訴用戶找不到合適的版本 } //確認 Windows Sockets DLL 支持 1.1 版本 //DLL 版本可以高于 1.1 //系統返回的版本號始終是最低要求的 1.1,即應用程序與DLL 中可支持的最低版本號 if(LOBYTE(wsaData.wVersion)!= 1|| HIBYTE(wsaData.wVersion)!=1) { WSACleanup();//告訴用戶找不到合適的版本 return; } //Windows Sockets DLL 被進程接受,可以進入下一步操作 |
關閉函數使用時,任何打開并已連接的 SOCK_STREAM 套接字被復位,但那些已由 closesocket() 函數關閉的但仍有未發送數據的套接字不受影響,未發送的數據仍將被發送。程序運行時可能會多次調用 WSAStartuo() 函數,但必須保證每次調用時的 wVersionRequested 的值是相同的。
2、異步請求服務
Windows Sockets 除支持 Berkeley Sockets 中同步請求,還增加了了一類異步請求服務函數 WSAAsyncGerXByY()。該函數是阻塞請求函數的異步版本。應用程序調用它時,由 Windows Sockets DLL 初始化這一操作并返回調用者,此函數返回一個異步句柄,用來標識這個操作。當結果存儲在調用者提供的緩沖區,并且發送一個消息到應用程序相應窗口。常用結構如下:
HANDLE taskHnd; char hostname="rs6000"; taskHnd = WSAAsyncBetHostByName(hWnd,wMsg,hostname,buf,buflen); |
需要注意的是,由于 Windows 的內存對像可以設置為可移動和可丟棄,因此在操作內存對象是,必須保證 WIindows Sockets DLL 對象是可用的。
3、異步數據傳輸
使用 send() 或 sendto() 函數來發送數據,使用 recv() 或recvfrom() 來接收數據。Windows Sockets 不鼓勵用戶使用阻塞方式傳輸數據,因為那樣可能會阻塞整個 Windows 環境。下面我們看一個異步數據傳輸實例:
假設套接字 s 在連接建立后,已經使用了函數 WSAAsyncSelect() 在其上注冊了網絡事件 FD_READ 和 FD_WRITE,并且 wMsg 值為 UM_SOCK,那么我們可以在 Windows 消息循環中增加如下的分支語句:
case UM_SOCK: switch(lParam) { case FD_READ: len = recv(wParam,lpBuffer,length,0); break; case FD_WRITE: while(send(wParam,lpBuffer,len,0)!=SOCKET_ERROR) break; } break; |
4、出錯處理
Windows 提供了一個函數來獲取最近的錯誤碼 WSAGetLastError(),推薦的編寫方式如下:
len = send (s,lpBuffer,len,0); of((len==SOCKET_ERROR)&&(WSAGetLastError()==WSAWOULDBLOCK)){...} |
基于Visual C++的Winsock API研究
為了方便網絡編程,90年代初,由Microsoft聯合了其他幾家公司共同制定了一套WINDOWS下的網絡編程接口,即Windows Sockets規范,它不是一種網絡協議,而是一套開放的、支持多種協議的Windows下的網絡編程接口。現在的Winsock已經基本上實現了與協議無關,你可以使用Winsock來調用多種協議的功能,但較常使用的是TCP/IP協議。Socket實際在計算機中提供了一個通信端口,可以通過這個端口與任何一個具有Socket接口的計算機通信。應用程序在網絡上傳輸,接收的信息都通過這個Socket接口來實現。
微軟為VC定義了Winsock類如CAsyncSocket類和派生于CAsyncSocket 的CSocket類,它們簡單易用,讀者朋友當然可以使用這些類來實現自己的網絡程序,但是為了更好的了解Winsock API編程技術,我們這里探討怎樣使用底層的API函數實現簡單的 Winsock 網絡應用程式設計,分別說明如何在Server端和Client端操作Socket,實現基于TCP/IP的數據傳送,最后給出相關的源代碼。
在VC中進行WINSOCK的API編程開發的時候,需要在項目中使用下面三個文件,否則會出現編譯錯誤。
1.WINSOCK.H: 這是WINSOCK API的頭文件,需要包含在項目中。
2.WSOCK32.LIB: WINSOCK API連接庫文件。在使用中,一定要把它作為項目的非缺省的連接庫包含到項目文件中去。
3.WINSOCK.DLL: WINSOCK的動態連接庫,位于WINDOWS的安裝目錄下。
一、服務器端操作 socket(套接字)
1)在初始化階段調用WSAStartup()
此函數在應用程序中初始化Windows Sockets DLL ,只有此函數調用成功后,應用程序才可以再調用其他Windows Sockets DLL中的API函數。在程式中調用該函數的形式如下:WSAStartup((WORD)((1<<8|1),(LPWSADATA)&WSAData),其中(1<<8|1)表示我們用的是WinSocket1.1版本,WSAata用來存儲系統傳回的關于WinSocket的資料。
2)建立Socket
初始化WinSock的動態連接庫后,需要在服務器端建立一個監聽的Socket,為此可以調用Socket()函數用來建立這個監聽的Socket,并定義此Socket所使用的通信協議。此函數調用成功返回Socket對象,失敗則返回INVALID_SOCKET(調用WSAGetLastError()可得知原因,所有WinSocket 的函數都可以使用這個函數來獲取失敗的原因)。
SOCKET PASCAL FAR socket( int af, int type, int protocol )
參數: af:目前只提供 PF_INET(AF_INET);
type:Socket 的類型 (SOCK_STREAM、SOCK_DGRAM);
protocol:通訊協定(如果使用者不指定則設為0);
如果要建立的是遵從TCP/IP協議的socket,第二個參數type應為SOCK_STREAM,如為UDP(數據報)的socket,應為SOCK_DGRAM。
3)綁定端口
接下來要為服務器端定義的這個監聽的Socket指定一個地址及端口(Port),這樣客戶端才知道待會要連接哪一個地址的哪個端口,為此我們要調用bind()函數,該函數調用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );
參 數: s:Socket對象名;
name:Socket的地址值,這個地址必須是執行這個程式所在機器的IP地址;
namelen:name的長度;
如果使用者不在意地址或端口的值,那么可以設定地址為INADDR_ANY,及Port為0,Windows Sockets 會自動將其設定適當之地址及Port (1024 到 5000之間的值)。此后可以調用getsockname()函數來獲知其被設定的值。
4)監聽
當服務器端的Socket對象綁定完成之后,服務器端必須建立一個監聽的隊列來接收客戶端的連接請求。listen()函數使服務器端的Socket 進入監聽狀態,并設定可以建立的最大連接數(目前最大值限制為 5, 最小值為1)。該函數調用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR listen( SOCKET s, int backlog ); 參 數: s:需要建立監聽的Socket; backlog:最大連接個數; |
服務器端的Socket調用完listen()后,如果此時客戶端調用connect()函數提出連接申請的話,Server 端必須再調用accept() 函數,這樣服務器端和客戶端才算正式完成通信程序的連接動作。為了知道什么時候客戶端提出連接要求,從而服務器端的Socket在恰當的時候調用accept()函數完成連接的建立,我們就要使用WSAAsyncSelect()函數,讓系統主動來通知我們有客戶端提出連接請求了。該函數調用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent ); 參數: s:Socket 對象; hWnd :接收消息的窗口句柄; wMsg:傳給窗口的消息; lEvent:被注冊的網絡事件,也即是應用程序向窗口發送消息的網路事件,該值為下列值FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、FD_CONNECT、FD_CLOSE的組合,各個值的具體含意為FD_READ:希望在套接字S收到數據時收到消息;FD_WRITE:希望在套接字S上可以發送數據時收到消息;FD_ACCEPT:希望在套接字S上收到連接請求時收到消息;FD_CONNECT:希望在套接字S上連接成功時收到消息;FD_CLOSE:希望在套接字S上連接關閉時收到消息;FD_OOB:希望在套接字S上收到帶外數據時收到消息。 |
具體應用時,wMsg應是在應用程序中定義的消息名稱,而消息結構中的lParam則為以上各種網絡事件名稱。所以,可以在窗口處理自定義消息函數中使用以下結構來響應Socket的不同事件:
switch(lParam) {case FD_READ: … break; case FD_WRITE、 … break; … } |
5)服務器端接受客戶端的連接請求
當Client提出連接請求時,Server 端hwnd視窗會收到Winsock Stack送來我們自定義的一個消息,這時,我們可以分析lParam,然后調用相關的函數來處理此事件。為了使服務器端接受客戶端的連接請求,就要使用accept() 函數,該函數新建一Socket與客戶端的Socket相通,原先監聽之Socket繼續進入監聽狀態,等待他人的連接要求。該函數調用成功返回一個新產生的Socket對象,否則返回INVALID_SOCKET。
SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen ); 參數:s:Socket的識別碼; addr:存放來連接的客戶端的地址; addrlen:addr的長度 |
6)結束 socket 連接
結束服務器和客戶端的通信連接是很簡單的,這一過程可以由服務器或客戶機的任一端啟動,只要調用closesocket()就可以了,而要關閉Server端監聽狀態的socket,同樣也是利用此函數。另外,與程序啟動時調用WSAStartup()憨數相對應,程式結束前,需要調用 WSACleanup() 來通知Winsock Stack釋放Socket所占用的資源。這兩個函數都是調用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR closesocket( SOCKET s ); 參 數:s:Socket 的識別碼; int PASCAL FAR WSACleanup( void ); 參 數: 無 |
二、客戶端Socket的操作 1)建立客戶端的Socket
客戶端應用程序首先也是調用WSAStartup() 函數來與Winsock的動態連接庫建立關系,然后同樣調用socket() 來建立一個TCP或UDP socket(相同協定的 sockets 才能相通,TCP 對 TCP,UDP 對 UDP)。與服務器端的socket 不同的是,客戶端的socket 可以調用 bind() 函數,由自己來指定IP地址及port號碼;但是也可以不調用 bind(),而由 Winsock來自動設定IP地址及port號碼。
2)提出連接申請
客戶端的Socket使用connect()函數來提出與服務器端的Socket建立連接的申請,函數調用成功返回0,否則返回SOCKET_ERROR。
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen ); 參 數:s:Socket 的識別碼; name:Socket想要連接的對方地址; namelen:name的長度 |
三、數據的傳送 雖然基于TCP/IP連接協議(流套接字)的服務是設計客戶機/服務器應用程序時的主流標準,但有些服務也是可以通過無連接協議(數據報套接字)提供的。先介紹一下TCP socket 與UDP socket 在傳送數據時的特性:Stream (TCP) Socket 提供雙向、可靠、有次序、不重復的資料傳送。Datagram (UDP) Socket 雖然提供雙向的通信,但沒有可靠、有次序、不重復的保證,所以UDP傳送數據可能會收到無次序、重復的資料,甚至資料在傳輸過程中出現遺漏。由于UDP Socket 在傳送資料時,并不保證資料能完整地送達對方,所以絕大多數應用程序都是采用TCP處理Socket,以保證資料的正確性。一般情況下TCP Socket 的數據發送和接收是調用send() 及recv() 這兩個函數來達成,而 UDP Socket則是用sendto() 及recvfrom() 這兩個函數,這兩個函數調用成功發揮發送或接收的資料的長度,否則返回SOCKET_ERROR。
int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags ); 參數:s:Socket 的識別碼 buf:存放要傳送的資料的暫存區 len buf:的長度 flags:此函數被調用的方式 |
對于Datagram Socket而言,若是 datagram 的大小超過限制,則將不會送出任何資料,并會傳回錯誤值。對Stream Socket 言,Blocking 模式下,若是傳送系統內的儲存空間不夠存放這些要傳送的資料,send()將會被block住,直到資料送完為止;如果該Socket被設定為 Non-Blocking 模式,那么將視目前的output buffer空間有多少,就送出多少資料,并不會被 block 住。flags 的值可設為 0 或 MSG_DONTROUTE及 MSG_OOB 的組合。
int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags ); 參數:s:Socket 的識別碼 buf:存放接收到的資料的暫存區 len buf:的長度 flags:此函數被調用的方式 |
對Stream Socket 言,我們可以接收到目前input buffer內有效的資料,但其數量不超過len的大小。
四、自定義的CMySocket類的實現代碼: 根據上面的知識,我自定義了一個簡單的CMySocket類,下面是我定義的該類的部分實現代碼:
////////////////////////////////////// CMySocket::CMySocket() : file://類的構造函數 { WSADATA wsaD; memset( m_LastError, 0, ERR_MAXLENGTH ); // m_LastError是類內字符串變量,初始化用來存放最后錯誤說明的字符串; // 初始化類內sockaddr_in結構變量,前者存放客戶端地址,后者對應于服務器端地址; memset( &m_sockaddr, 0, sizeof( m_sockaddr ) ); memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) ); int result = WSAStartup((WORD)((1<<8|1), &wsaD);//初始化WinSocket動態連接庫; if( result != 0 ) // 初始化失敗; { set_LastError( "WSAStartup failed!", WSAGetLastError() ); return; } }
////////////////////////////// CMySocket::~CMySocket() { WSACleanup(); }//類的析構函數; //////////////////////////////////////////////////// int CMySocket::Create( void ) {// m_hSocket是類內Socket對象,創建一個基于TCP/IP的Socket變量,并將值賦給該變量; if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET ) { set_LastError( "socket() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } /////////////////////////////////////////////// int CMySocket::Close( void )//關閉Socket對象; { if ( closesocket( m_hSocket ) == SOCKET_ERROR ) { set_LastError( "closesocket() failed", WSAGetLastError() ); return ERR_WSAERROR; } file://重置sockaddr_in 結構變量; memset( &m_sockaddr, 0, sizeof( sockaddr_in ) ); memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) ); return ERR_SUCCESS; } ///////////////////////////////////////// int CMySocket::Connect( char* strRemote, unsigned int iPort )//定義連接函數; { if( strlen( strRemote ) == 0 || iPort == 0 ) return ERR_BADPARAM; hostent *hostEnt = NULL; long lIPAddress = 0; hostEnt = gethostbyname( strRemote );//根據計算機名得到該計算機的相關內容; if( hostEnt != NULL ) { lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr; m_sockaddr.sin_addr.s_addr = lIPAddress; } else { m_sockaddr.sin_addr.s_addr = inet_addr( strRemote ); } m_sockaddr.sin_family = AF_INET; m_sockaddr.sin_port = htons( iPort ); if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR ) { set_LastError( "connect() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } /////////////////////////////////////////////////////// int CMySocket::Bind( char* strIP, unsigned int iPort )//綁定函數; { if( strlen( strIP ) == 0 || iPort == 0 ) return ERR_BADPARAM; memset( &m_sockaddr,0, sizeof( m_sockaddr ) ); m_sockaddr.sin_family = AF_INET; m_sockaddr.sin_addr.s_addr = inet_addr( strIP ); m_sockaddr.sin_port = htons( iPort ); if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR ) { set_LastError( "bind() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } ////////////////////////////////////////// int CMySocket::Accept( SOCKET s )//建立連接函數,S為監聽Socket對象名; { int Len = sizeof( m_rsockaddr ); memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) ); if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET ) { set_LastError( "accept() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } ///////////////////////////////////////////////////// int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent ) file://事件選擇函數; { if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 ) return ERR_BADPARAM; if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR ) { set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } //////////////////////////////////////////////////// int CMySocket::Listen( int iQueuedConnections )//監聽函數; { if( iQueuedConnections == 0 ) return ERR_BADPARAM; if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR ) { set_LastError( "listen() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } //////////////////////////////////////////////////// int CMySocket::Send( char* strData, int iLen )//數據發送函數; { if( strData == NULL || iLen == 0 ) return ERR_BADPARAM; if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR ) { set_LastError( "send() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ERR_SUCCESS; } ///////////////////////////////////////////////////// int CMySocket::Receive( char* strData, int iLen )//數據接收函數; { if( strData == NULL ) return ERR_BADPARAM; int len = 0; int ret = 0; ret = recv( m_hSocket, strData, iLen, 0 ); if ( ret == SOCKET_ERROR ) { set_LastError( "recv() failed", WSAGetLastError() ); return ERR_WSAERROR; } return ret; } void CMySocket::set_LastError( char* newError, int errNum ) file://WinSock API操作錯誤字符串設置函數; { memset( m_LastError, 0, ERR_MAXLENGTH ); memcpy( m_LastError, newError, strlen( newError ) ); m_LastError[strlen(newError)+1] = '\0'; } |
有了上述類的定義,就可以在網絡程序的服務器和客戶端分別定義CMySocket對象,建立連接,傳送數據了。例如,為了在服務器和客戶端發送數據,需要在服務器端定義兩個CMySocket對象ServerSocket1和ServerSocket2,分別用于監聽和連接,客戶端定義一個CMySocket對象ClientSocket,用于發送或接收數據,如果建立的連接數大于一,可以在服務器端再定義CMySocket對象,但要注意連接數不要大于五。
由于Socket API函數還有許多,如獲取遠端服務器、本地客戶機的IP地址、主機名等等,讀者可以再此基礎上對CMySocket補充完善,實現更多的功能。
TCP/IP Winsock編程要點利用Winsock編程由同步和異步方式,同步方式邏輯清晰,編程專注于應用,在搶先式的多任務操作系統中(WinNt、Win2K)采用多線程方式效率基本達到異步方式的水平,應此以下為同步方式編程要點。
1、快速通信
Winsock的Nagle算法將降低小數據報的發送速度,而系統默認是使用Nagle算法,使用
int setsockopt(
SOCKET s,
int level,
int optname,
const char FAR *optval,
int optlen
);函數關閉它 |
例子:
SOCKET sConnect;
sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
int bNodelay = 1;
int err;
err = setsockopt(
sConnect,
IPPROTO_TCP,
TCP_NODELAY,
(char *)&bNodelay,
sizoeof(bNodelay));//不采用延時算法
if (err != NO_ERROR)
TRACE ("setsockopt failed for some reason\n");; |
2、SOCKET的SegMentSize和收發緩沖
TCPSegMentSize是發送接受時單個數據報的最大長度,系統默認為1460,收發緩沖大小為8192。
在SOCK_STREAM方式下,如果單次發送數據超過1460,系統將分成多個數據報傳送,在對方接受到的將是一個數據流,應用程序需要增加斷幀的判斷。當然可以采用修改注冊表的方式改變1460的大小,但MicrcoSoft認為1460是最佳效率的參數,不建議修改。
在工控系統中,建議關閉Nagle算法,每次發送數據小于1460個字節(推薦1400),這樣每次發送的是一個完整的數據報,減少對方對數據流的斷幀處理。
3、同步方式中減少斷網時connect函數的阻塞時間
同步方式中的斷網時connect的阻塞時間為20秒左右,可采用gethostbyaddr事先判斷到服務主機的路徑是否是通的,或者先ping一下對方主機的IP地址。
A、采用gethostbyaddr阻塞時間不管成功與否為4秒左右。
例子:
LONG lPort=3024;
struct sockaddr_in ServerHostAddr;//服務主機地址
ServerHostAddr.sin_family=AF_INET;
ServerHostAddr.sin_port=::htons(u_short(lPort));
ServerHostAddr.sin_addr.s_addr=::inet_addr("192.168.1.3");
HOSTENT* pResult=gethostbyaddr((const char *) &
(ServerHostAddr.sin_addr.s_addr),4,AF_INET);
if(NULL==pResult)
{
int nErrorCode=WSAGetLastError();
TRACE("gethostbyaddr errorcode=%d",nErrorCode);
}
else
{
TRACE("gethostbyaddr %s\n",pResult->h_name);;
} |
B、采用PING方式時間約2秒左右
暫略
4、同步方式中解決recv,send阻塞問題
采用select函數解決,在收發前先檢查讀寫可用狀態。
A、讀
例子:
TIMEVAL tv01 = {0, 1};//1ms鐘延遲,實際為0-10毫秒
int nSelectRet;
int nErrorCode;
FD_SET fdr = {1, sConnect};
nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);//檢查可讀狀態
if(SOCKET_ERROR==nSelectRet)
{
nErrorCode=WSAGetLastError();
TRACE("select read status errorcode=%d",nErrorCode);
::closesocket(sConnect);
goto 重新連接(客戶方),或服務線程退出(服務方);
}
if(nSelectRet==0)//超時發生,無可讀數據
{
繼續查讀狀態或向對方主動發送
}
else
{
讀數據
} |
B、寫
TIMEVAL tv01 = {0, 1};//1ms鐘延遲,實際為9-10毫秒
int nSelectRet;
int nErrorCode;
FD_SET fdw = {1, sConnect};
nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);//檢查可寫狀態
if(SOCKET_ERROR==nSelectRet)
{
nErrorCode=WSAGetLastError();
TRACE("select write status errorcode=%d",nErrorCode);
::closesocket(sConnect);
//goto 重新連接(客戶方),或服務線程退出(服務方);
}
if(nSelectRet==0)//超時發生,緩沖滿或網絡忙
{
//繼續查寫狀態或查讀狀態
}
else
{
//發送
} |
5、改變TCP收發緩沖區大小
系統默認為8192,利用如下方式可改變。
SOCKET sConnect;
sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
int nrcvbuf=1024*20;
int err=setsockopt(
sConnect,
SOL_SOCKET,
SO_SNDBUF,//寫緩沖,讀緩沖為SO_RCVBUF
(char *)&nrcvbuf,
sizeof(nrcvbuf));
if (err != NO_ERROR)
{
TRACE("setsockopt Error!\n");
}
在設置緩沖時,檢查是否真正設置成功用
int getsockopt(
SOCKET s,
int level,
int optname,
char FAR *optval,
int FAR *optlen
); |
6、服務方同一端口多IP地址的bind和listen
在可靠性要求高的應用中,要求使用雙網和多網絡通道,再服務方很容易實現,用如下方式可建立客戶對本機所有IP地址在端口3024下的請求服務。
SOCKET hServerSocket_DS=INVALID_SOCKET;
struct sockaddr_in HostAddr_DS;//服務器主機地址
LONG lPort=3024;
HostAddr_DS.sin_family=AF_INET;
HostAddr_DS.sin_port=::htons(u_short(lPort));
HostAddr_DS.sin_addr.s_addr=htonl(INADDR_ANY);
hServerSocket_DS=::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP);
if(hServerSocket_DS==INVALID_SOCKET)
{
AfxMessageBox("建立數據服務器SOCKET 失敗!");
return FALSE;
}
if(SOCKET_ERROR==::bind(hServerSocket_DS,(struct
sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR)))
{
int nErrorCode=WSAGetLastError ();
TRACE("bind error=%d\n",nErrorCode);
AfxMessageBox("Socket Bind 錯誤!");
return FALSE;
}
if(SOCKET_ERROR==::listen(hServerSocket_DS,10))//10個客戶
{
AfxMessageBox("Socket listen 錯誤!");
return FALSE;
}
AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL); |
在客戶方要復雜一些,連接斷后,重聯不成功則應換下一個IP地址連接。也可采用同時連接好后備用的方式。
7、用TCP/IP Winsock實現變種Client/Server
傳統的Client/Server為客戶問、服務答,收發是成對出現的。而變種的Client/Server是指在連接時有客戶和服務之分,建立好通信連接后,不再有嚴格的客戶和服務之分,任何方都可主動發送,需要或不需要回答看應用而言,這種方式在工控行業很有用,比如RTDB作為I/O Server的客戶,但I/O Server也可主動向RTDB發送開關狀態變位、隨即事件等信息。在很大程度上減少了網絡通信負荷、提高了效率。
采用1-6的TCP/IP編程要點,在Client和Server方均已接收優先,適當控制時序就能實現。
Windows Sockets API實現網絡異步通訊摘要:本文對如何使用面向連接的流式套接字實現對網卡的編程以及如何實現異步網絡通訊等問題進行了討論與闡述。
一、 引言 在80年代初,美國加利福尼亞大學伯克利分校的研究人員為TCP/IP網絡通信開發了一個專門用于網絡通訊開發的API。這個API就是Socket接口(套接字)--當今在TCP/IP網絡最為通用的一種API,也是在互聯網上進行應用開發最為通用的一種API。在微軟聯合其它幾家公司共同制定了一套Windows下的網絡編程接口Windows Sockets規范后,由于在其規范中引入了一些異步函數,增加了對網絡事件異步選擇機制,因此更加符合Windows的消息驅動特性,使網絡開發人員可以更加方便的進行高性能網絡通訊程序的設計。本文接下來就針對Windows Sockets API進行面向連接的流式套接字編程以及對異步網絡通訊的編程實現等問題展開討論。
二、 面向連接的流式套接字編程模型的設計 本文在方案選擇上采用了在網絡編程中最常用的一種模型--客戶機/服務器模型。這種客戶/服務器模型是一種非對稱式編程模式。該模式的基本思想是把集中在一起的應用劃分成為功能不同的兩個部分,分別在不同的計算機上運行,通過它們之間的分工合作來實現一個完整的功能。對于這種模式而言其中一部分需要作為服務器,用來響應并為客戶提供固定的服務;另一部分則作為客戶機程序用來向服務器提出請求或要求某種服務。
本文選取了基于TCP/IP的客戶機/服務器模型和面向連接的流式套接字。其通信原理為:服務器端和客戶端都必須建立通信套接字,而且服務器端應先進入監聽狀態,然后客戶端套接字發出連接請求,服務器端收到請求后,建立另一個套接字進行通信,原來負責監聽的套接字仍進行監聽,如果有其它客戶發來連接請求,則再建立一個套接字。默認狀態下最多可同時接收5個客戶的連接請求,并與之建立通信關系。因此本程序的設計流程應當由服務器首先啟動,然后在某一時刻啟動客戶機并使其與服務器建立連接。服務器與客戶機開始都必須調用Windows Sockets API函數socket()建立一個套接字sockets,然后服務器方調用bind()將套接字與一個本地網絡地址捆扎在一起,再調用listen()使套接字處于一種被動的準備接收狀態,同時規定它的請求隊列長度。在此之后服務器就可以通過調用accept()來接收客戶機的連接。
相對于服務器,客戶端的工作就顯得比較簡單了,當客戶端打開套接字之后,便可通過調用connect()和服務器建立連接。連接建立之后,客戶和服務器之間就可以通過連接發送和接收資料。最后資料傳送結束,雙方調用closesocket()關閉套接字來結束這次通訊。整個通訊過程的具體流程框圖可大致用下面的流程圖來表示:
 面向連接的流式套接字編程流程示意圖 |
三、 軟件設計要點以及異步通訊的實現 根據前面設計的程序流程,可將程序劃分為兩部分:服務器端和客戶端。而且整個實現過程可以大致用以下幾個非常關鍵的Windows Sockets API函數將其慣穿下來:
服務器方:
socket()->bind()->listen->accept()->recv()/send()->closesocket() |
客戶機方:
socket()->connect()->send()/recv()->closesocket() |
有鑒于以上幾個函數在整個網絡編程中的重要性,有必要結合程序實例對其做較深入的剖析。服務器端應用程序在使用套接字之前,首先必須擁有一個Socket,系統調用socket()函數向應用程序提供創建套接字的手段。該套接字實際上是在計算機中提供了一個通信埠,可以通過這個埠與任何一個具有套接字接口的計算機通信。應用程序在網絡上傳輸、接收的信息都通過這個套接字接口來實現的。在應用開發中如同使用文件句柄一樣,可以對套接字句柄進行讀寫操作:
sock=socket(AF_INET,SOCK_STREAM,0); |
函數的第一個參數用于指定地址族,在Windows下僅支持AF_INET(TCP/IP地址);第二個參數用于描述套接字的類型,對于流式套接字提供有SOCK_STREAM;最后一個參數指定套接字使用的協議,一般為0。該函數的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock);函數來將其釋放。服務器方一旦獲取了一個新的套接字后應通過bind()將該套接字與本機上的一個端口相關聯:
sockin.sin_family=AF_INET; sockin.sin_addr.s_addr=0; sockin.sin_port=htons(USERPORT); bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin))); |
該函數的第二個參數是一個指向包含有本機IP地址和端口信息的sockaddr_in結構類型的指針,其成員描述了本地端口號和本地主機地址,經過bind()將服務器進程在網絡上標識出來。需要注意的是由于1024以內的埠號都是保留的埠號因此如無特別需要一般不能將sockin.sin_port的埠號設置為1024以內的值。然后調用listen()函數開始偵聽,再通過accept()調用等待接收連接以完成連接的建立:
//連接請求隊列長度為1,即只允許有一個請求,若有多個請求, //則出現錯誤,給出錯誤代碼WSAECONNREFUSED。 listen(sock,1); //開啟線程避免主程序的阻塞 AfxBeginThread(Server,NULL); …… UINT Server(LPVOID lpVoid) { …… int nLen=sizeof(SOCKADDR); pView->newskt=accept(pView->sock,(LPSOCKADDR)& pView->sockin,(LPINT)& nLen); …… WSAAsyncSelect(pView->newskt,pView->m_hWnd,WM_SOCKET_MSG,FD_READ|FD_CLOSE); return 1; }
|
這里之所以把accept()放到一個線程中去是因為在執行到該函數時如沒有客戶連接服務器的請求到來,服務器就會停在accept語句上等待連接請求的到來,這勢必會引起程序的阻塞,雖然也可以通過設置套接字為非阻塞方式使在沒有客戶等待時可以使accept()函數調用立即返回,但這種輪詢套接字的方式會使CPU處于忙等待方式,從而降低程序的運行效率大大浪費系統資源。考慮到這種情況,將套接字設置為阻塞工作方式,并為其單獨開辟一個子線程,將其阻塞控制在子線程范圍內而不會造成整個應用程序的阻塞。對于網絡事件的響應顯然要采取異步選擇機制,只有采取這種方式才可以在由網絡對方所引起的不可預知的網絡事件發生時能馬上在進程中做出及時的響應處理,而在沒有網絡事件到達時則可以處理其他事件,這種效率是很高的,而且完全符合Windows所標榜的消息觸發原則。前面那段代碼中的WSAAsyncSelect()函數便是實現網絡事件異步選擇的核心函數。
通過第四個參數注冊應用程序感興取的網絡事件,在這里通過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; } } |
在這里需要實現對自定義消息WM_SOCKET_MSG的響應,需要在頭文件和實現文件中分別添加其消息映射關系:
頭文件:
//{{AFX_MSG(CNetServerView) //}}AFX_MSG void OnSocket(WPARAM wParam,LPARAM lParam); DECLARE_MESSAGE_MAP() |
實現文件:
BEGIN_MESSAGE_MAP(CNetServerView, CView) //{{AFX_MSG_MAP(CNetServerView) //}}AFX_MSG_MAP ON_MESSAGE(WM_SOCKET_MSG,OnSocket) END_MESSAGE_MAP()
|
在進行異步選擇使用WSAAsyncSelect()函數時,有以下幾點需要引起特別的注意:
1. 連續使用兩次WSAAsyncSelect()函數時,只有第二次設置的事件有效,如:
WSAAsyncSelect(s,hwnd,wMsg1,FD_READ); WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE); |
這樣只有當FD_CLOSE事件發生時才會發送wMsg2消息。
2.可以在設置過異步選擇后通過再次調用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所設置的異步事件。
3.Windows Sockets DLL在一個網絡事件發生后,通常只會給相應的應用程序發送一個消息,而不能發送多個消息。但通過使用一些函數隱式地允許重發此事件的消息,這樣就可能再次接收到相應的消息。
4.在調用過closesocket()函數關閉套接字之后不會再發生FD_CLOSE事件。
以上基本完成了服務器方的程序設計,下面對于客戶端的實現則要簡單多了,在用socket()創建完套接字之后只需通過調用connect()完成同服務器的連接即可,剩下的工作同服務器完全一樣:用send()/recv()發送/接收收據,用closesocket()關閉套接字:
sockin.sin_family=AF_INET; //地址族 sockin.sin_addr.S_un.S_addr=IPaddr; //指定服務器的IP地址 sockin.sin_port=m_Port; //指定連接的端口號 int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin));
|
本文采取的是可靠的面向連接的流式套接字。在數據發送上有write()、writev()和send()等三個函數可供選擇,其中前兩種分別用于緩沖發送和集中發送,而send()則為可控緩沖發送,并且還可以指定傳輸控制標志為MSG_OOB進行帶外數據的發送或是為MSG_DONTROUTE尋徑控制選項。在信宿地址的網絡號部分指定數據發送需要經過的網絡接口,使其可以不經過本地尋徑機制直接發送出去。這也是其同write()函數的真正區別所在。由于接收數據系統調用和發送數據系統調用是一一對應的,因此對于數據的接收,在此不再贅述,相應的三個接收函數分別為:read()、readv()和recv()。由于后者功能上的全面,本文在實現上選擇了send()-recv()函數對,在具體編程中應當視具體情況的不同靈活選擇適當的發送-接收函數對。
小結:TCP/IP協議是目前各網絡操作系統主要的通訊協議,也是 Internet的通訊協議,本文通過Windows Sockets API實現了對基于TCP/IP協議的面向連接的流式套接字網絡通訊程序的設計,并通過異步通訊和多線程等手段提高了程序的運行效率,避免了阻塞的發生。
用VC++6.0的Sockets API實現一個聊天室程序1.VC++網絡編程及Windows Sockets API簡介 VC++對網絡編程的支持有socket支持,WinInet支持,MAPI和ISAPI支持等。其中,Windows Sockets API是TCP/IP網絡環境里,也是Internet上進行開發最為通用的API。最早美國加州大學Berkeley分校在UNIX下為TCP/IP協議開發了一個API,這個API就是著名的Berkeley Socket接口(套接字)。在桌面操作系統進入Windows時代后,仍然繼承了Socket方法。在TCP/IP網絡通信環境下,Socket數據傳輸是一種特殊的I/O,它也相當于一種文件描述符,具有一個類似于打開文件的函數調用-socket()。可以這樣理解篠ocket實際上是一個通信端點,通過它,用戶的Socket程序可以通過網絡和其他的Socket應用程序通信。Socket存在于一個"通信域"(為描述一般的線程如何通過Socket進行通信而引入的一種抽象概念)里,并且與另一個域的Socket交換數據。Socket有三類。第一種是SOCK_STREAM(流式),提供面向連接的可靠的通信服務,比如telnet,http。第二種是SOCK_DGRAM(數據報),提供無連接不可靠的通信,比如UDP。第三種是SOCK_RAW(原始),主要用于協議的開發和測試,支持通信底層操作,比如對IP和ICMP的直接訪問。
2.Windows Socket機制分析 2.1一些基本的Socket系統調用
主要的系統調用包括:socket()-創建Socket;bind()-將創建的Socket與本地端口綁定;connect()與accept()-建立Socket連接;listen()-服務器監聽是否有連接請求;send()-數據的可控緩沖發送;recv()-可控緩沖接收;closesocket()-關閉Socket。
2.2Windows Socket的啟動與終止
啟動函數WSAStartup()建立與Windows Sockets DLL的連接,終止函數WSAClearup()終止使用該DLL,這兩個函數必須成對使用。
2.3異步選擇機制
Windows是一個非搶占式的操作系統,而不采取UNIX的阻塞機制。當一個通信事件產生時,操作系統要根據設置選擇是否對該事件加以處理,WSAAsyncSelect()函數就是用來選擇系統所要處理的相應事件。當Socket收到設定的網絡事件中的一個時,會給程序窗口一個消息,這個消息里會指定產生網絡事件的Socket,發生的事件類型和錯誤碼。
2.4異步數據傳輸機制
WSAAsyncSelect()設定了Socket上的須響應通信事件后,每發生一個這樣的事件就會產生一個WM_SOCKET消息傳給窗口。而在窗口的回調函數中就應該添加相應的數據傳輸處理代碼。
3.聊天室程序的設計說明 3.1實現思想
在Internet上的聊天室程序一般都是以服務器提供服務端連接響應,使用者通過客戶端程序登錄到服務器,就可以與登錄在同一服務器上的用戶交談,這是一個面向連接的通信過程。因此,程序要在TCP/IP環境下,實現服務器端和客戶端兩部分程序。
3.2服務器端工作流程
服務器端通過socket()系統調用創建一個Socket數組后(即設定了接受連接客戶的最大數目),與指定的本地端口綁定bind(),就可以在端口進行偵聽listen()。如果有客戶端連接請求,則在數組中選擇一個空Socket,將客戶端地址賦給這個Socket。然后登錄成功的客戶就可以在服務器上聊天了。
3.3客戶端工作流程
客戶端程序相對簡單,只需要建立一個Socket與服務器端連接,成功后通過這個Socket來發送和接收數據就可以了。
4.核心代碼分析 限于篇幅,這里僅給出與網絡編程相關的核心代碼,其他的諸如聊天文字的服務器和客戶端顯示讀者可以自行添加。
4.1服務器端代碼
開啟服務器功能:
void OnServerOpen() //開啟服務器功能 { WSADATA wsaData; int iErrorCode; char chInfo[64]; if (WSAStartup(WINSOCK_VERSION, &wsaData)) //調用Windows Sockets DLL { MessageBeep(MB_ICONSTOP); MessageBox("Winsock無法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP); WSACleanup(); return; } else WSACleanup(); if (gethostname(chInfo, sizeof(chInfo))) { ReportWinsockErr("\n無法獲取主機!\n "); return; } CString csWinsockID = "\n==>>服務器功能開啟在端口:No. "; csWinsockID += itoa(m_pDoc->m_nServerPort, chInfo, 10); csWinsockID += "\n"; PrintString(csWinsockID); //在程序視圖顯示提示信息的函數,讀者可自行創建 m_pDoc->m_hServerSocket=socket(PF_INET, SOCK_STREAM, DEFAULT_PROTOCOL); //創建服務器端Socket,類型為SOCK_STREAM,面向連接的通信 if (m_pDoc->m_hServerSocket == INVALID_SOCKET) { ReportWinsockErr("無法創建服務器socket!"); return;} m_pDoc->m_sockServerAddr.sin_family = AF_INET; m_pDoc->m_sockServerAddr.sin_addr.s_addr = INADDR_ANY; m_pDoc->m_sockServerAddr.sin_port = htons(m_pDoc->m_nServerPort); if (bind(m_pDoc->m_hServerSocket, (LPSOCKADDR)&m_pDoc->m_sockServerAddr, sizeof(m_pDoc->m_sockServerAddr)) == SOCKET_ERROR) //與選定的端口綁定 {ReportWinsockErr("無法綁定服務器socket!"); return;} iErrorCode=WSAAsyncSelect(m_pDoc->m_hServerSocket,m_hWnd, WM_SERVER_ACCEPT, FD_ACCEPT); //設定服務器相應的網絡事件為FD_ACCEPT,即連接請求, // 產生相應傳遞給窗口的消息為WM_SERVER_ACCEPT if (iErrorCode == SOCKET_ERROR) { ReportWinsockErr("WSAAsyncSelect設定失敗!"); return;} if (listen(m_pDoc->m_hServerSocket, QUEUE_SIZE) == SOCKET_ERROR) //開始監聽客戶連接請求 {ReportWinsockErr("服務器socket監聽失敗!"); m_pParentMenu->EnableMenuItem(ID_SERVER_OPEN, MF_ENABLED); return;} m_bServerIsOpen = TRUE; //監視服務器是否打開的變量 return; } |
響應客戶發送聊天文字到服務器:ON_MESSAGE(WM_CLIENT_READ, OnClientRead)
LRESULT OnClientRead(WPARAM wParam, LPARAM lParam) { int iRead; int iBufferLength; int iEnd; int iRemainSpace; char chInBuffer[1024]; int i; for(i=0;(i //MAXClient是服務器可響應連接的最大數目 {} if(i==MAXClient) return 0L; iBufferLength = iRemainSpace = sizeof(chInBuffer); iEnd = 0; iRemainSpace -= iEnd; iBytesRead = recv(m_aClientSocket[i], (LPSTR)(chInBuffer+iEnd), iSpaceRemaining, NO_FLAGS); //用可控緩沖接收函數recv()來接收字符 iEnd+=iRead; if (iBytesRead == SOCKET_ERROR) ReportWinsockErr("recv出錯!"); chInBuffer[iEnd] = '\0'; if (lstrlen(chInBuffer) != 0) {PrintString(chInBuffer); //服務器端文字顯示 OnServerBroadcast(chInBuffer); //自己編寫的函數,向所有連接的客戶廣播這個客戶的聊天文字 } return(0L); } |
對于客戶斷開連接,會產生一個FD_CLOSE消息,只須相應地用closesocket()關閉相應的Socket即可,這個處理比較簡單。
4.2客戶端代碼
連接到服務器:
void OnSocketConnect() { WSADATA wsaData; DWORD dwIPAddr; SOCKADDR_IN sockAddr; if(WSAStartup(WINSOCK_VERSION,&wsaData)) //調用Windows Sockets DLL {MessageBox("Winsock無法初始化!",NULL,MB_OK); return; } m_hSocket=socket(PF_INET,SOCK_STREAM,0); //創建面向連接的socket sockAddr.sin_family=AF_INET; //使用TCP/IP協議 sockAddr.sin_port=m_iPort; //客戶端指定的IP地址 sockAddr.sin_addr.S_un.S_addr=dwIPAddr; int nConnect=connect(m_hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)); //請求連接 if(nConnect) ReportWinsockErr("連接失敗!"); else MessageBox("連接成功!",NULL,MB_OK); int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,FD_READ); //指定響應的事件,為服務器發送來字符 if(iErrorCode==SOCKET_ERROR) MessageBox("WSAAsyncSelect設定失敗!"); } |
接收服務器端發送的字符也使用可控緩沖接收函數recv(),客戶端聊天的字符發送使用數據可控緩沖發送函數send(),這兩個過程比較簡單,在此就不加贅述了。
5.小結 通過聊天室程序的編寫,可以基本了解Windows Sockets API編程的基本過程和精要之處。本程序在VC++6.0下編譯通過,在使用windows 98/NT的局域網里運行良好。
用VC++制作一個簡單的局域網消息發送工程本工程類似于oicq的消息發送機制,不過他只能夠發送簡單的字符串。雖然簡單,但他也是一個很好的VC網絡學習例子。
本例通過VC帶的SOCKET類,重載了他的一個接受類mysock類,此類可以吧接收到的信息顯示在客戶區理。以下是實現過程:
建立一個MFC 單文檔工程,工程名為oicq,在第四步選取WINDOWS SOCKetS支持,其它取默認設置即可。為了簡單,這里直接把about對話框作些改變,作為發送信息界面。
這里通過失去對話框來得到發送的字符串、獲得焦點時把字符串發送出去。創建oicq類的窗口,獲得VIEW類指針,進而可以把接收到的信息顯示出來。
extern CString bb; void CAboutDlg::OnKillFocus(CWnd* pNewWnd) { // TODO: Add your message handler code here CDialog::OnKillFocus(pNewWnd); bb=m_edit; } 對于OICQVIEW類 char aa[100]; CString mm; CDC* pdc; class mysock:public CSocket //派生mysock類,此類既有接受功能 {public:void OnReceive(int nErrorCode) //可以隨時接收信息 { CSocket::Receive((void*)aa,100,0); mm=aa; CString ll=" ";//在顯示消息之前,消除前面發送的消息 pdc->TextOut(50,50,ll); pdc->TextOut(50,50,mm); } };
mysock sock1; CString bb; BOOL COicqView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { CView::OnSetFocus(pOldWnd);
// TODO: Add your message handler code here and/or call default bb="besting:"+bb; //確定發送者身份為besting sock1.SendTo(bb,100,1060,"192.168.0.255",0); //獲得焦點以廣播形式發送信息,端口號為1060
return CView::OnSetCursor(pWnd, nHitTest, message); }
int COicqView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1; sock1.Create(1060,SOCK_DGRAM,NULL);//以數據報形式發送消息
static CClientDC wdc(this); //獲得當前視類的指針 pdc=&wdc; // TODO: Add your specialized creation code here
return 0; } |
運行一下,打開ABOUT對話框,輸入發送信息,enter鍵就可以發送信息了,是不是有點像qq啊?
用Winsock實現語音全雙工通信使用摘要:在Windows 95環境下,基于TCP/IP協議,用Winsock完成了話音的端到端傳輸。采用雙套接字技術,闡述了主要函數的使用要點,以及基于異步選擇機制的應用方法。同時,給出了相應的實例程序。
一、引言
Windows 95作為微機的操作系統,已經完全融入了網絡與通信功能,不僅可以建立純Windows 95環境下的“對等網絡”,而且支持多種協議,如TCP/IP、IPX/SPX、NETBUI等。在TCP/IP協議組中,TPC是一種面向連接的協義,為用戶提供可靠的、全雙工的字節流服務,具有確認、流控制、多路復用和同步等功能,適于數據傳輸。UDP協議則是無連接的,每個分組都攜帶完整的目的地址,各分組在系統中獨立傳送。它不能保證分組的先后順序,不進行分組出錯的恢復與重傳,因此不保證傳輸的可靠性,但是,它提供高傳輸效率的數據報服務,適于實時的語音、圖像傳輸、廣播消息等網絡傳輸。
Winsock接口為進程間通信提供了一種新的手段,它不但能用于同一機器中的進程之間通信,而且支持網絡通信功能。隨著Windows 95的推出。Winsock已經被正式集成到了Windows系統中,同時包括了16位和32位的編程接口。而Winsock的開發工具也可以在Borland C++4.0、Visual C++2.0這些C編譯器中找到,主要由一個名為winsock.h的頭文件和動態連接庫winsock.dll或wsodk32.dll組成,這兩種動態連接庫分別用于Win16和Win32的應用程序。
本文針對話音的全雙工傳輸要求,采用UDP協議實現了實時網絡通信。使用VisualC++2.0編譯環境,其動態連接庫名為wsock32.dll。
二、主要函數的使用要點
通過建立雙套接字,可以很方便地實現全雙工網絡通信。
1.套接字建立函數:
SOCKET socket(int family,int type,int protocol) |
對于UDP協議,寫為:
SOCKRET s; s=socket(AF_INET,SOCK_DGRAM,0); 或s=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) |
為了建立兩個套接字,必須實現地址的重復綁定,即,當一個套接字已經綁定到某本地地址后,為了讓另一個套接字重復使用該地址,必須為調用bind()函數綁定第二個套接字之前,通過函數setsockopt()為該套接字設置SO_REUSEADDR套接字選項。通過函數getsockopt()可獲得套接字選項設置狀態。需要注意的是,兩個套接字所對應的端口號不能相同。此外,還涉及到套接字緩沖區的設置問題,按規定,每個區的設置范圍是:不小于512個字節,大大于8k字節,根據需要,文中選用了4k字節。
2.套接字綁定函數
int bind(SOCKET s,struct sockaddr_in*name,int namelen) |
s是剛才創建好的套接字,name指向描述通訊對象的結構體的指針,namelen是該結構體的長度。該結構體中的分量包括:IP地址(對應name.sin_addr.s_addr)、端口號(name.sin_port)、地址類型(name.sin_family,一般都賦成AF_INET,表示是internet地址)。
(1)IP地址的填寫方法:在全雙工通信中,要把用戶名對應的點分表示法地址轉換成32位長整數格式的IP地址,使用inet_addr()函數。
(2)端口號是用于表示同一臺計算機不同的進程(應用程序),其分配方法有兩種:1)進程可以讓系統為套接字自動分配一端口號,只要在調用bind前將端口號指定為0即可。由系統自動分配的端口號位于1024~5000之間,而1~1023之間的任一TCP或UDP端口都是保留的,系統不允許任一進程使用保留端口,除非其有效用戶ID是零(超級用戶)。
2)進程可為套接字指定一特定端口。這對于需要給套接字分配一眾所端口的服務器是很有用的。指定范圍為1024和65536之間。可任意指定。
在本程序中,對兩個套接字的端口號規定為2000和2001,前者對應發送套接字,后者對應接收套接字。
端口號要從一個16位無符號數(u_short類型數)從主機字節順序轉換成網絡字節順序,使用htons()函數。
根據以上兩個函數,可以給出雙套接字建立與綁定的程序片斷。
//設置有關的全局變量 SOCKET sr,ss; HPSTR sockBufferS,sockBufferR; HANDLE hSendData,hReceiveData; DWROD dwDataSize=1024*4; struct sockaddr_in therel.there2; #DEFINE LOCAL_HOST_ADDR 200.200.200.201 #DEFINE REMOTE_HOST-ADDR 200.200.200.202 #DEFINE LOCAL_HOST_PORT 2000 #DEFINE LOCAL_HOST_PORT 2001 //套接字建立函數 BOOL make_skt(HWND hwnd) { struct sockaddr_in here,here1; ss=socket(AF_INET,SOCK_DGRAM,0); sr=socket(AF_INET,SOCK_DGRAM,0); if((ss==INVALID_SOCKET)||(sr==INVALID_SOCKET)) { MessageBox(hwnd,“套接字建立失敗!”,“”,MB_OK); return(FALSE); } here.sin_family=AF_INET; here.sin_addr.s_addr=inet_addr(LOCAL_HOST_ADDR); here.sin_port=htons(LICAL_HOST_PORT); //another socket herel.sin_family=AF_INET; herel.sin_addr.s_addr(LOCAL_HOST_ADDR); herel.sin_port=htons(LOCAL_HOST_PORT1); SocketBuffer();//套接字緩沖區的鎖定設置 setsockopt(ss,SOL_SOCKET,SO_SNDBUF,(char FAR*)sockBufferS,dwDataSize); if(bind(ss,(LPSOCKADDR)&here,sizeof(here))) { MessageBox(hwnd,“發送套接字綁定失敗!”,“”,MB_OK); return(FALSE); } setsockopt(sr SQL_SOCKET,SO_RCVBUF|SO_REUSEADDR,(char FAR*) sockBufferR,dwDataSize); if(bind(sr,(LPSOCKADDR)&here1,sizeof(here1))) { MessageBox(hwnd,“接收套接字綁定失敗!”,“”,MB_OK); return(FALSE); } return(TRUE); } //套接字緩沖區設置 void sockBuffer(void) { hSendData=GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize); if(!hSendData) { MessageBox(hwnd,“發送套接字緩沖區定位失敗!”,NULL, MB_OK|MB_ICONEXCLAMATION); return; } if((sockBufferS=GlobalLock(hSendData)==NULL) { MessageBox(hwnd,“發送套接字緩沖區鎖定失敗!”,NULL, MB_OK|MB_ICONEXCLAMATION); GlobalFree(hRecordData[0]; return; } hReceiveData=globalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize); if(!hReceiveData) { MessageBox(hwnd,"“接收套接字緩沖區定位敗!”,NULL MB_OK|MB_ICONEXCLAMATION); return; } if((sockBufferT=Globallock(hReceiveData))=NULL) MessageBox(hwnd,"發送套接字緩沖區鎖定失敗!”,NULL, MB_OK|MB_ICONEXCLAMATION); GlobalFree(hRecordData[0]); return; } { |
3.數據發送與接收函數;
int sendto(SOCKET s.char*buf,int len,int flags,struct sockaddr_in to,int tolen); int recvfrom(SOCKET s.char*buf,int len,int flags,struct sockaddr_in fron,int*fromlen) |
其中,參數flags一般取0。
recvfrom()函數實際上是讀取sendto()函數發過來的一個數據包,當讀到的數據字節少于規定接收的數目時,就把數據全部接收,并返回實際接收到的字節數;當讀到的數據多于規定值時,在數據報文方式下,多余的數據將被丟棄。而在流方式下,剩余的數據由下recvfrom()讀出。為了發送和接收數據,必須建立數據發送緩沖區和數據接收緩沖區。規定:IP層的一個數據報最大不超過64K(含數據報頭)。當緩沖區設置得過多、過大時,常因內存不夠而導致套接字建立失敗。在減小緩沖區后,該錯誤消失。經過實驗,文中選用了4K字節。
此外,還應注意這兩個函數中最后參數的寫法,給sendto()的最后參數是一個整數值,而recvfrom()的則是指向一整數值的指針。
4.套接字關閉函數:closesocket(SOCKET s)
通訊結束時,應關閉指定的套接字,以釋與之相關的資源。
在關閉套接字時,應先對鎖定的各種緩沖區加以釋放。其程序片斷為:
void CloseSocket(void) { GlobalUnlock(hSendData); GlobalFree(hSenddata); GlobalUnlock(hReceiveData); GlobalFree(hReceiveDava); if(WSAAysncSelect(ss,hwnd,0,0)=SOCKET_ERROR) { MessageBos(hwnd,“發送套接字關閉失敗!”,“”,MB_OK); return; } if(WSAAysncSelect(sr,hwnd,0,0)==SOCKET_ERROR) { MessageBox(hwnd,“接收套接字關閉失敗!”,“”,MB_OK); return; } WSACleanup(); closesockent(ss); closesockent(sr); return; } |
三、Winsock的編程特點與異步選擇機制 1 阻塞及其處理方式
在網絡通訊中,由于網絡擁擠或一次發送的數據量過大等原因,經常會發生交換的數據在短時間內不能傳送完,收發數據的函數因此不能返回,這種現象叫做阻塞。Winsock對有可能阻塞的函數提供了兩種處理方式:阻塞和非阻塞方式。在阻塞方式下,收發數據的函數在被調用后一直要到傳送完畢或者出錯才能返回。在阻塞期間,被阻的函數不會斷調用系統函數GetMessage()來保持消息循環的正常進行。對于非阻塞方式,函數被調用后立即返回,當傳送完成后由Winsock給程序發一個事先約定好的消息。
在編程時,應盡量使用非阻塞方式。因為在阻塞方式下,用戶可能會長時間的等待過程中試圖關閉程序,因為消息循環還在起作用,所以程序的窗口可能被關閉,這樣當函數從Winsock的動態連接庫中返回時,主程序已經從內存中刪除,這顯然是極其危險的。
2 異步選擇函數WSAAsyncSelect()的使用
Winsock通過WSAAsyncSelect()自動地設置套接字處于非阻塞方式。使用WindowsSockets實現Windows網絡程序設計的關鍵就是它提供了對網絡事件基于消息的異步存取,用于注冊應用程序感興趣的網絡事件。它請求Windows Sockets DLL在檢測到套接字上發生的網絡事件時,向窗口發送一個消息。對UDP協議,這些網絡事件主要為:
FD_READ 期望在套接字收到數據(即讀準備好)時接收通知;
FD_WRITE 期望在套接字可發送數(即寫準備好)時接收通知;
FD_CLOSE 期望在套接字關閉時接電通知
消息變量wParam指示發生網絡事件的套接字,變量1Param的低字節描述發生的網絡事件,高字包含錯誤碼。如在窗口函數的消息循環中均加一個分支:
int ok=sizeof(SOCKADDR); case wMsg; switch(1Param) { case FD_READ: //套接字上讀數據 if(recvfrom(sr.lpPlayData[j],dwDataSize,0,(struct sockaddr FAR*)&there1, (int FAR*)&ok)==SOCKET_ERROR0 { MessageBox)hwnd,“數據接收失敗!”,“”,MB_OK); return(FALSE); } case FD_WRITE: //套接字上寫數據 } break; |
在程序的編制中,應根據需要靈活地將WSAAsyncSelect()函靈敏放在相應的消息循環之中,其它說明可參見文獻[1]。此外,應該指出的是,以上程序片斷中的消息框主要是為程序調試方便而設置的,而在正式產品中不再出現。同時,按照程序容錯誤設計,應建立一個專門的容錯處理函數。程序中可能出現的各種錯誤都將由該函數進行處理,依據錯誤的危害程度不同,建立幾種不同的處理措施。這樣,才能保證雙方通話的順利和可靠。
四、結論 本文是多媒體網絡傳輸項目的重要內容之一,目前,結合硬件全雙工語音卡等設備,已經成功地實現了話音的全雙工的通信。有關整個多媒體傳輸系統設計的內容,將有另文敘述。
VC編程輕松獲取局域網連接通知摘要:本文從解決實際需要出發,通過采用Windows Socket API等網絡編程技術實現了在局域網共享一條電話線的情況下,當服務器撥號上網時能及時通知各客戶端通過代理服務器進行上網。本文還特別給出了基于Microsoft Visual C++ 6.0的部分關鍵實現代碼。
一、 問題提出的背景 筆者所使用的局域網擁有一個服務器及若干分布于各辦公室的客戶機,通過網卡相連。服務器不提供專線上網,但可以撥號上網,而各客戶機可以通過裝在服務器端的代理服務器共用一條電話線上網,但前提必須是服務器已經撥號連接。考慮到經濟原因,服務器不可能長時間連在網上,因此經常出現由于分布于各辦公室的客戶機不能知道服務器是否處于連線狀態而造成的想上網時服務器沒有撥號,或是服務器已經撥號而客戶機卻并不知曉的情況,這無疑會在工作中帶來極大的不便。而筆者作為一名程序設計人員,有必要利用自己的專業優勢來解決實際工作中所遇到的一些問題。通過對實際情況的分析,可以歸納為一點:當服務器在進行撥號連接時能及時通知在網絡上的各個客戶機,而各客戶機在收到服務器發來的消息后可以根據自己的情況來決定是否上網。這樣就可以在同一時間內同時為較多的客戶機提供上網服務,此舉不僅提高了利用效率也大大節省了上網話費。
二、 程序主要設計思路及實現 由于本網絡是通過網卡連接的局域網,因此可以首選Windows Socket API進行套接字編程。整個系統分為兩部分:服務端和客戶端。服務端運行于服務器上負責監視服務器是否在進行撥號連接,一旦發現馬上通過網絡發送消息通知客戶端;而客戶端軟件則只需完成同服務端軟件的連接并能接收到從服務端發送來的通知消息即可。服務器端要完成比客戶端更為繁重的任務。下面對這幾部分的實現分別加以描述:
(一)監視撥號連接事件的發生
在采用撥號上網時,首先需要通過撥號連接通過電話線連接到ISP上,然后才能享受到ISP所提供的各種互聯網服務。而要捕獲撥號連接發生的事件不能依賴于消息通知,因為此時發出的消息同一個對話框出現在屏幕上時所產生的消息是一樣的。唯一同其他對話框區別的是其標題是固定的"撥號連接",因此在無其他特殊情況下(如其他程序的標題也是"撥號連接"時)可以認定當桌面上的所有程序窗口出現以"撥號連接" 為標題的窗口時,即可認定此時正在進行撥號連接。因此可以通過搜尋并判斷窗口標題的辦法對撥號連接進行監視,具體可以用CWnd類的FindWindows()函數來實現:
CWnd *pWnd=CWnd::FindWindow(NULL,"撥號連接"); |
第一個參數為NULL,指定對當前所有窗口都進行搜索。第二個參數就是待搜尋的窗口標題,一旦找到將返回該窗口的窗口句柄。因此可以在窗口句柄不為空的情況下去通知客戶端服務器現在正在撥號。由于一般的撥號連接都需要一段時間的連接應答后才能登錄到ISP上,因此從提高程序運行效率角度出發可以通過定時器的使用來每間隔一段時間(如500毫秒)去搜尋一次,以確保能監視到每一次的撥號連接而又不致過分加重CPU的負擔。
(二)服務器端網絡通訊功能的實現
在此采用的是可靠的有連接的流式套接字,并且采用了多線程和異步通知機制能有效避免一些函數如accept()等的阻塞會引起整個程序的阻塞。由于套接字編程方面的書籍資料非常豐富,對其進行網絡編程做了很詳細的描述,故本文在此只針對一些關鍵部分做簡要說明,有關套接字網絡編程的詳細內容請參閱相關資料。采用流式套接字的服務器端的主要設計流程可以歸結為以下幾步:
1. 創建套接字
sock=socket(AF_INET,SOCK_STREAM,0); |
該函數的第一個參數用于指定地址族,在Windows下僅支持AF_INET(TCP/IP地址);第二個參數用于描述套接字的類型,對于流式套接字提供有SOCK_STREAM;最后一個參數指定套接字使用的協議,一般為0。該函數的返回值保存了新套接字的句柄,在程序退出前可以用closesocket()函數來將其釋放。
2. 綁定套接字
服務器方一旦獲取了一個新的套接字后應通過bind()將該套接字與本機上的一個端口相關聯。此時需要預先對一個指向包含有本機IP地址和端口信息的sockaddr_in結構填充一些必要的信息,如本地端口號和本地主機地址等。然后就可經過bind()將服務器進程在網絡上標識出來。需要注意的是由于1024以內的埠號都是保留的端口號因此如無特別需要一般不能將sockin.sin_port的端口號設置為1024以內的值:
…… sockin.sin_family=AF_INET; sockin.sin_addr.s_addr=0; sockin.sin_port=htons(USERPORT); bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); …… |
3. 偵聽套接字
4. 等待客戶機的連接
這里需要通過accept()調用等待接收客戶端的連接以完成連接的建立,由于該函數在沒有客戶端進行申請連接之前會處于阻塞狀態,因此如果采取通常的單線程模式會導致整個程序一直處于阻塞狀態而不能響應其他的外界消息,因此為該部分代碼單獨開辟一個線程,這樣阻塞將被限制在該線程內而不會影響到程序整體。
AfxBeginThread(Server,NULL);//創建一個新的線程 …… UINT Server(LPVOID lpVoid)//線程的處理函數 { //獲取當前視類的指針,以確保訪問的是當前的實例對象。 CNetServerView* pView=((CNetServerView*)( (CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveView()); while(pView->nNumConns<1)//當前的連接者個數 { int nLen=sizeof(SOCKADDR); pView->newskt= accept(pView->sock, (LPSOCKADDR)& pView->sockin,(LPINT)& nLen); WSAAsyncSelect(pView->newskt, pView->m_hWnd,WM_SOCKET_MSG,FD_CLOSE); pView->nNumConns++; } return 1; } |
這里在accept ()后使用了WSAAsyncSelect()異步選擇函數。對于網絡事件的響應最好采取異步選擇機制,只有采取這種方式才可以在由網絡對方所引起的不可預知的網絡事件發生時能馬上在進程中做出及時的響應處理,而在沒有網絡事件到達時則可以處理其他事件,這種效率是很高的,而且完全符合Windows所標榜的消息觸發原則。WSAAsyncSelect()函數便是實現網絡事件異步選擇的核心函數。通過第四個參數FD_CLOSE注冊了應用程序感興取的網絡事件是網絡斷開,當客戶方端開連接時該事件會被檢測到,同時會發出由第三個參數指定的自定義消息WM_SOCKET_MSG。
5. 發送/接收
當客戶機同服務器建立好連接后就可以通過send()/recv()函數進行發送和接收數據了,對于本程序只需在監測到有撥號連接事件發生時向客戶機發送通知消息即可:
char buffer[1]={'a'}; send(newskt,buffer,1,0);//向客戶機發送字符a,表示現在服務器正在撥號。 |
6. 關閉套接字
在全部通訊完成之后,在退出程序之前需要調用closesocket();函數把創建的套接字關閉。
(三)客戶機端的程序設計
客戶機的編程要相對簡單許多,全部通訊過程只需以下四步:
1. 創建套接字
2. 建立連接
3. 發送/接收
4. 關閉套接字
具體實現過程同服務器編程基本類似,只是由于需要接收數據,因此待監測的網絡事件為FD_CLOSE和FD_READ,在消息響應函數中可以通過對消息參數的低位字節進行判斷而區分出具體發生是何種網絡事件,并對其做出響應的反應。下面結合部分主要實現代碼對實現過程進行解釋:
…… m_ServIP=SERVERIP; //指定服務器的IP地址 m_Port=htons(USERPORT); //指定服務器的端口號 if((IPaddr=inet_addr(m_ServIP))==INADDR_NONE) //轉換成網絡地址 return FALSE; else { sock=socket(AF_INET,SOCK_STREAM,0); //創建套接字 sockin.sin_family=AF_INET; //填充結構 sockin.sin_addr.S_un.S_addr=IPaddr; sockin.sin_port=m_Port; connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); //建立連接 //設定異步選擇事件 WSAAsyncSelect(sock,m_hWnd,WM_SOCKET_MSG,FD_CLOSE|FD_READ); //在這里可以通過震鈴、彈出對話框等方式通知客戶已經連上服務器 } ……
//網絡事件的消息處理函數 int message=lParam & 0x0000FFFF;//取消息參數的低位 switch(message) //判斷發生的是何種網絡事件 { case FD_READ: //讀事件 AfxBeginThread(Read,NULL); break; case FD_CLOSE: //服務器關閉事件 …… break; }
|
在讀事件的消息處理過程中,單獨為讀處理過程開辟了一個線程,在該線程中接收從服務器發送過來的信息,并通過震鈴、彈出對話框等方式通知客戶端現在服務器正在撥號:
…… int a=recv(pView->sock,cDataBuffer,1,0); //接收從服務器發送來的消息 if(a>0) AfxMessageBox("撥號連接已啟動!"); //通知用戶 …… |
三、必要的完善 前面只是介紹了程序設計的整體框架和設計思路,僅僅是一個雛形,有許多重要的細節沒有完善,不能用于實際使用。下面就對一些完全必要的細節做適當的完善:
(一) 界面的隱藏
由于本程序系自動檢測、自動通知,完全不需要人工干預,因此可以將其視為后臺運行的服務程序,因此程序主界面現在已無存在的必要,可以在應用程序類的初始化實例函數InitInstance()中將ShowWindow();的參數SW_SHOW改成SW_HIDE即可。當需要有對話框彈出通知用戶時僅對話框出現,主界面仍隱藏,因此是完全可行的。
(二) 自啟動的實現
由于服務端軟件需要時刻監視有無進行撥號連接,所以必須具缸云舳奶匭浴6突Ф巳砑捎誚郵障⒑屯ㄖ突Ф伎梢宰遠瓿桑虼巳綣芫弒缸云舳匭栽蚩梢醞耆牙胗沒У母稍ざ〉媒細叩淖遠潭取I柚米云舳奶匭裕梢源右韻錄父鐾揪都右鑰悸牽?BR>
1. 在"啟動"菜單上添加指向程序的快捷方式。
2. 在Autoexec.bat中添加啟動程序的命令行。
3. 在Win.ini中的[windows]節的run項目后添加程序路徑。
4. 修改注冊表,添加鍵值的具體路徑為:
"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run"
并將添加的鍵值修改為程序的存放路徑即可。以上幾種方法既可以手工添加,也可以通過編程使之自動完成。
(三) 自動續聯
對于服務/客戶模式的網絡通訊程序普遍要求服務端要先于客戶端運行,而本系統的客戶、服務端均為自啟動,不能保證服務器先于客戶機啟動,而且本系統要求只要客戶機和服務器連接在網絡上就要不間斷保持連接,因此需要使客戶和服務端都要具備自動續聯的功能。
對于服務器端,當客戶端斷開時,需要關閉當前的套接字,并重新啟動一個新的套接字以等待客戶機的再次連接。這可以放在FD_CLOSE事件對應的消息WM_SOCKET_MSG的消息響應函數中來完成。而對于客戶端,如果先于服務器而啟動,則connect()函數將返回失敗,因此可以在程序啟動時用SetTimer()設置一個定時器,每隔一段時間(10秒)就試圖連接服務器一次,當connect()函數返回成功即服務器已啟動并與之連接上之后可以用KillTimer()函數將定時器關閉。另外當服務器關閉時需要再次開啟定時器,以確保當服務器再次運行時能與之建立連接,可以通過響應FD_CLOSE事件來捕獲該事件的發生。
小結:本文通過Windows Sockets API實現了基于TCP/IP協議的面向連接的流式套接字的網絡通訊程序的設計,通過網絡通訊程序的支持可以把服務器捕獲到的撥號連接發生的事件及時通知給客戶端,最后通過對一些必要的細節的完善很好解決了在局域網上能及時得到服務器撥號連接的消息通知。本文所述程序在Windows 98 SE下,由Microsoft Visual C++ 6.0編譯通過;使用的代理服務器軟件為WinGate 4.3.0;上網方式為撥號上網。
VC++編程實現網絡嗅探器引言
從事網絡安全的技術人員和相當一部分準黑客(指那些使用現成的黑客軟件進行攻擊而不是根據需要去自己編寫代碼的人)都一定不會對網絡嗅探器(sniffer)感到陌生,網絡嗅探器無論是在網絡安全還是在黑客攻擊方面均扮演了很重要的角色。通過使用網絡嗅探器可以把網卡設置于混雜模式,并可實現對網絡上傳輸的數據包的捕獲與分析。此分析結果可供網絡安全分析之用,但如為黑客所利用也可以為其發動進一步的攻擊提供有價值的信息。可見,嗅探器實際是一把雙刃劍。 雖然網絡嗅探器技術被黑客利用后會對網絡安全構成一定的威脅,但嗅探器本身的危害并不是很大,主要是用來為其他黑客軟件提供網絡情報,真正的攻擊主要是由其他黑軟來完成的。而在網絡安全方面,網絡嗅探手段可以有效地探測在網絡上傳輸的數據包信息,通過對這些信息的分析利用是有助于網絡安全維護的。權衡利弊,有必要對網絡嗅探器的實現原理進行介紹。
嗅探器設計原理
嗅探器作為一種網絡通訊程序,也是通過對網卡的編程來實現網絡通訊的,對網卡的編程也是使用通常的套接字(socket)方式來進行。但是,通常的套接字程序只能響應與自己硬件地址相匹配的或是以廣播形式發出的數據幀,對于其他形式的數據幀比如已到達網絡接口但卻不是發給此地址的數據幀,網絡接口在驗證投遞地址并非自身地址之后將不引起響應,也就是說應用程序無法收取到達的數據包。而網絡嗅探器的目的恰恰在于從網卡接收所有經過它的數據包,這些數據包即可以是發給它的也可以是發往別處的。顯然,要達到此目的就不能再讓網卡按通常的正常模式工作,而必須將其設置為混雜模式。
具體到編程實現上,這種對網卡混雜模式的設置是通過原始套接字(raw socket)來實現的,這也有別于通常經常使用的數據流套接字和數據報套接字。在創建了原始套接字后,需要通過setsockopt()函數來設置IP頭操作選項,然后再通過bind()函數將原始套接字綁定到本地網卡。為了讓原始套接字能接受所有的數據,還需要通過ioctlsocket()來進行設置,而且還可以指定是否親自處理IP頭。至此,實際就可以開始對網絡數據包進行嗅探了,對數據包的獲取仍象流式套接字或數據報套接字那樣通過recv()函數來完成。但是與其他兩種套接字不同的是,原始套接字此時捕獲到的數據包并不僅僅是單純的數據信息,而是包含有 IP頭、 TCP頭等信息頭的最原始的數據信息,這些信息保留了它在網絡傳輸時的原貌。通過對這些在低層傳輸的原始信息的分析可以得到有關網絡的一些信息。由于這些數據經過了網絡層和傳輸層的打包,因此需要根據其附加的幀頭對數據包進行分析。下面先給出結構.數據包的總體結構:
數據在從應用層到達傳輸層時,將添加TCP數據段頭,或是UDP數據段頭。其中UDP數據段頭比較簡單,由一個8字節的頭和數據部分組成,具體格式如下:
16位 |
16位 |
源端口 |
目的端口 |
UDP長度 |
UDP校驗和 |
而TCP數據頭則比較復雜,以20個固定字節開始,在固定頭后面還可以有一些長度不固定的可選項,下面給出TCP數據段頭的格式組成:
16位 |
16位 |
源端口 |
目的端口 |
順序號 |
確認號 |
TCP頭長 |
(保留)7位 |
URG |
ACK |
PSH |
RST |
SYN |
FIN |
窗口大小 |
校驗和 |
緊急指針 |
可選項(0或更多的32位字) |
數據(可選項) |
對于此TCP數據段頭的分析在編程實現中可通過數據結構_TCP來定義:
typedef struct _TCP{ WORD SrcPort; // 源端口 WORD DstPort; // 目的端口 DWORD SeqNum; // 順序號 DWORD AckNum; // 確認號 BYTE DataOff; // TCP頭長 BYTE Flags; // 標志(URG、ACK等) WORD Window; // 窗口大小 WORD Chksum; // 校驗和 WORD UrgPtr; // 緊急指針 } TCP; typedef TCP *LPTCP; typedef TCP UNALIGNED * ULPTCP; |
在網絡層,還要給TCP數據包添加一個IP數據段頭以組成IP數據報。IP數據頭以大端點機次序傳送,從左到右,版本字段的高位字節先傳輸(SPARC是大端點機;Pentium是小端點機)。如果是小端點機,就要在發送和接收時先行轉換然后才能進行傳輸。IP數據段頭格式如下:
16位 |
16位 |
版本 |
IHL |
服務類型 |
總長 |
標識 |
標志 |
分段偏移 |
生命期 |
協議 |
頭校驗和 |
源地址 |
目的地址 |
選項(0或更多) |
同樣,在實際編程中也需要通過一個數據結構來表示此IP數據段頭,下面給出此數據結構的定義:
typedef struct _IP{ union{ BYTE Version; // 版本 BYTE HdrLen; // IHL }; BYTE ServiceType; // 服務類型 WORD TotalLen; // 總長 WORD ID; // 標識 union{ WORD Flags; // 標志 WORD FragOff; // 分段偏移 }; BYTE TimeToLive; // 生命期 BYTE Protocol; // 協議 WORD HdrChksum; // 頭校驗和 DWORD SrcAddr; // 源地址 DWORD DstAddr; // 目的地址 BYTE Options; // 選項 } IP; typedef IP * LPIP; typedef IP UNALIGNED * ULPIP;
|
在明確了以上幾個數據段頭的組成結構后,就可以對捕獲到的數據包進行分析了。
嗅探器的具體實現
根據前面的設計思路,不難寫出網絡嗅探器的實現代碼,下面就給出一個簡單的示例,該示例可以捕獲到所有經過本地網卡的數據包,并可從中分析出協議、IP源地址、IP目標地址、TCP源端口號、TCP目標端口號以及數據包長度等信息。由于前面已經將程序的設計流程講述的比較清楚了,因此這里就不在贅述了,下面就結合注釋對程序的具體是實現進行講解,同時為程序流程的清晰起見,去掉了錯誤檢查等保護性代碼。主要代碼實現清單為:
// 檢查 Winsock 版本號,WSAData為WSADATA結構對象 WSAStartup(MAKEWORD(2, 2), &WSAData); // 創建原始套接字 sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)); // 設置IP頭操作選項,其中flag 設置為ture,親自對IP頭進行處理 setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag)); // 獲取本機名 gethostname((char*)LocalName, sizeof(LocalName)-1); // 獲取本地 IP 地址 pHost = gethostbyname((char*)LocalName)); // 填充SOCKADDR_IN結構 addr_in.sin_addr = *(in_addr *)pHost->h_addr_list[0]; //IP addr_in.sin_family = AF_INET; addr_in.sin_port = htons(57274); // 把原始套接字sock 綁定到本地網卡地址上 bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in)); // dwValue為輸入輸出參數,為1時執行,0時取消 DWORD dwValue = 1; // 設置 SOCK_RAW 為SIO_RCVALL,以便接收所有的IP包。其中SIO_RCVALL // 的定義為: #define SIO_RCVALL _WSAIOW(IOC_VENDOR,1) ioctlsocket(sock, SIO_RCVALL, &dwValue); |
前面的工作基本上都是對原始套接字進行設置,在將原始套接字設置完畢,使其能按預期目的工作時,就可以通過recv()函數從網卡接收數據了,接收到的原始數據包存放在緩存RecvBuf[]中,緩沖區長度BUFFER_SIZE定義為65535。然后就可以根據前面對IP數據段頭、TCP數據段頭的結構描述而對捕獲的數據包進行分析:
while (true) { // 接收原始數據包信息 int ret = recv(sock, RecvBuf, BUFFER_SIZE, 0); if (ret > 0) { // 對數據包進行分析,并輸出分析結果 ip = *(IP*)RecvBuf; tcp = *(TCP*)(RecvBuf + ip.HdrLen); TRACE("協議: %s\r\n",GetProtocolTxt(ip.Protocol)); TRACE("IP源地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.SrcAddr)); TRACE("IP目標地址: %s\r\n",inet_ntoa(*(in_addr*)&ip.DstAddr)); TRACE("TCP源端口號: %d\r\n",tcp.SrcPort); TRACE("TCP目標端口號:%d\r\n",tcp.DstPort); TRACE("數據包長度: %d\r\n\r\n\r\n",ntohs(ip.TotalLen)); } } |
其中,在進行協議分析時,使用了GetProtocolTxt()函數,該函數負責將IP包中的協議(數字標識的)轉化為文字輸出,該函數實現如下:
#define PROTOCOL_STRING_ICMP_TXT "ICMP" #define PROTOCOL_STRING_TCP_TXT "TCP" #define PROTOCOL_STRING_UDP_TXT "UDP" #define PROTOCOL_STRING_SPX_TXT "SPX" #define PROTOCOL_STRING_NCP_TXT "NCP" #define PROTOCOL_STRING_UNKNOW_TXT "UNKNOW" …… CString CSnifferDlg::GetProtocolTxt(int Protocol) { switch (Protocol){ case IPPROTO_ICMP : //1 /* control message protocol */ return PROTOCOL_STRING_ICMP_TXT; case IPPROTO_TCP : //6 /* tcp */ return PROTOCOL_STRING_TCP_TXT; case IPPROTO_UDP : //17 /* user datagram protocol */ return PROTOCOL_STRING_UDP_TXT; default: return PROTOCOL_STRING_UNKNOW_TXT; } |
最后,為了使程序能成功編譯,需要包含頭文件winsock2.h和ws2tcpip.h。在本示例中將分析結果用TRACE()宏進行輸出,在調試狀態下運行,得到的一個分析結果如下:
協議: UDP
IP源地址: 172.168.1.5
IP目標地址: 172.168.1.255
TCP源端口號: 16707
TCP目標端口號:19522
數據包長度: 78
……
協議: TCP
IP源地址: 172.168.1.17
IP目標地址: 172.168.1.1
TCP源端口號: 19714
TCP目標端口號:10
數據包長度: 200
……
從分析結果可以看出,此程序完全具備了嗅探器的數據捕獲以及對數據包的分析等基本功能。
小結
本文介紹的以原始套接字方式對網絡數據進行捕獲的方法實現起來比較簡單,尤其是不需要編寫VxD虛擬設備驅動程序就可以實現抓包,使得其編寫過程變的非常簡便,但由于捕獲到的數據包頭不包含有幀信息,因此不能接收到與 IP 同屬網絡層的其它數據包, 如 ARP數據包、RARP數據包等。在前面給出的示例程序中考慮到安全因素,沒有對數據包做進一步的分析,而是僅僅給出了對一般信息的分析方法。通過本文的介紹,可對原始套接字的使用方法以及TCP/IP協議結構原理等知識有一個基本的認識。本文所述代碼在Windows 2000下由Microsoft Visual C++ 6.0編譯調試通過。