VC++深入詳解(12):網路程式設計
這一小節介紹網路程式設計。首先我們介紹一下計算機網路的基本知識,然後著重介紹一下Windows Socket程式的編寫。
首先,介紹幾個基本概念。什麼是計算機網路?它是相互連線的獨立自主的計算機的集合。它們是如何通訊的呢,需要一個東西來表明我要跟哪個計算機進行通訊,在網路上,為每個計算機分配了一個“IP”地址,通過地址來找到想要通訊的計算機。具體的通訊是計算機的某個程式實現的,一臺計算機可能同時有多個程式在使用網路。為了區分它們,為每個程式提供了一個“埠號”來標識自己。具體通訊時所傳送的內容,成為協議,它規定了我們傳送的格式:除了傳送的內容以外,還包括這些東西是誰傳送的,要發給誰,總共有多長等等。而資訊具體是如何從一臺主機,傳送到另一臺主機,則會有很大的不確定性:
不同的通訊媒介:是通過有線傳輸的,還是無線網路?
不同的作業系統:Unix、Windows
不同的應用環境:移動、固定
不同的業務型別:對時延的要求、對差錯控制的要求。
等等,使得實際中的相互通訊的網路異常複雜,如何解決這個問題呢?國際標準化組織(ISO)提出了OSI(Open System Interconnected)七層參考模型,將網路按照不同的功能劃分為7層:
應用層
表示層
會話層
傳輸層
網路層
物理層:提供二進位制傳輸,確定在通訊通道上如何傳輸位元流。一條物理通道上所能傳送的資訊的最快速度是有限制的,不是我們想傳送多塊就能傳送多快的。為了對抗複雜的傳輸環境(這一點在無線通訊中尤其明顯),物理層通常要使用非常複雜的調製、編碼技術,來在在一定差錯容忍度的前提下,儘可能的多傳送。
資料鏈路層:提供介質訪問,增強物理層的傳輸功能,建立一條無差錯的傳輸線路。比如對於收到物理層傳送過來的資料,需要通過確認請求或者簡單的差錯控制編碼(比如奇偶效驗)來判斷這一幀資料是否有錯誤,如果錯誤了,則通知傳送發重新傳送。
網路層:IP定址和路由。因為網路上的資料可以經由多條路線到達目的地,所以其中要考慮路由演算法、擁塞控制等問題。
傳輸層:為源端主機到目的端主機提供可靠的資料傳輸服務,隔離網路的上下層協議,是得網路應用於下層協議無關。也就是應用程式與應用程式之間的連線。
會話層:兩個相互通訊的應用程式之間建立、組織和協調其相互之間的通訊。比如電影裡使用對講機時,一句話說完後總要加一句“over”。
表示層:被傳送的資料如何表示。
應用層:使用者所提供的服務。
要注意,這7層模型是功能上的劃分,並不是具體一定要有這七層。
下面介紹一下應用層、傳輸層和網路層的常見協議(這是筆試題中常考的):
應用層協議:遠端登入協議(Telnet)、檔案傳輸協議(FTP)、超文字傳輸協議(HTTP)、域名服務(DNS)、簡單喲見傳輸協議(SMTP)、郵局協議(POP3)等。
傳輸層:傳輸控制協議(TCP)、使用者資料包協議(UDP)。
這兩個協議值得仔細說說。
TCP協議是面向連線的,也就是說,在雙方通訊之前,已經安排好了一條通訊線路(不管它具體是什麼)共他倆使用,別人不能使用,等他倆通訊結束後,需要釋放這條線路。TCP是通過3次握手建立的:
1.客戶端給伺服器傳送SYN(syn = j)包,進入SYN_SEND狀態。
2.伺服器接收到SYN包,確認客戶的SYN(ack = j+1),同時自己也傳送一個SYN包(syn = k),把它倆都傳送出去,伺服器進入SYN_RECV狀態。
3.客戶端收到伺服器的SYN+ACK包,向伺服器傳送ACK(ack = k+1)。客戶端和伺服器都進入ESTABLISHED狀態。此時,連線已經建立完畢,可以傳送訊息了。
上面只是正常的建立連線過程,其中的任何一步都有可能失敗,至於失敗以後的操作,這裡就不細說了。
下面再看看UDP協議:這是一種無連線的、不可靠的協議。這意味著可能向對方傳送的訊息時對方無法接到。或者,向一個根本不存在的IP地址或者埠傳送訊息。既然是不可靠的,為什麼還要使用它呢?因為UDP協議不需要建立連線,沒有資料重傳,所以實時性較高。比如我們看視訊時,一兩個畫素的錯誤我們根本不會發覺。
網路層:網際協議(IP),Internet網際網路報文控制協議(ICMP)、Internet組管理協議IGMP。
傳輸層
網路層
在TCP/IP網路應用中,通訊的兩個程式間相互作用的主要模式是客戶機、伺服器模式:客戶向伺服器傳送請求,伺服器收到請求後,提供相應的服務。為什麼這麼設計呢?首先,建立網路的原因是因為網路中軟硬體資源、運算能力和資訊分佈不均,需要共享,從而擁有資源多的主機提供服務,資源少的客戶請求服務。其次,網間程式通訊完全是非同步的,相互通訊的程式間即不存在父子關係,又不存在共享的記憶體緩衝區,需要一種機制為希望通訊的程式間建立聯絡,為二者的資料交換提供同步。
它們的通訊過程如下:
伺服器先執行:
1.開啟一個通訊通道並告知本地主機,他願意在某一地址和埠上接收客戶的請求。
2.等待客戶請求到達埠。
3.接收到重複服務請求沒處理該請求併傳送應答訊號。收到併發服務的請求,要啟用一個新的程式或者執行緒來處理這個客戶請求。新的程式或者執行緒處理此客戶的請求,並不需要對其他請求作出相應。等服務完成後,關閉新程式與客戶的通訊鏈路,並終止。
4.返回第二步,等待另一請求。
5.關閉伺服器。
客戶端:
1.開啟一個通訊通道並連線到伺服器所在的主機的特定埠。
2.向伺服器傳送服務請求報文,等待接收應答,繼續提出請求。
3.請求結束後關閉通訊通道並終止。
講了這麼多,我們可以隱約感覺到,網路程式設計是一件很麻煩的事情,為了方便的開發應用軟體程式,美國伯克利大學在UNIX上推出了一種應用程式訪問通訊協議的作業系統套接字(socket),是得程式設計師可以很方便的訪問TCP/IP協議,從而開發各種網路應用程式。後來,socket又引入到windows作業系統。我們先介紹與之相關的函式,然後給出幾個例子:
1.使用WSAStartup進行版本協商。
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中wVersionRequested指明瞭版本號。高位元組是副版本,低位元組是主版本,可以通過MAEWORD巨集來獲得。lpWSAData是用來返回值的,這個函式會把它載入的版本資訊填到這個結構裡面。具體的使用方法可以參照MSDN給出的例子。
2.使用WSACleanup()來釋放為應用程式分配的資源,與WSAStartup相對應。
3.使用socket函式來建立套接字:
SOCKET socket( int af, int type,int protocol );
af引數是地址族,對於TCP/IP協議,它只能是AF_INET。
type指明瞭socket的型別,對於1.1版本的socket,他只接受兩種型別:SOCK_STREAM、SOCK_DGRAM
protocol指明瞭與特定地址家族相關的協議,如果為0,則根據地址格式和套接字類別,自動選擇一個合適的協議。
如果函式呼叫成功,則會返回一個SOCK資料型別的套接字描述符:如果呼叫失敗,則返回一個INVALID_SOCKET值。
4.使用bind函式將套接字繫結到本地的某個地址和埠上:
int bind( SOCKET s, const struct sockaddr FAR *name,int namelen);
s指定要繫結的套接字,name是一個指向sockaddr結構體型別的指標,這個結構體表明瞭本地資訊。
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
由於這個結構是為所有地址族準備的,所以不同的協議會有一定的區別,所以用第三個引數指明結構的長度。
再回過頭來看第二個引數,第一成員指明瞭地址族,第二個成員是14個位元組的記憶體區域,對於不同的協議,有不同的內容。對於TCP/IP協議,使用sockaddr_in 結構來替換sockaddr結構:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family指明瞭地址族,應使用AF_INET;sin_port指明瞭埠號;sin_addr指明瞭主機的IP地址;sin_zero則只是為了填充位元組,是得sockaddr_in 與sockaddr長度相同。其中sin_addr又是一個結構體,定義如下:
typedef struct in_addr
{
union
{
struct
{
unsigned char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct
{
unsigned short s_w1,s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR
這個結構其實是一個聯合體,通常我們都是將點分十進位制的IP地址轉換為u_long型別,並賦值給S_addr成員。
一般情況下,我們可以使用INADDR_ANY允許套接字向任何分配本地機器的IP地址傳送或者接收資料。這個引數的實際意義在於,一般情況下,一臺主機只有一個IP地址,但如果主機有兩個網路卡,那麼他會有兩個IP地址,如果我們只想使用其中的一個供套接字使用,我們可以使用inet_addr函式將本地的IP地址(點分十進位制字串形式),轉化為unsigned long並賦給S_addr;與之相反的轉化為inet_ntoa函式,將S_addr轉化為點分十進位制,供列印輸出使用。
5.listen函式將指定的套接字設定為監聽模式。
int listen( SOCKET s, int backlog );
s 為指定的套接字;backlog為等待佇列的最大長度。
6.使用accept函式來接受客戶端的連線請求
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
s為已被設定為監聽模式的套接字;addr為指向客戶端的sockaddr的地址,通過函式來獲取值;addrlen為地址長度。
6.通過send函式傳送訊息
int send(SOCKET s,const char FAR *buf,int len,int flags);
s為已建立連線的套接字,buf為要傳送訊息的地址,len為訊息長度,flags一般設為0即可。
7.通過recv函式獲取訊息:
int recv(SOCKET s,char FAR *buf,int len,int flags);
s為已建立連線的套接字,buf用來儲存接收的資料,len表示緩衝區的長度,flags一般填0即可。
8.使用connect函式與特定的套接字連線
int connect( SOCKET s,const struct sockaddr FAR *name,int namelen);
s為即將建立連線的那個套接字;name指定了伺服器端的地址資訊,s為地址資訊長度。
9.使用recvfrom接受訊息
int recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR *from,int FAR *fromlen);
s為準備接受資料的套接字,buf為接收資料的緩衝區,len為緩衝區的長度,flag一般填0,from指標用來儲存傳送方的地址資訊,fromlen為地址的長度。
10.使用sendto向一個特定的目的方傳送資料。
int sendto( SOCKET s,const char FAR *buf,int len,int flags,const struct sockaddr FAR *to,int tolen );
s為套接字,buf為傳送的資料的地址,len為資料的長度,flags一般為0,to指標指向目標套接字的地址,tolen為地址的長度
11.位元組序的轉換函式。
首先先搞清楚什麼是位元組序。一般情況下,我們使用電腦都是低位在前、高位在後的,這被稱為小端位元組序;而網路傳輸時使用的是低位在前,高位在後的大端位元組序。如果不進行轉化一個16為資料0X1234就被網路認為是0X3412了。轉換的函式有兩個:
u_short htons( u_short hostshort );將一個16位數轉換為網路位元組序
u_long htonl( u_long hostlong );將一個32位數轉換為網路位元組序
介紹完函式,我們就先舉一個利用TCP協議編寫的簡單的網路通訊的例子。我們先看一下基本步驟:
伺服器端:
1.進行版本協商(WSAStartup)。
2.建立一個套接字(socket)。
3.將套接字設為監聽狀態(listen)。
4.接受客戶端的傳送請求(accept)。
5.傳送或者接收資料(send/recv)。
6.關閉套接字(closesocket),一次通訊結束。
7.轉4.
客戶端端:
1.進行版本協商(WSAStartup)。
2.建立一個套接字(socket)。
3.連線到伺服器(connect)。
4.傳送或者接收訊息(send/recv)。
5.關閉套接字(closesocket)。
6.釋放資源(WSACleanup)。
伺服器:
1.進行版本協商(WSAStartup)。
2.建立一個套接字(socket)。
3.繫結套接字(bind)。
4.接收或者傳送訊息(recvfrom/sendto)。
5.關閉套接字(closesocket)。
6.釋放資源(WSACleanup)。
客戶端:
1.進行版本協商(WSAStartup)。
2.建立套接字(socket)。
3.傳送或接收訊息(sendto/recvfrom)。
4.關閉套接字(closesocket)。
5.釋放資源(WSACleanup)。
對應的程式如下:
感覺要簡單許多,這裡稍微總結一下TPC與UDP編寫程式的區別:
首先,介紹幾個基本概念。什麼是計算機網路?它是相互連線的獨立自主的計算機的集合。它們是如何通訊的呢,需要一個東西來表明我要跟哪個計算機進行通訊,在網路上,為每個計算機分配了一個“IP”地址,通過地址來找到想要通訊的計算機。具體的通訊是計算機的某個程式實現的,一臺計算機可能同時有多個程式在使用網路。為了區分它們,為每個程式提供了一個“埠號”來標識自己。具體通訊時所傳送的內容,成為協議,它規定了我們傳送的格式:除了傳送的內容以外,還包括這些東西是誰傳送的,要發給誰,總共有多長等等。而資訊具體是如何從一臺主機,傳送到另一臺主機,則會有很大的不確定性:
不同的通訊媒介:是通過有線傳輸的,還是無線網路?
不同的作業系統:Unix、Windows
不同的應用環境:移動、固定
不同的業務型別:對時延的要求、對差錯控制的要求。
等等,使得實際中的相互通訊的網路異常複雜,如何解決這個問題呢?國際標準化組織(ISO)提出了OSI(Open System Interconnected)七層參考模型,將網路按照不同的功能劃分為7層:
應用層
表示層
會話層
傳輸層
網路層
資料鏈路層
物理層
物理層:提供二進位制傳輸,確定在通訊通道上如何傳輸位元流。一條物理通道上所能傳送的資訊的最快速度是有限制的,不是我們想傳送多塊就能傳送多快的。為了對抗複雜的傳輸環境(這一點在無線通訊中尤其明顯),物理層通常要使用非常複雜的調製、編碼技術,來在在一定差錯容忍度的前提下,儘可能的多傳送。
資料鏈路層:提供介質訪問,增強物理層的傳輸功能,建立一條無差錯的傳輸線路。比如對於收到物理層傳送過來的資料,需要通過確認請求或者簡單的差錯控制編碼(比如奇偶效驗)來判斷這一幀資料是否有錯誤,如果錯誤了,則通知傳送發重新傳送。
網路層:IP定址和路由。因為網路上的資料可以經由多條路線到達目的地,所以其中要考慮路由演算法、擁塞控制等問題。
傳輸層:為源端主機到目的端主機提供可靠的資料傳輸服務,隔離網路的上下層協議,是得網路應用於下層協議無關。也就是應用程式與應用程式之間的連線。
會話層:兩個相互通訊的應用程式之間建立、組織和協調其相互之間的通訊。比如電影裡使用對講機時,一句話說完後總要加一句“over”。
表示層:被傳送的資料如何表示。
應用層:使用者所提供的服務。
要注意,這7層模型是功能上的劃分,並不是具體一定要有這七層。
下面介紹一下應用層、傳輸層和網路層的常見協議(這是筆試題中常考的):
應用層協議:遠端登入協議(Telnet)、檔案傳輸協議(FTP)、超文字傳輸協議(HTTP)、域名服務(DNS)、簡單喲見傳輸協議(SMTP)、郵局協議(POP3)等。
傳輸層:傳輸控制協議(TCP)、使用者資料包協議(UDP)。
這兩個協議值得仔細說說。
TCP協議是面向連線的,也就是說,在雙方通訊之前,已經安排好了一條通訊線路(不管它具體是什麼)共他倆使用,別人不能使用,等他倆通訊結束後,需要釋放這條線路。TCP是通過3次握手建立的:
1.客戶端給伺服器傳送SYN(syn = j)包,進入SYN_SEND狀態。
2.伺服器接收到SYN包,確認客戶的SYN(ack = j+1),同時自己也傳送一個SYN包(syn = k),把它倆都傳送出去,伺服器進入SYN_RECV狀態。
3.客戶端收到伺服器的SYN+ACK包,向伺服器傳送ACK(ack = k+1)。客戶端和伺服器都進入ESTABLISHED狀態。此時,連線已經建立完畢,可以傳送訊息了。
上面只是正常的建立連線過程,其中的任何一步都有可能失敗,至於失敗以後的操作,這裡就不細說了。
下面再看看UDP協議:這是一種無連線的、不可靠的協議。這意味著可能向對方傳送的訊息時對方無法接到。或者,向一個根本不存在的IP地址或者埠傳送訊息。既然是不可靠的,為什麼還要使用它呢?因為UDP協議不需要建立連線,沒有資料重傳,所以實時性較高。比如我們看視訊時,一兩個畫素的錯誤我們根本不會發覺。
網路層:網際協議(IP),Internet網際網路報文控制協議(ICMP)、Internet組管理協議IGMP。
由於7層模型在使用起來很不方便,實際中應用更廣泛的是TCP/IP模型:
傳輸層
網路層
網路介面層
在TCP/IP網路應用中,通訊的兩個程式間相互作用的主要模式是客戶機、伺服器模式:客戶向伺服器傳送請求,伺服器收到請求後,提供相應的服務。為什麼這麼設計呢?首先,建立網路的原因是因為網路中軟硬體資源、運算能力和資訊分佈不均,需要共享,從而擁有資源多的主機提供服務,資源少的客戶請求服務。其次,網間程式通訊完全是非同步的,相互通訊的程式間即不存在父子關係,又不存在共享的記憶體緩衝區,需要一種機制為希望通訊的程式間建立聯絡,為二者的資料交換提供同步。
它們的通訊過程如下:
伺服器先執行:
1.開啟一個通訊通道並告知本地主機,他願意在某一地址和埠上接收客戶的請求。
2.等待客戶請求到達埠。
3.接收到重複服務請求沒處理該請求併傳送應答訊號。收到併發服務的請求,要啟用一個新的程式或者執行緒來處理這個客戶請求。新的程式或者執行緒處理此客戶的請求,並不需要對其他請求作出相應。等服務完成後,關閉新程式與客戶的通訊鏈路,並終止。
4.返回第二步,等待另一請求。
5.關閉伺服器。
客戶端:
1.開啟一個通訊通道並連線到伺服器所在的主機的特定埠。
2.向伺服器傳送服務請求報文,等待接收應答,繼續提出請求。
3.請求結束後關閉通訊通道並終止。
講了這麼多,我們可以隱約感覺到,網路程式設計是一件很麻煩的事情,為了方便的開發應用軟體程式,美國伯克利大學在UNIX上推出了一種應用程式訪問通訊協議的作業系統套接字(socket),是得程式設計師可以很方便的訪問TCP/IP協議,從而開發各種網路應用程式。後來,socket又引入到windows作業系統。我們先介紹與之相關的函式,然後給出幾個例子:
1.使用WSAStartup進行版本協商。
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中wVersionRequested指明瞭版本號。高位元組是副版本,低位元組是主版本,可以通過MAEWORD巨集來獲得。lpWSAData是用來返回值的,這個函式會把它載入的版本資訊填到這個結構裡面。具體的使用方法可以參照MSDN給出的例子。
2.使用WSACleanup()來釋放為應用程式分配的資源,與WSAStartup相對應。
3.使用socket函式來建立套接字:
SOCKET socket( int af, int type,int protocol );
af引數是地址族,對於TCP/IP協議,它只能是AF_INET。
type指明瞭socket的型別,對於1.1版本的socket,他只接受兩種型別:SOCK_STREAM、SOCK_DGRAM
protocol指明瞭與特定地址家族相關的協議,如果為0,則根據地址格式和套接字類別,自動選擇一個合適的協議。
如果函式呼叫成功,則會返回一個SOCK資料型別的套接字描述符:如果呼叫失敗,則返回一個INVALID_SOCKET值。
4.使用bind函式將套接字繫結到本地的某個地址和埠上:
int bind( SOCKET s, const struct sockaddr FAR *name,int namelen);
s指定要繫結的套接字,name是一個指向sockaddr結構體型別的指標,這個結構體表明瞭本地資訊。
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
由於這個結構是為所有地址族準備的,所以不同的協議會有一定的區別,所以用第三個引數指明結構的長度。
再回過頭來看第二個引數,第一成員指明瞭地址族,第二個成員是14個位元組的記憶體區域,對於不同的協議,有不同的內容。對於TCP/IP協議,使用sockaddr_in 結構來替換sockaddr結構:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family指明瞭地址族,應使用AF_INET;sin_port指明瞭埠號;sin_addr指明瞭主機的IP地址;sin_zero則只是為了填充位元組,是得sockaddr_in 與sockaddr長度相同。其中sin_addr又是一個結構體,定義如下:
typedef struct in_addr
{
union
{
struct
{
unsigned char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct
{
unsigned short s_w1,s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR
這個結構其實是一個聯合體,通常我們都是將點分十進位制的IP地址轉換為u_long型別,並賦值給S_addr成員。
一般情況下,我們可以使用INADDR_ANY允許套接字向任何分配本地機器的IP地址傳送或者接收資料。這個引數的實際意義在於,一般情況下,一臺主機只有一個IP地址,但如果主機有兩個網路卡,那麼他會有兩個IP地址,如果我們只想使用其中的一個供套接字使用,我們可以使用inet_addr函式將本地的IP地址(點分十進位制字串形式),轉化為unsigned long並賦給S_addr;與之相反的轉化為inet_ntoa函式,將S_addr轉化為點分十進位制,供列印輸出使用。
5.listen函式將指定的套接字設定為監聽模式。
int listen( SOCKET s, int backlog );
s 為指定的套接字;backlog為等待佇列的最大長度。
6.使用accept函式來接受客戶端的連線請求
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
s為已被設定為監聽模式的套接字;addr為指向客戶端的sockaddr的地址,通過函式來獲取值;addrlen為地址長度。
6.通過send函式傳送訊息
int send(SOCKET s,const char FAR *buf,int len,int flags);
s為已建立連線的套接字,buf為要傳送訊息的地址,len為訊息長度,flags一般設為0即可。
7.通過recv函式獲取訊息:
int recv(SOCKET s,char FAR *buf,int len,int flags);
s為已建立連線的套接字,buf用來儲存接收的資料,len表示緩衝區的長度,flags一般填0即可。
8.使用connect函式與特定的套接字連線
int connect( SOCKET s,const struct sockaddr FAR *name,int namelen);
s為即將建立連線的那個套接字;name指定了伺服器端的地址資訊,s為地址資訊長度。
9.使用recvfrom接受訊息
int recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR *from,int FAR *fromlen);
s為準備接受資料的套接字,buf為接收資料的緩衝區,len為緩衝區的長度,flag一般填0,from指標用來儲存傳送方的地址資訊,fromlen為地址的長度。
10.使用sendto向一個特定的目的方傳送資料。
int sendto( SOCKET s,const char FAR *buf,int len,int flags,const struct sockaddr FAR *to,int tolen );
s為套接字,buf為傳送的資料的地址,len為資料的長度,flags一般為0,to指標指向目標套接字的地址,tolen為地址的長度
11.位元組序的轉換函式。
首先先搞清楚什麼是位元組序。一般情況下,我們使用電腦都是低位在前、高位在後的,這被稱為小端位元組序;而網路傳輸時使用的是低位在前,高位在後的大端位元組序。如果不進行轉化一個16為資料0X1234就被網路認為是0X3412了。轉換的函式有兩個:
u_short htons( u_short hostshort );將一個16位數轉換為網路位元組序
u_long htonl( u_long hostlong );將一個32位數轉換為網路位元組序
介紹完函式,我們就先舉一個利用TCP協議編寫的簡單的網路通訊的例子。我們先看一下基本步驟:
伺服器端:
1.進行版本協商(WSAStartup)。
2.建立一個套接字(socket)。
3.將套接字設為監聽狀態(listen)。
4.接受客戶端的傳送請求(accept)。
5.傳送或者接收資料(send/recv)。
6.關閉套接字(closesocket),一次通訊結束。
7.轉4.
客戶端端:
1.進行版本協商(WSAStartup)。
2.建立一個套接字(socket)。
3.連線到伺服器(connect)。
4.傳送或者接收訊息(send/recv)。
5.關閉套接字(closesocket)。
6.釋放資源(WSACleanup)。
再看我們的程式:
//伺服器端程式
#include <Winsock2.h>
#include <stdio.h>
int main()
{
//進行版本協商
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
return -1;
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return -1;
}
//建立套接字
SOCKET socksrv = socket(AF_INET,SOCK_STREAM,0);
//填寫本地資訊
SOCKADDR_IN addrSrv;
//本機IP地址
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//協議族
addrSrv.sin_family = AF_INET;
//埠資訊,必須使用1024以上,注意位元組序轉換
addrSrv.sin_port = htons(6000);
//繫結套接字
bind(socksrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
//設為監聽模式
listen(socksrv,5);
//客戶端地址資訊結構體
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
while(1)
{
//接受客戶端請求,返回值為已經建立連線的SOCKET
SOCKET sockConn = accept(socksrv,(SOCKADDR*)&addrClient,&len);
//儲存資料的緩衝區
char sendBuf[100];
//將資料格式化到緩衝區中
sprintf(sendBuf,"Welcom %s to the Service!",
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);
}
//由於伺服器一直處理工作狀態,所以不呼叫WSACleanup釋放資源
return 0;
}
//客戶端程式
#include <Winsock2.h>
#include <stdio.h>
int main()
{
//進行版本協商
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
return -1;
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return -1;
}
//建立套接字
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 revcBuf[100];
//接收伺服器的資料
recv(sockClient,revcBuf,100,0);
//列印資料
printf("%s",revcBuf);
//傳送資料緩
send(sockClient,"hello",strlen("hello")+1,0);
closesocket(sockClient);
//釋放資源
WSACleanup();
return 0;
}
下面我們再看看基於UDP編寫的客戶端/伺服器的應用程式的步驟:伺服器:
1.進行版本協商(WSAStartup)。
2.建立一個套接字(socket)。
3.繫結套接字(bind)。
4.接收或者傳送訊息(recvfrom/sendto)。
5.關閉套接字(closesocket)。
6.釋放資源(WSACleanup)。
客戶端:
1.進行版本協商(WSAStartup)。
2.建立套接字(socket)。
3.傳送或接收訊息(sendto/recvfrom)。
4.關閉套接字(closesocket)。
5.釋放資源(WSACleanup)。
對應的程式如下:
//伺服器程式
#include <Winsock2.h>
#include <stdio.h>
int main()
{
//進行版本協商
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
return -1;
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return -1;
}
//建立套接字
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(addrClient);
char recvBuf[100];
//接收資料
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len);
printf("%s\n",recvBuf);
//關閉套接字
closesocket(sockSrv);
//釋放資源
WSACleanup();
return 0;
}
//客戶端程式
#include <Winsock2.h>
#include <stdio.h>
int main()
{
//進行版本協商
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
return -1;
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return -1;
}
//建立套接字
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();
return 0;
}
感覺要簡單許多,這裡稍微總結一下TPC與UDP編寫程式的區別:
TCP伺服器端要先設定為監聽(listen)模式,然後要等待響應(accept),並且利用響應的套接字進行傳送(send)和接受(recv)。由於是面向連線的方式,所以傳送和接受時都不需要指明地址資訊。而客戶端要連線到伺服器(connect)。而UDP的過程相對簡單,沒有監聽、連線、響應的過程,直接可以使用sendto和recvfrom來傳送和接收訊息。由於UDP是面向非連線的,所以要在引數中指明要傳送或者接收的物件。
最後,我們利用UDP協議實現一個簡單的聊天程式。為什麼時候UDP協議?因為聊天時候,稍微錯一點不要緊,可以通過上下文、語義,甚至讓對法重新傳送來克服;而且,聊天時經常出現一個屌絲向一個女神搭訕,但是女神遲遲不回覆的情況,這時是應該關閉連線呢,還是應該保持連線,讓它們繼續佔用資源?這是一個很糾結的問題。相比之下,UDP協議的優勢就體現出來了。
再考慮一個問題,如何結束聊天?可以通過傳送一個特定的字元來實現,當伺服器收到這個特定的字元時,向客戶端也傳送這個字元,並且關閉自己的套接字;同理客戶端如果收到這個字元,也可以向伺服器傳送這個字元(儘管可能此時伺服器的套接字已經關閉了),並關閉自己的套接字。
想清楚了這兩點,我們就開始編寫程式吧:
//伺服器程式
#include <Winsock2.h>
#include <stdio.h>
int main()
{
//進行版本協商
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
return -1;
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return -1;
}
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(addrSrv));
char recvBuf[100];
char sendBuf[100];
char tempBuf[100];
SOCKADDR_IN addClient;
int len = sizeof(SOCKADDR);
while(1)
{
//接收資料
recvfrom(sockSrv,recvBuf,100,0,(SOCKADDR*)&addClient,&len);
//判斷對話是否應該被終止
if('#' == recvBuf[0])
{
sendto(sockSrv,"#",strlen("#")+1,0,(SOCKADDR*)&addClient,len);
printf("chat end!\n");
break;
}
//列印接收的資料
sprintf(tempBuf,"%s says: %s",inet_ntoa(addClient.sin_addr),recvBuf);
printf("%s\n",tempBuf);
//傳送資料
printf("please input data:\n");
gets(sendBuf);
sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addClient,len);
}
closesocket(sockSrv);
WSACleanup( );
return 0;
}
//客戶端程式
#include <Winsock2.h>
#include <stdio.h>
int main()
{
//進行版本協商
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
return -1;
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return -1;
}
//建立套接字
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[100];
int len = sizeof(SOCKADDR);
while(1)
{
//客戶端線傳送資料:
//輸入傳送資料
printf("please input data:\n");
gets(sendBUf);
//傳送資料
sendto(sockClient,sendBUf,100,0,(SOCKADDR*)&addrSrv,len);
//接收資料
recvfrom(sockClient,recvBuf,100,0,(SOCKADDR*)&addrSrv,&len);
//判斷對話是否終止
if('#' == recvBuf[0])
{
sendto(sockClient,"#",strlen("#")+1,0,(SOCKADDR*)&addrSrv,len);
printf("chat over!");
break;
}
//列印接收的資料
sprintf(tempBuf,"%s say: %s",inet_ntoa(addrSrv.sin_addr),recvBuf);
printf("%s",tempBuf);
}
closesocket(sockClient);
WSACleanup( );
return 0;
}
相關文章
- VC++串列埠通訊程式設計詳解C++串列埠程式設計
- VC++深入詳解--之複習筆記(一)C++筆記
- VC++深入詳解--之複習筆記(二)C++筆記
- 【網路程式設計】socket詳解程式設計
- UDP&TCP Linux網路應用程式設計詳解UDPTCPLinux程式設計
- 網路程式設計TCP/IP詳解程式設計TCP
- java網路程式設計(TCP詳解)Java程式設計TCP
- Java 網路程式設計 —— Socket 詳解Java程式設計
- MFC--網路程式設計之CAsyncSocket詳解程式設計
- VC++ MFC程式設計版本資訊控制C++C程式程式設計
- 【計算機網路知識掃盲】12、★Net命令詳解☆(轉)計算機網路
- Java網路程式設計和NIO詳解3:IO模型與Java網路程式設計模型Java程式設計模型
- PHP Socket 程式設計詳解PHP程式設計
- 第12章、網路程式設計程式設計
- 網路爬蟲詳細設計方案爬蟲
- 網路通訊程式設計程式設計
- 網路協程程式設計程式設計
- Socket 程式設計 (網路篇)程式設計
- py網路工具程式設計程式設計
- Java網路程式設計和NIO詳解9:基於NIO的網路程式設計框架NettyJava程式設計框架Netty
- 泛型程式設計詳解(一)泛型程式設計
- Flutter非同步程式設計詳解Flutter非同步程式設計
- MFC下CSocket程式設計詳解程式設計
- Java網路程式設計和NIO詳解6:Linux epoll實現原理詳解Java程式設計Linux
- VC++視覺化程式設計第一個程式設計例項出錯C++視覺化程式設計
- shell程式設計-sed命令詳解(超詳細)程式設計
- Python網路Socket程式設計Python程式設計
- python 網路篇(網路程式設計)Python程式設計
- VC++程式設計師成長--之必看書籍C++程式設計師
- 深入講解 Lotus Notes 外掛程式設計程式設計
- 利用VC++程式設計實現程式自動啟動 (轉)C++程式設計
- 併發程式設計——IO模型詳解程式設計模型
- MFC下CSocket程式設計詳解(轉)程式設計
- 併發程式設計 — CAS 原理詳解程式設計
- 萬字詳解 | Java 流式程式設計Java程式設計
- RequireJS 模組化程式設計詳解UIJS程式設計
- python超程式設計詳解(3)Python程式設計
- python超程式設計詳解(4)Python程式設計