c/c++:網絡通信基礎socket(網絡設計模式、字節序、IP地址轉換、sockaddr數據結構、套接字函數、TCP通信流程)

目錄

1. 概念

1.1 網絡設計模式

  - B/S

  - C/S

- IP和端口

- OSI/ISO 網絡分層模型

2. 協議格式

3. socket編程

3.1 字節序

- 接口轉換函數

3.2 IP地址轉換

3.3 sockaddr數據結構

3.4 套接字函數

4. TCP通信流程

tcp 服務器server通信操作流程:

tcp 客戶端client通信操作流程:


 

1. 概念

1.1 網絡設計模式

  - B/S

    - 客戶端: 瀏覽器
    - 服務器: 服務器

    優勢: 跨平臺, 開發成本低

    劣勢: 

    ​    是的協議的固定的: http, https

    ​    不能處理大的數據

  - C/S

    - 客戶端: 桌面應用程序
    - 服務器: 後臺服務器

     優勢: 可以處理大量的磁盤數據

     劣勢: 如果跨平臺, 需要重新開發, 成本高

 

- IP和端口

  •   - IP地址

    - IPV4

      - 實際是一個32位的整形數 -> 本質 -> 4字節   int a;
      - 我們看的的不是這個整形數, 點分十進制字符串 -> 192.168.247.135 
        - 分成了4份, 每份1字節, 8bit   ->  char , 最大值爲 255  -> 最大取值: 255.255.255.255
      - IP可以有多少個  2^32^ - 1 個

    - IPV6

      - 實際是一個128位的整形數
      - xxx:xxx:xxx:xxx:xxx:xxx:xxx:xxx ,  分成了8分, 每份16位 -> 每一部分以16進制的方式表示
      - IP可以有多少個  2^128^ - 1 個
    - IP地址的作用:
      - 通過IP地址能夠找到某一臺主機

  •   - 端口

    - 在一個主機上運行着很多進程
    - 將數據發送到某臺主機上的某個進程
    - 如果要進程網絡通信, 可以讓這個進程綁定一個端口
      - 通過這個端口就可以確定某個進程
    - 端口號: unsigned short int   ->   16位
      - 端口取值範圍: 0 -65535    (2^16^)

 

- OSI/ISO 網絡分層模型

  > OSI(Open System Interconnect),即開放式系統互聯。 一般都叫OSI參考模型,是ISO(國際標準化組織組織)在1985年研究的網絡互聯模型。

  •   - 七層模型

    底層 --------->上層
    物 數 網 傳 會 表 應

  •     > - 物理層:

    >   - 物理層負責最後將信息編碼成電流脈衝或其它信號用於網上傳輸

  •     > - 數據鏈路層:  

    >   - 數據鏈路層通過物理網絡鏈路供數據傳輸。
    >   - 規定了0和1的分包形式,確定了網絡數據包的形式;

  •     > - 網絡層

    >   - 網絡層負責在源和終點之間建立連接;
    >   - 此處需要確定計算機的位置,怎麼確定?IPv4,IPv6

  •     > - 傳輸層

    >   - 傳輸層向高層提供可靠的端到端的網絡數據流服務。
    >   - 每一個應用程序都會在網卡註冊一個端口號,該層就是端口與端口的通信

  •     > - 會話層

    >   - 會話層建立、管理和終止表示層與實體之間的通信會話;
    >   - 建立一個連接(自動的手機信息、自動的網絡尋址);

  •     > - 表示層:

    >   - 對應用層數據編碼和轉化, 確保以一個系統應用層發送的信息 可以被另一個系統應用層識別;
    >   - 可以理解爲:解決不同系統之間的通信,eg:手機上的QQ和Windows上的QQ可以通信;

  •     > - 應用層:

    >   - 規定數據的傳輸協議

  • 四層模型

 

 

2. 協議格式

 

3. socket編程

// 套接字通信分兩部分:
 - 服務器端: 被動接受連接的角色, 不會主動發起連接
 - 客戶端通信: 主動向服務器發起連接
 
 socket是一套通信接口, 下linux和windows都可以使用, 但是有細微差別

 

3.1 字節序

字節序,顧名思義字節的順序,就是大於一個字節類型的數據在內存中的存放順序(一個字節的數據當然就無需談順序的問題了)

  • - 概念

  - Little-Endian -> 主機字節序
    - 有一個數據: 0x12345678, 在內存中進行存儲
    - 內存的低地址位存儲數據低位字節, 內存高地址位存儲數據的高位字節
  - Big-Endian -> 網絡字節序
    - 內存的低地址位存儲數據高位字節, 內存的高地址位存儲數據的低位字節

  • - 字節序舉例
