说到网络编程一定都离不开套接字,以前用起来的时候大多靠记下来它的用法,这一次希望能理解一些更底层的东西,当然这些都是网络编程的基础~
大多说套接字函数都需要一个指向套接字地址结构的指针作为参数,每个协议族都定义它自己的套接字地址结构,这些结构都以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,再简单说两句,前者在应用层使用,后者在内核态使用,详情可参考:Sockaddr和Sockaddr_in的区别。套接字作为参数传递时总是以引用的方式(指向该结构的指针),所以说指针就需要支持任何协议族套接字的地址结构,因为函数是通用的,于是,就有了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在大/小端模式下应该是这样的
地址偏移 | 大端模式 | 小端模式 |
---|---|---|
0x00 | 0x12(高序字节) | 0x34(低序字节) |
0x01 | 0x34(低序字节) | 0x12(高序字节) |
参考代码:
//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
命令来查看相应的函数需要的参数及返回值等信息。