1. 程式人生 > >C++ socket程式設計基礎一(概念和常用API)

C++ socket程式設計基礎一(概念和常用API)

SOCKET一種程序通訊的方式,簡言之就是呼叫這個網路庫的一些API函式就能實現分佈在不同主機的相關程序之間的資料交換.

·SOCKET幾個定義概念

一、是IP地址:

IP Address我想很容易理解,就是依照TCP/IP協議分配給本地主機的網路地址,就向兩個程序要通訊,任一程序要知道通訊對方的位置,位置如何來確定,就用對方的IP

二、是埠號:

用來標識本地通訊程序,方便OS提交資料.就是說程序指定了對方程序的網路IP,但這個IP只是用來標識程序所在的主機,如何來找到執行在這個主機的這個程序呢,就用埠號.

三、是連線:

指兩個程序間的通訊鏈路.

四、是半相關:

網路中用一個三元組可以在全域性唯一標誌一個程序:(協議,本地地址,本地埠號)

這樣一個三元組,叫做一個半相關,它指定連線的每半部分。

五、是全相關:

    一個完整的網間程序通訊需要由兩個程序組成,並且只能使用同一種高層協議。也就是說,不可能通訊的一端用TCP協議,而另一端用UDP協議。因此一個完整的網間通訊需要一個五元組來標識:

    (協議,本地地址,本地埠號,遠地地址,遠地埠號)

    這樣一個五元組,叫做一個相關(association),即兩個協議相同的半相關才能組合成一個合適的相關,或完全指定組成一連線。

六、客戶/伺服器模式

TCP/IP網路應用中,通訊的兩個程序間相互作用的主要模式是客戶/伺服器模 式(Client/Server model),即客戶向伺服器發出服務請求,伺服器接收到請求後,提供相應的服務。

客戶/伺服器模式的建立基於以下兩點:

首先,建立網路的起因是網路中軟 硬體資源、運算能力和資訊不均等,需要共享,從而造就擁有眾多資源的主機提供服務,資源較少的客戶請求服務這一非對等作用。

其次,網間程序通訊完全是非同步的,相互通訊的程序間既不存在父子關係,又不共享記憶體緩衝區,因此需要一種機制為希望通訊的程序間建立聯絡,為二者的資料交換提供同步,這就是客戶/伺服器模式的TCP/IP

    客戶/伺服器模式操作過程中採取的是主動請求方式

    一、首先伺服器方要先啟動,並根據請求提供相應服務:

    1. 開啟一通訊通道並告知本地主機,它願意在某一公認地址上(周知口,如FTP21)接收客戶請求;

    2. 等待客戶請求到達該埠;

    3. 接收到重複服務請求,處理該請求併發送應答訊號。接收到併發服務請求,要啟用一新程序來處理這個客戶請求(如UNIX系統中用forkexec)。新程序處理此客戶請求,並不需要對其它請求作出應答。服務完成後,關閉此新程序與客戶的通訊鏈路,並終止。

    4. 返回第二步,等待另一客戶請求。

    5. 關閉伺服器

  二、客戶方:

    1. 開啟一通訊通道,並連線到伺服器所在主機的特定埠;

    2. 向伺服器發服務請求報文,等待並接收應答;繼續提出請求......

3. 請求結束後關閉通訊通道並終止。

從上面所描述過程可知:

    1. 客戶與伺服器程序的作用是非對稱的,因此編碼不同。

    2. 服務程序一般是先於客戶請求而啟動的。只要系統執行,該服務程序一直存在,直到正常或強迫終止。

·API函式:

1、建立套接字──socket()

應用程式在使用套接字前,首先必須擁有一個套接字,系統呼叫socket()嚮應用程式提供建立套接字的手段,其呼叫格式如下:

    SOCKET PASCAL FAR socket(
int af, 
int type, 
int protocol);


該呼叫要接收三個引數:aftypeprotocol

引數af指定通訊發生的區域:AF_UNIXAF_INETAF_NS等,而DOS、 WINDOWS中僅支援AF_INET,它是網際網區域。因此,地址族與協議族相同。    

