最簡單的網絡程序如圖:
提示:IP地址就相當於一個公司的總機號碼,端口號就相當於分機號碼。在打電話時,撥通總機後,還需要轉到分機上。
(1)協議
·為進行網絡中的數據交換(通信)而建立的規則、標准或約定(=語義+語法+規則);
·不同層具有各自不同的協議;
(2)網路的狀況
·多種通信媒介---有線、無線···
·不同種類的設備---通用、專用···
·不同的操作系統---UNIX、Windows···
·不同的應用環境---固定、移動···
·不同的業務種類---分時、交互、實時···
·寶貴的投資和積累---有形、無形···
·用戶業務的延續性---不允許出現大的跌巖起伏;
他們互相交織,形成了非常復雜的系統應用環境。
(3)ISO/OSI七層參考模型
·物理層:提供二進制傳輸,確定在通信信道上如何傳輸比特流;
·數據鏈路層:提供介質訪問,加強物理層的傳輸功能,建立一條無差錯的傳輸線路;
·網絡層:提供IP尋址和路由(網絡上數據可以經由多條線路到達目的地,網絡層負責找出最佳的傳輸線路);
·傳輸層:為源端主機到目的端主機提供可靠的數據傳輸服務,隔離網絡的上下層協議,使得網絡應用與下層協議無關;
·會話層:在兩個相互通信的應用進程之間建立、組織和協調其相互之間的通信;
·表示層:處理被傳送數據的表示問題,即信息的語法和語義;如有必要,可使用一種通用的數據表示格式,在多種數據表示之間進行轉換。例如在日期、貨幣、數值等本地數據表示格式和標准數據表示格式之間進行轉換,還有數據的加解密、壓縮和解壓縮等;
·應用層:為用戶的網絡應用程序提供網絡通信的服務;
注意:在進行一次網絡通信時,每一層為本次通信提供本次的服務(通信實體的對等體之間不允許直接通信);
各層之間是嚴格單向依賴;
上層使用下層提供的服務---Service user;
下層向上層提供服務---Service provider;
(圖片引用於別處)
應用層、傳輸層、網絡層各使用的協議:
應用層:Telnet(遠程登錄協議)、FTP(文件傳輸協議)、HTTP(超文本傳輸協議)、DNS(域名服務)、SMTP(簡單郵件傳輸協議)、POP3(郵局協議)等;
傳輸層:TCP(傳輸控制協議)、UDP(用戶數據報協議);
網絡層:網際協議IP、Internet互聯網控制報文協議ICMP、Internet組管理協議IGMP;
(4)數據封裝
·一台計算機要想另一台計算機發送數據,首先必須將該數據打包,打包的過程成為封裝(即:在數據前面加上特定的協議頭部);
·PDU(協議數據單元):對等層協議之間交換的信息單元的統稱;
·頭部含有的數據中含有王城數據傳輸所需的控制信息;
(5)TCP/IP模型
(6)端口
·傳輸層提供進程(活動的應用程序)通信的能力;為了標識通信實體中進行通信的進程(應用程序),TCP/IP協議提出了協議端口(protocol port,簡稱端口)的概念;
·端口用一個整數型標識符來表示,即端口號;端口號跟協議相關,TCP/IP傳輸層的兩個協議TCP和UDP是完全獨立的兩個軟件模塊,因此各自的端口號也相互獨立;
·我們在編寫網絡應用程序時,要為程序指定1024以上的端口號;1024以下端口號保留給預定義的服務;
(7)套接字的引入
·套接字存在於通信區域中;通信區域也叫地址族,主要用於將通過套接字通信的進程的共有特性綜合在一起;
·套接字通常只與同一區域的套接字交換數據(也有可能跨區域通信,但這只在執行了某種轉換進程後才能實現);
·Windows Sockets只支持一個通信區域:網際域(AF_INET),這個域被使用網際協議簇通信的進程使用;
(8)網絡字節順序
·不同的計算機存放多字節的順序不同;
·基於Inter的CPU,采用的是低位先存。為保證數據的正確性,在網絡協議中需要指定網絡字節順序,TCP/IP協議使用16位整數和32位整數的高位先存格式;
·網絡中不同主機間進行通信時,要同一采用網絡字節順序;
(9)客戶機/服務器模式
圖片來自:http://pic002.cnblogs.com/images/2012/387401/2012111509190090.jpg
客戶機/服務器在操作過程中采用主動請求的方式,首先服務器方要先啟動,並根據請求提供相應的服務:
①打開一個通信通道並告知本地主機,他願意在某一地址可端口上接收客戶請求;
②等待客戶請求到達該端口;
③接收到重復服務請求,處理請求並發送應答信息。接收到並發起服務請求,要激活一個新的進程(或線程)來處理這個客戶請求。新進程(或線程)處理此客戶請求,並不需要對其他請求做出應答。服務完成後,關閉此新進程與客戶的通信鏈接,並終止;
④返回第二步,等待另一客戶請求;
⑤關閉服務器;
客戶方:
①打開一個通信通道,並連接到服務器所在主機的特定端口;
②想服務器發送服務請求報文,等待並接收應答;繼續提出請求;
③請求結束後關閉通信通道並終止;
Socket是連接應用程序與網絡驅動程序的橋梁,Socket在應用程序中創建,通過綁定操作與驅動程序建立關系。此後,應用程序送給Socket的數據,由Socket交給驅動程序向網絡上發送出去。計算機從網絡上收到與該Socket綁定的IP地址和端口號相關的數據後,由驅動程序交給Socket。應用程序便可從該Socket中提取接收到的數據。
(1)套接字的類型
·流式套接字(SOCK_STREAM)
提供面向連接、可靠的數據傳輸服務,數據烏差錯、無重復的發送,且按接發送順序接收;SOCK_STREAM是基於TCP協議實現的;
·數據報式套接字(SOCK_DGRAM)
提供無連接服務;數據包以獨立包形式發送,不提供無差錯保證,數據可能丟失和重復,並且接收順序混亂;SOCK_DGRAM是基於 UDP協議實現的;
·原始套接字(SOCK_RAW)
(2)基於TCP(面向連接)的Socket編程
基於基於TCP(面向連接)的Socket編程的服務端程序流程如下:
①創建套接字(socket);
②將套接字綁定到一個本地地址和端口上(bind);(解釋:告訴本地主機它打算在哪個IP地址和哪個端口上等待客戶請求)
③將套接字設為監聽模式,准備接收客戶請求(listen);
④等待客戶請求到來;當請求到來後,接受連接請求,返回一個新的對應於此連接的套接字(accept);
⑤用返回的套接字和客戶端進行通信(send/recv);
⑥返回,等待另一個客戶請求;
⑦關閉套接字;
基於基於TCP(面向連接)的Socket編程的客戶端程序流程如下:
①創建套接字(socket);
②向服務器發出連接請求(connect);
③和服務器端進行通信(send/recv);
④關閉套接字;
在服務器端,當調用accept函數時,程序就會等待,等待客戶端調用connect函數發出連接請求,然後服務器端接收該請求,於是雙方就建立了連接,之後,服務器端和客戶端就可以利用send和recv函數進行通信了。
(3)基於UDP(面向無連接)的socket編程
接收端(服務器端):先啟動的一端;發送端(客戶端):發送數據的一端;
接收端程序的編寫:
①創建套接字(socket);
②將套接字綁定到一個本地地址和端口上(bind);(解釋:接收端告訴本地主機,它是在哪個地址和端口上等待數據的到來)
③等待接收數據(recvfrom);
④關閉套接字;
客戶端程序的編寫:
①創建套接字(socket);
②向服務器發送數據(sendto);
③關閉套接字;
提示:套接字表示了通信的端點;利用套接字通信與利用電話機通信是一樣的,套接字相當於電話機,IP地址相當於總機號碼,端口號相當於分機。
(1)WSAStartup函數
int WSAStartup( WORD wVersionRequested, //指定准備加載的Winsock庫的版本; LPWSADATA lpWSAData //是一個返回值,指向WSADATA結構的指針 ); //lpWSAdata:這是一個返回值,指向WSADATA結構的指針,WSAStartup函數用其加載的庫版本有關的信息填在這個結構中;
·功能:①加載套接字庫;
②進行套接字庫的版本的協商(確定將使用的socket版本);
·對於每一個WSAStartup函數的成功調用(即成功加載WinSock動態庫後),在最後對應一個WASCleanUp調用,來釋放該程序占用的資源,終止對WinSock動態庫的使用。
typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION_LEN+1]; char szSystemStatus[WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADATA, *LPWSADATA; View Code·WSAStartup函數把WSADate結構中的第一個字段wVersion設置為打算使用的Winsock版本,wHighVersion字段容納的是現有的Winsock庫的最高版本;
·注意:這兩個字段中,高位字節代表的是Winsock副版本,而低字節代表的則是Winsock朱版本;
(2)socket函數
SOCKET socket( int af, //指定地址簇,對於TCP/IP協議的套接字,它只能是AF_INET(或PF_INET); int type, //指定socket類型(SOCK_STREAM、SOCK_DGRAM) int protocol //與特定的地址家族相關的協議; );
·如果socket函數調用成功,它就會返回一個新的SOCKET數據類型的套接字描述符;調用失敗,返回一個INVALID_SOCKET值,錯誤信息可以通過WSAGetLastError函數返回。
(3)bind函數
int bind( SOCKET s, //指定要綁定的套接字; const struct sockaddr FAR *name, //指向sockaddr結構的指針變量,指定了該套接字的本地地址信息; int namelen //指定sockaddr地址結構的長度; );
·name:指定了該套接字的本地地址信息;指向sockaddr結構的指針變量,由於該地址結構是為所有的地址家族准備的,這個結構可能隨所使用的網絡協議不同而不同,故:第三個參數(namelen)指定改地址結構的長度;
·功能:創建套接字成功之後,將該套接字綁定到本地的某個地址和端口上;
sockaddr結構定義如下:
struct sockaddr { u_short sa_family; char sa_data[14]; };
·sockaddr結構的第一個字段(sa_family)指定地址家族,對於TCP/IP協議的套接字,必須設置為AF_INET;
·第二個地段(sa_data)僅僅是表示要求一塊內存分配區,起到占用的作用,該區域中指定與協議相關的具體地址信息;
·注意:由於實際要求的只是內存去,所以對於不同的協議家族,用不同的結構來替代sockaddr。處理sa_family外,sockaddr是按網絡字節順序表示的。
**在基於TCP/IP的socket編輯過程中,可以用sockaddr_in結構替換sockaddr以方便我們填寫地址信息**。
sockaddr_in結構體的定義如下:
struct sockaddr_in{ short sin_family; //表示地址族;(對於IP地址,改變量一直是AF_INET) unsigned short sin_port; //指定將要分配給套接字的端口; IN_ADDR sin_addr; //給出套接字的主機IP地址; char sin_zero[8];}; //填充數(使sockaddr_in和sockaddr長度一樣);
·sockaddr_in結構中sin_addr成員的類型是in_addr,該結構的定義如下所示:
struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; };
·提示:in_addr結構實際上是一個聯合,通常利用這個結構將一個點分十進制格式的IP地址轉換為u_long類型,並將結果賦給成員S_addr。
(4)inet_addr和inet_intoa函數
unsigned long inet_addr( const char FAR *cp );
·inet_addr函數需要一個字符串作為其參數,該字符串指定了以點分十進制格式表示的IP地址(如192.168.0.16);而且inet_addr函數會返回一個適合分配給S_addr的u_long類型的數值;
char FAR * inet_ntoa( struct in_addr in );
·inet_ntoa函數完成與inet_addr相反的轉換,它接收一個in_addr結構體類型的參數並返回一個以點分十進制格式表示的IP地址字符串;
(5)listen函數
int listen( SOCKET s, // 套接字描述符; int backlog // 等待**連接隊列**的最大長度; ); View Code·作用:將指定的套接字設置為監聽模式;
(6)accept函數
SOCKET accept( SOCKET s, //套接字描述符,該套接字已經通過listen函數將其設置為監聽狀態; struct sockaddr FAR *addr, int FAR *addrlen //是一個返回值,指向一個整型的指針,返回包含地址信息的長度; );
·功能:接受客戶端發出的連接請求;
·addr參數:指向一個緩沖區的指針,該緩沖區用來接收連接實體的地址,也就是當客戶端服務器發起連接,服務器接受這個連接時,保存發起連接的這個客戶端的IP地址信息和端口信息;
(7)send函數
int send( SOCKET s, //一個已經建立連接的套接字; const char FAR *buf, //buf指向一個緩沖區,該緩沖區包含將要傳遞的數據; int len, //len是緩沖區的長度; int flags //flogs:設定的值將影響函數的行為,一般將器設置為0即可; );
·功能:通過一個已建立連接的套接字發送數據;
(8)recv函數
int recv( SOCKET s, //s:建立連接後准備接收數據的那個套接字; char FAR *buf, //buf:指向緩沖區的指針,用來保存接收的數據; int len, //len:緩沖區的長度; int flags //同send的flags; );
·功能:從一個已連接的套接字接收數據;
(9)connect
int connect( SOCKET s, //s:即將在其上建立連接的那個套接字; const struct sockaddr FAR *name, //name:設定連接的服務器地址信息; int namelen //namelen:指定服務器端地址的長度; );
·功能:將與一個特定的套接字建立連接;
(10)recvfrom
int recvfrom( SOCKET s, //s:准備接收數據的套接字; char FAR* buf, //buf:指向緩沖區的指針,該緩沖區用來接收數據; int len, //len:緩沖區長度; int flags, //不解釋 struct sockaddr FAR *from, //from:是一個指向地址結構的指針,主要是用來接收發送數據方的地址信息; int FAR *fromlen //整型指針,且是一個in/out類型的參數; );
·功能:將接收一個數據報信息並保存源地址;
·fromlen:是一個in/out類型的參數,表明在調用前需要給它指定一個初始值,當函數調用之後,會通過這個參數返回一個值,該返回值是底地址結構的大小;
(11)sendto
int sendto( SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, //可選的指針,指定目標套接字的地址; int tolen //tolen:是參數to中指定的地址的長度; );
·功能:將向一個特定的目的方發送數據;
(12)htons 和htonl 函數
u_short htons( u_short hostshort //hostshort:是一個以主機字節順序表示的16為數值; );
·功能:(Windows Sockets的htons函數)將把一個u_short類型的值從主機字節順序轉換為TCP/IP網絡字節順序;
u_long htonl( u_long hostlong //是一個以主機字節順序表示的32位數值; );
·功能:將把一個u_long類型的值從主機字節順序轉換為TCP/IP網絡字節順序;
服務器端程序:
#include <stdio.h> #include <Winsock2.h> void main() { //加載套接字庫 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); //MAKEWORD宏創建一個包含一個請求版本號的WORD值; //MAKEWORD(x,y)宏(x是高位字節, y是低位字節)可以方便的獲取wVersionRequested的正確值; err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //①創建用於監聽的套接字 SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(6000); bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR) ); //②綁定套接字 listen(sockSrv, 5); //③將套接字設為監聽模式,准備就收客戶請求 SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); while(1) { SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len); //④等待客戶請求到來 char sendBuf[100]; sprintf(sendBuf, "welcome %s to http://www.cnblog.com.aze-003", inet_ntoa(addrClient.sin_addr)); send( sockConn, sendBuf, strlen(sendBuf)+1, 0); //發送數據 注意:“+1”:表示增加1個“\0”結尾標志; char recvBuf[100]; recv( sockConn, recvBuf, 100, 0); //接收數據 printf("%s\n", recvBuf); //打印接收的數據 closesocket(sockConn); //⑦關閉套接字 } } /* ④等待客戶請求到來;當請求到來後,接收連接請求,返回一個新的對應於此次連接的套接字(accept) ⑤用返回的套接字和客戶端進行連接通信(send/recv); ⑥返回,等待另一客戶請求; 注意:在調用accept函數前,必須為它的第三個參數賦予一個初始值,即:SOCKADDR_IN結構體的長度; 進入循環,首先調用accept函數等待並接收客戶的連接請求,其中第一個參數是處於監聽狀態的套接字; 第二個參數利用addrClient變量接收客戶端的地址信息。當客戶端連接請求到來時,該函數接受該請求,建 立連接,同時它將返回一個相對於當前這個新連接的一個套接字描述符,保存於sockConn變量中,然後利用 這個套i蛾子就可以與客戶端進行通信了,而我們先前的套接字仍然繼續監聽客戶端的連接請求; */ TCP_Srv
客戶端程序:
#include <stdio.h> #include <Winsock2.h> void main() { //加載套接字庫 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //①創建套接字 SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(6000); //②向服務器發出連接請求 connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //③和服務器進行通信(send/recv) char recvBuf[100]; recv(sockClient, recvBuf, 100, 0); //接收數據 printf("%s\n", recvBuf); send(sockClient, "this is lisi", strlen("this is lisi")+1, 0); //發送數據 closesocket(sockClient); //④關閉套接字 WSACleanup(); } TCP_Client
服務器端程序:
//UDP_Srv.cpp #include <stdio.h> #include <Winsock2.h> void main() { //加載套接字庫 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //①創建套接字 SOCKET sockSrv = socket( AF_INET, SOCK_DGRAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = htonl(ADDR_ANY); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(6000); //②綁定套接字 bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //③等待並接收數據 (UDP服務器就是一個接收端) SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); char recvBuf[100]; recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len); printf("%s\n", recvBuf); closesocket(sockSrv); //④關閉套接字 WSACleanup(); } UDP_Srv.cpp
客戶端程序:
//UDP_Client.cpp #include<WINSOCK2.H> #include<STDIO.H> void main() { //加載套接字庫 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //創建套接字 SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0); SOCKADDR_IN sockSrv; sockSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); sockSrv.sin_family = AF_INET; sockSrv.sin_port = htons(6000); //發送數據 sendto(sockClient, "hello!", strlen("hello!")+1, 0, (SOCKADDR*)&sockSrv, sizeof(SOCKADDR)); //關閉套接字 closesocket(sockClient); WSACleanup(); } UDP_Client.cpp
2014-08-14
23:56:21
我是初學者,最好能從基礎講起.不知道你有好基礎,基本什麼都不用就用孫鑫的 VC深入詳解 很好的入門書!
是QQ群哦!我加了!