26、TCP服務器原理


TCP / IP的工作

  TCP / IP是Internet上使用的網絡協議。它是協議,ESP32本身自帶了TCP/IP協議,所以,我們只需了解並學會運用即可。

  首先,有IP地址。這是一個32位值,應該是唯一的每個設備連接到互聯網。一個32位的值可以被認為一個的 的四個不同的8位值(4-×8 = 32)。由於我們可以表示一個8位的數目為0到255之間的數值,我們通常代表與符號的IP地址:

<數字> <數> <數> <數>例如173.194.64.102。

  這些IP address不常用作的應用程序輸入。取而代之的是文本名稱鍵入如“ google.com , 但不要被誤導,這些名字是在TCP / IP的IL延髓水平。所有的工作都與32位的IP地址有一種映射。

  需要一個名稱(例如,“ google.com ”)來檢索其對應的IP地址。 該技術,這就是所謂的“域名系統”或DNS。

  當我們學習TCP / IP的,其實有三個不同的協議在這里。 第一個是IP(互聯網協議)。這是下面的傳輸層數據報傳遞協議。再其上面的IP層是TCP(傳輸控制協議),其提供的在是無連接的IP協議的連接。最后是UDP(用戶數據報協議),其在IP協議之上,並提供數據報在應用程序之間(無連接)傳輸。當我們說TCP / IP, 我們並不是說的剛才講的在IP上運行TCP,但可看出作為一個核心協議,該協議是IP,TCP和UDP和其他相關應用水平協議,如DNS,HTTP,FTP,Telnet及更多。

 

輕量級IP協議棧 - LWIP

  如果我們認為TCP / IP作為一種協議,那么我們就可以結束我們的理解聯網成兩個不同的層。

一個是負責硬件層:從一個地方到另一個地方獲得的1個0的流 。對於常見的實現包括以太網,令牌環...這是由從設備物理線路特點。無線網絡本身就是一個傳輸層。

一旦我們可以發送和接收數據,一個新的水平就在該數據物理網絡上建立起來了,這便是TCP/IP通訊,它提供了硬件中的數據傳輸規則,但是TCP / IP是一個大的協議,它包含大量的部件。Espressif中為我們綜合了LwIP輕便式通訊協議技術以方便開發,提供的LwIP包括下列服務:

•               IP

•               ICMP

•               IGMP

•               MLD

•               ND

•               UDP

•               TCP

•              sockets API

•               DNS

 

TCP

  TCP連接,通過該協議數據可以在兩個方向上流動,在連接建立之前,它是被動監聽傳入的連接請求。連接的另一方負責啟動連接,它主動請求連接形成。一旦連接形成,兩邊都可以發送和接受數據,為了“客戶端”請求連接,它必須知道的地址信息,供服務器監聽。 這個地址有兩個不同的部分。第一部分是服務器是IP地址和第二部分是特定的“端口號”。我們無法看到一個ESP32如何設置自己為一個偵聽傳入的TCP / IP連接,這就要求我們開始了解的重要socket  API

TCP連接過程

  TCP連接過程需要三次交互才能完成,如下圖所示:

 

  首先客戶端向服務器發售那個一個SYN報文段指明客戶端打算連接的服務器端口,以及出生序號,服務器發回包含服務器初始序號的SYN報文段作為應答,接着,客戶端對服務器的SYN報文段進行確認。這三次報文段完成連接的過程,稱為三次握手。

 TCP關閉過程

  終止一個連接需要四次握手,如下圖所示

 

 

   產生四次握手的原因是由於TCP的半關閉造成的,既然一個TCP連接是全雙工的,那么每個方向必須單獨的進行關閉,原則就是當一方完成它的數據發送過程后就能發送一個FIN來終止這個方向連接,當一端收到一個FIN,他必須通知應用層另一端已經終止了哪個方向的數據傳送。發送FIN通常是應用層進行關閉的結果,收到一個FIN只意味着這一方向沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。

TCP/IP Sockets(對於詳細的socker API可看我的這篇隨筆: SOCKET API)

  TCP/IP socker API是一個編程接口,它是網絡編程中最重要的API,其根據不同模式的編程風格不同:

 

對於TCP服務器是通過建立:

1.創建TCP套接字

2.關聯本地端口與插座

3.設置 套接字監聽模式

4.接受來自客戶端的新連接