引數type 描述要建立的套接字的型別。這裡分三種:

一是TCP流式套接字(SOCK_STREAM)提供了一個面向連線、可靠的資料傳輸服務,資料無差錯、無重複地 傳送,且按傳送順序接收。內設流量控制,避免資料流超限;資料被看作是位元組流,無長度限制。檔案傳送協議(FTP)即使用流式套接字。

二是資料報式套接字(SOCK_DGRAM)提供了一個無連線服務。資料包以獨立包形式被髮送,不提供無錯保證,資料可能丟失或重複,並且接收順序混亂。 網路檔案系統(NFS)使用資料報式套接字。

三是原始式套接字 (SOCK_RAW)該介面允許對較低層協議,如IPICMP直接訪問。常用於檢驗新的協議實現或訪問現有服務中配置的新裝置.

引數protocol說 明該套接字使用的特定協議,如果呼叫者不希望特別指定使用的協議,則置為0,使用預設的連線模式。根據這三個引數建立一個套接字,並將相應的資源分配給它,同時返回一個整型套接字號。因此,socket()系統呼叫實際上指定了相關五元組中的“協議”這一元。

2、指定本地地址──bind()

當一個套接字用socket()建立後,存在一個名字空間(地址族),但它沒有被命名。bind()將套接字地址(包括本地主機地址和本地埠地址)與所建立的套接字號聯絡起來,即將名字賦予套接字,以指定本地半相關。其呼叫格式如下:

    int PASCAL FAR bind(
SOCKET s, 
const struct sockaddr FAR * nam e,
  int namelen);


引數s是由socket()呼叫返回的並且未作連線的套接字描述符(套接字號)

引數name 是賦給套接字s的本地地址(名字),其長度可變,結構隨通訊域的不同而不同。

namelen表明了name的長度.如果沒有錯誤發生,bind()返回 0。否則返回SOCKET_ERROR

3、建立套接字連線──connect()accept()

這兩個系統呼叫用於完成一個完整相關的建立,其中connect()用於建立連線。無連線的套接字程序也可以呼叫connect(),但這時在程序之間沒有實際的報文交換,呼叫將從本地作業系統直接返回。這樣做的優點是程式設計師不必為每一資料指定目的地址,而且如果收到的一個數據報,其目的埠未與任何套接 字建立“連線”,便能判斷該端靠紀紀可操作。accept()用於使伺服器等待來自某客戶程序的實際連線。

    connect()的呼叫格式如下:

    int PASCAL FAR connect(
	SOCKET s, 
	const struct sockaddr FAR * name,
  	int namelen);


引數s是欲建立連線的本地套接字描述符。

引數name指出說明對方套接字地址結構的指標。對方套接字地址長度由namelen說明。

    如果沒有錯誤發生,connect()返回0。否則返回值SOCKET_ERROR。在面向連線的協議中,該呼叫導致本地系統和外部系統之間連線實際建立。

    由於地址族總被包含在套接字地址結構的前兩個位元組中,並通過socket()呼叫與某個協議族相關。因此bind()connect()無須協議作為引數。

    accept()的呼叫格式如下:

    SOCKET PASCAL FAR accept(
	SOCKET s, 
	struct sockaddr FAR* addr, 
	int FAR* addrlen);


引數s為本地套接字描述符,在用做accept()呼叫的引數前應該先呼叫過listen()

addr 指向客戶方套接字地址結構的指標,用來接收連線實體的地址。addr的確切格式由套接字建立時建立的地址族決定。addrlen 為客戶方套接字地址的長度(位元組數)。如果沒有錯誤發生,accept()返回一個SOCKET型別的值,表示接收到的套接字的描述符。否則返回值 INVALID_SOCKET

    accept() 用於面向連線伺服器。引數addraddrlen存放客戶方的地址資訊。呼叫前,引數addr 指向一個初始值為空的地址結構,而addrlen 的初始值為0;呼叫accept()後,伺服器等待從編號為s的套接字上接受客戶連線請求,而連線請求是由客戶方的connect()呼叫發出的。當有連 接請求到達時,accept()呼叫將請求連線佇列上的第一個客戶方套接字地址及長度放入addr addrlen,並建立一個與s有相同特性的新套接字號。新的套接字可用於處理伺服器併發請求。

