目錄
TCP/IP協議介紹
TCP/IP協議與WinSock網絡編程接口的關係
WinSock編程簡單流程
VC中socket編程
·服務器實現
·客戶端實現
測試結果
TCP/IP協議介紹
TCP/IP協議包含的範圍非常的廣,它是一種四層協議,包含了各種硬件、軟件需求的定義。TCP/IP協議確切的說法應該是TCP/UDP/IP協議。UDP協議(User Datagram Protocol 用戶數據報協議),是一種保護消息邊界的,不保障可靠數據的傳輸。TCP協議(Transmission Control Protocol 傳輸控制協議),是一種流傳輸的協議。他提供可靠的、有序的、雙向的、面向連接的傳輸。
保護消息邊界,就是指傳輸協議把數據當作一條獨立的消息在網上傳輸,接收端只能接收獨立的消息。也就是說存在保護消息邊界,接收端一次只能接收發送端發出的一個數據包。
而面向流則是指無保護消息邊界的,如果發送端連續發送數據,接收端有可能在一次接收動作中,會接收兩個或者更多的數據包。
舉例來說,假如,我們連續發送三個數據包,大小分別是2k、4k、8k,這三個數據包都已經到達了接收端的網絡堆棧中,如果使用UDP協議,不管我們使用多大的接收緩衝區去接收數據,我們必須有三次接收動作,才能夠把所有的數據包接收完。而使用TCP協議,我們只要把接收的緩衝區大小設置在14k以上,我們就能夠一次把所有的數據包接收下來,只需要有一次接收動作。
這就是因爲UDP協議的保護消息邊界使得每一個消息都是獨立的。而流傳輸,卻把數據當作一串數據流,它不認爲數據是一個一個的消息。所以有很多人在使用TCP協議通訊的時候,並不清楚TCP是基於流的傳輸,當連續發送數據的時候,他們時常會認爲TCP會丟包。其實不然,因爲當它們使用的緩衝區足夠大時,它們有可能會一次接收到兩個甚至更多的數據包,而很多人往往會忽視這一點,只解析檢查了第一個數據包,而已經接收的其它據包卻被忽略了。
TCP/IP協議與WinSock網絡編程接口的關係
WinSock 並不是一種網絡協議,它只是一個網絡編程接口,也就是說,它不是協議,但是它可以訪問很多種網絡協議,你可以把它當作一些協議的封裝。現在的 WinSock已經基本上實現了與協議無關。你可以使用WinSock來調用多種協議的功能。那麼,WinSock和TCP/IP協議到底是什麼關係呢?實際上,WinSock就是TCP/IP協議的一種封裝,你可以通過調用WinSock的接口函數來調用TCP/IP的各種功能.例如我想用TCP/IP 協議發送數據,你就可以使用WinSock的接口函數Send()來調用TCP/IP的發送數據功能,至於具體怎麼發送數據,WinSock已經幫你封裝好了這種功能。
WinSock編程簡單流程
WinSock編程分爲服務器端和客戶端兩部分,TCP服務器端的大體流程如下:
對於任何基於WinSock的編程首先必須要初始化WinSock DLL庫。
int WSAStarup( WORD wVersionRequested,LPWSADATA lpWsAData )。
wVersionRequested是我們要求使用的WinSock的版本。
調用這個接口函數可以初始化WinSock 。
然後必須創建一個套接字(Socket)。
SOCKET Socket(int af,int type,int protocol);
套接字可以說是WinSock通訊的核心。WinSock通訊的所有數據傳輸,都是通過套接字來完成的,套接字包含了兩個信息,一個是IP地址,一個是Port端口號,使用這兩個信息,就可以確定網絡中的任何一個通訊節點。
當調用了Socket()接口函數創建了一個套接字後,必須把套接字與你需要進行通訊的地址建立聯繫,可以通過綁定函數bind來實現這種聯繫。
int bind(SOCKET s,const struct sockaddr FAR* name,int namelen) ;
struct sockaddr_in{
short sin_family ;
u_short sin_port;
struct in_addr sin_addr ;
char sin_sero[8] ;
}
就包含了需要建立連接的本地的地址,包括地址族、IP和端口信息。sin_family字段必須把它設爲AF_INET,這是告訴WinSock使用的是IP地址族。sin_port就是要用來通訊的端口號。sin_addr就是要用來通訊的IP地址信息。
在這裏,必須還得提一下有關'大頭(big-endian)'小頭(little-endian)'。因爲各種不同的計算機處理數據時的方法是不一樣的,Intel X86處理器上是用'小頭'形式來表示多字節的編號,就是把低字節放在前面,把高字節放在後面,而互聯網標準卻正好相反,所以,必須把主機字節轉換成網絡字節的順序。WinSock API提供了幾個函數。
把主機字節轉化成網絡字節的函數;
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
把網絡字節轉化成主機字節的函數;
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort) ;
這樣,設置IP地址和port端口時,就必須把主機字節轉化成網絡字節後,才能用Bind()函數來綁定套接字和地址。
當綁定完成之後,服務器端必須建立一個監聽的隊列來接收客戶端的連接請求。
int listen(SOCKET s,int backlog);
這個函數可以把套接字轉成監聽模式。
如果客戶端有了連接請求,我們還必須使用
int accept(SOCKET s,struct sockaddr FAR* addr,int FAR* addrlen);
來接受客戶端的請求。
現在基本上已經完成了一個服務器的建立,而客戶端的建立的流程則是初始化WinSock,然後創建Socket套接字,再使用
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen) ;
來連接服務端。
下面是一個最簡單的創建服務器端和客戶端的例子:
服務器端的創建:
WSADATA wsd;
SOCKET sListen;
SOCKET sclient;
UINT port = 800;
int iAddrSize;
struct sockaddr_in local , client;
WSAStartup( 0x11 , &wsd );
sListen = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP );
local.sin_family = AF_INET;
local.sin_addr = htonl( INADDR_ANY );
local.sin_port = htons( port );
bind( sListen , (struct sockaddr*)&local , sizeof( local ) );
listen( sListen , 5 );
sClient = accept( sListen , (struct sockaddr*)&client , &iAddrSize );
客戶端的創建:
WSADATA wsd;
SOCKET sClient;
UINT port = 800;
char szIp[] = "127.0.0.1";
int iAddrSize;
struct sockaddr_in server;
WSAStartup( 0x11 , &wsd );
sClient = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP );
server.sin_family = AF_INET;
server.sin_addr = inet_addr( szIp );
server.sin_port = htons( port );
connect( sClient , (struct sockaddr*)&server , sizeof( server ) );
當服務器端和客戶端建立連接以後,無論是客戶端,還是服務器端都可以使用
int send( SOCKET s,const char FAR* buf,int len,int flags);
int recv( SOCKET s,char FAR* buf,int len,int flags);
函數來接收和發送數據,因爲,TCP連接是雙向的。
當要關閉通訊連接的時候,任何一方都可以調用
int shutdown(SOCKET s,int how);
來關閉套接字的指定功能,再調用
int closeSocket(SOCKET s) ;
來關閉套接字句柄,這樣一個通訊過程就算完成了。
可以參考教材計算機網絡(第6版)295頁圖6-32所示的系統調用使用順序:
注意:上面的代碼沒有任何檢查函數返回值,如果你作網絡編程就一定要檢查任何一個WinSock API函數的調用結果,因爲很多時候函數調用並不一定成功。上面介紹的函數,返回值類型是int的話,如果函數調用失敗的話,返回的都是SOCKET_ERROR。
VC中socket編程
·服務器實現
服務器端編程的步驟:
1:加載套接字庫,創建套接字(WSAStartup()/socket());
2:綁定套接字到一個IP地址和一個端口上(bind());
3:將套接字設置爲監聽模式等待連接請求(listen());
4:請求到來後,接受連接請求,返回一個新的對應於此次連接的套接字(accept());
5:用返回的套接字和客戶端進行通信(send()/recv());
6:返回,等待另一連接請求;
7:關閉套接字,關閉加載的套接字庫(closesocket()/WSACleanup())。
服務器端代碼如下:
#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_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[50];
sprintf(sendBuf,"Welcome %s to here!",inet_ntoa(addrClient.sin_addr));
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[50];
recv(sockConn,recvBuf,50,0);
printf("%s\n",recvBuf);
closesocket(sockConn);
}
}
·客戶端實現
客戶端編程的步驟:
1:加載套接字庫,創建套接字(WSAStartup()/socket());
2:向服務器發出連接請求(connect());
3:和服務器端進行通信(send()/recv());
4:關閉套接字,關閉加載的套接字庫(closesocket()/WSACleanup())。
客戶端的代碼如下:
#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(sockClient,"hello",strlen("hello")+1,0);
char recvBuf[50];
recv(sockClient,recvBuf,50,0);
printf("%s\n",recvBuf);
closesocket(sockClient);
WSACleanup();
}
測試結果