IP地址
IP網絡中每臺主機都必須有一個惟一的IP地址; IP地址是一個邏輯地址; 因特網上的IP地址具有全球唯一性; 32位,4個字節,常用點分十進制的格式表示,例如:192.168.0.16協議
爲進行網絡中的數據交換(通信)而建立的規則、標準或約定。(=語義+語法+規則) 不同層具有各自不同的協議。網絡異質性問題的解決
網絡體系結構就是使這些用不同媒介連接起來的不同設備和網絡系統在不同的應用環境下實現互操作性,並滿足各種業務需求的一種粘合劑,它營造了一種“生存空間” —— 任何廠商的任何產品、以及任何技術只要遵守這個空間的行爲規則,就能夠在其中生存並發展。 網絡體系結構解決異質性問題採用的是分層方法 —— 把複雜的網絡互聯問題劃分爲若干個較小的、單一的問題,在不同層上予以解決。就像我們在編程時把問題分解爲很多小的模塊來解決一樣。
ISO/OSI七層參考模型
通信實體的對等層之間不允許直接通信。
各層之間是嚴格單向依賴。 上層使用下層提供的服務 — Service user; 下層向上層提供服務 — Service provider。對等層通信的實質
對等層實體之間虛擬通信。 下層向上層提供服務,實際通信在最底層完成。OSI各層所使用的協議
應用層:遠程登錄協議Telnet、文件傳輸協議FTP、 超文本傳輸協議HTTP、域名服務DNS、簡單郵件傳輸協議SMTP、郵局協議POP3等。 傳輸層:傳輸控制協議TCP、用戶數據報協議UDP。TCP:面向連接的可靠的傳輸協議。
UDP:是無連接的,不可靠的傳輸協議。
網絡層:網際協議IP、Internet互聯網控制報文協議ICMP、Internet組管理協議IGMP。數據封裝
一臺計算機要發送數據到另一臺計算機,數據首先必須打包,打包的過程稱爲封裝。 封裝就是在數據前面加上特定的協議頭部。 OSI參考模型中,對等層協議之間交換的信息單元統稱爲協議數據單元(PDU,Protocol Data Unit)。 OSI參考模型中每一層都要依靠下一層提供的服務。 爲了提供服務,下層把上層的PDU作爲本層的數據封裝,然後加入本層的頭部(和尾部)。頭部中含有完成數據傳輸所需的控制信息。 這樣,數據自上而下遞交的過程實際上就是不斷封裝的過程。到達目的地後自下而上遞交的過程就是不斷拆封的過程。由此可知,在物理線路上傳輸的數據,其外面實際上被包封了多層“信封”。但是,某一層只能識別由對等層封裝的“信封”,而對於被封裝在“信封”內部的數據僅僅是拆封後將其提交給上層,本層不作任何處理。
TCP/IP模型
TCP/IP起源於美國國防部高級研究規劃署(DARPA)的一項研究計劃——實現若干臺主機的相互通信。 現在TCP/IP已成爲Internet上通信的工業標準。 TCP/IP模型包括4個層次:- 應用層
- 傳輸層
- 網絡層
- 網絡接口
TCP/IP與OSI參考模型的對應關係
OSI參考模型(7) |
|
TCP/IP模型(4) |
端口
按照OSI七層模型的描述,傳輸層提供進程(應用程序)通信的能力。爲了標識通信實體中進行通信的進程(應用程序),TCP/IP協議提出了協議端口(protocol port,簡稱端口)的概念。 端口是一種抽象的軟件結構(包括一些數據結構和I/O緩衝區)。應用程序通過系統調用與某端口建立連接(binding)後,傳輸層傳給該端口的數據都被相應的進程所接收,相應進程發給傳輸層的數據都通過該端口輸出。 端口用一個整數型標識符來表示,即端口號。端口號跟協議相關,TCP/IP傳輸層的兩個協議TCP和UDP是完全獨立的兩個軟件模塊,因此各自的端口號也相互獨立。 端口使用一個16位的數字來表示,它的範圍是0~65535,1024以下的端口號保留給預定義的服務。例如:http使用80端口。套接字(socket)的引入
網絡字節順序
不同的計算機存放多字節值的順序不同,有的機器在起始地址存放低位字節(低位先存),有的機器在起始地址存放高位字節(高位先存)。基於Intel的CPU,即我們常用的PC機採用的是低位先存。爲保證數據的正確性,在網絡協議中需要指定網絡字節順序。TCP/IP協議使用16位整數和32位整數的高位先存格式。
客戶機/服務器模式
在TCP/IP網絡應用中,通信的兩個進程間相互作用的主要模式是客戶機/服務器模式(client/server),即客戶向服務器提出請求,服務器接收到請求後,提供相應的服務。 客戶機/服務器模式的建立基於以下兩點:首先,建立網絡的起因是網絡中軟硬件資源、運算能力和信息不均等,需要共享,從而造就擁有衆多資源的主機提供服務,資源較少的客戶請求服務這一非對等作用。其次,網間進程通信完全是異步的,相互通信的進程間既不存在父子關係,又不共享內存緩衝區,因此需要一種機制爲希望通信的進程間建立聯繫,爲二者的數據交換提供同步,這就是基於客戶機/服務器模式的TCP/IP。 客戶機/服務器模式在操作過程中採取的是主動請求的方式。首先服務器方要先啓動,並根據請求提供相應的服務:
①打開一個通信通道並告知本地主機,它願意在某一地址和端口上接收客戶請求。
②等待客戶請求到達該端口。
③接收到重複服務請求,處理該請求併發送應答信號。接收到併發服務請求,要激活一個新的進程(或線程)來處理這個客戶請求。新進程(或線程) 處理此客戶請求,並不需要對其它請求作出應答。服務完成後,關閉此新進程與客戶的通信鏈路,並終止。
④返回第二步,等待另一客戶請求。
⑤關閉服務器。
客戶方:
①打開一個通信通道,並連接到服務器所在主機的特定端口。
②向服務器發服務請求報文,等待並接收應答;繼續提出請求。
③請求結束後關閉通信通道並終止。
Windows Sockets的實現
Windows Sockets是Microsoft Windows的網絡程序設計接口,它是從Berkeley Sockets擴展而來的,以動態鏈接庫的形式提供給我們使用。Windows Sockets在繼承了Berkeley Sockets主要特徵的基礎上,又對它進行了重要擴充。這些擴充主要是提供了一些異步函數,並增加了符合Windows消息驅動特性的網絡事件異步選擇機制。 Windows Sockets 1.1和Berkeley Sockets都是基於TCP/IP協議的;Windows Sockets 2從Windows Sockets 1.1發展而來,與協議無關並向下兼容,可以使用任何底層傳輸協議提供的通信能力,來爲上層應用程序完成網絡數據通訊,而不關心底層網絡鏈路的通訊情況,真正實現了底層網絡通訊對應用程序的透明。
套接字的類型
流式套接字(SOCK_STREAM)提供面向連接、可靠的數據傳輸服務,數據無差錯、無重複的發送,且按發送順序接收。基於TCP實現
數據報式套接字(SOCK_DGRAM)提供無連接服務。數據包以獨立包形式發送,不提供無錯保證,數據可能丟失或重複,並且接收順序混亂。基於UDP實現
原始套接字(SOCK_RAW)。
基於TCP(面向連接)的socket編程
服務器端程序:
1、創建套接字(socket)。
2、將套接字綁定到一個本地地址和端口上(bind)。
3、將套接字設爲監聽模式,準備接收客戶請求(listen)。
4、等待客戶請求到來;當請求到來後,接受連接請求,返回一個新的對應於此次連接的套接字(accept)。
5、用返回的套接字和客戶端進行通信(send/recv)。
已經保存了客戶端信息
6、返回,等待另一客戶請求。
7、關閉套接字。
客戶端程序:
1、創建套接字(socket)。
2、向服務器發出連接請求(connect)。
3、和服務器端進行通信(send/recv)。
4、關閉套接字。
不需要調用bind函數
基於UDP(面向無連接)的socket編程
服務器端(接收端)程序:
1、創建套接字(socket)。
2、將套接字綁定到一個本地地址和端口上(bind)。
3、等待接收數據(recvfrom)。
4、關閉套接字。
客戶端(發送端)程序:
1、創建套接字(socket)。
2、向服務器發送數據(sendto)。
3、關閉套接字。
相關函數說明
int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData );
wVersionRequested參數用於指定準備加載的Winsock庫的版本。高位字節指定所需要的Winsock庫的副版本,而低位字節則是主版本。可用MAKEWORD(x,y)(其中,x是高位字節,y是低位字節)方便地獲得wVersionRequested的正確值。 lpWSAData參數是指向WSADATA結構的指針,WSAStartup用其加載的庫版本有關的信息填在這個結構中。WSADATA結構定義如下:
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;
WSAStartup把第一個字段wVersion設成打算使用的Winsock版本。wHighVersion 參數容納的是現有的Winsock庫的最高版本。記住,這兩個字段中,高位字節代表的是Winsock副版本,而低位字節代表的則是Winsock主版本。szDescription和szSystemStatus這兩個字段由特定的Winsock實施方案設定,事實上沒有用。不要使用下面這兩個字段:iMaxSockets和iMaxUdpDg,它們是假定同時最多可打開多少套接字和數據報的最大長度。然而,要知道數據報的最大長度應該通過WSAEnumProtocols來查詢協議信息。同時最多可打開套接字的數目不是固定的,很大程度上和可用物理內存的多少有關。最後,lpVendorInfo字段是爲Winsock實施方案有關的指定廠商信息預留的。任何一個Win32平臺上都沒有使用這個字段。
如果WinSock.dll或底層網絡子系統沒有被正確初始化或沒有被找到,WSAStartup將返回WSASYSNOTREADY。此外這個函數允許你的應用程序協商使用某種版本的WinSock規範,如果請求的版本等於或高於DLL所支持的最低版本,WSAData的wVersion成員中將包含你的應用程序應該使用的版本,它是DLL所支持的最高版本與請求版本中較小的那個。反之,如果請求的版本低於DLL所支持的最低版本,WSAStartup將返回WSAVERNOTSUPPORTED。關於WSAStartup更詳細的信息,請查閱MSDN中的相關部分。
對於每一個WSAStartup的成功調用(成功加載WinSock DLL後),在最後都對應一個WSACleanUp調用,以便釋放爲該應用程序分配的資源。
SOCKET socket( int af, int type, int protocol );
該函數接收三個參數。第一個參數af指定地址族,對於TCP/IP協議的套接字,它只能是AF_INET(也可寫成PF_INET)。第二個參數指定Socket類型,對於1.1版本的Socket,它只支持兩種類型的套接字,SOCK_STREAM指定產生流式套接字,SOCK_DGRAM產生數據報套接字。第三個參數是與特定的地址家族相關的協議,如果指定爲0,那麼它就會根據地址格式和套接字類別,自動爲你選擇一個合適的協議。這是推薦使用的一種選擇協議的方法。 如果這個函數調用成功,它將返回一個新的SOCKET數據類型的套接字描述符。如果調用失敗,這個函數就會返回一個INVALID_SOCKET,錯誤信息可以通過WSAGetLastError函數返回。
int bind( SOCKET s, const struct sockaddr FAR *name, int namelen );
這個函數接收三個參數。第一個參數s指定要綁定的套接字,第二個參數指定了該套接字的本地地址信息,是指向sockaddr結構的指針變量,由於該地址結構是爲所有的地址家族準備的,這個結構可能(通常會)隨所使用的網絡協議不同而不同,所以,要用第三個參數指定該地址結構的長度。 sockaddr結構定義如下:struct sockaddr {
u_short sa_family;
char sa_data[14];
};
sockaddr的第一個字段sa_family指定該地址家族,在這裏必須設爲AF_INET。sa_data僅僅是表示要求一塊內存分配區,起到佔位的作用,該區域中指定與協議相關的具體地址信息。由於實際要求的只是內存區,所以對於不同的協議家族,用不同的結構來替換sockaddr。除了sa_family外,sockaddr是按網絡字節順序表示的。在TCP/IP中,我們可以用sockaddr_in結構替換sockaddr,以方便我們填寫地址信息。sockaddr_in的定義如下:
struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中,sin_family表示地址族,對於IP地址,sin_family成員將一直是AF_INET。成員sin_port指定的是將要分配給套接字的端口。成員sin_addr給出的是套接字的主機IP地址。而成員sin_zero只是一個填充數,以使sockaddr_in結構和sockaddr結構的長度一樣。如果這個函數調用成功,它將返回0。如果調用失敗,這個函數就會返回一個SOCKET_ERROR,錯誤信息可以通過WSAGetLastError函數返回。
將IP地址指定爲INADDR_ANY,允許套接字向任何分配給本地機器的IP地址發送或接收數據。多數情況下,每個機器只有一個IP地址,但有的機器可能會有多個網卡,每個網卡都可以有自己的IP地址,用INADDR_ANY可以簡化應用程序的編寫。將地址指定爲INADDR_ANY,允許一個獨立應用接受發自多個接口的迴應。如果我們只想讓套接字使用多個IP中的一個地址,就必須指定實際地址,要做到這一點,可以用inet_addr()函數,這個函數需要一個字符串作爲其參數,該字符串指定了以點分十進制格式表示的IP地址(如192.168.0.16)。而且inet_addr()函數會返回一個適合分配給S_addr的u_long類型的數值。inet_ntoa()函數會完成相反的轉換,它接受一個in_addr結構體類型的參數並返回一個以點分十進制格式表示的IP地址字符串。
The htonl function converts a u_long from host to TCP/IP network byte order (which is big endian).
u_long htonl(
u_long hostlong
);
The htons function converts a u_short from host to TCP/IP network byte order (which is big-endian).
u_short htons(
u_short hostshort
);
基於TCP服務器端程序
經過3步握手建立連接
#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 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,// 監聽狀態的Socket
(SOCKADDR*)&addrClient,
&len);
// 開始通信
char sendBuf[100];
sprintf(sendBuf,"Welcome %s to http://blog.csdn.net/persuper",
inet_ntoa(addrClient.sin_addr));
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[100];
recv(sockConn,recvBuf,100,0);
printf("%s/n",recvBuf);
closesocket(sockConn);
}
}
在工程設置中加入ws2_32.lib
基於TCP客戶端程序
#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_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));
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();
}
基於UDP服務器端程序(接收端)
不需要監聽和連接步驟,綁定後即可接收數據了
#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 sockSrv=socket(AF_INET,SOCK_DGRAM,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));
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客戶端程序(發送端)
#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 addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
sendto(sockClient,"Hello",strlen("Hello")+1,0,
(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
closesocket(sockClient);
WSACleanup();
}
基於TCP和UDP的發送和接受函數不同
基於字符界面的聊天程序
服務器端程序
#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 sockSrv=socket(AF_INET,SOCK_DGRAM,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));
char recvBuf[100];
char sendBuf[100];
char tempBuf[200];
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);
while(1)
{
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);
if('q'==recvBuf[0])
{
sendto(sockSrv,"q",strlen("q")+1,0,(SOCKADDR*)&addrClient,len);
printf("Chat end!/n");
break;
}
sprintf(tempBuf,"%s say : %s",inet_ntoa(addrClient.sin_addr),recvBuf);
printf("%s/n",tempBuf);
printf("Please input data:/n");
gets(sendBuf); // 等待用戶輸入,從標準輸入流中獲取一行數據,一般回車爲界
sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrClient,len);
}
closesocket(sockSrv);
WSACleanup();
}
客戶端聊天程序
#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 addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
char recvBuf[100];
char sendBuf[100];
char tempBuf[200];
int len=sizeof(SOCKADDR);
while(1) // 死循環
{
printf("Please input data:/n");
gets(sendBuf);
sendto(sockClient,sendBuf,strlen(sendBuf)+1,0,
(SOCKADDR*)&addrSrv,len);
recvfrom(sockClient,recvBuf,100,0,(SOCKADDR*)&addrSrv,&len);
if('q'==recvBuf[0])
{
sendto(sockClient,"q",strlen("q")+1,0,
(SOCKADDR*)&addrSrv,len);
printf("Chat end!/n");
break;
}
sprintf(tempBuf,"%s say : %s",inet_ntoa(addrSrv.sin_addr),recvBuf);
printf("%s/n",tempBuf);
}
closesocket(sockClient);
WSACleanup();
}