第十二課:樹莓派搭建伺服器

2020-08-10 18:26:12

第一課:什麼是樹莓派
第二課:基於樹莓派的10個經典專案
第三課:購買您的第一個樹莓派
第四課:如何安裝樹莓派系統
第五課:樹莓派C語言程式設計手冊
第六課:樹莓派led控制
第七課:樹莓派按鍵控制
第八課:樹莓派PWM(脈寬調製)
第九課:樹莓派數碼管顯示
第十課:樹莓派如何讀取溫溼度感測器(dht11)數據
第十一課:樹莓派控制電機

我們要實現的功能是什麼

很多時候,我們需要遠端控制樹莓派,給它發送一個命令,再讓樹莓派去控制某一個裝置,此時我們需要在樹莓派上用C或者C++或者Python搭建一個伺服器,然後在另一臺裝置開發一個用戶端,這個裝置可以是筆電,臺式機,或者手機,也可以是另一個樹莓派,場景是這樣的:
在这里插入图片描述

如何做實驗

我們並不能去控制昂貴的裝置,因爲成本太貴,我們先從小東西開始做,比如,我們可以讓樹莓派控制燈,然後用QT在筆電上開發一個控制介面,或者用手機開發一個小程式,給樹莓派發送命令。就像這樣:
在这里插入图片描述

複雜一點的像這樣:
在这里插入图片描述

程式設計思路

我們通常把樹莓派叫伺服器,這個伺服器其實並不複雜,就是一個不到100行的c程式,在樹莓派上執行就可以搭建起來。而,用戶端就是執行另一個程式的裝置,用於發送控制命令給伺服器的。
伺服器和用戶端的通訊協定比較常見的使用TCP(或者UDP),所以這裏面更多的知識就是TCP/IP協定了。
搭建TCP伺服器的步驟如下:
1) 建立TCP通訊端
我們需要呼叫socket函數,建立一個通訊端,得到一個檔案描述符。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
參數說明:
第一個參數傳AF_INET,表示IPV4地址域;
第二個參數傳SOCK_STREAM,表示建立的通訊端是基於TCP協定的;
第三個參數傳0
返回值:
如果成功,則返回一個大於0的整數,代表通訊端檔案描述符
如果失敗,則返回-1
2)系結IP地址和埠號
通訊端建立後,我們需要把樹莓派的IP地址和埠號系結到通訊端上,怎麼操作呢
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
參數說明:
第一個參數就是步驟一中,socket函數的返回值
第二個參數是一個結構體,我們一般傳另一個同等結構體
struct sockaddr_in{
short int sin_family; //地址族(Address Family)
unsigned short int sin_port; //16位元 TCP/UDP埠號
struct in_addr sin_addr; //32位元 IP地址
char sin_zero[8]; //不使用,對齊作用
};
上面結構體中第一個成員sin_family,表示地址家族,什麼是地址家族呢?
地址家族:
地址家族表示有一個家族,裏面都是地址,那麼有哪些呢?
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)----------------------本地操作
AF_INET IPv4 Internet protocols ip(7) -----------------------IPV4,我們一般使用這個
AF_INET6 IPv6 Internet protocols ipv6(7) ------------------------IPV6
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk ddp(7)
AF_PACKET Low level packet interface packet(7)
埠號
結構體中的第二個參數,是埠號,什麼是埠號呢?
埠號是一個16位元正整數,用來標識計算機系統中的某一個網路應用,比如我們開啓電腦,電腦啓動了很多軟體,其中就有網路應用程式,每一個網路應用程式都對應一個唯一的埠號(在本機唯一)。
埠號,是一種國際資源,什麼意思呢,就是由世界統一指定的(IEEE,國際電子工程協會)統一規劃,分配,比如25這個埠號是給STMP(郵件使用的,我們通過發郵件就是使用的這個埠),23是給遠端登陸使用的,22號埠是給ssh使用的,80是給http協定使用的(我們上網,輸入網址http,預設就是這個埠號)
那麼,我們自己寫一個網路應用程式,也需要一個埠號,而且這個埠號不能跟國際通用的同號,一般使用8000以上。而且,伺服器和用戶端都需要一個埠號,且不同號。
所以,我一般使用8990,或者9000都可以。
IP地址
IP地址,雖然很多人對它的原理不是很理解,但是大家都見過,比如192.168.1.100,192.168.1.1等等,這些都是我們區域網常用的IP地址,這種IP地址是IPV4,所以我們在呼叫socket函數的時候,第一個參數就是AF_INET,就表示使用IPV4,這個V4就表示版本4。
那麼我們的IP地址使用多少呢?
這個IP不能隨便設定,必須是一個有效的IP地址,所以,我們就填樹莓派本身的IP地址,你可以ifconfig檢視:
在这里插入图片描述
這裏的192.168.137.243就是樹莓派的IP地址,我們就應該填這個。
結構體第四個成員——sin_zero[8]
註釋中說,用做填充用,這個涉及到結構體對齊,關於這個知識點,大家參考下面 下麪部落格
https://blog.csdn.net/kuimzzs/article/details/81605160