四個套接字系統呼叫,socket()bind()、 connect()accept(),可以完成一個完全五元相關的建立。

socket()指定五元組中的協議元,它的用法與是否為客戶或伺服器、是否面 向連線無關。

bind()指定五元組中的本地二元,即本地主機地址和埠號,其用法與是否面向連線有關:在伺服器方,無論是否面向連線,均要呼叫 bind(),若採用面向連線,則可以不呼叫bind()

    而通過connect()自動完成。若採用無連線,客戶方必須使用bind()以獲得一個唯一 的地址。

4、監聽連線──listen()

此呼叫用於面向連線伺服器,表明它願意接收連線。listen()需在accept()之前呼叫,其呼叫格式如下:

    int PASCAL FAR listen(
	SOCKET s, 
	int backlog);


引數s標識一個本地已建立、尚未連線的套接字號,伺服器願意從它上面接收請求。

backlog表示請求連線佇列的最大長度,用於限制排隊請求的個數,目前允許的最大值為5。如果沒有錯誤發生,listen()返回0。否則它返回SOCKET_ERROR

    listen()在執行呼叫過程中可為沒有呼叫過bind()的套接字s完成所必須的連線,並建立長度為backlog的請求連線佇列。

呼叫listen()是伺服器接收一個連線請求的四個步驟中的第三步。它在呼叫socket()分配一個流套接字,且呼叫bind()s賦於一個名字之後呼叫,而且一定要在accept()之前呼叫。

5、資料傳輸──send()recv()

當一個連線建立以後,就可以傳輸資料了。常用的系統呼叫有send()recv()

    1send()呼叫用於鑰紀紀數s指定的已連線的資料報或流套接字上傳送輸出資料,格式如下:

    int PASCAL FAR send(
	SOCKET s,
  	const char FAR *buf, 
	int len, 
	int flags);


引數s為已連線的本地套接字描述符。

buf 指向存有傳送資料的緩衝區的指標,其長度由len 指定。

flags 指定傳輸控制方式,如是否傳送帶外資料等。

如果沒有錯誤發生,send()返回總共傳送的位元組數。否則它返回SOCKET_ERROR

2recv()呼叫用於s指定的已連線的資料報或流套接字上接收輸入資料,格式如下:

    int PASCAL FAR recv(
	SOCKET s, 
	char FAR *buf, 
	int len, 
	int flags);


引數為已連線的套接字描述符。

buf指向接收輸入資料緩衝區的指標,其長度由len 指定。F

lags 指定傳輸控制方式,如是否接收帶外資料等。

如果沒有錯誤發生,recv()返回總共接收的位元組數。如果連線被關閉,返回0。否則它返回 SOCKET_ERROR

6、輸入/輸出多路複用──select()

select() 呼叫用來檢測一個或多個套接字的狀態。對每一個套接字來說,這個呼叫可以請求讀、寫或錯誤狀態方面的資訊。請求給定狀態的套接字集合由一個fd_set結 構指示。在返回時,此結構被更新,以反映那些滿足特定條件的套接字的子集,同時, select()呼叫返回滿足條件的套接字的數目,其呼叫格式如下:

int PASCAL FAR select(
int nfds, 
fd_set FAR * readfds, 
fd_set FAR * writefds, 
fd_set FAR * exceptfds,
 const struct timeval FAR * timeout);

       引數nfds指明被檢查的套接字描述符的值域,此變數一般被忽略。

       引數readfds指向要做讀檢測的套接字描述符集合的指標,呼叫者希望從中讀取資料。

    引數writefds 指向要做寫檢測的套接字描述符集合的指標。 

exceptfds指向要檢測是否出錯的套接字描述符集合的指標。