// 使用16進制在內存中表示這兩個數,即:
  	- 0x12 34 56 78   -> 四字節   char -> 255 -> ff  
        - 0x11223344   -> 四字節
  • - 小端

    低地址位 -------------> 高地址位
    0x78     0x56    0x34    0x12
    0x44    0x33    0x22    0x11

  • - 大端

    低地址位 -------------> 高地址位
    0x12    0x34    0x56    0x78
    0x11    0x22    0x33    0x44

  • - 接口轉換函數

BSD Socket提供了封裝好的轉換接口,方便程序員使用。

主機字節序(h)到網絡字節序(n)的轉換函數:htons、htonl;

網絡字節序(n)到主機字節序(h)的轉換函數:ntohs、ntohl。

#include <arpa/inet.h>
  // shot int -> 4字節(64位)
  // h -> host
  // n -> network
  // s -> short
  // l -> long
  // xtoxs() -> 進行端口轉換
  uint16_t htons(uint16_t hostshort);
  	參數: 主機字節數的short型數值 -> 要轉換的數(主機)
  	返回值: 轉換之後得到是數據 (網絡字節序)
  uint16_t ntohs(uint16_t netshort);

  // long -> 8字節(64位)
  // xtoxl() -> 進行IP轉換
  uint32_t htonl(uint32_t hostlong);
  uint32_t ntohl(uint32_t netlong);

 

3.2 IP地址轉換

#include <arpa/inet.h>
// 字符串: 192.168.1.100 (點分十進制字符串)
// p -> 點分十進制字符串 IP
// n -> network
// 將主機字節序的 字符串IP -> 網絡字節序的 整形數
int inet_pton(int af, const char *src, void *dst);
	參數: 
		- af: 地址族協議, ipv4, ipv6
			ipv4: AF_INET, ipv6:AF_INET6
		- src: 點分十進制字符串 IP
		- dst: 傳出參數, 執行一塊內存的地址, 將轉換得到的網絡字節序的整形數存儲到這塊內存中
	返回值:
		-1: 失敗
		1: 成功
		0: 查字典

// 網絡字節序的整形IP -> 點分十進制字符串 IP
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
	參數:
		- af: 地址族協議, ipv4, ipv6
			ipv4: AF_INET, ipv6:AF_INET6
		- src: 指向要轉換的 網絡字節序的整形IP 地址
		- dst: 轉換成功之後的 點分十進制字符串 存儲的位置
		- size: 修飾的就是第三個參數 dst 對應的內存大小
	返回值: 
		NULL: 失敗
		非空指針, 指向第三個三種指針的內存: 成功

 

3.3 sockaddr數據結構

結構體 sockaddr、sockaddr_in用於網絡通信

結構體 sockaddr_un用於進程間通信

結構體 sockaddr_in用於ipv6通信

由於結構體sockaddr需要用指針偏移添加IP地址,這樣很麻煩,在實際中我們使用sockaddr_in來添加端口號、IP地址。再強轉成sockaddr類型,因爲這2個結構體大小一樣,後面的服務器—客戶端程序會有具體體現。

struct sockaddr {
	sa_family_t sa_family;	// 地址族協議, ipv4, ipv6
	char        sa_data[14];
}

struct sockaddr_in
{
    sa_family_t sin_family;		IP選擇AF_INET(ipv4)、AF_INET6(ipv6)
    in_port_t sin_port;         端口(網絡字節序:htons() )
    struct in_addr sin_addr;    IP地址(網絡字節序:inet_pton() )
    //預留空間:
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
               sizeof (in_port_t) - sizeof (struct in_addr)];  
};

struct in_addr     
{
    in_addr_t s_addr;        IP地址(網絡字節序:inet_pton() )
};  

typedef unsigned short  uint16_t;
typedef unsigned int    uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

 

3.4 套接字函數

#include <arpa/inet.h>	
// 創建一個套接字
int socket(int domain, int type, int protocol);
	參數: 
		- domain: 地址族協議
			AF_INET: ipv4
			AF_INET6: ipv6
			AF_UNIX, AF_LOCAL: 進行本地套接字通信(進程間通信)
        - type: 通信過程中使用的協議
        	SOCK_STREAM: 流式協議
        	SOCK_DGRAM: 報式協議
        - protocol: 一般寫0
        	- SOCK_STREAM: 流式協議默認使用使用: tcp
        	- SOCK_DGRAM: 報式協議默認使用使用: udp
    返回值: 這個文件描述符操作的是內核緩衝區
		成功: 文件描述符 > 0
        失敗: -1

