网络编程套接字(1)——socket地址API

主机字节序和网络字节序
32位的机器CPU累加器一次能装载至少4字节(32位)的数据,即一个整数。这4个字节在内存中排列的顺序将影响它被累加器装载成整数的值,这就是字节序的问题
字节序分为大端字节序和小端字节序
  • 大端字节序是指一个整数的高位字节(23-31位)放在内存的低地址,低位字节(0-7位)放在内存的高地址处
  • 小端字节序是指一个整数的高位字节放在内存的高地址处,低位字节放在内存的低地址处


写一段程序来验证主机到底是什么字节序
#include <stdio.h>

int main(){
    int num = 1;
    char* endian;
    endian = (char*)&num;
    if(endian[0] == 1){
        //0表示低地址,1整个整数的低8个字节就是1
        //即如果endian[0] == 1,就是低8位放在低地址,就是小端字节序
        printf("little endian\n");
    }else{
        printf("big endian\n");
    }
    return 0;
}
我的主机这里是小端字节序,现在一般的主机都是小端字节序,因此 小端字节序又被称为主机字节序, 但不代表没有大端字节序的主机
 
  • 当格式化的数据在两台使用不同字节序的主机之间直接传递的时候,可想而知接收端肯定会错误地解释这个数据
  • 因此TCP/IP协议有了这么一个规定,即发送端总是把要发送的数据转化成大端字节序数据后发送(如果是大端字节序的主机就不用再转换,小端则需要转换
  • 而接收端知道对方传来的数据总是大端字节序,所以接收端可以根据自己的字节序决定是否要进行转换(如果是大端字节序的主机就不用再转换,小端则需要转换)
  • 因为这个规定,大端字节序也被称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机通常把从网络上接到的字节依次保存在接收缓冲区内,也是按内存地址从低到高的顺序保存
  • 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址

以下函数用于转化字节序
#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned long int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned long int netshort);
  • h表示host,n表示network,l表示32位长整数,s表示16位短整数
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后发送
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
  • 如果主机是大端字节序,这些函数不转换,将参数原封不动地返回
  • 长整型函数通常用来转换IP地址,短整型函数通常用来转换端口号(不局限于这两个,任何数据在网络上传输时都需要这些函数来转换字节序)



通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下
#include <bits/sockaddr.h>

struct sockaddr                                                             
{
    sa_family_t sa_family;  
    char sa_data[14];       
};
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议类型对应。常见的协议族和对应的地址族如下所示

协议族
地址族
描述
PF_UNIX
AF_UNIX
UNIX本地域协议族
PF_INET
AF_INET
TCP/IPv4协议族
PF_INET6
AF_INET6
TCP/IPv6协议族
PF_*和AF_*都定义在/bits/socket.h头文件中,两者具有完全相同的值,所以二者通常混用

sa_data成员用于存放socket地址值。但是,不同的协议族具有不同的含义和长度

协议族
地址值含义和长度
PF_UNIX
文件的路径名,长度可达到108字节
PF_INET
16位端口号和32位IPv4地址,共6字节
PF_INET6
16位端口号,32位流标识,128位IPv6地址,32位范围ID,共26字节
因此14字节的sa_data成员根本无法完全容纳多数协议族的地址值。因此,有了下面这个新的通用socket地址结构体
#include <bits/sockaddr.h>

struct sockaddr_storage                                                           
{
    sa_family_t sa_family;
    unsigned long int __ss_align;  
     char _ss_padding[128-sizeof(__ss_align)];       
};
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)


专用socket地址
    上面这两个通用socket地址结构体显然很不好用,比如设置与获取IP地址和端口号就需要执行繁琐的位操作。所以Linux为各个协议族提供了专门的socket地址结构体

各种网络协议的地址格式都不一样。


UNIX本地域协议族使用如下专用socket地址结构体
#include <sys/un.h>
struct sockaddr_un
{
    sa_family_t sin_family;        /* 地址族:AF_UNIX */
    char sun_path[108];            /* 文件路径名*/
};

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体
IPv4用如下地址结构体
struct sockaddr_in
{
     sa_family_t sin_family;        /* 地址族:AF_INET */
    u_int16_t sin_port;            /* 端口号,要用网络字节序表示*/
    struct in_addr sin_addr;       /* IPv4地址结构体 */
};