timeout指向select()函式等待的最大時間,如果設為NULL則為阻塞操 作。

select()返回包含在fd_set結構中已準備好的套接字描述符的總數目,或者是發生錯誤則返回SOCKET_ERROR

7、關閉套接字──closesocket()

closesocket()關閉套接字s,並釋放分配給該套接字的資源;如果s涉及一個開啟的TCP連線,則該連線被釋放。closesocket()的呼叫格式如下:

BOOL PASCAL FAR closesocket(SOCKET s);

        引數s待關閉的套接字描述符。如果沒有錯誤發生,closesocket()返回0。否則返回值SOCKET_ERROR

以下是實現一個簡單的客戶端服務端連結的例子:

server端:  
    
#include <WINSOCK2.H>  
#include <stdio.h>  
#pragma comment(lib,"ws2_32.lib")  
void main()  
{  
 //建立套接字  
 WORD myVersionRequest;  
 WSADATA wsaData;  
 myVersionRequest=MAKEWORD(1,1);  
 int err;  
 err=WSAStartup(myVersionRequest,&wsaData);  
 if (!err)  
 {  
  printf("已開啟套接字\n");  
 }   
 else  
 {  
  //進一步繫結套接字  
  printf("巢狀字未開啟!");  
  return;  
 }  
 SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//建立了可識別套接字  
 //需要繫結的引數  
 SOCKADDR_IN addr;  
 addr.sin_family=AF_INET;  
 addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址  
 addr.sin_port=htons(6000);//繫結埠  
    
 bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//繫結完成  
 listen(serSocket,5);//其中第二個引數代表能夠接收的最多的連線數  
    
 //////////////////////////////////////////////////////////////////////////  
 //開始進行監聽  
 //////////////////////////////////////////////////////////////////////////  
 SOCKADDR_IN clientsocket;  
 int len=sizeof(SOCKADDR);  
 while (1)  
 {  
  SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);//如果這裡不是accept而是conection的話。。就會不斷的監聽  
  char sendBuf[100];  
     
  sprintf(sendBuf,"welcome %s to bejing",inet_ntoa(clientsocket.sin_addr));//找對對應的IP並且將這行字列印到那裡  
  send(serConn,sendBuf,strlen(sendBuf)+1,0);  
  char receiveBuf[100];//接收  
  recv(serConn,receiveBuf,strlen(receiveBuf)+1,0);  
  printf("%s\n",receiveBuf);  
  closesocket(serConn);//關閉  
 WSACleanup();//釋放資源的操作  
 }  
}  
    
    
    
client端:  
    
   
#include <WINSOCK2.H>  
#include <stdio.h>  
#pragma comment(lib,"ws2_32.lib")  
void main()  
{  
 int err;  
 WORD versionRequired;  
 WSADATA wsaData;  
 versionRequired=MAKEWORD(1,1);  
 err=WSAStartup(versionRequired,&wsaData);//協議庫的版本資訊  
 if (!err)  
 {  
  printf("客戶端巢狀字已經開啟!\n");  
 }  
 else  
 {  
  printf("客戶端的巢狀字開啟失敗!\n");  
  return;//結束  
 }  
 SOCKET clientSocket=socket(AF_INET,SOCK_STREAM,0);  
 SOCKADDR_IN clientsock_in;  
 clientsock_in.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");  
 clientsock_in.sin_family=AF_INET;  
 clientsock_in.sin_port=htons(6000);  
 //bind(clientSocket,(SOCKADDR*)&clientsock_in,strlen(SOCKADDR));//注意第三個引數  
 //listen(clientSocket,5);  
 connect(clientSocket,(SOCKADDR*)&clientsock_in,sizeof(SOCKADDR));//開始連線  
 char receiveBuf[100];  
 recv(clientSocket,receiveBuf,101,0);  
 printf("%s\n",receiveBuf);  
 send(clientSocket,"hello,this is client",strlen("hello,this is client")+1,0);  
 closesocket(clientSocket);  
 WSACleanup();  
}