Linux網絡編程——socket 通信基礎
- 1. socket 介紹
- 2. 字節序
- 2.1 簡介
- 2.2 字節序舉例
- 2.3 字節序轉換函數
- 3. socket 地址
- 3.1 通用 socket 地址
- 3.2 專用 socket 地址
- 4. IP地址轉換(字符串ip -> 整數,主機、網絡字節序的轉換 )
- 5. TCP 通信流程
- 6. 套接字函數
1. socket 介紹
????所謂 socket
(套接字),就是對網絡中不同主機上的應用進程之間進行 雙向通信的 端點的抽象。一個套接字就是網絡上進程通信的一端,提供了 應用層進程 利用網絡協議交換數據的機制。從所處的地位來講,套接字 上聯 應用進程,下聯 網絡協議棧,是 應用程序 通過 網絡協議 進行通信的接口,是 應用程序 與 網絡協議根 進行交互的接口。
????socket
可以看成是兩個網絡應用程序進行通信時,各自通信連接中的端點,這是一個 邏輯上的概念。它是網絡環境中 進程間通信 的 API
,也是可以被命名和尋址的通信端點,使用中的每一個套接字都有其類型和一個與之相連進程。通信時其中一個網絡應用程序將要傳輸的一段信息寫入它所在主機的 socket
中,該 socket
通過與 網絡接口卡(NIC
)相連的傳輸介質將這段信息送到另外一臺主機的 socket
中,使對方能夠接收到這段信息。socket
是由 IP 地址
和 端口
結合的,提供向應用層進程傳送數據包的機制。
????socket
本身有“ 插座 ”的意思,在 Linux 環境下,用于表示 進程間網絡通信 的 特殊文件類型。本質為 內核 借助 緩沖區 形成的 偽文件。既然是文件,那么理所當然的,我們可以使用 文件描述符 引用套接字。與管道類似的,Linux 系統將其封裝成文件的目的是為了 統一接口,使得 讀寫套接字 和 讀寫文件 的操作一致。區別是 管道 主要應用于 本地進程間通信,而 套接字 多應用于 網絡進程間數據的傳遞。
使用 文件描述符 fd
引用 socket
:
套接字通信 分兩部分:
- 服務器端:被動 接受連接,一般不會主動發起連接
- 客戶端:主動 向服務器發起連接
socket
是一套 通信的接口,Linux 和 Windows 都有,但是有一些細微的差別。
2. 字節序
2.1 簡介
????現代 CPU 的累加器 一次都能 裝載(至少)4 字節
(這里考慮 32 位機),即一個整數。那么這 4 字節
在 內存 中排列的順序 將影響它被累加器裝載成的整數的值,這就是字節序問題。在各種計算機體系結構中,對于字節
、字
等的存儲機制有所不同,因而引發了計算機通信領域中一個很重要的問題,即通信雙方交流的信息單元(比特
、字節
、字
、雙字
等等)應該以什么樣的順序進行傳送。如果不達成一致的規則,通信雙方將無法進行正確的編碼/譯碼從而導致通信失敗。
????字節序,顧名思義 字節的順序,就是大于一個字節類型的數據在內存中的存放順序 (一個字節的數據當然就無需談順序的問題了)。
????字節序 分為 小端字節序(Big-Endian
) 和 大端字節序(Little-Endian
)。
- 小端字節序則 是指 整數的 低位字節 則存儲在內存的 低地址 處,而 高位字節 存儲在內存的 高地址 處。
- 大端字節序 是指一個 整數的 最高位字節 (
23 ~ 31 bit
)存儲在內存的 低地址 處,低位字節(0 ~ 7 bit
)存儲在 內存的 高地址 處;
2.2 字節序舉例
小端字節序
-
0x
01
02
03
04
(十六進制,四字節。ff = 255
) -
內存的方向 ----->
-
內存的低位
----->內存的高位
04
03
02
01
0x
11
22
33
44
12
34
56
78
(八個字節)
大端字節序
0x
01
02
03
04
- 內存的方向 ----->
內存的低位
----->內存的高位
01
02
03
04
0x
12
34
56
78
11
22
33
44
- 通過代碼檢測當前主機的字節序
#include <stdio.h>int main() {union {short value; // 2字節char bytes[sizeof(short)]; // char[2]} test;test.value = 0x0102;if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {printf("大端字節序\n");} else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {printf("小端字節序\n");} else {printf("未知\n");}return 0;
}
2.3 字節序轉換函數
當格式化的數據在兩臺使用不同字節序的主機之間直接傳遞時,接收端必然錯誤的解釋之。
- 解決問題的方法是:發送端 總是 把要發送的數據 轉換成 大端字節序數據 后再發送,而接收端知道對方傳送過來的數據總是采用大端字節序,所以接收端可以根據自身采用的字節序決定是否對接收到的數據進行轉換(小端機轉換,大端機不轉換)。
????網絡字節順序 是 TCP/IP
中規定好的 一種數據表示格式,它與具體的 CPU 類型、操作系統等無關,從而可以保證數據在不同主機之間傳輸時能夠被正確解釋,網絡字節順序 采用 大端排序 方式。
?
BSD Socket 提供了封裝好的 轉換接口,方便程序員使用。
- 包括從 主機字節序 到 網絡字節序 的 轉換函數:
htons
、htonl
; - 從 網絡字節序 到 主機字節序 的轉換函數:
ntohs
、ntohl
。
h - host 主機,主機字節序
to - 轉換成什么
n - network 網絡字節序
s - short : unsigned short 無符號短整型,兩個字節
l - long : unsigned int 無符號長整型,四個字節
#include <arpa/inet.h>
/*網絡通信時,需要將主機字節序轉換成網絡字節序(大端),另外一段獲取到數據以后根據情況將網絡字節序轉換成主機字節序。
*/// 32位,轉IP
uint32_t htonl(uint32_t hostlong); // 主機字節序 -> 網絡字節序
uint32_t ntohl(uint32_t netlong); // 網絡字節序 -> 主機字節序// 16位,轉換端口
uint16_t htons(uint16_t hostshort); // 主機字節序 -> 網絡字節序
uint16_t ntohs(uint16_t netshort); // 網絡字節序 -> 主機字節序
#include <stdio.h>
#include <arpa/inet.h>int main() {// htonl 轉換IPchar buf[4] = {192, 168, 1, 100};int num = *(int *)buf;int sum = htonl(num);unsigned char *p = (char *)∑printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));printf("=======================\n");// htons 轉換端口unsigned short a = 0x0102;printf("a : %x\n", a);unsigned short b = htons(a);printf("b : %x\n", b);printf("=======================\n");// ntohl 轉換IPunsigned char buf1[4] = {1, 1, 168, 192};int num1 = *(int *)buf1;int sum1 = ntohl(num1);unsigned char *p1 = (unsigned char *)&sum1;printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));// ntohs 轉換端口return 0;
}
3. socket 地址
?
????socket 地址 其實是一個 結構體,封裝 端口號
和 IP
等信息。 后面的 socket
相關的 api
中需要使用到這個 socket地址。
- 客戶端 -> 服務器(
IP
,Port
)
3.1 通用 socket 地址
?
????socket 網絡編程接口 中表示 socket 地址 的是 結構體 sockaddr
,其定義如下:
#include <bits/socket.h>struct sockaddr {sa_family_t sa_family; // 地址族類型char sa_data[14]; // 14字節
};typedef unsigned short int sa_family_t; // 2字節
????sa_family
成員是 地址族類型(sa_family_t
)的 變量。地址族類型 通常與 協議族類型 對應。常見的 協議族(protocol family
,也稱 domain
)和對應的 地址族(address family
) 如下所示:
???? 宏 PF_*
和 AF_*
都定義在 bits/socket.h
頭文件中,且 后者 與 前者 有 完全相同的值,所以二者通常混用。
????sa_data
成員用于存放 socket
地址值。但是,不同的協議族的地址值具有不同的含義和長度,如下所示:
???? 由上表可知,14 字節的 sa_data
根本無法容納多數協議族的地址值。因此,Linux 定義了下面這個 新的通用的 socket 地址結構體,這個結構體不僅提供了足夠大的空間用于存放地址值,而且是 內存對齊 的。
#include <bits/socket.h>
struct sockaddr_storage
{sa_family_t sa_family; // 地址族類型unsigned long int __ss_align; // 內存對齊char __ss_padding[ 128 - sizeof(__ss_align) ];
};typedef unsigned short int sa_family_t;
3.2 專用 socket 地址
???? 很 多網絡編程函數 誕生 早于 IPv4 協議,那時候都使用的是 struct sockaddr
結構體,為了向前兼容,現在 sockaddr
退化成了(void *
)的作用,傳遞一個地址 給函數,至于這個函數是 sockaddr_in
還是 sockaddr_in6
,由地址族確定,然后函數內部再 強制類型轉化 為所需的地址類型。
????UNIX 本地域協議族 使用如下 專用的 socket
地址結構體:
#include <sys/un.h>struct sockaddr_un
{sa_family_t sin_family;char sun_path[108];
};
????TCP/IP 協議族 有 sockaddr_in
和 sockaddr_in6
兩個專用的 socket
地址結構體,它們分別用于 IPv4
和 IPv6
:
#include <netinet/in.h>struct sockaddr_in
{sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) 地址族類型*/in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. 填充*/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;
};struct sockaddr_in6
{sa_family_t sin6_family;in_port_t sin6_port; /* Transport layer port # */uint32_t sin6_flowinfo; /* IPv6 flow information */struct in6_addr sin6_addr; /* IPv6 address */uint32_t sin6_scope_id; /* IPv6 scope-id */
};typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
????所有 專用 socket 地址
(以及 sockaddr_storage
)類型的變量 在實際使用時都需要轉化 為 通用 socket 地址
類型 sockaddr
(強制轉化 即可),因為所有 socket
編程接口 使用的地址參數類型都是 sockaddr
。
4. IP地址轉換(字符串ip -> 整數,主機、網絡字節序的轉換 )
????通常,人們習慣用 可讀性好的字符串 來表示 IP 地址,比如用 點分十進制字符串 表示 IPv4 地址,以及用 十六進制字符串 表示 IPv6 地址。但編程中我們需要先把它們 轉化為 整數(二進制數)方能使用。而記錄日志時則相反,我們要把整數表示的 IP 地址 轉化為 可讀的字符串。下面 3 個函數可用于用 點分十進制字符串 表示的 IPv4 地址 和 用 網絡字節序整數 表示的 IPv4 地址 之間的轉換:
#include <arpa/inet.h>// 下面的這些函數比較久,使用起來比較麻煩,不推薦使用
in_addr_t inet_addr(const char *cp); // 點分十進制字符串 -> 整數
int inet_aton(const char *cp, struct in_addr *inp); // 點分十進制字符串 -> 整數, 并保存到結構體指針 inp 中
char *inet_ntoa(struct in_addr in); // 整數 -> 點分十進制字符串
????下面這對更新的函數也能完成前面 3 個函數同樣的功能,并且它們同時適用 IPv4 地址 和 IPv6 地址:??
#include <arpa/inet.h>// p:點分十進制的IP字符串,n:表示network,網絡字節序的整數
int inet_pton(int af, const char *src, void *dst);af:地址族: AF_INET AF_INET6src:需要轉換的點分十進制的IP字符串dst:轉換后的結果保存在這個里面// 將網絡字節序的整數,轉換成點分十進制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);af:地址族: AF_INET AF_INET6src: 要轉換的ip的整數的地址dst: 轉換成IP地址字符串保存的地方size:第三個參數的大小(數組的大小)返回值:返回轉換后的數據的地址(字符串),和 dst 是一樣的
#include <stdio.h>
#include <arpa/inet.h>int main() {// 創建一個ip字符串,點分十進制的IP地址字符串char buf[] = "192.168.1.4"; // 后面默認還有一個字符串結束符unsigned int num = 0;// 將點分十進制的IP字符串轉換成網絡字節序的整數inet_pton(AF_INET, buf, &num);unsigned char * p = (unsigned char *)#printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3)); // 大端排序// 將網絡字節序的IP整數轉換成點分十進制的IP字符串char ip[16] = "";const char * str = inet_ntop(AF_INET, &num, ip, 16);printf("str : %s\n", str);printf("ip : %s\n", str);printf("%d\n", ip == str);return 0;
}
在這里插入圖片描述
5. TCP 通信流程
TCP
和 UDP
-> 傳輸層的協議
UDP
: 用戶數據報協議,面向無連接,可以單播,多播,廣播, 面向 數據報,不可靠TCP
: 傳輸控制協議,面向連接的,可靠的,基于 字節流,僅支持單播傳輸
? | UDP | TCP |
---|---|---|
是否創建連接 | 無連接 | 面向連接 |
是否可靠 | 不可靠 | 可靠的 |
連接的對象個數 | 一對一、一對多、多對一、多對多 | 支持一對一 |
傳輸的方式 | 面向數據報 | 面向字節流 |
首部開銷 | 8個字節 | 最少20個字節 |
適用場景 | 實時應用(視頻會議,直播) | 可靠性高的應用(文件傳輸) |
TCP 通信的流程 ??????
??服務器端 ( 被動接受連接的角色)??
- 創建 一個用于監聽的套接字
- 監聽:監聽有客戶端的連接
- 套接字 :這個套接字其實就是一個 文件描述符
- 將這個 監聽 文件描述符 和 本地的IP 和 端口 綁定(
IP
和端口
就是 服務器的地址信息)- 客戶端 連接 服務器 的時候使用的就是這個
IP
和端口
- 客戶端 連接 服務器 的時候使用的就是這個
- 設置 監聽,監聽的
fd
開始工作 - 阻塞等待,當有客戶端發起連接,解除阻塞,接受客戶端的連接,會得到一個 和客戶端通信的 套接字(
fd
) - 通信
- 接收數據
- 發送數據
- 通信結束,斷開連接
??客戶端??
- 創建一個用于通信的套接字(
fd
) - 連接服務器,需要指定連接的服務器的
IP
和端口
- 連接成功了,客戶端可以直接和服務器 通信
- 接收數據
- 發送數據
- 通信結束,斷開連接
6. 套接字函數
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了這個頭文件,上面兩個就可以省略int socket(int domain, int type, int protocol);- 功能:創建一個套接字- 參數:- domain: 協議族AF_INET : ipv4AF_INET6 : ipv6AF_UNIX, AF_LOCAL : 本地套接字通信(進程間通信)- type: 通信過程中使用的協議類型SOCK_STREAM : 流式協議SOCK_DGRAM : 報式協議- protocol : 具體的一個協議。一般寫0(默認)- SOCK_STREAM : 流式協議默認使用 TCP- SOCK_DGRAM : 報式協議默認使用 UDP- 返回值:- 成功:返回文件描述符,操作的就是內核緩沖區。- 失敗:-1int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名- 功能:綁定,將fd 和本地的IP + 端口進行綁定- 參數:- sockfd : 通過socket函數得到的文件描述符- addr : 需要綁定的socket地址,這個地址封裝了ip和端口號的信息- addrlen : 第二個參數結構體占的內存大小int listen(int sockfd, int backlog); /proc/sys/net/core/somaxconn- 功能:監聽這個socket上的連接- 參數:- sockfd : 通過socket()函數得到的文件描述符- backlog : 未連接的隊列 和 已經連接的隊列 和的最大值(以使用 cat /proc/sys/net/core/somaxconn 查看:4096),一般不用設置那么大,如:128int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);- 功能:接收客戶端連接,默認是一個阻塞的函數,阻塞等待客戶端連接- 參數:- sockfd : 用于監聽的文件描述符- addr : 傳出參數,記錄了連接成功后客戶端的地址信息(ip,port)- addrlen : 指定第二個參數的對應的內存大小- 返回值:- 成功 :用于通信的文件描述符- -1 : 失敗int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);- 功能: 客戶端連接服務器- 參數:- sockfd : 用于通信的文件描述符- addr : 客戶端要連接的服務器的地址信息- addrlen : 第二個參數的內存大小- 返回值:成功 0, 失敗 -1ssize_t write(int fd, const void *buf, size_t count); // 寫數據
ssize_t read(int fd, void *buf, size_t count); // 讀數據
TCP 通信實現
(1)服務器端
????創建 server.c
文件
// TCP 通信的服務器端#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>int main() {// 1.創建socket(用于監聽的套接字)int lfd = socket(AF_INET, SOCK_STREAM, 0);if(lfd == -1) {perror("socket");exit(-1);}// 2.綁定struct sockaddr_in saddr;saddr.sin_family = AF_INET;// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0saddr.sin_port = htons(9999);int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));if(ret == -1) {perror("bind");exit(-1);}// 3.監聽ret = listen(lfd, 8);if(ret == -1) {perror("listen");exit(-1);}// 4.接收客戶端連接struct sockaddr_in clientaddr;int len = sizeof(clientaddr);int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);if(cfd == -1) {perror("accept");exit(-1);}// 輸出客戶端的信息char clientIP[16];inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));unsigned short clientPort = ntohs(clientaddr.sin_port);printf("client ip is %s, port is %d\n", clientIP, clientPort);// 5.通信char recvBuf[1024] = {0};while(1) {// 獲取客戶端的數據int num = read(cfd, recvBuf, sizeof(recvBuf));if(num == -1) {perror("read");exit(-1);} else if(num > 0) {printf("recv client data : %s\n", recvBuf);} else if(num == 0) {// 表示客戶端斷開連接printf("clinet closed...");break;}char * data = "hello,i am server";// 給客戶端發送數據write(cfd, data, strlen(data));}// 關閉文件描述符close(cfd);close(lfd);return 0;
}
注: 僅供學習參考,如有不足,歡迎指正!