目錄
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;
}