說到網絡編程一定都離不開套接字,以前用起來的時候大多靠記下來它的用法,這一次希望能理解一些更底層的東西,當然這些都是網絡編程的基礎~
大多說套接字函數都需要一個指向套接字地址結構的指針作為參數,每個協議族都定義它自己的套接字地址結構,這些結構都以sockadd_
開頭。
IPv4套接字地址結構通常稱為“網際套接字地址結構”,以sockaddr_in命名,並定義在
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr; //32位的 IPv4地址
};
//下面部分書中與源碼中不太一樣,後面將貼出
//這應該跟系統支持的標准有關
//書中
struct sockaddr_in{
uint8_t sin_len; //無符號的8位整數(1個字節)
sa_family_t sin_family; //8位整數 (1個字節)
In_port_t sin_port; //至少16位無符號 (2個字節)
struct in_addr sin_addr; //32位 (4個字節)
char sin_zero[8]; //(8個字節)
};//套接字的大小至少時16個字節
書中有這麼一句話:sa_family_t
可以是任何無符號整數類型。在支持長度字段sin_len
的實現中,通常是一個8位的無符號整數,而在不支持長度字段的實現中,則是一個16位的無符號整數。
基於後者,在我的Ubuntu14.04的源碼中,就跟書本中給出的內容有出入了:
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'. */
//這部分是讓sockaddr_in與sockaddr的大小相等
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
關於這個__SOCKADDR_COMMON (sin_)
是一個宏定義,其內容如下:
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
說白了就是將sa_prefix前綴和這個family拼接在一起,也就是說,__SOCKADDR_COMMON (sin_);
就相當於下述語句:
sa_family_t sin_family;
也就意味著在我電腦的源碼中,沒有sin_len字段,剛好跟書中所說一種情況一樣,書中原話:
“在支持長度字段實現中,sa_family_t是一個8位無符號整數,在不支持長度字段的實現中,是一個16位的無符號整數”
這樣按照理論來說,sa_family_t通常是一個16位2字節的無符號整數,的確,找到了如下定義:
typedef unsigned short int sa_family_t;//unsigned short int 2個字節
在POSIX的規范中,只需要sin_family
sin_addr
(in_addr_t至少32位無符號)和sin_port
(in_port_t至少16位無符號)這些字段,不過除此之外,我們還需要一些額外的字段,使得整個結構體填滿至少16個字節。這樣就可以和sockaddr
相互轉化了,關於sockaddr_in和sockaddr,再簡單說兩句,前者在應用層使用,後者在內核態使用。套接字作為參數傳遞時總是以引用的方式(指向該結構的指針),所以說指針就需要支持任何協議族套接字的地址結構,因為函數是通用的,於是,就有了struct sockaddr
。
還有兩點比較重要的內容是:
1.IPv4地址和端口號在套接字地址結構中總是以網絡字節序來存儲,注意與主機字節序區別。
2.IPV4的地址有兩種不同的訪問方式:因為本身sin_addr是一個結構體。假設serv是一個網際套接字地址結構:
(a)serv.sin_addr將按照結構體的方式引用其中的32位IPv4地址。
(b)serv.sin_addr.s_addr將按照in_addr_t(通常32位無符號整數)引用同一個32位IPv4地址。
具體使用哪種方式,將根據實際情況決定。
定義在
struct in6_addr{
uint8_ts6_addr[16]; //128-bit Ipv6 address
};
#define SIN6_LEN
Struct sockaddr_in6{
uint8_t sin_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
}
跟IPv4類似,我電腦中的源碼跟書中給的也有一丟丟區別,大概也是因為支持的標准不一樣吧:
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 */
};
從套接字層面上來說IPv6和IPv4的一些小對比:IPv6的地址族時AF_INET6,IPv4地址族是AF_INET。IPv6套接字結構最小28個字節,IPv4套接字結構最小時16個字節,IPv6套接字API的一部分而定義的新的通用套接字地址結構客服了現有struct sockaddr
的一些缺點,新的struct sockaddr_storage
足以容納系統所支持的任何套接字地址結構。
我們往套接字函數中傳遞套接字結構時傳遞套接字結構的指針,該結構的長度sizeof
,也作為一個參數來傳遞,不過其傳遞方式取決於該結構的傳遞方向:是從進程到內核,還是從內核到進程。
這個方向傳遞套接字地址結構的套接字函數有3個:bind
,connect
和sendto
,例如:
struct sockaddr_in serv;
connect(sockfd,(struct sockaddr*)&serv,sizeof(serv));
指針和指針所指內容的大小都傳遞給了內核,於是內盒知道到底需從進程復制多少數據。
這個方向傳遞套接字地址結構的函數有4個:accept
,recvfrom
,getpeername
和getsockname
與進程到內核的函數類似,只不過這裡不論是套接字結構的指針還是長度參數,不過這個長度給了一個指針。這裡的原因很好理解,前者是為了告訴內核態結構的大小使得內核在該結構地址上操作時不至於越界,後者是作為結果來返回,告訴進程內核在結構中存儲了多少信息。
ps:對於IPv4的sockaddr_in傳遞與返回的大小都是16.
從進程傳遞給內核的參數是需要讓內核知道內核需要讀取多少字節
從內核傳遞給內核的參數是需要讓進程知道內核給進程寫了多少字節
關注如何在主機字節序和網絡字節序之間相互轉換。
這個問題從學計算機組成結構起就好像一直沒掌握的樣子,這次要徹底弄清楚!
首先,對於一個16位整數,其16進制表示假設是0x1234,那麼我們說:
這個數的高字節是0x12
這個數的低字節是0x34
而對於地址來說,我們常用1個字節的地址偏移來表示。這樣小端和大端的定義如下:
低序字節存儲在起始地址(偏移小的位置)稱為小端模式
高序字節存儲在起始地址(偏移小的位置)稱為大端模式
那麼按照上面的定義,0x1234在大/小端模式下應該是這樣的
參考代碼:
//test.c
#include
int main()
{
union{
short s;
char c[sizeof(short)];
}un;
un.s=0x0102;
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");
}
return 0;
}
我們把大端和小端的字節存儲順序統稱為“主機字節序”。對應的,當然就有網絡字節序了。網際協議中使用大端字節序來傳送這些多字節整數。兩種字節序之間的轉換有以下四個函數:
#include
//返回網絡字節序
uint16_t htons(uint16_t host16bitvalue);
uint16_t htonl(uint32_t host16bitvalue);
//返回主機字節序
uint16_t ntohs(uint16_t host16bitvalue);
uint16_t htohl(uint32_t host16bitvalue);
//-------------------
/*
//這四個函數通常被定義成宏定義。
h:host
n:network
l:long
s:short
*/
字節處理函數
#include
void bzero(void *dest,size_t nbytes); //初始化
void bcopy(const void *src,void *dest,size_t nbytes); //拷貝
int bcmp(const void *ptrl,const void *ptr2,size_t nbytes); //若相等則為0,否則為非0
在c語言中也有和這些功能一樣的函數
#include
void *memset(void *dest,int c,size_t len); //對應 bzero
void *memcpy(void *dest,const void *src,size_t nbytes); //對應bcopy
int memcmp(const void *ptrl,const void *ptr2,sieze_t nbytes);// 若相等則為0,否則為<0或者>0
使用的時候注意src和dest的順序即可,實在記不住就在終端輸入:
man bzero
man memcpy
這樣就知道這些函數的所需要的參數代表什麼意思了哈~
這些函數有:
inet_aton
,inet_addr
,inet_ntoa
,inet_pton
和inet_ntop
。
這些函數的功能是在ASCII 字符串網絡與字節序二進制之間轉換網際地址,因為人們更熟悉使用字符串來標記,而存放在套接字地址結構裡面的值往往是二進制。
#include
//將字符串形式的點分十進制字符串轉換成為IPv4地址
int inet_aton(const char * strptr,struct in_addr *addrptr);
in_addr_t inet_addr(const char * strptr);
//返回一個指向點分十進制字符串的指針
char * inet_ntoa(struct in_addr inaddr);
上述函數或被廢棄,或有更好的函數替換,inet_pton
和inet_ntop
就是隨著IPv6出現的新函數,對於IPv4和IPv6地址都適用。
#include
int inet_pton(int family,const char * strptr,void *addrptr);//成功返回1失敗返回0
const char * inet_ntop(int family, const void * addrptr,char *strptr,size_t len);
//---------------------
/*
p:代表 Presentation 表達
n:代表 numeric 數值
inet_pton做從表達格式到數值格式的轉化
inet_ntop做從數值格式到表達格式的轉化(strptr必須事先分配好空間,len防止緩沖區溢出)
*/
family參數既可以是AF_INET
也可以是AF_INET6
。
inet_pton(AF_INET,cp,&foo.sin_addr);
//上述語句等價於:
foo.sin_addr.s_addr=inet_addr(cp);
//----------------------------
char str[len];
ptr=inet_ntop(AF_INET,&foo.sin_addr,str,sizeof(str));
//上述語句等價於:
ptr=inet_ntoa(foo.sin_addr);
在《UNIX網絡編程》書中,對inet_ntop
做了一層封裝,調用者可以忽略其協議族:
#include "unp.h"
#ifdef HAVE_SOCKADDR_DL_STRUCT
#include
#endif
/* include sock_ntop */
char *
sock_ntop(const struct sockaddr *sa, socklen_t salen)
{
char portstr[8];
static char str[128]; /* Unix domain is largest */
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *) sa;
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return(NULL);
if (ntohs(sin->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return(str);
}
/* end sock_ntop */
#ifdef IPV6
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
str[0] = '[';
if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
return(NULL);
if (ntohs(sin6->sin6_port) != 0) {
snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
strcat(str, portstr);
return(str);
}
return (str + 1);
}
#endif
//...
return (NULL);
}
char *
Sock_ntop(const struct sockaddr *sa, socklen_t salen)//外部接口
{
char *ptr;
if ( (ptr = sock_ntop(sa, salen)) == NULL)
err_sys("sock_ntop error"); /* inet_ntop() sets errno */
return(ptr);
}
我們經常使用是,read
和write
書中提到,我們請求的字節數往往比輸入輸出的字節數要多,原因在於緩沖區大小的限制,這樣我們不得不多次調用read
和write
,於是作者為了方便期間又再一次的封裝。
readn
:從描述符中讀n個字節
wirten
:往描述符中寫n個字節
readline
:從描述符中讀文本行一次一個字節。
書中給出了這些函數的具體實現,無非就是在內部調用read和write
,這裡我們直到怎麼用就OK。
套接字編程的基礎:包括數據結構和一些函數及其書作者對函數的封裝,大概是以下順序:
套接字結構->以套接字結構為參數的函數->參數傳遞方式->網絡字節序和主機字節序的轉化(端口號用到)->IP地址的轉換->緩沖區操作(拷貝等)->I/O操作。
了解套接字的結構非常重要,另外在網絡編程中,還需要直到特地函數的調用,不需要死記硬背,是在記不住可以在linux終端輸入man
命令來查看相應的函數需要的參數及返回值等信息。