一、什么是套接字

所谓socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接是应用程序与网络协议根进行交互的接口。

  socket可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的socket中,该socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的socket中,使对方能够接收到这段信息。socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制。

  socket本身有"插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

 

二、字节序

  现代CPU的累加器一次都能装载(至少)4字节(这里考虑32位机),即一个整数。那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。仕各种订异机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。

  字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

  字节序分为大端字节序(Big-Endian)和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(23~31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

 2.1 小端字节序

 

 

2.2 大端字节序

 

  判断自己电脑的字节序:

/*
    字节序分为大端和小端字节序
    小端:高位存高位,低位存低位
    大端:高位存低位,低位存高位

*/
//检测当前主机字节序

#include<stdio.h>

int main(){

    union {
        short value;//2字节
        char bytes[sizeof(short)]; //2字节
    } test;
    
    test.value = 0x0102;
    //地址位低位等于高位
    if((test.bytes[0]==1)&&(test.bytes[1]==2)){
        printf("大端字节序\n");
    }else if((test.bytes[1]==1)&&(test.bytes[0]==2)){
        printf("小端字节序\n");
    }else{
        printf("未知\n");
    }

    return 0;
}

2.3 字节序转换函数

  当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。

  BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数: ntohs、ntohl。

 

 

 

 代码实例如下:

/*
    网络通信时,需要将主机字节序转换成网络字节序(大端)
    另外一端获取到数据以后根据情况将网络字节序转换成主机字节序
    //转换端口
    uint16_t htons(uint16_t hostshort);//主机字节序–网络字节序
    uint16_t ntohs(uint16_t netshort);//网络字节序-主机字节序
    //转IP
    uint32_t hton1(uint32_t hostlong);//主机字节序-网络字节序
    uint32_t ntoh1(uint32_t netlong) ;//网络字节序–主机字节序


*/

#include<stdio.h>
#include<arpa/inet.h>


int main(){

    //htons 转换端口
    unsigned short a = 0x0102;
    printf("%x\n",a);
    unsigned short b = htons(a);
    printf("%x\n",b);

    //htonl 转换IP
    unsigned char buf[4] = {192,168,1,100};
    int num = *(int *)buf;
    int sum = htonl(num);

    unsigned char *p = (char*) & sum;
    printf("%d %d %d %d\n",*p, *(p+1),*(p+2),*(p+3));
    printf("----------------------------------------------\n");

    //ntohs
    unsigned short a1 = 0x0201;
    printf("%x\n",a1);
    unsigned short b1 = ntohs(a1);
    printf("%x\n",b1);

    //ntohl
    unsigned char buf1[4] = {100,1,168,192};
    int num1 = *(int *)buf1;
    int sum1 = htonl(num1);

    unsigned char *p1 = (char*) & sum1;
    printf("%d %d %d %d\n",*p1, *(p1+1),*(p1+2),*(p1+3));

    return 0;
}

结果如下:

 三、SOCKET地址

  socket地址其实是一个结构体,封装端口号和IP等信息。socket相关的api中需要使用到这个socket地址。客户端 -> 服务器(IP, Port)。

3.1 通用SOCKET地址

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

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

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

 

 

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

 

 

 

 由上表可知,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) ];
};
typedef unsigned short int sa_family_t;

3.2 专用SOCKET地址

  很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

 

 

 

 

 

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

#include <sys/un.h>
struct sockaddr_un
{
  sa_family_t sin_family;
  char sun_path[108];
};

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

#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_)公共 */
in_port_t sin_port; /* Port number.端口号 */
struct in_addr sin_addr; /* Internet address. IP地址*/
/* Pad to size of `struct sockaddr'.填充 */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
    in_addr_t s_addr;
};
struct sockaddr_in6
{
    sa_family_t sin6_family;
    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 */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

 

四、IP地址转换

  通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

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

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

#include <arpa/inet.h>
/*p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
    af:地址族: AF_INET AF_INET6
    src:需要转换的点分十进制的IP字符串
    dst:转换后的结果保存在这个里面
将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    af:地址族: AF_INET AF_INET6
    src: 要转换的ip的整数的地址
    dst: 转换成IP地址字符串保存的地方
    size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

*/

 注意在网络字节序整数转IP地址时,使用返回值和数组接收结果是一样的。

/*
    #include <arpa/inet.h>
    p:点分十进制的IP字符串,n:表示network,网络字节序的整数
    int inet_pton(int af, const char *src, void *dst);
        af:地址族: AF_INET AF_INET6
        src:需要转换的点分十进制的IP字符串
        dst:转换后的结果保存在这个里面
        将网络字节序的整数,转换成点分十进制的IP地址字符串
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
        af:地址族: AF_INET AF_INET6
        src: 要转换的ip的整数的地址
        dst: 转换成IP地址字符串保存的地方
        size:第三个参数的大小(数组的大小)
        返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
*/

#include <arpa/inet.h>
#include<stdio.h>

int main(){
    //创建一个ip字符串
    //IP转网络字节序整数
    char buf[]="192.168.1.4";//点分十进制字符串
    unsigned int num=0;
    inet_pton(AF_INET, buf, &num);
    unsigned char* p =(unsigned char *)&num;
    printf("%d %d %d %d\n", *p, *(p+1),*(p+2),*(p+3));
    //网络字节序整数转IP字符转
    char ip[16] = "";
    const char *str = inet_ntop(AF_INET, &num, ip, 16);

    printf("ip: %s\n", ip);
    printf("str: %s\n", str);
    return 0;
}