套接字通信分两部分:
socket是一套通信的接口,Linux和Windows都有,但是有一些细微的差别。
0x01020304
内存的方向 ----->
内存的低位 -----> 内存的高位
04 03 02 01
0x1122334412345678
0x01020304
内存的方向 ----->
内存的低位 -----> 内存的高位
01 02 03 04
0x 12 34 56 78 11 22 33 44
/*
字节序:字节在内存中存储的顺序
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/
// 通过代码检测当前主机的字节序
#include
int main(){
union {
short value; // 2Bytes
char bytes[sizeof(short)]; // char[2]
} test;
test.value = 0x0102;
if(test.bytes[0] == 1 && test.bytes[1] == 2) printf("大端字节序\n");
else if(test.bytes[0] == 2 && test.bytes[1] == 1) printf("小端字节序, %d, %d, %d\n", test.value, test.bytes[0], test.bytes[1]);
else printf("未知\n");
return 0;
}
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然会错误解释。
解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。
网络字节序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节序采用大端排序方式。
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数: htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
h - host 主机,主机字节序
to - 转换成什么
n - network 网络字节序
s - short unsigned short
l - long unsigned int
// 网络通信时,需要将主机字节序转换成网络字节序
// 另外一端获取到数据以后根据情况将网络字节序转换成主机字节序。
#include
// 转换端口(16位 2字节)
uint16_t htons(uint16_t hostshort); // 主机字节序 -> 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 -> 主机字节序
// 转换IP(32位 4字节)
uint32_t htonl(uint32_t hostlong); // 主机字节序 -> 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 -> 主机字节序
#include
#include
int main()
{
// htons 转换端口
// 网络通信时 不会涉及负数传输 一般使用unsigned
unsigned short a = 0x0102;
printf("a: %x\n", a);
unsigned short b = htons(a);
printf("b: %x\n", b);
printf("---------------------\n");
// htonl 转换IP
char buf[4] = {192, 168, 1, 100};
int num = *(int *) buf;
int sum = htonl(num);
unsigned char* p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
printf("---------------------\n");
// ntohs
unsigned short a1 = 0x0201;
printf("a1: %x\n", a1);
unsigned short b1 = ntohs(a1);
printf("b1: %x\n", b1);
printf("---------------------\n");
// ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int*)buf1;
int sum1 = ntohl(num1);
unsigned char* p1 = (unsigned char*) &sum1;
printf("%d, %d, %d, %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
return 0;
}
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include
struct sockaddr {
sa_family_t sa_family; // 2字节
char sa_data[14];
};
typedef unsigned short int 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 bit 端口号和 32 bit IPv4 地址,共 6 字节 |
PF_INET6 | 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节 |
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include
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;
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include
struct sockaddr_un {
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
#include
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. IPv4*/
/* 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))
注意:
// 计算机有多个网卡 无线网卡、以太网卡...
// IP地址也都不同
// sockaddr.sin_addr.s_addr = 0; 表示所有网卡都绑定 客户端连接任何IP都可以访问到主机
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
// 比较旧的函数 并且只适用于IPv4
#include
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
// p:点分十进制的 IP字符串, n:表示 network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
参数:
af: 地址族: AF_INET AF_INET6
src: 需要转换的点分十进制的 IP字符串
dst: 转换后的结果保存在这个里面(传出参数)
返回值:
1: 成功
0: src 包含的不是目标地址族的IP字符串(不匹配)
-1: af 不是合法的地址族,and errno is set to EAFNOSUPPORT.
// 将网络字节序的整数,转换成点分十进制的 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的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst是一样的
示例:将点分十进制的IP字符串转换成网络字节序的整数,再转换回来
#include
#include
int main()
{
// 创建一个IP字符串,点分十进制的IP地址字符串
char buf[] = "192.168.1.4";
// 将点分十进制的IP字符串转换成网络字节序的整数
unsigned int num;
inet_pton(AF_INET, buf, &num);
unsigned char *p = (unsigned char *) #
printf("%d %d %d %d\n",*p, *(p+1), *(p+2), *(p+3));
// 将网络字节序的整数转换成点分十进制的IP字符串
char ip[16]; // 最大长度16个字节
const char* str = inet_ntop(AF_INET, &num, ip, sizeof(ip));
printf("%s\n", str);
return 0;
}