各位兄弟,在學習Linux編程基礎以前,必定要先學習Linux基礎知識和計算機網絡基礎知識,若是對這兩方面的基礎知識和基本概念不熟,談不上Linux編程和網絡通訊編程。程序員
socket也稱做「套接字」,描述了計算機的IP地址和端口,運行在計算機中的程序之間採用socket進行數據通訊。通訊的兩端都有socket,它是一個通道,數據在兩個socket之間進行傳輸。編程
socket把複雜的TCP/IP協議族隱藏在socket接口後面,對程序員來講,只要用好socket相關的函數,就能夠完成數據通訊。數組
TCP提供了流(stream)和數據報(datagram)兩種通訊機制,因此套接字也分爲流套接字和數據報套接字。服務器
流套接字的類型是SOCK_STREAM,它提供的是一個有序、可靠、雙向字節流的鏈接,所以發送的數據能夠確保不會丟失、重複或亂序到達,並且它還有出錯後從新發送的機制(就像兩我的在打電話,聊天您一句我一句,有來有往,沒聽清楚就再說一次)。網絡
數據報套接的類型是SOCK_DGRAM,它不須要創建和維持一個鏈接,採用UDP/IP協議實現。它對能夠發送的數據的長度有限制,數據報做爲一個單獨的網絡消息被傳輸,它可能會丟失、複製或錯亂到達,UDP不是一個可靠的協議,可是它的速度比較高,由於它不須要創建和維持鏈接(就像一我的向另外一我的發短信,一條短信發出去,對方不必定能收到)。數據結構
在實際開發中,數據報套接字(即UDP)的應用場景極少,本章節只介紹流套接字。dom
1)服務端程序將一個套接字綁定到指定的ip地址和端口,並經過此套接字等待和監聽客戶的鏈接請求。socket
2)客戶程序向服務端程序綁定的地址和端口發出鏈接請求。函數
3)服務端接受鏈接請求。學習
4)客戶端和服務端經過讀寫套接字進行通訊。
在TCP/IP網絡應用中,兩個程序之間通訊模式是客戶/服務端模式(client/server),客戶/服務端也叫做客戶/服務器,各人習慣。
1)建立服務端的socket。
2)把服務端用於通訊的地址和端口綁定到socket上。
3)把socket設置爲監聽模式。
4)接受客戶端的鏈接。
5)與客戶端通訊,接收客戶端發過來的報文後,回覆處理結果。
6)不斷的重複第5)步,直到客戶端斷開鏈接。
7)關閉socket,釋放資源。
服務端示例(book242.cpp)
/* * 程序名:book242.cpp,此程序用於演示socket通訊的服務端 * 做者:C語言技術網(www.freecplus.net) 日期:20190525 */ #include "_public.h" int main() { // 第1步:建立服務端的socket。 int listenfd = socket(AF_INET,SOCK_STREAM,0); // 第2步:把服務端用於通訊的地址和端口綁定到socket上。 struct sockaddr_in servaddr; // 服務端地址信息的數據結構。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; // 協議族,在socket編程中只能是AF_INET。 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。 //servaddr.sin_addr.s_addr = inet_addr("118.89.50.198"); // 指定ip地址。 servaddr.sin_port = htons(5051); // 指定通訊端口。 if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ) { perror("bind"); close(listenfd); return -1; } // 第3步:把socket設置爲監聽模式。 if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; } // 第4步:接受客戶端的鏈接。 int clientfd; // 客戶端的socket。 int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小 struct sockaddr_in clientaddr; // 客戶端的地址信息。 clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen); printf("客戶端(%s)已鏈接。\n",inet_ntoa(clientaddr.sin_addr)); // 第5步:與客戶端通訊,接收客戶端發過來的報文後,回覆ok。 char buffer[1024]; while (1) { memset(buffer,0,sizeof(buffer)); if (recv(clientfd,buffer,sizeof(buffer),0)<=0) break; // 接收客戶端的請求報文。 printf("接收:%s\n",buffer); strcpy(buffer,"ok"); if (send(clientfd,buffer,strlen(buffer),0)<=0) break; // 向客戶端發送響應結果。 printf("發送:%s\n",buffer); } // 第6步:關閉socket,釋放資源。 close(listenfd); close(clientfd); }
1)建立客戶端的socket。
2)向服務器發起鏈接請求。
3)與服務端通訊,發送一個報文後等待回覆,而後再發下一個報文。
4)不斷的重複第3)步,直到所有的數據被髮送完。
5)第4步:關閉socket,釋放資源。
客戶端示例(book241.cpp)
/* * 程序名:book241.cpp,此程序用於演示socket的客戶端 * 做者:C語言技術網(www.freecplus.net) 日期:20190525 */ #include "_public.h" int main() { // 第1步:建立客戶端的socket。 int sockfd = socket(AF_INET,SOCK_STREAM,0); // 第2步:向服務器發起鏈接請求。 struct hostent* h; if ( (h = gethostbyname("118.89.50.198")) == 0 ) // 指定服務端的ip地址。 { perror("gethostbyname"); close(sockfd); return -1; } struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5051); // 指定服務端的通訊端口。 memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) // 向服務端發起鏈接清求。 { perror("connect"); close(sockfd); return -1; } char buffer[1024]; // 第3步:與服務端通訊,發送一個報文後等待回覆,而後再發下一個報文。 for (int ii=0;ii<3;ii++) { memset(buffer,0,sizeof(buffer)); sprintf(buffer,"這是第%d個超級女生,編號%03d。",ii+1,ii+1); if (send(sockfd,buffer,strlen(buffer),0)<=0) break; // 向服務端發送請求報文。 printf("發送:%s\n",buffer); memset(buffer,0,sizeof(buffer)); if (recv(sockfd,buffer,sizeof(buffer),0)<=0) break; // 接收服務端返回的結果。 printf("接收:%s\n",buffer); } // 第4步:關閉socket,釋放資源。 close(sockfd); }
在運行程序以前,必須保證服務器的防火牆已經開通了網絡訪問策略,若是您不明這句話的意思,說明您的Linux基礎知識不夠,請先學習Linux基礎知識以後再來學習socket通訊。
先啓動服務端程序book242,服務端啓動後,進入等待客戶端鏈接狀態,而後啓動客戶端。
客戶端的輸出以下:
服務端的輸出以下:
在socket通訊的客戶端和服務器的程序裏,出現了多種數據結構,調用了多個函數,涉及到不少方面的知識,對初學者來講,更重要的是瞭解socket通訊的過程、每段代碼的用途和函數調用的功能,不要去糾纏這些結構體和函數的參數,這些函數和參數雖然比較多,但能夠修改的很是少,別抄錯就能夠了,須要注意的地方我會提出。
若是服務器有多個網卡,多個IP地址,socket通訊能夠指定用其中一個地址來進行通訊,也能夠任意ip地址。
1)指定ip地址的代碼
m_servaddr.sin_addr.s_addr = inet_addr("192.168.149.129"); // 指定ip地址
2)任意ip地址的代碼
m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本主機的任意ip地址
在實際開發中,採用任意ip地址的方式比較多。
m_servaddr.sin_port = htons(5000); // 通訊端口
struct hostent* h; // ip地址信息的數據結構 if ( (h = gethostbyname("192.168.149.129")) == 0 ) { perror("gethostbyname"); close(sockfd); return -1; }
servaddr.sin_port = htons(5000);
send函數用於把數據經過socket發送給對端。不管是客戶端仍是服務端,應用程序都用send函數來向TCP鏈接的另外一端發送數據。
函數聲明:
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),表示通訊鏈路已不可用。
recv函數用於接收對端socket發送過來的數據。
recv函數用於接收對端經過socket發送過來的數據。不管是客戶端仍是服務端,應用程序都用recv函數接收來自TCP鏈接的另外一端發送過來數據。
函數聲明:
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),表示通訊通道已不可用。
對服務端來講,有兩個socket,一個是用於監聽的socket,還有一個就是客戶端鏈接成功後,由accept函數建立的用於與客戶端收發報文的socket。
socket是系統資源,操做系統打開的socket數量是有限的,在程序退出以前必須關閉已打開的socket,就像關閉文件指針同樣,就像delete已分配的內存同樣,極其重要。
值得注意的是,關閉socket的代碼不能只在main函數的最後,那是程序運行的理想狀態,還應該在main函數的每一個return以前關閉。
socket函數用於建立一個新的socket,也就是向系統申請一個socket資源。socket函數用戶客戶端和服務端。
函數聲明:
int socket(int domain, int type, int protocol);
參數說明:
domain:協議域,又稱協議族(family)。經常使用的協議族有AF_INET、AF_INET六、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函數通常不會返回失敗。
把ip地址或域名轉換爲hostent 結構體表達的地址。
函數聲明:
struct hostent *gethostbyname(const char *name);
參數name,域名或者主機名,例如"192.168.1.3"、"www.freecplus.net"等。
返回值:若是成功,返回一個hostent結構指針,失敗返回NULL。
gethostbyname只用於客戶端。
gethostbyname只是把字符串的ip地址轉換爲結構體的ip地址,只要地址格式沒錯,通常不會返回錯誤。函數失敗不會設置errno的值。
向服務器發起鏈接請求。
函數聲明:
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
函數說明:connect函數用於將參數sockfd 的socket 連至參數serv_addr
指定的服務端,參數addrlen爲sockaddr的結構長度。
返回值:成功則返回0, 失敗返回-1, 錯誤緣由存於errno 中。
connect函數只用於客戶端。
若是服務端的地址錯了,或端口錯了,或服務端沒有啓動,connect必定會失敗。
服務端把用於通訊的地址和端口綁定到socket上。
函數聲明:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
參數sockfd,須要綁定的socket。
參數addr,存放了服務端用於通訊的地址和端口。
參數addrlen表示addr結構體的大小。
若是綁定的地址錯誤,或端口已被佔用,bind函數必定會報錯,不然通常不會返回錯誤。
listen函數把主動鏈接套接字變爲被動鏈接的套接字,使得這個socket能夠接受其它socket的鏈接請求,從而成爲一個服務端的socket。
函數聲明:
int listen(int sockfd, int backlog);
返回:0-成功, -1-失敗
參數sockfd是已經被bind過的套接字。socket函數返回的套接字是一個主動鏈接的套接字,在服務端的編程中,程序員但願這個套接字能夠接受外來的鏈接請求,也就是被動等待客戶端來鏈接。因爲系統默認時認爲一個套接字是主動鏈接的,因此須要經過某種方式來告訴系統,程序員經過調用listen函數來完成這件事。
參數backlog,這個參數涉及到一些網絡的細節,比較麻煩,填五、10都行,通常不超過30。
當調用listen以後,服務端的套接字就能夠調用accept來接受客戶端的鏈接請求。
listen函數通常不會返回錯誤。
服務端接受客戶端的鏈接。
函數聲明:
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。
服務端函數調用的流程是:socket->bind->listen->accept->recv/send->close
客戶端函數調用的流程是:socket->connect->send/recv->close
其中send/recv能夠進行屢次交互。
1)把book241.cpp和book242.cpp抄下來,編譯運行,試試修改參數再運行。
2)book241.cpp和book242.cpp程序中,有些代碼不能動,有些代碼能夠動,把能動的都動一下,就算是抄代碼,也要抄個明白。
3)服務端的accept函數會阻塞,阻塞是專業名詞,即等待,能夠用代碼測試一下。
4)不論是服務端仍是客戶端recv函數也會阻塞,能夠用代碼測試一下。
5)修改book241.cpp和book242.cpp,實現點對點的聊天功能,用戶在客戶端輸入一個字符串,而後發送給服務端,服務端收到客戶端的報文後,也提示用戶輸入一個字符串,返回給客戶端,若是服務端收到客戶端的報文是「bye」通訊結束。
6)若是以上做業都能完成,建議再把本文章的內容再看一次,對文章開始部分的理論知識將有新的理解。
C語言技術網原創文章,轉載請說明文章的來源、做者和原文的連接。
來源:C語言技術網(www.freecplus.net)
做者:碼農有道
若是文章有錯別字,或者內容有錯誤,或其餘的建議和意見,請您留言指正,很是感謝!!!