// 綁定函數 -> 將fd 和本地的 IP + Port進程綁定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	參數:
		- sockfd: 通過socket函數得到的
		- addr: 需要將IP和Port初始化到這個結構體中
               - addrlen: 第二個參數結構體佔的內存大小

// 設置監聽
int listen(int sockfd, int backlog);	// /proc/sys/net/core/somaxconn
	參數:
		- sockfd: 通過socket函數得到的
		- backlog: 已經連接成功, 但是還沒有被處理的連接指定的數值不能大於/proc/sys/net/core/somaxconn 中存儲的數據, 默認爲128

// 默認是一個阻塞函數, 阻塞等待客戶端請求。請求到達, 接收客戶端連接,得到一個用於通信的文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	參數:
		- sockfd: 用於監聽的文件描述符(套接字)
            - addr: 傳出參數, 記錄了連接成功的客戶端的IP和端口信息
            - addrlen: 第二個參數結構體對應的內存大小
    返回值:
		- 成功: 通信的文件描述符 > 0
        - 失敗: -1
            
// 客戶端使用該函數連接服務器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	參數:
		- sockfd: 用於通信的文件描述符
		- addr: 客戶端要連接的服務器的地址信息
		- addrlen: 第二個參數結構體佔的內存大小
	返回值:
		連接成功: 0
        連接失敗: -1
            
 // 寫數據
ssize_t write(int fd, const void *buf, size_t count);
 // 讀數據
ssize_t read(int fd, void *buf, size_t count);

 

4. TCP通信流程

// tcp / udp-> 傳輸層協議
tcp: 面向連接的, 安全的, 流式傳輸協議
    - 安全: 不會丟數據
udp: 面向無連接的, 不安全, 報式傳輸協議

tcp 服務器通信操作流程:

1. 創建一個用於監聽的套接字
    - 監聽: 監聽有客戶的連接
    - 套接字: 這個套接字是一個文件描述符
2. 將這個監聽文件描述符和本地的IP和端口綁定  (IP和端口 == 服務器地址信息)
    - 客戶端連接服務器的時候使用的就是這個IP和端口
3. 設置監聽, 監聽的fd開始工作
4. 阻塞等待, 當有客戶端發起連接, 解除阻塞, 接受客戶端的連接, 會得到一個用戶通信的套接字(fd)
5. 通信
    - 接收數據
    - 發送數據
6. 通信結束, 斷開連接

tcp 客戶端的通信流程:

1. 創建一個用於通信的套接字 (fd)
2. 連接服務器, 需要指定連接的服務器的 IP 和 Port
3. 連接成功, 客戶端可以直接和服務通信
    - 接收數據
    - 發送數據
4. 斷開連接

tcp 服務器server通信操作流程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    // 1.創建用於監聽的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2.綁定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;            //ipv4
    addr.sin_addr.s_addr = INADDR_ANY;    //獲取IP的操作交給了內核
    // 上面的代碼等價於:inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
    addr.sin_port = htons(8989);          //端口
    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(0);
    }

    // 3.設置監聽
    int lis_ret = listen(fd, 100);
    if (lis_ret == -1)
    {
        perror("listen");
        exit(0);
    }

    // 4.等待被連接
    struct sockaddr_in addr_cli;
    int len = sizeof(addr_cli);
    int connfd = accept(fd, (struct sockaddr*)&addr_cli, &len);
    if (connfd == -1)
    {
        perror("accept");
        exit(0);
    }

    // 通訊
    while (1)
    {
        // 讀數據
        char recvBuf[1024];
        read(connfd, recvBuf, sizeof(recvBuf));
        printf("recv buf : %s\n", recvBuf);
        // 寫數據
        write(connfd, recvBuf, strlen(recvBuf));
    }
    
    //釋放
    close(fd);
    close(connfd);

    return 0;
}

 

tcp 客戶端client通信操作流程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    // 1. 創建用於通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 連接服務器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;  // ipv4
    addr.sin_port = htons(8989);   // 服務器監聽的端口, 字節序應該是網絡字節序
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("connect");
        exit(0);
    }


    int i = 0;
    // 通信
    while(1)
    {
        // 讀數據
        char recvBuf[1024];
        // 寫數據
        sprintf(recvBuf, "data: %d\n", i++);
        write(fd, recvBuf, strlen(recvBuf));
        // 如果客戶端沒有發送數據, 默認阻塞
        read(fd, recvBuf, sizeof(recvBuf));
        printf("recv buf: %s\n", recvBuf);
        sleep(1);
    }

    // 釋放資源
    close(fd); 

    return 0;
}

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章