5.接收和發送數據

6.關閉客戶機/服務器連接

7.返回到步驟4

 

對於TCP客戶端建立:

1.創建TCP套接字

2.連接到TCP服務器

3.發送數據/接收數據

4.關閉連接

 

其編程模型如下所示:

 

 

socket API頭定義中可以找到 <LWIP / sockets.h> 。對於客戶端和服務器,創建套接字的任務是一樣的,調用 

int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

返回 的sock 是用來指向套接字的整數句柄。

 

  當我們創建了一個服務器端的socket,我們希望它監聽傳入連接要求。要做到這一點,我們需要告訴socket 哪個TCP/IP端口他需要監聽(注意,我們並不提供端口類型是int還是short),我們通過調用htons()函數提供類型,它的功能是將數據轉換為我們的網絡字節順序,在互聯網上多字節的二進制數據實際是“大端的格式,如9876(Decima的 升),那么它以二進制表示為00100110 10010100或0x26D4的十六進制。對於網絡字節傳輸順序,我們首先傳送10010100(0xD4),再傳輸00100110(0×26),而ESP32是一個 小端機體系結構,這意味着我們必須改造2字節和4張字節數為網絡字節順序(big endian)的。

  在給定的裝置中,在一個時間只有一個應用程序可以使用給定的本地端口,如果我們想端口關聯與應用,我們可以調用bind()函數來完成,下面給出一個實例:

struct sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddress.sin_port = htons(portNumber);
bind(sock, (struct sockaddr *)&serverAddress, sizeof(serverAddress));

  現在socket已經和相關的接口連接起來了,我們下一步就要開始調用listen()函數來監聽輸入的數據,listen() 接口函數看到如下:

listen(sock, backlog)

  這里的backlog是,當我們監聽的接口上esp32發生多次請求時,由於無法立即處理便會產生backlog(積壓),這里是對積壓值進行設定,當發送請求的數目大於誰的那個的backlog數時,ESP32便不會將這個請求放入積壓隊列中,而是立刻拒絕這個請求,這樣不僅防止了是空的資源消耗再服務器上,也可以作為指示給調用者。從服務器的角度來看,我們還需要做一些 工作,當服務器正在處理一個客戶端的請求時,此時另外一個客戶端也發清了對端口的請求,此時,accept() API即可調用解決這個問題,當accept()被調用時,下面兩中情況中的一種可能發生:如果沒有客戶端連接等待者,我們將阻塞等待直到客戶端連接到來。另一種情況是,如果已經有一個客戶端在那等待鏈接了,我們將立刻處理連接。這輛中情況的區別在於我們是否需要等待連接到來。

  API調用示例如下:

struct sockaddr_in clientAddress;
socklen_t clientAddressLength = sizeof(clientAddress);
int clientSock = accept(sock, (struct sockaddr *)&clientAddress,
&clientAddressLength);

  需要關注的是,從accpt()返回的是一個新的socket(整數句柄)。

  和所有的TCP連接是相似的,連接是對稱和雙向的,這意味着,不再具有客戶端服務器的概念,雙方都可以發送和接收,不同的是,我們沒有必要調用bind()/listen()/accept()

struct sockaddr_in clientAddress;
socklen_t clientAddressLength = sizeof(clientAddress);
int clientSock = accept(sock, (struct sockaddr *)&clientAddress,
&clientAddressLength);

 SOCKET系列函數

  1、socket函數:函數功能是打開網絡通信接口,為了執行I/O操作,第一件要做的事情是調用socket函數,socket函數的原型如下:

socket(int falmily, int type, int protocol);

 這里family指明協議簇,取值如表所示:

 

 

 

 

通常情況下我們都是用AF_INET,但是IPv6大規模的普及,AF_INET6取值也會廣泛用到,在某些程序中,可能還會看到PF_INET等以PF為前綴的宏,最早的時候定義AF_表示地址簇,PF表示協議簇,但是現在PF已經很少使用了。

  type指明了套接字的類型,取值如下表:

通常使用SOCK_STREAM 和SOCK_DRGRAM取值,當使用TCP或者SCTP時,就取SOCK_STREAM,當使用UDP時就用SOCK_DGRAM。

  

  protocol參數指明協議類型,取值如下表所示:

   

  socket函數在成功時返回一個小的非負整數,他和文件描述符類似,我們稱為套接字描述符。

 

  2、connect函數:TCP客戶端用來和TCP服務器建立連接的,原型如下:

