Linux下socket编程基础——socket地址API

作者: veaxen 分类: Linux,网络 发布时间: 2017-05-02 22:33

socket编程指的是一整套关于socket的API编程,不是只有socket()系统调用的使用,还有围绕着这一个系统调用的一整套函数。下面我们将从基础的地方开始,了解如何初步的编写socket程序。

主机字节序和网络字节序

要学习socket编程,首先先了解下什么是字节序。

现代CPU的累加器一次都能装载(至少)4个字节(这里考虑32位机器,下同)。那么这4个字节在内存中的排列的顺序将影响它被累加器装载成整数的值,这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节序是指一个整数的高位字节在内存的低地址,小端字节序是指一个整数的低位字节在内存的低地址。下面给一个测试机器字节序的程序:

#include <stdio.h>

int main()
{
    union{
        short s;
        char c[sizeof(short)];
    } test;

    test.s = 0x1234;

    if((test.c[0] == 0x34) && (test.c[1] == 0x12)){
        printf("little endian\n");
    }else((test.c[0] == 0x12) && (test.c[1] == 0x34)){
        printf("big endian\n");
    }else{
        printf("unknow...\n");
    }

}

现代PC大多数采用小端字节序,因此小端字节序又称为主机字节序。

当格式化的数据(比如32bit整数和16bit短整数)在两台使用不同字节序的主机上传输时,接收端必然错误的解释。解决的办法就是进行统一格式化,都采用大端字节序发送和接收,因此,大端字节序又被称为网络字节序。

需要注意的是,即使在同一台主机上的两个进程(一个用C编写,一个用java编写)通信,也要考虑字节序问题(java虚拟机采用大端字节序)。

linux提供了如下四个函数来完成主机字节序和网络字节序的转换:

#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshrot);

如何记住他们,举一个例子就可以了:htonl表示“host to network long”。
这四个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号

通用socket地址

socket网络编程接口中表示socket地址的结构体sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr
{
    sa_family_t sa_family;
    char sa_data[14];
};

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议类型对应。常见的协议族(protocol family,也称domain)和对应的地址族下表:

协议族 地址族 描述
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 16bit端口号和32bit IPv4地址,共6个字节
PF_INET6 16bit端口号和32bit 流标识,128bit IPv6地址,32bit范围ID,共26字节

由上表可见,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)];
};

这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)

专用socket地址

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

Unix本地域协议族使用如下专用socket地址结构体:

#include <sys/un.h>
struct sockaddr_un
{
    sa_family_t sa_family;/* 地址族协议:AF_UNIX */
    char sun_path[108]; /* 文件路径名 */
};

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,他们分别用于IPv4和IPv6:

struct sockaddr_in
{
    sa_family sin_family;/* 地址族:AF_INET */
    u_int16_t sin_port;/* 端口号(网络字节序) */
    struct in_addr sin_addr;/* IPv4地址结构体,下见 */
};
struct in_addr
{
    u_int32_t s_addr;/* IPv4地址(网络字节序) */
};

struct sockaddr_in6
{
    sa_family_t sin6_family;/* 地址族:AF_INT6 */
    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地址(网络字节序) */
}

这两个专用socket地址结构体各字段的含义都很明确,所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr

IP地址转化函数

通常,我们习惯用字符串来表示IP地址,如“192.168.1.1”,在进行socket编程的时候需要把这个字符串IP转换成struct in_addr类型的,下面几个函数就是提供地址类型转换的。

#include <arpa/inet.h>
in_addr_t inet_addr(const char *strptr);
int inet_aton(const char *cp,struct in_addr *inp);
char* inet_ntoa(struct in_addr in);

inet_addr函数将用点分十进制字符串表示的IPv4地址转换为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。

inet_aton函数完成inet_addr同样的功能,但转化结果存在inp指向的in_addr地址结构体中。

inet_ntoa函数将网络字节序整数表示的IPv4地址转换为用点分十进制字符串表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量储存转换结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。下面代码显示了这种不可重入:

char *szVaule1 = inet_ntoa(/*1.2.3.4*/);
char *szValue2 = inet_ntoa(/*5.6.7.8*/);
printf("%s\n",szVaule1);
printf("%s\n",szVaule2);

结果是:
5.6.7.8
5.6.7.8

另外还有针对IPv6的(也兼容IPv4):

#include <arpa/inet.h>
int inet_pton(int af,const char *src,void *dst);
const char* inet_ntop(int af,const void *src,char *dst,socklen_t cnt);

这里我暂时没有去了解,目前IPv4对我来说已经够了,以后有需要再来深入学习吧。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据