socket核心定義:
socket是用來讓不同電腦之間,不同進程之間互相通訊的一套接口;是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層復雜的操作抽象為幾個簡單的接口供應用層調用已實現進程在網絡中通信。
socket起源於UNIX,在Unix一切皆文件哲學的思想下,socket是一種"打開—讀/寫—關閉"模式的實現,服務器和客戶端各自維護一個"文件",在建立連接打開后,可以向自己文件寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉文件。
socket支持TCP和UDP兩種協議:
TCP:對應流式socket,面向連接,即時刻保持連接,數據以流形式傳輸。
UDP:對應數據報式socket,無連接,數據中途可能丟失,但速度更快。
常用接口:
socket():創建socket
bind():綁定socket到本地地址和端口,通常由服務端調用
listen():TCP專用,開啟監聽模式
accept():TCP專用,服務器等待客戶端連接,一般是阻塞態
connect():TCP專用,客戶端主動連接服務器
send():TCP專用,發送數據
recv():TCP專用,接收數據
sendto():UDP專用,發送數據到指定的IP地址和端口
recvfrom():UDP專用,接收數據,返回數據遠端的IP地址和端口
closesocket():關閉socket
2.1 socket()
原型:int socket (int domain, int type, int protocol)
功能描述:初始化創建socket對象,通常是第一個調用的socket函數。 成功時,返回非負數的socket描述符;失敗是返回-1。socket描述符是一個指向內部數據結構的指針,它指向描述符表入口。調用socket()函數時,socket執行體將建立一個socket,實際上"建立一個socket"意味着為一個socket數據結構分配存儲空間。socket執行體為你管理描述符表。
參數解釋:
domain -- 指明使用的協議族。常用的協議族有,AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址
type -- 指明socket類型,有3種:
SOCK_STREAM -- TCP類型,保證數據順序及可靠性;
SOCK_DGRAM -- UDP類型,不保證數據接收的順序,非可靠連接;
SOCK_RAW -- 原始類型,允許對底層協議如IP或ICMP進行直接訪問,不太常用。
protocol -- 通常賦值"0",由系統自動選擇。
2.2 bind()
原型:int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen)
功能描述:將創建的socket綁定到指定的IP地址和端口上,通常是第二個調用的socket接口。返回值:0 -- 成功,-1 -- 出錯。當socket函數返回一個描述符時,只是存在於其協議族的空間中,並沒有分配一個具體的協議地址(這里指IPv4/IPv6和端口號的組合),bind函數可以將一組固定的地址綁定到sockfd上。
通常服務器在啟動的時候都會綁定一個眾所周知的協議地址,用於提供服務,客戶就可以通過它來接連服務器;而客戶端可以指定IP或端口也可以都不指定,未分配則系統自動分配。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
注意:
(1) 如果有多個可用的連接(多個IP),內核會根據優先級選擇一個IP作為源IP使用。
(2) 如果socket使用bind綁定到特定的IP和port,則無論是TCP還是UDP,都會從指定的IP和port發送數據。
參數解釋:
sockfd -- socket()函數返回的描述符;
myaddr -- 指明要綁定的本地IP和端口號,使用網絡字節序,即大端模式(詳見3.1)。
addrlen -- 常被設置為sizeof(struct sockaddr)。
可以利用下邊的賦值語句,自動綁定本地IP地址和隨機端口:
my_addr.sin_port = 0; /* 系統隨機選擇一個未被使用的端口號 */ my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本機IP地址 */
另外要注意的是,當調用函數時,一般不要將端口號置為小於1024的值,因為1~1024是保留端口號,你可以使用大於1024中任何一個沒有被占用的端口號。
2.3 listen()
原型:int listen(int sockfd, int backlog)
功能描述:listen()函數僅被TCP類型的服務器程序調用,實現監聽服務,所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字才會被“喚醒”來響應請求。
請求隊列
當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩沖區,待當前請求處理完畢后,再從緩沖區中讀取出來處理。如果不斷有新的請求進來,它們就按照先后順序在緩沖區中排隊,直到緩沖區滿。這個緩沖區,就稱為請求隊列(Request Queue)。
緩沖區的長度(能存放多少個客戶端請求)可以通過 listen() 函數的 backlog 參數指定,但究竟為多少並沒有什么標准,可以根據你的需求來定,並發量小的話可以是10或者20。
當請求隊列滿時,就不再接收新的請求,對於 Linux,客戶端會收到 ECONNREFUSED 錯誤
注意:listen() 只是讓套接字處於監聽狀態,並沒有接收請求。接收請求需要使用 accept() 函數。當套接字處於監聽狀態時,可以通過 accept() 函數來接收客戶端請求。
listen()成功時返回0,錯誤時返回-1。
參數解釋:
sockfd -- socket()函數返回的描述符;
backlog -- 指定內核為此套接字維護的最大連接個數,包括“未完成連接隊列--未完成3次握手”、“已完成連接隊列--已完成3次握手,建立連接”。大多數系統缺省值為20。
2.4 accept()
原型: int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen)
功能描述:accept()函數僅被TCP類型的服務器程序調用,從已完成連接隊列返回下一個建立成功的連接,如果已完成連接隊列為空,線程進入阻塞態睡眠狀態。成功時返回套接字描述符,錯誤時返回-1。
如果accpet()執行成功,返回由內核自動生成的一個全新socket描述符,用它引用與客戶端的TCP連接。通常我們把accept()第一個參數成為監聽套接字(listening socket),把accept()功能返回值成為已連接套接字(connected socket)。一個服務器通常只有1個監聽套接字,監聽客戶端的連接請求;服務器內核為每一個客戶端的TCP連接維護1個已連接套接字,用它實現數據雙向通信。
最后需要說明的是:listen() 只是讓套接字進入監聽狀態,並沒有真正接收客戶端請求,listen() 后面的代碼會繼續執行,直到遇到 accept()。accept() 會阻塞程序執行(后面代碼不能被執行),直到有新的請求到來。
參數解釋:
sockfd -- socket()函數返回的描述符;
addr -- 輸出一個的sockaddr_in變量地址,該變量用來存放發起連接請求的客戶端的協議地址;
addrten -- 作為輸入時指明緩沖器的長度,作為輸出時指明addr的實際長度。
2.5 connetct()
原型: int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
功能描述:connect()通常由TCP類型客戶端調用,用來與服務器建立一個TCP連接,實際是發起3次握手過程,連接成功返回0,連接失敗返回1。
注意:
(1) 可以在UDP連接使用使用connect(),作用是在UDP套接字中記住目的地址和目的端口。
(2) UDP套接字使用connect后,如果數據報不是connect中指定的地址和端口,將被丟棄。沒有調用connect的UDP套接字,將接收所有到達這個端口的UDP數據報,而不區分源端口和地址。
參數解釋:
sockfd -- 本地客戶端額socket描述符;
serv_addr -- 服務器協議地址;
addrlen -- 地址緩沖區的長度。
2.6 send()
原型:int send(int sockfd, const void *msg, int len, int flags)
功能描述:TCP類型的數據發送。
每個TCP套接口都有一個發送緩沖區,它的大小可以用SO_SNDBUF這個選項來改變。調用send函數的過程,實際是內核將用戶數據拷貝至TCP套接口的發送緩沖區的過程:並不立即向網絡中傳輸數據,而是先將數據寫入緩沖區中,再由TCP協議將數據從緩沖區發送到目標機器。一旦將數據寫入到緩沖區,函數就可以成功返回,不管它們有沒有到達目標機器,也不管它們何時被發送到網絡,這些都是TCP協議負責的事情。
若len大於發送緩沖區大小,則返回-1;否則,查看緩沖區剩余空間是否容納得下要發送的len長度,若不夠,則拷貝一部分,並返回拷貝長度(指的是非阻塞send,若為阻塞send,則一定等待所有數據拷貝至緩沖區才返回,因此阻塞send返回值必定與len相等);若緩沖區滿,則等待發送,有剩余空間后拷貝至緩沖區;若在拷貝過程出現錯誤,則返回-1。關於錯誤的原因,查看errno的值。
如果send在等待協議發送數據時出現網絡斷開的情況,則會返回-1。注意:send成功返回並不代表對方已接收到數據,如果后續的協議傳輸過程中出現網絡錯誤,下一個send便會返回-1發送錯誤。TCP給對方的數據必須在對方給予確認時,方可刪除發送緩沖區的數據。否則,會一直緩存在緩沖區直至發送成功(TCP可靠數據傳輸決定的)。
參數解釋:
sockfd -- 發送端套接字描述符(非監聽描述符)。
msg -- 待發送數據的緩沖區。
len -- 待發送數據的字節長度。
flags -- 一般情況下置為0。
2.7 recv()
原型:int recv(int sockfd, void *buf, int len, unsigned int flags)
功能描述:TCP類型的數據接收。
recv()從接收緩沖區拷貝數據。成功時,返回拷貝的字節數,失敗返回-1。阻塞模式下,recv/recvfrom將會阻塞到緩沖區里至少有一個字節(TCP)/至少有一個完整的UDP數據報才返回,沒有數據時處於休眠狀態。若非阻塞,則立即返回,有數據則返回拷貝的數據大小,否則返回錯誤-1。
參數解釋:
sockefd -- 接收端套接字描述符(非監聽描述符);
buf -- 接收緩沖區的基地址;
len -- 以字節計算的接收緩沖區長度;
flags -- 一般情況下置為0。
2.8 sendto()
原型:int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *dst_addr, int addrlen)
功能描述:用於非可靠連接(UDP)的數據發送,因為UDP方式未建立連接socket,因此需要制定目的協議地址。
當本地與不同目的地址通信時,只需指定目的地址,可使用同一個UDP套接口描述符sockfd,而TCP要預先建立連接,每個連接都會產生不同的套接口描述符,體現在:客戶端要使用不同的fd進行connect,服務端每次accept產生不同的fd。
因為UDP沒有真正的發送緩沖區,因為是不可靠連接,不必保存應用進程的數據拷貝,應用進程中的數據在沿協議棧向下傳遞時,以某種形式拷貝到內核緩沖區,當數據鏈路層把數據傳出后就把內核緩沖區中數據拷貝刪除。因此它不需要一個發送緩沖區。寫UDP套接口的sendto/write返回表示應用程序的數據或數據分片已經進入鏈路層的輸出隊列,如果輸出隊列沒有足夠的空間存放數據,將返回錯誤ENOBUFS.
參數解釋:
sockfd -- 發送端套接字描述符(非監聽描述符);
msg -- 待發送數據的緩沖區;
len -- 待發送數據的字節長度;
flags -- 一般情況下置為0;
dst_addr -- 數據發送的目的地址;
addrlen -- 地址長度。
2.9 recvfrom()
原型:int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, int*fromlen)
功能描述:用於非可靠連接(UDP)的數據接收。
參數解釋:
sockfd -- 接收端套接字描述;
buf -- 用於接收數據的應用緩沖區地址;
len -- 指名緩沖區大小;
flags -- 通常為0;
src_addr -- 數據來源端的地址;
fromlen -- 作為輸入時,fromlen常置為sizeof(struct sockaddr);當輸出時,fromlen包含實際存入buf中的數據字節數。
代碼參考
服務端;
#include "stdafx.h" #include <stdio.h> #include <winsock2.h> #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]) { //初始化WSA WORD sockVersion = MAKEWORD(2,2); WSADATA wsaData; if(WSAStartup(sockVersion, &wsaData)!=0) { return 0; } //創建套接字 SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(slisten == INVALID_SOCKET) { printf("socket error !"); return 0; } //綁定IP和端口 sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(8888); sin.sin_addr.S_un.S_addr = INADDR_ANY; if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) { printf("bind error !"); } //開始監聽 if(listen(slisten, 5) == SOCKET_ERROR) { printf("listen error !"); return 0; } //循環接收數據 SOCKET sClient; sockaddr_in remoteAddr; int nAddrlen = sizeof(remoteAddr); char revData[255]; while (true) { printf("等待連接...\n"); sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen); if(sClient == INVALID_SOCKET) { printf("accept error !"); continue; } printf("接受到一個連接:%s \r\n", inet_ntoa(remoteAddr.sin_addr)); //接收數據 int ret = recv(sClient, revData, 255, 0); if(ret > 0) { revData[ret] = 0x00; printf(revData); } //發送數據 char * sendData = "你好,TCP客戶端!\n"; send(sClient, sendData, strlen(sendData), 0); closesocket(sClient); } closesocket(slisten); WSACleanup(); return 0; }
客戶端:
#include "stdafx.h" #include <WINSOCK2.H> #include <STDIO.H> #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]) { WORD sockVersion = MAKEWORD(2,2); WSADATA data; if(WSAStartup(sockVersion, &data) != 0) { return 0; } SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sclient == INVALID_SOCKET) { printf("invalid socket !"); return 0; } sockaddr_in serAddr; serAddr.sin_family = AF_INET; serAddr.sin_port = htons(8888); serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) { printf("connect error !"); closesocket(sclient); return 0; } char * sendData = "你好,TCP服務端,我是客戶端!\n"; send(sclient, sendData, strlen(sendData), 0); char recData[255]; int ret = recv(sclient, recData, 255, 0); if(ret > 0) { recData[ret] = 0x00; printf(recData); } closesocket(sclient); WSACleanup(); return 0; }