bind函數的第三個參數
第三個參數的意思是,告訴bind函數,第二個參數專用記憶體多少位元組,其實這是一個固定值——16位元組。你可以直接填16位元組,也可以用sizeof(struct sockaddr_in),也可以用sizeof(struct sockaddr)。
bind返回值
bind呼叫,如果返回0,則表示系結成功,-1表示系結失敗,如果返回-1,你可以通過perror函數列印出失敗原因。
3) 偵聽
偵聽,是伺服器系結好IP和埠號後要求做的事情,表示準備就緒了,像偵察兵一樣偵聽是否有人要存取伺服器(連線伺服器,善意的)。
偵聽的函數:
int listen(int sockfd, int backlog);
這個函數很簡單
第一個參數:
傳socket函數的返回值
第二個參數:
傳10或者5,總之不要太大,也不要太小,比如0,1,2之類的,表示偵聽佇列長度。
第二個參數,其實你不用關心這個函數的內部實現原理,你按這樣做就行了。但是還是有人不死心,不放心,總覺得還不夠理解,如果是這樣,你可以這樣理解:
這個參數,形象的解釋,就像我們去銀行大廳辦理業務,前面有一個1米線,我們有兩撥人,一撥人在1米線前排隊,還有一撥人,坐在椅子上等候,在1米線上排隊的人,這個佇列長度就是我們這裏的第2個參數10,表示佇列長度最大是10個人,但是不是代表我們的伺服器總共只能接收10個用戶端,不是的,我們的伺服器可以接收很多個用戶端(你想想淘寶這些超大型伺服器吧),只是說我們的伺服器一次可以處理10個用戶端。
4) 接收用戶端連線
第四步就是接收用戶端的連線了,函數是accept。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
這個函數怎麼理解呢?
它可以跟bind函數比較着理解。
第一個參數:
accept函數的第一個參數,就是socket函數的返回值,沒有什麼難理解的。
第二個參數
accept函數的第二個參數,用於存放用戶端的IP地址的埠號,所以是一個結構體,這個結構體跟bind一樣的結構體,區別在於:
bind中的sockaddr結構體是我們賦值好IP地址和埠號,送進去,而accept函數,是我們給一個空的結構體進去,當有用戶端連線過來的時候,accept函數會把用戶端的資訊(IP地址和埠號)存放在這個結構體中。
第三個參數
accept函數的第三個參數,用於存放用戶端資訊的大小,就是用戶端過來的資訊有多少位元組,accept函數會把這個數據放在這裏面,所以是一個指針。
返回值
accept函數的返回值,很重要,它唯一對應用戶端,代表用戶端,就像socket函數,他們都叫檔案描述符,區別如下:
socket函數的返回值代表——伺服器
accept函數的返回值代表——用戶端
accept函數最重要的特點:
accept函數最重要的特點是,具有阻塞功能,什麼意思呢?
就是如果沒有用戶端鏈接,accept函數會停留在這裏,不往下走。當有用戶端連線的時候,纔會往下走,並且把用戶端資訊(IP和埠號存放在第二個參數中,資訊大小存放在第三個參數中)
5)讀取用戶端發過來的數據
如果有用戶端連線過來,accept函數會往下走,我們就可以通過read函數讀取用戶端發過來的數據了。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
第一個參數也是fd(檔案描述符),這個應該填用戶端的檔案描述符,也就是accept函數的返回值。
第二個參數,一般給它一個數組名稱,用於存放收到的數據。
第三個參數,告訴read函數,最大可以接收多少位元組。這個數位不應該大於第二個參數的大小,就是說如果第二個參數(陣列)只能存放100個位元組,那麼第三個參數只能小於或等於100,不能大於,如果大於就會出現段錯誤。
read函數,還有一個重要特點,阻塞功能,像accept函數一樣。如果用戶端不發數據過來,則函數不往下走。
好了,完整的程式碼如下:

伺服器完整程式碼

pi@xiajiashan:~/$ cat -n server.c 
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <sys/types.h>          /* See NOTES */
     4  #include <sys/socket.h>
     5  #include <netinet/in.h>
     6  #include <arpa/inet.h>
     7  #include <string.h>
     8  //int socket(int domain, int type, int protocol);
     9  int main()
    10  {
    11      int fd,result;
    12      fd = socket(AF_INET,SOCK_STREAM,0);
    13      printf("fd=%d\n",fd);
    14      if(fd==-1){
    15         perror("socket函數呼叫失敗...");
    16         exit(-1);
    17      }
    18      //step2----------------
    19      //       int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    20      struct sockaddr_in  server;//存放伺服器的IP地址
    21      server.sin_family = AF_INET;
    22      server.sin_port = htons(8990);//把主機位元組序轉成網路位元組序
    23      server.sin_addr.s_addr = inet_addr("192.168.137.243");
    24
    25      result = bind(fd,(struct sockaddr*)&server,sizeof(struct sockaddr));
    26      printf("bind result=%d\n",result);
    27      if(result==-1)
    28      {
    29         perror("bind failed....");
    30         close(fd);
    31         exit(-1);
    32      }
    33      //step3--------
    34      result =  listen(fd, 10);
    35      printf("listen result=%d\n",result);
    36      if(result==-1)
    37      {
    38         perror("listen failed....");
    39         close(fd);
    40         exit(-1);
    41      }
    42      //step4----------------------
    43      struct sockaddr_in  client;//存放用戶端的IP地址和埠號
    44      memset((void*)&client,0,sizeof(client));
    45      socklen_t addrlen=0;
    46      int client_fd;
    47      int size;
    48      char buff[1000]="";
    49      while(1){
    50            puts("accept....");
    51            //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    52            client_fd = accept(fd,(struct sockaddr*)&client,&addrlen);
    53            printf("client_fd = %d,ip = %s, port = %d\n",client_fd,inet_ntoa(client.sin_addr),&addrlen);
    54            while(1){
    55                 size = read(client_fd,buff,1000);
    56                 printf("從用戶端讀到 %d 位元組:%s\n",size,buff);
    57                 if(size==0) {
    58                    perror("用戶端已經關閉...");
    59                    break;
    60                 }
    61                 memset((void*)buff,0,1000);
    62                 //write()
    63            }
    64      }
    65      close(fd);
    66  }
pi@xiajiashan:~/$ 

END

本節課,就在這裏結束,用戶端部分,我們再分一節課介紹。
如果覺得本部落格對你有幫助,就收藏吧!

第一課:什麼是樹莓派
第二課:基於樹莓派的10個經典專案
第三課:購買您的第一個樹莓派
第四課:如何安裝樹莓派系統
第五課:樹莓派C語言程式設計手冊
第六課:樹莓派led控制
第七課:樹莓派按鍵控制
第八課:樹莓派PWM(脈寬調製)
第九課:樹莓派數碼管顯示
第十課:樹莓派如何讀取溫溼度感測器(dht11)數據
第十一課:樹莓派控制電機