int connect(int sockfd, const struct sockaddr *servaddr,socklen_t  addrlen);

  sockfd參數是由socket函數返回的套接字描述符。

  aervaddr參數是需要連接的遠端的服務器的地址信息。

  addrlen參數則是servaddr的字節大小。

  connect()連接成功返回0,出錯返回-1,返回-1后可以獲取錯誤碼得到具體的失敗的原因。當TCP調用connect函數后,就會觸發一個三次握手過程,這里強調TCP的元嬰是因為使用UDP的時候也可以調用connect函數。

  3、bind函數:把一個本地協議地址賦予一個套接字,更簡單點來說,就是將本地IP地址和端口與套接字綁定在一起。

   bind函數原型如下:

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

 

   sockfd參數是由socket函數返回的套接字描述符。

  myaddr參數是本地的需要綁定的地址信息。

  addrlen參數則是myaddr的字節大小。

bind()成功返回0,失敗返回-1。

bind函數會應為myaddr參數設置不同情況,表現出不同的行為:

1)TCP服務器端。服務器端不指定端口的情況非常少見,應為客戶端需要知道服務器的端口號,如果不指定的話,內核會指定一個臨時端口,通過一些其他措施來通知客戶端自己的端口號,如果不指定的話,內核會指定一個臨時端口,在RPC(Remote Procedure Call,遠過程調用)服務器中就會不指定端口,通過一些其他措施來通知客戶端自己的端口,服務器端不指定地址的話,內核就會把客戶端發送給SYN時攜帶的目的IP地址作為服務器的源地址。如果制定IP地址的話,那么服務器就只接收這個IP地址的數據。

2)TCP客戶端。客戶端一般不需要調用bind函數,在這種情況下,內核會根據外出接口綁定一個IP地址,並臨時指定一個端口。如果調用bind函數的話,那么就會使用制定的IP或者端口。

3)UDP服務器端:服務器端不指定IP地址,套接口會接收到達它綁定端口的任何UDP數據報。並以數據報的外出接口的主IP地址為源IP地址,以接收到的源IP地址作為它的目的IP地址發回應答。當指定定本機IP地址,這就限制了套接口只接收到達它綁定端口並且目的地址為此IP地址的UDP數據報。並以綁定的IP地址作為源IP地址,以接收的源IP地址作為它的目的IP地址發回應答。

4)UDP客戶端。和TCP客戶端的行為類似,若UDP客戶端未綁定IP地址,當它調用sendto時內核會根據外出接口給它綁定一個IP地址和一個臨時端口號,若UDP客戶端綁定了IP地址,他就為發出的數據報指定了一個源IP地址,並且UDP服務器在接到這個數據報后會以這個IP地址作為回應數據報的目的IP地址。

  對於不指定地址的情況,我們稱之為通配地址,使用常量INADDR_ANY表示,這個值通常也是0,在不指定端口的情況,就是端口為0,下表為這幾種組合的情況

bind地址組合

   4、listen函數:僅在TCP服務器調用,監聽客戶發起的connect,如果監聽到客戶的connect,則和額客戶進行三次握手,listen函數的原型如下:

int listen(int sockfd, int backlog);

   sockfd參數是由socket函數返回的套接字描述符。

  backlog參數表示最多允許有backlog個客戶端處於連接等待狀態,如果接收到更多的連接請求就忽略。

  listen()成功返回0,失敗返回-1.

  5、accept函數:當完成三次握手后,接受這個連接,從未完成連接隊列轉移到已完成連接隊列。accept函數原型如下:

int accept(int socket, struct sockaddr *cliaddrm, socklen_t *addrlen);

 

、  sockfd參數是由socket函數返回的套接字。

  cliaddr參數是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。

  addrlen參數是一個傳入傳出參數,傳入的是調用者提供的緩沖去cliaddr的長度,以避免緩沖區溢出問題,傳出的是客戶端地址結構體的實際長度,如果給cliaddr參數傳NULL,表示不關心客戶端的地址。

accept()的返回值是另外一個文件描述符connfd,之后與客戶端之間就通過connfd通信,最后關閉connfd斷開連接,而不關閉listenfd。accept()成功返回一個文件描述符,出錯返回-1。

  6、recv和send函數:recv函數和send函數分別是接收和發送數據的函數,在有些地方通常也使用read和write來代替這兩個函數。recv和send函數的原型如下;

