chapter_03 傳輸層:基本套接字簡介
這篇文章是我自己看的。閱讀《unix網絡編程》,整理自己的思路,僅放在網上便於保存&&分享。
相對於書而言,內容沒有什麼價值。書上的內容全面可靠。
PS:文章是建立在我的知識體系之上。因而,文中也不會有多餘解釋。(如果有不清楚的閱讀書本/google)
一些必要的內容放在正文中。還有些相對而言非主線的內容放在附錄中。這兩個集合之外的內容,可能沒有寫出,或者隨它而去。
文章目錄
一、內容
1 摘要(概述)
主要講述,套接字地址結構和地址轉換函數。
1.1、套接字地址結構
套接字地址結構:套接字的地址結構。而這裏的套接字,指網絡套機字。
網絡套接字(英語:Network socket;又譯網絡套接字、網絡接口、網絡插槽)在計算機科學中是電腦網絡中行程間數據流的端點。使用以網際協議(Internet Protocol)爲通信基礎的網絡套接字,稱爲網際套接字(Internet socket)。因爲網際協議的流行,現代絕大多數的網絡套接字,都是屬於網際套接字。 (來自wiki百科)
其實不用這麼術語話,我們大體清楚就好。
1.2 、地址轉換函數
地址轉換函數作用:地址的文本表達和它們存放在套接字地址結構中的二進制值之間進行轉換。 eg:
文本表達式:127.0.0.1 、二進制:01111111 00000000 00000000 000000000
二進制總共4×8=32位。所以用一個uint32_t
就可以存儲。
2、套接字的地址結構
“罪魁禍首”是intro/daytimetcpcli.c
中 struct sockaddr_in servaddr;
這一小結圍繞着sockaddr_in
展開。確實是由薄變厚。
2.1 、IPv4套接字的地址結構
我列出的是代碼中顯示的,和書上有稍微區別。本質是一樣的。而且我的代碼中沒有長度字段sin_len。
POSIX規範只需要這個結構中的3個字段: sin_family、 sin_addr和sin_port。
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__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)];
};
/**************************下面是分析部分**********************/
// __SOCKADDR_COMMON (sin_)的分析--->存儲的是哪種協議。比如ipv4對應的是AF_INET(常量)
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
宏定義:__SOCKADDR_COMMON (sin_) ---> sa_family_t sin_family
其中 “##”起連接作用
//in_port_t分析 --->存儲的是:以網絡字節序來存儲的端口號。
typedef uint16_t in_port_t;
//in_addr分析:至於爲什麼放在一個結構體中,這是歷史原因;詳細見書上對應章節 ---》存儲ip
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
//sin_zero[]
sin_zero字段未曾使用, 不過在填寫這種套接字地址結構時, 我們總是把該字段置爲0。
很明顯:sockaddr_in和sockaddr大小相同。
2.2、 IPv6套接字地址結構
參考文章: 內存對齊
這是一篇很好的介紹。可惜沒有用gdb等方式,真實的展現變量內存狀態。當然,我也沒有這麼幹過。😄
結構中字段的先後順序做過編排, 使得如果sockaddr_in6結構本身是64位對齊的, 那麼128位的sin6_addr字段也是64位對齊的。 在一些64位處理機上, 如果64位數據存儲在某個64位邊界位置, 那麼對它的訪問將得到優化
處理。 (沒看出來。。)
結構體中sin6_flowinfo
: sin6_flowinfo字段分成兩個字段:低序20位是流標(flow label) 、高序12位保留。我不清楚它的使用,暫時不介紹。
#if !__USE_KERNEL_IPV6_DEFS
/* Ditto, for IPv6. */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
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 */
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
#if !__USE_KERNEL_IPV6_DEFS
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
/***********************分析**********************/
在結構體in6_addr中定義了一個名爲__in6_u的union。
至於可以enable unino中的哪一項,由宏定義決定。
無論哪種存法,毫無疑問的可以存儲下128位的ipv6
2.3 、不同套接字地址結構對比
3、地址轉換函數
3.1、前提:字節序轉換建立統一
字節序,即字節存放的順序。一種是將低序字節存儲在起始地址, 這稱爲小端(little-endian) 節序; 另一種方法是將高序字節存儲在起始地址, 這稱爲大端(bigendian) 字節序。
在網絡應用中,字節序是一個必須被考慮的因素,因爲不同機器類型可能採用不同標準的字節序,所以均按照網絡標準轉化。
字節之間,有大端和小端。字節之內的8位沒有這個區分。最左邊的是高位。位序一般用於串行設備的傳輸順序。
關於字節序的判斷,可以見附錄3。這裏我們要做的是字節序之間的轉換。
網際協議使用大端字節序來傳送這些多字節整數。
從理論上說, 具體實現可以按主機字節序存儲套接字地址結構中的各個字段, 等到需要在這些字段和協議首部相應字段之間移動時, 再在主機字節序和網絡字節序之間進行互轉, 讓我們免於操心轉換細節。 然而由於歷史的原因和POSIX規範的規定, 套接字地址結構中的某些字段必須按照網絡字節序進行維護。 因此我們要關注如何在主機字節序和網絡字節序之間相互轉換。
h代表host, n代表network, s代表short, l代表long
均返回: 網絡字節序的值 | 均返回: 主機字節序的值 |
---|---|
uint16_t htons(uint16_t host16bitvalue); | uint16_t ntohs(uint16_t net16bitvalue); |
uint32_t htonl(uint32_t host32bitvalue); | uint32_t ntohl(uint32_t net32bitvalue); |
3.2 、準備:字節操縱函數
c語言字符串的結尾爲:\0; 在ascii中爲0;IP如果按照字符串來處理,在遇到\0會提前終止。所以我們不能用字符串來處理。用字節來處理。
以空字符結尾的C字符串是由在<string.h>頭文件中定義、 名字以str(表示字符串) 開頭的函數處理的。
名字以b(表示字節) 開頭的第一組函數起源於4.2BSD, 幾乎所有現今支持套接字函數的系統仍然提供它們。
名字以mem(表示內存) 開頭的第二組函數起源於ANSI C標準, 支持ANSI C函數庫的所有系統都提供它們。
以b(表示字節) 開頭 | 以mem(表示內存) 開頭 |
---|---|
void bzero(void *dest, size_t nbytes); | void *memset(void *dest, int c, size_t len); |
void bcopy(const void *src, void *dest, size_t nbytes); | void *memcpy(void *dest, const void *src, size_t nbytes); |
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes); | int memcmp(const void *ptr1, const void *ptr2, size_t nbytes); |
3.3上手:地址轉換函數
inet_pton和inet_ntop;
這兩個函數是隨IPv6出現的新函數, 對於IPv4地址和IPv6地址都適用。 函數名中p和n分別代表表達
(presentation) 和數值(numeric) 。 地址的表達格式通常是ASCII字符串, 數值格式則是存放到套接字地址結構中的二進制值
轉換成數值模式 | 轉換成表達式模式 |
---|---|
int inet_pton(int family, const char *strptr, void *addrptr); | const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); |
返回: 若成功則爲1, 若輸入不是有效的表達格式則爲 0, 若出錯則爲-1 | 返回: 若成功則爲指向結果的指針, 若出錯則爲NULL |
二、代碼
此時,可以修改上一章的代碼。
//修改客戶端代碼:使用readn(),進行讀取;
//修改服務端代碼:使用write(),進行寫入
//具體代碼略
附錄
1、IPv4通用套接字地址結構
在沒有void*
之前,用來作爲強制轉換的。以保證:以這樣的指針作爲參數之一的任何套接字函數必須處理來自所支持的任何協議族的套接字地址結構 。但是現在有了void *
。這個也就沒有必要了。
void 有什麼好講的呢?如果你認爲沒有,那就沒有;但如果你認爲有,那就真的有。有點像“色即是空,空即是色”。
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
2、新的通用套接字地址結構
作爲IPv6套接字API的一部分而定義的新的通用套接字地址結構克服了現有struct sockaddr的一些缺點。 不像struct sockaddr, 新的structsockaddr_storage足以容納系統所支持的任何套接字地址結構。sockaddr_storage結構在<netinet/in.h>頭文件中定義
3、大端字節序和小端字節序
參考文章:理解字節序
阮一峯 的介紹很好。很流暢。👍
首先,爲什麼會有小端字節序?
答案是,計算機電路先處理低位字節,效率比較高,因爲計算都是從低位開始的。所以,計算機的內部處理都是小端字節序。
但是,人類還是習慣讀寫大端字節序。所以,除了計算機的內部處理,其他的場合幾乎都是大端字節序,比如網絡傳輸和文件儲存。
通過代碼來判斷大端/小端。代碼也很巧妙。
#include "unp.h"
int
main(int argc, char **argv)
{
union {
short s;
char c[sizeof(short)];
} un;
un.s = 0x0102;
printf("%s: ", CPU_VENDOR_OS);
if (sizeof(short) == 2) {
if (un.c[0] == 1 && un.c[1] == 2)
printf("big-endian\n");//符合人的習慣
else if (un.c[0] == 2 && un.c[1] == 1)
printf("little-endian\n");//符合計算機的習慣
else
printf("unknown\n");
} else
printf("sizeof(short) = %d\n", sizeof(short));
exit(0);
}
//輸出:x86_64-unknown-linux-gnu: little-endian
文中參考彙總
當將局部變量聲明爲靜態局部變量的時候,也就改變了局部變量的存儲位置,即從原來的棧中存放改爲靜態存儲區存放。這讓它看起來很像全局變量,其實靜態局部變量與全局變量的主要區別就在於可見性,靜態局部變量只在其被聲明的代碼塊中是可見的。