socket通訊基礎

各位兄弟,在學習Linux編程基礎以前,必定要先學習Linux基礎知識和計算機網絡基礎知識,若是對這兩方面的基礎知識和基本概念不熟,談不上Linux編程和網絡通訊編程。程序員

1、socket通訊的概念

socket也稱做「套接字」,描述了計算機的IP地址和端口,運行在計算機中的程序之間採用socket進行數據通訊。通訊的兩端都有socket,它是一個通道,數據在兩個socket之間進行傳輸。編程

socket把複雜的TCP/IP協議族隱藏在socket接口後面,對程序員來講,只要用好socket相關的函數,就能夠完成數據通訊。數組

2、套接字(socket)

TCP提供了流(stream)和數據報(datagram)兩種通訊機制,因此套接字也分爲流套接字和數據報套接字。服務器

流套接字的類型是SOCK_STREAM,它提供的是一個有序、可靠、雙向字節流的鏈接,所以發送的數據能夠確保不會丟失、重複或亂序到達,並且它還有出錯後從新發送的機制(就像兩我的在打電話,聊天您一句我一句,有來有往,沒聽清楚就再說一次)。網絡

數據報套接的類型是SOCK_DGRAM,它不須要創建和維持一個鏈接,採用UDP/IP協議實現。它對能夠發送的數據的長度有限制,數據報做爲一個單獨的網絡消息被傳輸,它可能會丟失、複製或錯亂到達,UDP不是一個可靠的協議,可是它的速度比較高,由於它不須要創建和維持鏈接(就像一我的向另外一我的發短信,一條短信發出去,對方不必定能收到)。數據結構

在實際開發中,數據報套接字(即UDP)的應用場景極少,本章節只介紹流套接字。dom

3、socket通訊的過程

1)服務端程序將一個套接字綁定到指定的ip地址和端口,並經過此套接字等待和監聽客戶的鏈接請求。socket

2)客戶程序向服務端程序綁定的地址和端口發出鏈接請求。函數

3)服務端接受鏈接請求。學習

4)客戶端和服務端經過讀寫套接字進行通訊。

在這裏插入圖片描述

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,服務端啓動後,進入等待客戶端鏈接狀態,而後啓動客戶端。

客戶端的輸出以下:

在這裏插入圖片描述
服務端的輸出以下:

在這裏插入圖片描述

5、注意事項

一、別去糾纏細節

在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);  // 通訊端口

四、客戶端程序指定服務端的ip地址

struct hostent* h; // ip地址信息的數據結構
if ( (h = gethostbyname("192.168.149.129")) == 0 )
{ perror("gethostbyname"); close(sockfd); return -1; }

五、客戶端程序指定服務端的通訊端口

servaddr.sin_port = htons(5000);

六、send函數

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函數

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,一個是用於監聽的socket,還有一個就是客戶端鏈接成功後,由accept函數建立的用於與客戶端收發報文的socket。

九、程序退出時先關閉socket

socket是系統資源,操做系統打開的socket數量是有限的,在程序退出以前必須關閉已打開的socket,就像關閉文件指針同樣,就像delete已分配的內存同樣,極其重要。

值得注意的是,關閉socket的代碼不能只在main函數的最後,那是程序運行的理想狀態,還應該在main函數的每一個return以前關閉。

6、相關的庫函數

一、socket函數

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函數通常不會返回失敗。

二、gethostbyname函數

把ip地址或域名轉換爲hostent 結構體表達的地址。

函數聲明:

struct hostent *gethostbyname(const char *name);

參數name,域名或者主機名,例如"192.168.1.3"、"www.freecplus.net"等。

返回值:若是成功,返回一個hostent結構指針,失敗返回NULL。

gethostbyname只用於客戶端。

gethostbyname只是把字符串的ip地址轉換爲結構體的ip地址,只要地址格式沒錯,通常不會返回錯誤。函數失敗不會設置errno的值。

三、connect函數

向服務器發起鏈接請求。

函數聲明:

int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);

函數說明:connect函數用於將參數sockfd 的socket 連至參數serv_addr
指定的服務端,參數addrlen爲sockaddr的結構長度。

返回值:成功則返回0, 失敗返回-1, 錯誤緣由存於errno 中。

connect函數只用於客戶端。

若是服務端的地址錯了,或端口錯了,或服務端沒有啓動,connect必定會失敗。

四、bind函數

服務端把用於通訊的地址和端口綁定到socket上。

函數聲明:

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

參數sockfd,須要綁定的socket。

參數addr,存放了服務端用於通訊的地址和端口。

參數addrlen表示addr結構體的大小。

若是綁定的地址錯誤,或端口已被佔用,bind函數必定會報錯,不然通常不會返回錯誤。

五、listen函數

listen函數把主動鏈接套接字變爲被動鏈接的套接字,使得這個socket能夠接受其它socket的鏈接請求,從而成爲一個服務端的socket。

函數聲明:

int listen(int sockfd, int backlog);

返回:0-成功, -1-失敗

參數sockfd是已經被bind過的套接字。socket函數返回的套接字是一個主動鏈接的套接字,在服務端的編程中,程序員但願這個套接字能夠接受外來的鏈接請求,也就是被動等待客戶端來鏈接。因爲系統默認時認爲一個套接字是主動鏈接的,因此須要經過某種方式來告訴系統,程序員經過調用listen函數來完成這件事。

參數backlog,這個參數涉及到一些網絡的細節,比較麻煩,填五、10都行,通常不超過30。

當調用listen以後,服務端的套接字就能夠調用accept來接受客戶端的鏈接請求。

listen函數通常不會返回錯誤。

六、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。

七、函數小結

服務端函數調用的流程是:socket->bind->listen->accept->recv/send->close

客戶端函數調用的流程是:socket->connect->send/recv->close

其中send/recv能夠進行屢次交互。

7、課後做業

1)把book241.cpp和book242.cpp抄下來,編譯運行,試試修改參數再運行。

2)book241.cpp和book242.cpp程序中,有些代碼不能動,有些代碼能夠動,把能動的都動一下,就算是抄代碼,也要抄個明白。

3)服務端的accept函數會阻塞,阻塞是專業名詞,即等待,能夠用代碼測試一下。

4)不論是服務端仍是客戶端recv函數也會阻塞,能夠用代碼測試一下。

5)修改book241.cpp和book242.cpp,實現點對點的聊天功能,用戶在客戶端輸入一個字符串,而後發送給服務端,服務端收到客戶端的報文後,也提示用戶輸入一個字符串,返回給客戶端,若是服務端收到客戶端的報文是「bye」通訊結束。

6)若是以上做業都能完成,建議再把本文章的內容再看一次,對文章開始部分的理論知識將有新的理解。

8、版權聲明

C語言技術網原創文章,轉載請說明文章的來源、做者和原文的連接。
來源:C語言技術網(www.freecplus.net)
做者:碼農有道

若是文章有錯別字,或者內容有錯誤,或其餘的建議和意見,請您留言指正,很是感謝!!!

相關文章
相關標籤/搜索