Linux 網絡編程 一

1、網絡編程基礎

網絡編程自己是一門很大的學問,涉及到的東西也不少,尤爲是各類協議。先看圖:

網絡編程

正如上圖所示,網絡編程中包含五大層面(也有區分六個層面),從應用層到物理層能夠明顯看出 越往下越接近計算機硬件。本身並非專業網絡編程的工程師,因此僅對這五大層面有一點點粗淺的瞭解,這篇文章網絡編程技巧博主寫的比較詳細. 平時大多數所謂網絡編程,實際上是在傳輸層、網絡層方面.html

2、socket編程

首先,socket(套接字)編程應該屬於傳輸層,主要實現的是端到端的通訊,很是相似於好久好久之前的固話通訊,應用程序能夠經過它發送或者接受數據,能夠對它進行像文件似得讀寫、關閉等操做。套接字容許應用程序將I/O插入網絡中,並與網絡中的其餘應用程序進行通訊。編程

其次,網絡中兩臺或多臺主機之間進行通訊,必須知道對應主機的地址,也就是其IP地址,可是隻知道IP地址是遠遠不夠的,試想如你在本機A發送了一個消息,另外一個臺主機B也接收到到了該消息,可是究竟是B主機的哪個進程接受並處理該消息?就像你用QQ給B發送消息,可是B不可能經過陌陌收到該消息。 所以,相互通訊的主機之間還必須肯定一一對應的消息處理接口--端口。端口的存在,主要是爲了確認消息一一對應性。另外,端口號其實就是一個從0開始的到65535之間的一個整型數字,0~1023端口,也就是常說的靜態端口,已被操做系統另作它用(http,https,ftp等各類協議佔用),咱們本身所能使用的端口範圍只能從1024開始,即動態端口取值[1024,65535].服務器

但是看出,若要進行網絡間通訊,socket至少要包含IP+port兩個方面,其實事實也是如此.仍是以有線電話作爲類比,socket其實就是本身家中的一部電話,其中IP就是家庭地址,port就是本身家的電話號碼,當要給別人打電話時,別人家固然也必須有本身的座機和專屬於該座機的號碼.
或許咱們也能猜出,socket編程是網絡編程裏邊必不可少且及其重要的一個環節.網絡

3、Linux+socket實踐

一、目的

熟悉Linux(這裏用Ubuntu16.04版本,其餘版本相似)下socket編程基本流程,掌握socket編程基本原理,搞懂Linux下socket編程所必須的函數及其用法.dom

實驗:在本地模擬兩臺機器,服務器和客戶端,服務器監聽客戶端信息並能發送廣播,客戶端能夠主動給服務器發送消息,其中消息的輸入是從標準輸入設備輸入,並輸出到標準輸出--Linux 終端.socket

開始以前必須瞭解一點 什麼是文件描述符,在Unix Linux系統中,文件描述符是一個非負整數,其存在做用更像一個索引,系統內核經過該"索引"找到對應的文件、設備、外設、安裝的軟件等等, 並經過描述符對它們進行操做。總而言之,文件描述符對應了系統上的全部文件,這裏的文件並不是"傳統意義上的普通文件",而是指Linux系統內核所能管理1的一切,包含文檔、文件、硬件設備、系統軟件等等。這也體現了Linux系統的設計思想----把一切視做文件.函數

二、必要接口

1)、socket函數

既然socket這麼重要,來看它究竟是個什麼東西.在Linux終端執行:man socket,出現:
圖片描述
經過Linux手冊查詢能夠知道該函數所必須的頭文件,函數聲明和函數描述等信息.從[DESCRIPTION]字段可知,函數建立了一個用於通訊的端點並返回該端點的描述符,若建立成功,返回建立套接字的文件描述符,不然返回一負數.
函數聲明 int socket(int domain,int type,int protocol);
圖片描述
參數 domain:表示建立該socket所使用的通信協議家族--地址族,如今通常用IPv4協議,因此一般會選擇AF_INET;
參數type:指定所需的通訊類型。包括數據流(SOCK_STREAM)<-->TCP協議、數據報(SOCK-DGRAM)<-->UDP協議和原始類型(S0CK_RAW)<-->新網格協議的開發測試.
參數protocol:說明該套接字使用的協議族中的特定協議。若是不但願特別指定使用的協議,則置爲0,使用默認的鏈接模式.
若要進行 基於TCP IP的網絡開發測試,則函數建立方式通常爲:測試