struct in_addr
{
    u_int32_t s_addr               /* IPv4地址,要用网络字节序表示 */
};
IPv6用如下地址结构体
struct sockaddr_in6
{
     sa_family_t sin6_family;        /* 地址族:AF_INET6 */
    u_int16_t sin6_port;            /* 端口号,要用网络字节序表示*/
    u_int32_t sin6_flowinfo;        /* 流信息,应设置为0 */
    struct in6_addr sin6_addr;        /* IPv6地址结构体 */
    u_int32_t sin6_scope_id;        /* scope ID,尚处于实验阶段 */
};

struct in6_addr
{
    unsigned char sa_addr[16];        /* IPv6地址,要用网络字节序表示 */
};
  • IPv4和IPv6地址类型分别定义为常数AF_INET和AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要具体知道这是那种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • 所有专用socket地址(以及socket_storage)类型的变量在实际使用中都需要转化为通用socket地址类型sockaddr(强制转换),因为所有socket编程接口使用的地址参数的类型都是sockaddr。


IP地址转换函数

字符串转in_addr函数(aton,字符串转化为in_addr)
in_addr_t inet_addr(const char *strptr);//一般用这个比较多
int inet_aton(const char *strptr, struct in_addr *addrptr);
int inet_pton(int af, const char *src, void *dst);
  • inet_addr函数将点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE
  • inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数addrptr指向的地址结构中。成功返回0,失败返回1
  • inet_pton函数也完成类似的功能,但它还可以转换IPv6的in6_addr。af表示地址族,AF_INET表示IPv4,AF_INET6表示IPv6;src是一个用点分十进制表示的IPv4地址或用十六进制字符串表示的IPv6地址;把转换好的结果存储于dst指向的内存中。该函数成功返回0,失败返回1。

in_addr转字符串函数(ntoa,in_addr转化为字符串)
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
  • inet_nota 函数将用网络字节序整数表示的IPv4地址转化为点分十进制字符串表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的
  • inet_ntop也能完成类似的功能,但它还可以转换IPv6的in6_addr。前三个参数和inet_pton类似,最后一个参数size表示目标存储单元的大小,下面两个宏可以帮助我们指定这个大小(分别用于IPv4和IPv6)。该函数成功时返回存储单元地址,失败返回NULL并设置errno
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

关于inet_ntoa

inet_ntoa这个函数返回了一个char *,很显然是这个函数自己在内部为我们申请了一块内存来保存IP的结果。man手册上说,这个结果被放到了静态存储区,这个时候不需要我们手动释放。那么我们多次调用inet_ntoa会出现上面样的情况呢?

看下面的代码
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(){
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;

    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    char *ptr1 = inet_ntoa(addr1.sin_addr);
    char *ptr2 = inet_ntoa(addr2.sin_addr);

    printf("%s    %s\n",ptr1,ptr2);
    return 0;
}

运行结果如下

  • 因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
  • inet_ntoa这个函数实际上是线程安全的,这在man手册中明确指出了

  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供⼀个缓冲区保存结果, 可以规避线程安全问题

下面是多线程下测试inet_ntoa的代码
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

void *Func1(void *p){
    sockaddr_in* addr = (sockaddr_in *)p;
    while(1){
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr1 : %s\n",ptr);
        sleep(1);
    }
    return NULL;
}

void *Func2(void *p){
    sockaddr_in* addr = (sockaddr_in *)p;
    while(1){
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr2 : %s\n",ptr);
        sleep(1);
    }
    return NULL;
}

int main(){
    pthread_t tid1;
    pthread_t tid2;

    sockaddr_in addr1;
    sockaddr_in addr2;

    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    pthread_create(&tid1,NULL,Func1,&addr1);
    pthread_create(&tid2,NULL,Func2,&addr2);

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);

    return 0;
}

测试结果如下


由此可见多线程调用inet_ntoa,并不会出现上述覆盖的情况,可见它是线程安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值