SSIZE_T recv(int sockfd, void *buff, size_t nbytes, int flags);
SSIZE_T send(int sockfd, const void *buff, size_t nbytes, int flags);

  sockfd參數對於recv來說就是接收端的描述符,對於send來說就是發送端的描述符。如果是服務器端,就是accept返回的描述符,如果是客戶端時,就是socket返回的描述符。

  buff參數是數據緩沖去,對recv來說就是接收數據的緩沖區,對於send來說就是發送數據的緩沖區。

  nbytes參數是緩沖去的字節大小。

  flags參數是一些手法的特殊標記,值一般為0或下表的取值。

   

收發特殊標記

   recv和send函數如果成功都會返回接收或者發送的數據字節數,否則返回-1。

  

 1) recv先等待s的發送緩沖區的數據被協議傳送完畢,如果協議在傳送sock的發送緩沖區中的數據時出現網絡錯誤,那么recv函數返回SOCKET_ERROR

 2) 如果套接字sockfd的發送緩沖區中沒有數據或者數據被協議成功發送完畢后,recv先檢查套接字sockfd的接收緩沖區,如果sockfd的接收緩沖區中沒有數據或者協議正在接收數據,那么recv就一起等待,直到把數據接收完畢。當協議把數據接收完畢,recv函數就把s的接收緩沖區中的數據copy到buff中(注意協議接收到的數據可能大於buff的長度,所以在這種情況下要調用幾次recv函數才能把sockfd的接收緩沖區中的數據copy完。recv函數僅僅是copy數據,真正的接收數據是協議來完成的)

 3) recv函數返回其實際copy的字節數,如果recv在copy時出錯,那么它返回SOCKET_ERROR。如果recv函數在等待協議接收數據時網絡中斷了,那么它返回0。

 4) 在unix系統下,如果recv函數在等待協議接收數據時網絡斷開了,那么調用 recv的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。

  

1) send先比較發送數據的長度nbytes和套接字sockfd的發送緩沖區的長度,如果nbytes > 套接字sockfd的發送緩沖區的長度, 該函數返回SOCKET_ERROR;

 2) 如果nbtyes <= 套接字sockfd的發送緩沖區的長度,那么send先檢查協議是否正在發送sockfd的發送緩沖區中的數據,如果是就等待協議把數據發送完,如果協議還沒有開始發送sockfd的發送緩沖區中的數據或者sockfd的發送緩沖區中沒有數據,那么send就比較sockfd的發送緩沖區的剩余空間和nbytes

 3) 如果 nbytes > 套接字sockfd的發送緩沖區剩余空間的長度,send就一起等待協議把套接字sockfd的發送緩沖區中的數據發送完

 4) 如果 nbytes < 套接字sockfd的發送緩沖區剩余空間大小,send就僅僅把buf中的數據copy到剩余空間里(注意並不是send把套接字sockfd的發送緩沖區中的數據傳到連接的另一端的,而是協議傳送的,send僅僅是把buf中的數據copy到套接字sockfd的發送緩沖區的剩余空間里)。

 5) 如果send函數copy成功,就返回實際copy的字節數,如果send在copy數據時出現錯誤,那么send就返回SOCKET_ERROR; 如果在等待協議傳送數據時網絡斷開,send函數也返回SOCKET_ERROR。

 6) send函數把buff中的數據成功copy到sockfd的改善緩沖區的剩余空間后它就返回了,但是此時這些數據並不一定馬上被傳到連接的另一端。如果協議在后續的傳送過程中出現網絡錯誤的話,那么下一個socket函數就會返回SOCKET_ERROR。(每一個除send的socket函數在執行的最開始總要先等待套接字的發送緩沖區中的數據被協議傳遞完畢才能繼續,如果在等待時出現網絡錯誤那么該socket函數就返回SOCKET_ERROR)

 7) 在unix系統下,如果send在等待協議傳送數據時網絡斷開,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的處理是進程終止。

   6、close函數:用來關閉socket,並且終止TCP連接。其函數原型如下:

