socket的概念
socket用于網絡中一台計算機中的程式與其他計算機的程式之間需要交換資料。
socket也稱作“套接字”,用于描述IP位址和端口,是一個通信鍊路的描述符。應用程式通常通過“套接字”向對端送出請求或者應答網絡請求。
socket是連接配接運作在網絡上的兩個程式之間的通信端點。通信的兩端都有socket,它是一個通道,資料在兩個socket之間進行傳輸。socket把複雜的TCP/IP協定族隐藏在socket接口後面,對程式員來說,隻要用好socket相關的函數,就可以完成資料通信。
套接字(socket)
TCP提供了流(stream)和資料報(datagram)兩種通信機制,是以套接字也分為流套接字和資料報套接字,本章節講流套接字,在實際開發中,資料報套接字的應用場景很少。
流套接字的類型是SOCK_STREAM,采用TCP/IP協定實作。它提供的是一個有序、可靠、雙向位元組流的連接配接,是以發送的資料可以確定不會丢失、重複或亂序到達,而且它還有出錯後重新發送的機制。類似于打電話。
資料報套接的類型是SOCK_DGRAM,它不需要建立和維持一個連接配接,采用UDP/IP協定實作。它對可以發送的資料的長度有限制,資料報作為一個單獨的網絡消息被傳輸,它可能會丢失、複制或錯亂到達,UDP不是一個可靠的協定,但是它的速度比較高,因為它并不需要總是要建立和維持一個連接配接。類似于短信。
socket通信的過程
1)服務端程式将一個套接字綁定到一個指定的位址和端口,并通過此套接字等待和監聽客戶的連接配接請求。
2)客戶程式向服務端程式綁定服務端的位址和端口發出連接配接請求。
3)服務端接受連接配接請求。并獲得一個新的套接字。
4)服務端通過讀、寫新的套接字與用戶端進行通信。
客戶/服務端模式
在TCP/IP網絡應用中,兩個程式之間通信模式是客戶/服務端模式(client/server),即用戶端向服務端送出請求,服務端接受到請求後,提供相應的服務。
客戶/服務端也叫作客戶/伺服器,各人習慣。
1、服務端
1)服務端要先啟動,準備好一個通信通道,表示可以在某位址和端口上可以接收客戶連接配接。
2)等待用戶端的連接配接請求。
3)等待并接收用戶端發過來的資料,處理該客戶的資料,處理完成後,向用戶端傳回處理結果。
4)不斷的重複第3)步,直到用戶端斷開連接配接。
5)關閉服務端
示例(book242.cpp)
2、用戶端
1)打開一個通信通道,連接配接到服務端已準備好的端口。
2)向服務端發送資料,等待并接收服務端處理結果。
3)不斷的重複第2)步,直到全部的資料被處理完。
4)關閉與伺服器的連接配接。
示例(book241.cpp)
先啟動服務端程式book242,服務端啟動後,進入等待用戶端連接配接狀态,然後啟動用戶端。
服務端的輸出如下:
用戶端的輸出如下:
注意事項
1、别去糾纏細節
在socket通信的用戶端和伺服器,出現了多種資料結構,調用了多個函數,涉及到很多方面的知識,對初學者來說,更重要的是了解socket通信的過程、每段代碼的用途和函數調用的功能,不要去糾纏這些結構體和函數的參數,這些函數的參數雖然很多,但可以調整的非常少,别抄錯就可以了。需要程式員注意的地方我會列出。
2、服務端程式綁定的位址
一般情況下,伺服器有多個網卡,多個IP位址,socket通信可以指定用哪個位址來進行通信,對程式員來說有兩種選擇:1)任意ip位址;2)指定ip位址。
1)任意ip位址的代碼
m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本主機的任意ip位址
2)指定ip位址的代碼
m_servaddr.sin_addr.s_addr = inet_addr("192.168.149.129"); // 指定ip位址
在實際開發中,采用任意ip位址的做法比較多。
3、服務端程式綁定的通信端口
m_servaddr.sin_port = htons(5000); // 通信端口
4、用戶端程式指定服務端的ip位址
struct hostent* h; // ip位址資訊的資料結構
if ( (h = gethostbyname("192.168.149.129")) == 0 )
{ perror("gethostbyname"); close(sockfd); return -1; }
5、用戶端程式指定服務端的通信端口
servaddr.sin_port = htons(5000);
6、send函數
send函數用于把資料通過socket發送給對端。不論是用戶端還是服務端,應用程式都用send函數來向TCP連接配接的另一端發送資料。客戶程式一般用send函數向伺服器發送請求,而伺服器則通常用send函數來向客戶程式發送應答。
包含頭檔案:
#include <sys/types.h>
#include <sys/socket.h>
函數聲明:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd為已建立好連接配接的socket。
buf為需要發送的資料的記憶體位址,可以是C語言基本資料類型變量的位址,也可以數組、結構體、字元串,記憶體中有什麼就發送什麼。
len需要發送的資料的長度,為buf中有效資料的長度。
flags填0, 其他數值意義不大。
函數傳回已發送的字元數。出錯時傳回-1,錯誤資訊errno被标記。
注意,就算是網絡斷開,或socket已被對端關閉,send函數不會立即報錯,要過幾秒才會報錯。
如果send函數傳回的錯誤(<=0),表示通信鍊路已不可用。
7、recv函數
recv函數用于接收對端socket發送過來的資料。
recv函數用于接收對端通過socket發送過來的資料。不論是用戶端還是服務端,應用程式都用recv函數接收來自TCP連接配接的另一端發送過來資料。
包含頭檔案:
#include <sys/types.h>
#include <sys/socket.h>
函數聲明:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd為已建立好連接配接的socket。
buf為用于接收資料的記憶體位址,可以是C語言基本資料類型變量的位址,也可以數組、結構體、字元串,隻要是一塊記憶體就行了。
len需要接收資料的長度,不能超過buf的大小,否則記憶體溢出。
flags填0, 其他數值意義不大。
如果socket的對端沒有發送資料,recv函數就會等待,如果對端發送了資料,函數傳回接收到的字元數。出錯時傳回-1,錯誤資訊errno被标記。如果socket被對端關閉,傳回值為0。
如果recv函數傳回的錯誤(<=0),表示通信鍊路已不可用。
8、服務端有兩個socket
對服務端來說,有兩個socket,一個是用于監聽的socket,還有一個就是用戶端連接配接成功後,由accept函數建立的用于與用戶端收發封包的socket。
9、程式退出時先關閉socket
socket是系統資源,作業系統打開的socket數量是有限的,在程式退出之前必須關閉已打開的socket,就像關閉檔案指針一樣,就像delete已配置設定的記憶體一樣,極其重要。
值得注意的是,關閉socket的代碼不能隻在main函數的最後,那是程式運作的理想狀态,還應該在main函數的每個return之前關閉。
相關的庫函數
1、socket函數
socket函數用于建立一個新的socket,也就是向系統申請一個socket資源。socket函數使用者用戶端和服務端。
函數聲明:
int socket(int domain, int type, int protocol);
參數說明:
domain:協定域,又稱協定族(family)。常用的協定族有AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域Socket)、AF_ROUTE等。協定族決定了socket的位址類型,在通信中必須采用對應的位址,如AF_INET決定了要用ipv4位址(32位的)與端口号(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為位址。
type:指定socket類型。常用的socket類型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一種面向連接配接的socket,針對于面向連接配接的TCP服務應用。資料報式socket(SOCK_DGRAM)是一種無連接配接的socket,對應于無連接配接的UDP服務應用。
protocol:指定協定。常用協定有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别對應TCP傳輸協定、UDP傳輸協定、STCP傳輸協定、TIPC傳輸協定。
說了一大堆廢話,第一個能數隻能填AF_INET,第二個參數隻能填SOCK_STREAM,第三個參數隻能填0。
除非系統資料耗盡,socket函數一般不會傳回失敗。
2、gethostbyname函數
把ip位址或域名轉換為hostent 結構體表達的位址。
函數聲明:
struct hostent *gethostbyname(const char *name);
參數name,域名或者主機名,例如"192.168.1.3"、"www.google.com"等。
傳回值:如果成功,傳回一個hostent結構指針,失敗傳回NULL。
gethostbyname隻用于用戶端。
gethostbyname隻是把字元串的ip位址轉換為結構體的ip位址,隻要位址格式沒錯,一般不會傳回錯誤。函數失敗不會設定errno的值。
3、connect函數
向伺服器發起連接配接請求。
函數聲明:
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
函數說明:connect函數用于将參數sockfd 的socket 連至參數serv_addr 指定的服務端,參數addrlen為sockaddr的結構長度。
傳回值:成功則傳回0, 失敗傳回-1, 錯誤原因存于errno 中。
connect函數隻用于用戶端。
如果服務端的位址錯了,或端口錯了,或服務端沒有啟動,connect一定會失敗。
4、bind函數
服務端把用于通信的位址和端口綁定到socket上。
函數聲明:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
參數sockfd,需要綁定的socket。
參數addr,存放了服務端用于通信的位址和端口。
參數addrlen表示addr結構體的大小。
如果綁定的位址錯誤,或端口已被占用,bind函數一定會報錯,否則一般不會傳回錯誤。
5、listen函數
listen函數把主動連接配接套接口變為被連接配接套接口,使得這個socket可以接受其它socket的連接配接請求,進而成為一個服務端的socket。
函數聲明:
int listen(int sockfd, int backlog)
傳回:0──成功, -1──失敗
參數sockfd是已經被bind過的套接字。socket函數傳回的套接字是一個主動連接配接的套接字,在服務端的程式設計中,程式員希望這個套接字可以接受外來的連接配接請求,也就是被動等待用戶端來連接配接。由于系統預設時認為一個套接字是主動連接配接的,是以需要通過某種方式來告訴系統,程式員通過調用listen函數來完成這件事。
參數backlog,這個參數涉及到一些網絡的細節,比較麻煩,填5、10都行,一般不超過30。
當調用listen之後,服務端的套接字就可以調用accept來接受用戶端的連接配接請求。
listen函數一般不會傳回錯誤。
6、accept函數
服務端接受用戶端的連接配接。
函數聲明:
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
參數sockfd是已經被listen過的套接字。
參數addr用于存放用戶端的位址資訊,用sockaddr結構體表達,如果不需要用戶端的位址,可以填0。
參數addrlen用于存放addr參數的長度,如果addr為0,addrlen也填0。
accept函數等待用戶端的連接配接,如果沒有用戶端連上來,它就一直等待,這種方式稱之為阻塞。
accept等待到用戶端的連接配接後,建立一個新的套接字,函數傳回值就是這個新的套接字,服務端使用這個新的套接字和用戶端進行封包的收發。
accept在等待的過程中,如果被中斷或其它的原因,函數傳回-1,表示失敗,如果失敗,可以重新accept。
7、函數小結
服務端函數調用的流程是:socket->bind->listen->accept->recv/send->close
用戶端函數調用的流程是:socket->connect->send/recv->close
版權聲明
C語言技術網原創文章,轉載請說明文章的來源、作者和原文的連結。
來源:C語言技術網(www.freecplus.net)
作者:碼農有道
如果這篇文章對您有幫助,請點贊支援,或在您的部落格中轉發我的文章,謝謝!!!
如果文章有錯别字,或者内容有誤,或其他的建議或意見,請您留言指正,非常感謝!!!