int listenfd = socket(AF_INET,SOCK_STREAM,0);

2)、bind函數

既然有了一部「電話」,那麼就須要爲該電話綁定惟一的「所屬地址」,一樣Linux命令行執行:man bind,一樣函數聲明爲:spa

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

從手冊的描述中能夠看出,當成功建立socket套接字後,調用該函數能夠將所建立的套接字(sockfd)和指定的地址(addr)綁定.
地址是由這樣一個結構體指定:操作系統

struct sockaddr {
    sa_family_t sa_family;  //地址族
    char sa_data[14];       //14字節的協議地址
}

上面struct sockaddr是通用地址,在網絡編程中 internet sockaddr使用下面地址,兩種地址能夠互換:

struct sockaddr_in { 
    short int sin_family; /* 地址族,AF_xxx 在socket編程中只能是AF_INET */ 
    unsigned short int sin_port; /* 端口號 (使用網絡字節順序) */ 
    struct in_addr sin_addr; /* 存儲IP地址 4字節 */ 
    unsigned char sin_zero[8]; /* 總共8個字節,實際上沒有什麼用,只是爲了和struct sockaddr保持同樣的長度 */ 
};

bind()函數的第三個參數表示地址所佔字節長度,socklen_t本質上是一個 unsigned int宏定義.
能夠經過這樣方式指定地址:

struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(5188);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

首先聲明網絡接口地址結構,在給該地址賦值前必須將其清空.依次設置該地址的地址族、IP和端口(這裏隨便設置了一個),上邊出現另外一個新函數htons,一樣終端下man htons,可知該函數的主要做用是將主機字節序轉化爲網絡字節序,關於這兩個字節序後續再深刻研究.這裏能夠理解爲:htons()的主要做用就是將十進制的ip地址和端口號轉化爲網絡能夠識別的"東東".

至此,基本能夠完成座機的安裝入戶和號碼綁定:

bing(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));

3)、listen監聽函數

對於咱們的服務器而言,它須要監聽來自客戶端發來的消息,Linix終端中 man listen能夠看到詳細信息. 函數聲明爲:

int listen(int socdfd,int backlog);

其中參數sockfd代指所要監聽的套接字文件描述符,參數backlog表示在套接字掛起時,所能接受請求的最大隊列長度.函數執行成功返回 0,不然返回 -1.

必須說明一點,當調用該函數後,參數socdfd所指定的套接字將變爲被動套接字,所謂被動套接字,是指其只能用來接收來自其餘用戶的連接請求. 相似於改變了套接字的狀態,使其只能用於接收.

4)、accept 接收函數

對於咱們的服務器而言,因爲其只具有接收功能,所以必須建立一個接受函數:

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

函數參數不言自明,參數1sockfd表示服務器socket描述符,參數2是指客戶端的協議地址,參數3爲地址長度. 函數成功返回監聽的等待隊列中第一個套接字的描述符.

三、服務器實現

服務器的功能是監聽客戶端發來的消息,並將消息廣播給客戶端.所以須要一個循環實時監聽客戶端發來的消息,在本地構建一個簡單的服務器以下:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define ERR_EXIT(m) \
  do \
  { \
    perror(m);\
    exit(EXIT_FAILURE);\
   }while(0)

