1、套接字編程原理
一個完整的網間通信進程需要由兩個進程組成,並且只能用同一種高層協議。也就是說,不可能通信的一段用TCP,而另一端用UDP。一個完整的網絡信息需要一個五元組來標識:協議、本地地址、本地端口號、遠端地址、遠端端口號。
1.1Client/Server通信模型
在客戶端/服務器模式中我們將請求服務的一方成為客戶,將提供某種服務的一方稱為服務器(Server)
一個服務程序通常在一個眾所周知的地址監聽對服務的請求,也就是說服務進程一直處於休眠狀態,直到一個客戶對這個服務的地址提出了連接請求。在這個時刻,服務程序被“驚醒”,並且為客戶提供服務----對客戶的請求做出適當的反應。雖然基於鏈接的服務是設計客戶機/服務器應用程序時的標准,但有些服務也是可以通過無連接的接口提供的。
客戶及/服務器的請求/響應過程示意圖如下所示:
1.2Windows Sockets規范
Windows Sockets規范從90年代的1.0版本開始,經過不斷的完善和發展,目前已經有了Windows Sockets 2版本。值得注意的是,Microsoft的MFC庫現在只支持Windows Sockets1版本,不支持WindowsSockets2版本。
MFC提供了兩個類用以封裝Windows Sockets API。一個是CAsyncSocket類,它主要是提供給那些具有一定網絡編程經驗,希望同時擁有Socket API編程的靈活性和類庫編程便利性的開發者。另一個是CSocket類,它有CAsyncSocket類派生,它具有更高的抽象化,致力於簡化網絡編程所需的操作。
1.3套接字
1.3.1套接字定義
套接字是一個通信終結點,它是Sockets應用程序用來在網絡上發送或接收數據包的對象。套接字具有類型,與正在運行的進程相關聯,並且可以有名稱。目前,套接字一般只與使用網際協議組的同一“通信域”中德其他套接字交換數據。使用套接字的應用程序間通信模型如圖:
1.3.2分類
1.3.2.1流式套接字
流式套接字提供沒有記錄邊界的數據流,即字節流。字節流能確保以正確的順序無重復地被送達。
1.3.2.2數據報套接字
數據報套接字支持面向記錄的數據流,但不能確保能被送達,也無法確保按照發送順序或不重復。
“有序”指數據包按發送的順序送達。“不重復”指一個特定的數據包只能獲取一次。這兩種套接字都是雙向,是可以同時在兩個方向上(全雙工)進行通信的數據流.
注意:在某些網絡協議下(如XNS),流可以面向記錄,即作為記錄流而非字節流。但在更常用的TCP/IP協議下,流為字節流,Win Sockets提供與基礎協議無關的抽象化級別。
1.3.3套接字的作用
套接字的作用非常大,至少在下面三種通信上下文中如此:
- 客戶端/服務器模型
- 對等網絡方案,如聊天應用程序
- 通過讓接受應用程序將消息解釋為函數調用來進行遠程過程調用
1.3.4 端口與地址
在網絡上,一個套接字的標識主要借助於地址和端口來描述。
套接字的地址指該套接字所在計算機的網絡地址,可以為域名或IP地址的形式。通常創建套接字時不必指明網絡地址,只有在擁有多個網絡地址的機器時,才需要顯式指定的一個網絡地址。
同一機器上可以運行多個網絡應用程序,每個應用程序都有自己的套接字用以進行網絡通信,此時如果只有地址表示套接字,則當一個通信包到達機器時,將無法確定究竟是哪個應用程序的套接字需要接收此信息。由此增加了端口的概念,以協助區分同一機器上不同應用程序的套接字。
段開口用於標識進程,同一機器上不同的網絡應用程序各有不同的端口,這樣,通過“網絡地址+端口號”的標識方法,便唯一標識了機器上的應用程序了。
實例應用程序:
======客戶端
#include < WINSOCK2.H> #pragma comment( lib, "WS2_32" ) #include < stdio.h> int main() { printf( "------------------------\n| 客戶端 |\n|---------------------------------------\n" ); //------①加載動態鏈接庫winsock DLL----------- printf( "|加載等待中.... " ); WSADATA wsaData; WORD wVersionRequested= MAKEWORD( 2 ,2 ); if ( WSAStartup( wVersionRequested,& wsaData)!= 0 ) { printf( "|WSAStartup Failed\n" ); printf( "|WSAStartup Error=%d\n" , WSAGetLastError()); } else { printf( "加載Winsock 庫成功 |\n" ); } printf( "|---------------------------------------\n" ); //-------②創建用於監聽的流式套接口s,使用socket()----------------- SOCKET s= socket( AF_INET, SOCK_STREAM, IPPROTO_TCP); if ( s== INVALID_SOCKET) { printf( "|Failed socket\n" ); printf( "|socket Error=%d\n" , WSAGetLastError()); } else printf( "|已創建用於監聽的套接口,套接口號:[%u]\n" , s); printf( "|---------------------------------------\n" ); //------③本地地址bind(可以不做這部分,如果不綁定,系統將自動分配)-------- /*struct sockaddr_in Cadd; Cadd.sin_family=AF_INET; Cadd.sin_port=htons(4444); Cadd.sin_addr.S_un.S_addr=htonl(INADDR_ANY); if (bind(s,(sockaddr*)&Cadd,sizeof(Cadd))==SOCKET_ERROR) { printf("|Failed bind()/n"); }*/ //-------填寫要連接的服務器地址信息--------- struct sockaddr_in Sadd; Sadd.sin_family= AF_INET; Sadd.sin_port= htons( 5555 ); Sadd.sin_addr.S_un.S_addr= inet_addr( "127.0.0.1" ); //--------④將套接口s與遠程主機相連-------------- if ( connect( s,( sockaddr*)& Sadd, sizeof ( Sadd))== INVALID_SOCKET) { printf( "|Failed connect()\n" ); printf( "|connect Error=%d\n" , WSAGetLastError()); } else { //####################開始發接數據######################## printf( "|連接成功,可以開始發送接收數據了!\n" ); printf( "|服務器IP地址:[%s]\n 端口號:[%u]\n" , inet_ntoa( Sadd.sin_addr), ntohs( Sadd.sin_port)); //####################結束發接數據######################## } //--------------⑤關閉套接字s,終止對動態鏈接庫的訪問---------- closesocket( s); printf( "|---------------------------------------\n" ); printf( "|連接完畢\n" ); WSACleanup(); return 0 ; }
========服務端
#include <winsock2.h> #include <stdio.h> #pragma comment(lib,"ws2_32") int main(int argc, char* argv[]) { printf("---------\n| 服務端 |\n-----------"); //----------1、加載動態鏈接庫Winsock Dll--------- WORD wVersionRequested = MAKEWORD(2,2); WSADATA wsaData; if(WSAStartup(wVersionRequested,&wsaData) != 0) { printf("WSAStartup Failed \n"); printf("WSAStartup Error = %d \n",WSAGetLastError()); } else { printf("加載WinSocket成功 \n"); printf("調用者希望使用的WinSocket版本號=%x\n",wsaData.wVersion); printf("加載的WinSock庫支持最高WinSock版本號=%x \n",wsaData.wHighVersion); printf("wWinSock庫的說明字符串=%s \n",&wsaData.szDescription[0]); printf("系統狀態或配置信息的說明字符串=%s\n",&wsaData.szSystemStatus[0]); } //----------2、創建監聽的流式套接口---------- SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(s == INVALID_SOCKET) { printf("| Failed socket\n"); printf("| Socket Error = %d\n",WSAGetLastError()); } else printf("毅創建用於監聽的套接口,套接口號:[%u]\n",s); //----3、綁定使用bind() struct sockaddr_in Sadd; Sadd.sin_family = AF_INET; Sadd.sin_port = htons(5555); Sadd.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); if(bind(s,(sockaddr*)&Sadd,sizeof(Sadd)) == SOCKET_ERROR) { printf("Failed bind()\n"); printf("Bind Error = %d\n",WSAGetLastError()); } else { printf("綁定成功\n"); printf("本地Ip地址:[%s],本地端口號:[%u]\n",inet_ntoa(Sadd.sin_addr),ntohs(Sadd.sin_port)); } //---------------④監聽狀態------------- if ( listen( s, 2 )== SOCKET_ERROR) { printf( "Failed listen()\n" ); printf( "listen Error=%d\n" , WSAGetLastError()); } //----------------⑤循環接受客戶的連接請求--------------------------- struct sockaddr_in Cadd; int caddLen= sizeof ( Cadd); SOCKET c; while ( TRUE ) { printf( "|----------------------------\n" ); printf( "|進入監聽狀態.....\n|--------------------------------|\n" ); c= accept( s,( sockaddr*)& Cadd,& caddLen); if ( c== INVALID_SOCKET) { printf( "|Failed accept()\n" ); printf( "|accept Error=%d\n" , WSAGetLastError()); } else printf( "|可以在套接口[%u]上發送接收數據了!\n" , c); //#########################開始發送、接收###################### 注意都要在新套接口c上進行 //#########################結束發送、接收###################### closesocket( c); printf( "|與主機IP地址是:[%s]\n|端口號是:[%u]的連接完畢\n" , inet_ntoa( Cadd.sin_addr), ntohs( Cadd.sin_port)); char xx; printf( "|-------------------------------------\n需要退出嗎?(Y\n)" ); scanf( "%c" ,& xx); if ( xx== 'Y' || xx== 'y' ) { break ; } } closesocket( s); WSACleanup(); return 0; }
運行結果: