網路通信之套接字Socket
預備知識根據自身基礎選擇性查看
文章目錄
- 網路通信之套接字Socket
- 預備知識:
- IP 及 埠
- 網路模型
- 位元組序
- IP地址轉換
- 虛擬地址空間
- 檔案描述符
- sockaddr結構體 / sockaddr_in
- socket 編程
- TCP通信流程
- socket常用函式詳解
- 服務器端代碼實作
- 客戶端代碼實作
預備知識:
IP 及 埠
IP: 本質為一個整型數,表示計算機在網路中的地址,IP協議有兩個:IPv4 和 IPv6
? IPv4協議:(目前用的最多)32位整型數表示,4個位元組 ,也可使用點分十進制描述: 192.168.255.65
? IPv4可是使用的最大地址有 2^32個,數量較少所有開始發展IPv6
? IPv6協議:128位整型數表示,16位元組,
? 使用字串描述IP地址 :2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
? *IPv4可是使用的最大地址有 2^128個
埠 : 用于定位到主機上的某一個行程 ,通過這個埠行程進行通信,
埠是一個無符號短整型(unsigned short),有效埠(0 ~ 2^16 -1)
注意:一個埠只能給一個行程使用
簡單理解: IP地址理解為 酒店的地址,埠理解為 具體房間的通信電話號碼
網路模型
網路分成模型

網路各層的作用:
- 物理層: 負責最后將資訊編碼成電流脈沖或其他信號用于傳輸
- 資料鏈路層: 通過物理網路提供資料傳輸,確定網路資料包形式
- 網路層: 負責源和終點之間連接(通過 IPv4 或 IPv6 查找對應主機)
- 傳輸層 : 向高層提供端對端的網路資料流服務 (TCP / UDP 協議)
- 會話層 : 會話層建立、管理、終止表示層與物體之間的通信會話
- 表示層: 對應用層資料編碼和轉化,確保另一個應用層識別
位元組序
各計算機體系結構中,對存盤機制有所不用,為了正常通信,雙方應同一規則避免通信失敗,
顧名思義位元組的順序,就是大于一個位元組型別的資料在記憶體中的存放順序,也就是說對于單字符來說是沒有位元組序問題的,字串是單字符的集合,因此字串也沒有位元組序問題,
目前主要兩種位元組存盤機制:大端 、小端
- 大端 :
低位位元組存盤到記憶體的高地址位,高位元組存盤到記憶體低地址位
? 其中網路通信使用大端存盤
- 小端:
低位位元組存盤到記憶體的低地址位,高位元組存盤到記憶體高地址位
? 平時主機使用小端存盤
示例:
// 有一個16進制的數, 有32位 (int): 0xab5c01ff
// 位元組序, 最小的單位: char 位元組, int 有4個位元組, 需要將其拆分為4份
// 一個位元組 unsigned char, 最大值是 255(十進制) ==> ff(16進制)
記憶體低地址位 記憶體的高地址位
--------------------------------------------------------------------------->
小端: 0xff 0x01 0x5c 0xab
大端: 0xab 0x5c 0x01 0xff
Socket提供好了封裝介面,用與在主機 ->網路 -> 主機之間轉換位元組序的函式 主->網(htons \ htonl) // 網->z主(ntohs\ntohl)
//主機位元組序 -> 網路位元組序 (短整型)
uint16_t htons(uint16_t hostshort);
//主機位元組序 -> 網路位元組序 (長整型)
uint32_t htonl(uint16_t hostlong);
//網路位元組序 -> 主機位元組序 (短整型)
uint16_t ntohs(uint16_t netshort);
//網路位元組序 -> 主機位元組序 (長整型)
uint32_t ntohl(uint16_t hostlong);
注意:在傳輸資料時,記得轉換位元組序
IP地址轉換
雖然IP的本質是整型資料,但是使用程序中通常通過字串描述,
IP地址使用時也需要轉換大小端
//主機位元組序 -> 網路位元組序
//主機位元組序的IP地址為 字串 , 網路位元組序的IP地址為整型
int inet_pton(int af, const char* src , void* dst);
//引數
af :地址族協議 (IPv4協議 : AF_INET) (IPv6協議: AF_INET6)
src: 輸入引數,(點分十進制) 192.168.255.45
dst: 輸出引數 , (大端整型)
//大端整型 ->小端點分十進制IP地址
const char* inet_ntop(int af , const char* src , char *dest , socklen_t size);
//引數
af :地址族協議 (IPv4協議 : AF_INET) (IPv6協議: AF_INET6)
src: 輸入引數,(大端整型)
dst: 輸出引數 , (點分十進制) 192.168.255.45
size: 標記dst指向記憶體最多儲存多少位元組
虛擬地址空間
虛擬地址空間 :作業系統知識補充
當我們運行磁盤上的一段可執行程式,就會得到一個行程(行程是一段程式的執行程序,是動態概念) ,內核會給每一個運行的行程分配一塊屬于自己的虛擬地址空間,并將應用程式資料裝載在對應的虛擬記憶體地址上,
行程運行程序中處理的資料從物理記憶體中加載,行程中的資料通過CPU的記憶體管理單元從虛擬地址空間中映射過去,

虛擬地址的意義
為什么作業系統不直接從物理記憶體上獲取資料?如果直接讀取會發生什么?

行程A需要100m記憶體 ,直接在物理記憶體上從0地址開始分配置100 , 同理行程B分配250m記憶體,并且行程A和行程B占用的記憶體為連續的,會發生如下問題:
每個行程的地址不隔離,有安全風險:程式直接訪問物理記憶體可能存在bug的程式修改其他程式的記憶體結構記憶體效率低:直接物理記憶體,行程對應整塊記憶體,如果記憶體不足時資料量大,記憶體和磁盤拷貝時間會很長,效率低行程中資料的地址不確定,每次都會有變化:物理記憶體使用是動態的,無法確定記憶體使用情況,起始地址都不同,加載資料效率低
檔案描述符
檔案描述符
在Linux系統中,一切皆是檔案,如何將應用程式和檔案對應?
解決方案是使用檔案描述符(fd),當在行程中打開一個現有檔案或創建一個新檔案時,內核會向行程回傳一個檔案描述符用于對應這個打開/新建的檔案,這些檔案描述符存盤在內核為維護行程的一個檔案描述符表中,
檔案描述符的概念往往只適用于 Linux 或 Unix 系統
檔案描述符表
前面講到啟動一個行程就會得到一個對應的虛擬地址空間,這個虛擬地址空間分為兩大部分,Linux 的行程控制塊 PCB (pcb簡單理解為一個行程所有基本資訊),里邊包括管理行程所需的各種資訊,其中有一個結構體叫做 file ,我們將它叫做檔案描述符表,里邊有一個整形索引表,用于存盤檔案描述符,

檔案描述符表性質 1: 每個行程對應的檔案描述符表默認支持打開的最大檔案數為 1024,可以修改
檔案描述符表性質 2: 每個行程的檔案描述符表中都已經默認分配了三個檔案描述符,對應的都是當前終端檔案
檔案描述符表性質3:.每打開新的檔案,內核會從行程的檔案描述符表中找到一個空閑的沒有別占用的檔案描述符與其進行關聯
檔案描述符表性質4 : 每個行程檔案描述符表中的檔案描述符值是唯一的,不會重復
sockaddr結構體 / sockaddr_in
struct sockaddr 是一個通用地址結構 , 是為了同一地址結構的表示方式,統一介面函式,使不同的地址結構可以被 bind() —connect() 等函式呼叫;
sockaddr 結構體的缺陷 : sa_data把目標地址和埠資訊混在一起了
struct sockaddr{
unsigned short sa_family;
char sa_data[14];
};
- sa_family : 通信型別,地址族協議(IPv4/IPv6)
- sa_data : 14位元組 ,包含套接字中的
目標地址 和 埠資訊
sockaddr_in 結構體:struct sockaddr_in中的in 表示internet,就是網路地址,這只是我們比較常用的地址結構,屬于AF_INET地址族
sockaddr_in結構體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變數中
struct in_addr{
unsigned long s_addr;
}
struct sockaddr_in {
short int sinfamily;
unsigned short int sin_port ;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
sin_port / s_addr 一般為主機位元組序(小端),使用時注意初始化轉化為網路位元組序(htons()函式)
使用方式
一般先把sockaddr_in 變數賦值后 ,強制轉換后傳入用于sockaddr做引數
- sockaddr_in 用于socked 定義和賦值
- sockaddr 用于函式引數
socket 編程
為了將TCP/IP 協議相關軟體移植UNIX類系統中,設計者開發一個介面以便于應用程式能簡單的呼叫介面通信,最終形成了Socket套接字,Linux系統采用套接字通信,因此廣泛使用,其頭檔案包含在 sys/socket.h中
TCP通信流程
TCP是一個 面向連接(雙向連接) 、 安全(資料校驗) 、 流式傳輸(發送/接受資料速度,資料量可以不一致)傳輸層協議

服務器端通信流程
//創建用于監聽的套接字 ,這個套接字是一個檔案描述符
int lfd = socket();
//系結 監聽的檔案描述符 的 IP地址 和 埠號
bind();
//設定監聽是否有客戶端發起連接請求
listen();
//等待客戶端連接 ,當有客戶端連且請求會創建用于通信的檔案描述符 ,沒有連接便阻塞
int cfd = accept();
//通信 :讀、寫 默認都是阻塞的
write(); //send();
read(); //recv();
//斷開連接 、 關閉套接字
close();
注意:服務器端有兩種檔案描述符,一類是用于監聽客戶端連接請求(只存在一個),檢測到連接請求后會呼叫accept創建新連接, 另一類檔案描述符用于和客戶端通信(N個),每個客戶端和服務器都對應一個通信的檔案描述符 簡化理解:服務器檔案描述符 (用于監聽客戶端連接)、(用于和已連接客戶端通信)
此處檔案描述符記憶體結構

對應兩塊記憶體: 讀緩沖區 、 寫緩沖區
讀緩沖區 :通過檔案描述符將記憶體中的資料讀出
寫緩沖區 :通過檔案描述符將資料寫入記憶體中
客戶端通信流程
單執行緒情況下,只有一個通信的檔案描述符
//創建一個通信的套接字
int cfd = socket();
//連接服務器 ,需要知道 IP地址 和 埠號
connect();
//通信 :讀、寫
write(); //send();
read(); //recv();
//斷開連接 ,關閉套接字(檔案描述符)
close();
socket常用函式詳解
//創建一個套接字
int socked(int domain ,int type ,int protocol); //創建失敗 回傳 -1
domain : 使用的地址族協議(IPv4 、IPv6)
- IPv4 :AF_INET
- IPv6 :AF_INET6
type: 傳輸方式 (流式 、報式)
- SOCK_STREAM: 使用流式的傳輸協議
- SOCK_DGRAM: 使用報式 (報文) 的傳輸協議
protocol : 默認 0
- 選擇流式傳輸 使用TCP
- 選擇報式傳輸 使用UDP
//系結用于監聽檔案描述符 IP 、埠
int bind(int sockfd , const struct sockaddr* addr , socklen_t addrlen); //創建失敗 回傳 -1
- sockfd: 監聽的檔案描述符,通過 socket () 呼叫得到的回傳值
- addr: 傳入引數,要系結的 IP 和埠資訊需要初始化到這個結構體中,
IP和埠要轉換為網路位元組序 - addrlen: 引數 addr 指向的記憶體大小,sizeof (struct sockaddr)
//給監聽的套接字設定監聽
int listen(int sockfd ,int backlog); //創建失敗 回傳 -1
- sockfd: 檔案描述符,可以通過呼叫 socket () 得到,在監聽之前必須要系結 bind ()
- backlog: 同時能處理的最大連接要求,最大值為 128
//等待并接受客戶端的連接,如果或連接請求創建新的用于通信的套接字 ,沒有連接便會阻塞
int accept(int sockfd , struct sockaddr *addr ,socklen_t *addrlen); //成功回傳檔案描述符,失敗 -1
- sockfd: 監聽的檔案描述符
- addr: 傳出引數,里邊存盤了建立連接的客戶端的地址資訊
- addrlen: 傳入傳出引數,用于存盤 addr 指向的記憶體大小
這個函式是一個阻塞函式,當沒有新的客戶端連接請求的時候,該函式阻塞;當檢測到有新的客戶端連接請求時,阻塞解除,新連接就建立了,得到的回傳值也是一個檔案描述符,基于這個檔案描述符就可以和客戶端通信了
//接受資料
ssize_t read(int socket , void* buf,size_t len); //失敗 -1
//發送資料
ssize_t write(int socket , void* buf,size_t len); //失敗 -1
- fd: 通信的檔案描述符,accept () 函式的回傳值
- buf: 傳入引數,要發送的字串
- len: 要發送的字串的長度
//成功連接后,客戶端隨機系結一個埠
//服務器端呼叫 accept() ,其中第二個引數存盤客戶端 IP \埠
int connect(int sockfd ,struct sockaddr *addr ,socklen_t *addrlen); //成功回傳 0,失敗 -1
服務器端代碼實作
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
int main()
{
//創建用于監聽的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket() fail");
return -1;
}
//系結本地ip port
//使用sockaddr_in 初始化避免IP和埠混亂
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);//轉化為網路位元組序
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0 對于0,沒有大小端差別
//使用時在強轉為sockaddr型別
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind() fail");
return -1;
}
//設定監聽
ret = listen(fd, 128);
if (ret == -1)
{
perror("listen() fail");
return -1;
}
//阻塞并等待客戶端的連接
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
int cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
if (cfd == -1)
{
perror("accept() fail");
return -1;
}
//新連接成功,列印客戶端的 IP 和 Port 資訊
char ip[32];
printf("客戶端的 IP :%s Port :%d \n",
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(caddr.sin_port));
//通信
while (1)
{
//接受客戶端資料
char buff[1024];
int len = recv(cfd, buff, sizeof(buff), 0);
if (len > 0) {
printf("client say :%s", buff);
send(cfd, buff, len, 0);//發送訊息給客戶端
}
else if (len == 0)
{
printf("客戶端已經斷開連接");
break;
}
else
{
perror("recv() fail");
break;
}
}
//關閉檔案描述符(兩類)
close(fd);
close(cfd);
return 0;
}
客戶端代碼實作
// client.c
#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;
addr.sin_port = htons(10000); // 大端埠
inet_pton(AF_INET, "192.168.110.138 ", &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1)
{
perror("connect");
exit(0);
}
// 3. 和服務器端通信
int number = 0;
while (1)
{
// 發送資料
char buf[1024];
sprintf(buf, "你好, 服務器...%d\n", number++);
write(fd, buf, strlen(buf) + 1);
// 接收資料
memset(buf, 0, sizeof(buf));
int len = read(fd, buf, sizeof(buf));
if (len > 0)
{
printf("服務器say: %s\n", buf);
}
else if (len == 0)
{
printf("服務器斷開了連接...\n");
break;
}
else
{
perror("read");
break;
}
sleep(1); // 每隔1s發送一條資料
}
close(fd);
return 0;
}
推薦大家多了解一些底層知識
推薦大丙老師的教程
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/356775.html
標籤:其他
上一篇:阿里云部署nginx
下一篇:華為網路配置(DHCP)