int main(void){
  
  int listenfd;
    if(( listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0){
        ERR_EXIT("socket");
    }
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(5188);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if(bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0)
      ERR_EXIT("bind");
      
    //一旦監聽,則爲被動套接字(只能接受鏈接,調用accep函數以前調用),這裏隨便給了一個最大隊列長度
    if(listen(listenfd,100)< 0)  
      ERR_EXIT("listen");
    
    //聲明一個地址,用於存儲客戶端連接時的協議地址
    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;  //返回的一個主動套接字
    if((conn= accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
        ERR_EXIT("accept");

    char recvbuff[1024];
    while(1){
            memset(recvbuff,0,sizeof(recvbuff));
            int ret = read(conn,recvbuff,sizeof(recvbuff));
            fputs(recvbuff,stdout);
            write(conn,recvbuff,ret);

        }
    close(listenfd);    
  return 0;
}

其中用到了幾個非socket API的函數:

ssize_t read(int fd,void *buf,size_t count);
ssize_t write(int fd, const void *buf, size_t count);

read()函數:負責從fd所指定文件描述符讀取字節大小爲count的數據到buf中.若成功返回實際讀取到的字節大小,不然返回負數,返回0表示讀取到文件結束.
write():將buf中的count個字節內容寫入文件描述符fd.成功時返回寫的字節數.

四、客戶端實現

客戶端的實現和服務器的實現之間大同小異,一樣都須要 」 安裝電話 「 ,可是客戶端的功能僅在於向外」撥打電話「. 區別在於客戶端是主動發起鏈接請求,因此它必須知道本身所要鏈接的目標,以後服務器纔有響應.一樣客戶端並不須要監聽,只須要接收到服務器的廣播便可. 發起鏈接請求須要函數 connect

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

在上述鏈接函數中,參數sockfd表示本機(客戶端)的socket套接字描述符,參數addr表示服務器端的地址,參數3表示地址長度.
代碼實現:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define ERR_EXIT(m) \
  do \
  { \
    perror(m);\
    exit(EXIT_FAILURE);\
   }while(0)

int main(void){
  
  int sock;
    if(( sock = socket(PF_INET,SOCK_STREAM,0)) < 0){
        ERR_EXIT("socket");
    }
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(5188);
   // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    //發起鏈接
    connect(sock,(struct sockadddr*)&serveraddr,sizeof(serveraddr));
 
    char recvbuf[1024]={0};
    char sendbuf[1024]={0};
    while(fgets(sendbuf,sizeof(sendbuf),stdin)!= NULL){
            write(sock,sendbuf,strlen(sendbuf));
            read(sock,recvbuf,sizeof(recvbuf));
            fputs(recvbuf,stdout);
            memset(recvbuf,0,sizeof(recvbuf));
        }

     close(sock);
  return 0;
}

上述函數功能就是從客戶端主動向服務器發送鏈接請求,並在客戶端機器的標準設備上如字符,服務器接受並返回. 實現兩臺機器通訊的模擬.

五、結果

效果以下圖:
gcc編譯上述兩個文件,首先啓動服務器,以後啓動客戶端.在客戶端隨便輸入字符,服務器解收到並廣播返回. 至此基本完成目的.
圖片描述

3、總結

目前來看,建立服務器的通常流程是:

1.建立socket套接字(`socket`函數);
2.建立服務器地址,地址包含協議族、IP和端口號(`const struct sockaddr*`);
3.綁定套接字和服務器地址(bind函數);
4.系統監聽服務器,一旦監聽則該套接字變爲被動套接字,只能用於接收數據(`listen`函數);
5.做爲服務器,應該能接收客戶端信息(`accept`函數),該函數返回一個主動套接字;

基於以上步驟,基本能搭建一個簡單的服務器.

客戶端的搭建相比而言簡單許多:

1.建立用於鏈接的套接字;
2.將套接字和服務器地址鏈接;
3.發送消息
網絡編程畢竟浪大水深,畢竟初涉,慢慢填充.
相關文章
相關標籤/搜索