int close(int sockfd);

   參數sockfd就是需要關閉的套接字的描述符。

   close函數默認行為是吧套接字標記為已關閉,然后立即返回調用進程。此時,調用進程中將不能再使用該描述符。值得注意的是,函數是立即返回,其中的含義就是TCP連接並不是立即被終止,也就是說,盡管close函數已經返回,但是TCP協議還在工作,還要嘗試將在緩沖區中未發送的數據發送到對端,然后在進行四次交互的關閉流程。對於這種行為可以使用套接字選項SO_LINGER來改變。

  

   7、shutdown函數:shutdown也是關閉socket,並且終止TCP連接,通常情況況下都會使用close函數進行關閉,但某些情況下也可以使用shutdown函數。shutdown函數的原型如下:

int shutdown(int sockfd, int howto);

  參數sockfd就是需要關閉的套接字的描述符。

  參數howto表示關閉選項。選項值如下:

  SHUT_RD,取值為0,表示關閉連接的杜這半部;SHUT_WR,取值為1,表示關閉連接的寫這半部;SHUT_RDWR,取值為2,表示關閉連接的讀這半部和寫這半部;當參數取2時的效果和連續調用兩次shutdown函數分別取0和1的效果相同。這個涉及TCP的半關閉概念,詳情可以查看查理。史蒂芬文斯的《TCP-IP詳解卷 I:協議》。

  SOCKET中的地址轉換

    sockaddr_in,sockaddr,in_addr在socket中都有應用,這里來對它們進行區分

sockaddr和sockaddr_in在字節長度上都為16個BYTE,可以進行轉換

struct   sockaddr   {  
                unsigned   short   sa_family;    //2 
                char   sa_data[14];     //14
        };  

 上面是通用的socket地址,具體到Internet   socket,用下面的結構,二者可以進行類型轉換           

struct   sockaddr_in   {  
                short   int   sin_family;     //2
                unsigned   short   int   sin_port;     //2
                struct   in_addr   sin_addr;     ‘//4
                unsigned   char   sin_zero[8];     //8
        };  
  struct in_addr就是32位IP地址。  
        struct   in_addr   {  
                union {
                        struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
                        struct { u_short s_w1,s_w2; } S_un_w;
                        u_long S_addr; 
                } S_un;

                #define s_addr  S_un.S_addr
        };  

或者;

struct in_addr {
    in_addr_t s_addr;
};

結構體in_addr 用來表示一個32位的IPv4地址
 inet_addr()是將一個點分制的IP地址(如192.168.0.1)轉換為上述結構中需要的32位二進制方式的IP地址(0xC0A80001)。//server_addr.sin_addr.s_addr=htonl(INADDR_ANY); 

通常的做法是:填值的時候使用sockaddr_in結構,而作為函數(如bin, accept, connect等)的參數傳入的時候轉換成sockaddr結構就行了,畢竟都是16個字符長。

通常的用法是:  

 int   sockfd;  
  struct   sockaddr_in   my_addr;  //賦值時用這個結構
  sockfd   =   socket(AF_INET,   SOCK_STREAM,   0);      
  my_addr.sin_family   =   AF_INET;     
  my_addr.sin_port   =   htons(MYPORT);     
  my_addr.sin_addr.s_addr   =   inet_addr("192.168.0.1");     
  bzero(&(my_addr.sin_zero),   8);         
  bind(sockfd,   (struct   sockaddr   *)&my_addr,   sizeof(struct   sockaddr));//用(struct   sockaddr   *)轉換即滿足要求
//int accept(int s,struct sockaddr * addr,int * addrlen);//這三個函數的第二個參數結構都為struct sockaddr,所以一般做法都如上所示。
//int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
//int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);

struct sockaddr 是一個通用地址結構,這是為了統一地址結構的表示方法,統一接口函數,使不同的地址結構可以被bind() , connect() 等函數調用;struct sockaddr_in中的in 表示internet,就是網絡地址,這只是我們比較常用的地址結構,屬於AF_INET地址族,他非常的常用,以至於我們都開始討論它與 struct sockaddr通用地址結構的區別。另外還有struct sockaddr_un 地址結構,我們可以認為 struct sockaddr_in 和 struct sockaddr_un 是 struct sockaddr 的子集。

 

到這里,SOCKET編程的基本函數就介紹完成了,下篇文章:

通過visual s'tudio 驗證 SOCKET編程:搭建一個TCP服務器

 

 

參考資料

Linux-socket編程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM