一.大端序小端序

如果数据字节占用内存大小超过1字节,那么CPU数据存储在内存中有两种方式:

大端序(Big Endian):低位字节存放在高位,高位字节存放在低位;

小端序(Little Endian):低位字节存放在低位,高位字节存放在高位;

假设从内存0x00000001开始存储十六位进制数0x12345678那么

大端序:

0x00000001 0x12

0x00000002 0x34

0x00000003 0x56

0x00000004 0x78

小端序:

0x00000001 0x78

0x00000002 0x56

0x00000003 0x34

0x00000004 0x12

在我们平常中正常是使用大端序来对齐大多为英特尔cpu。

操作文件的本质是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件。

大小端序主要是在不同设备之间传输数据可能会造成问题。

在网络编程中,数据收发时有自动转化机制,不需要程序员手动转换,只有向sockaddr_in结构体成员变量填充数据时,才需要考虑字节问题。


二.网络字节序

为了解决不同设备之间的数据传输问题,采用网络字节序(大端序)。

c语言提供了四个函数,用于在主机字节序和网络字节序之间进行切换:

1
2
3
4
uint16_t htons(uint16_t hostshort); //2字节整数
uint32_t htonl(uint32_t hostlong); //4字节整数
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netslong);

h:host (主机)

to:转换

n:network (网络)

s:short (2字节 16位的整数)

l:long (4字节 32位整数)


三.IP地址和通讯端口

在计算机中,IPv4的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放。IP地址最高为:255.255.255.255

例如:192.168.190.134 –转化为整数–> 3232284294转化为整数时占用4字节,不转化占用15字节

​ 192 168 190 134

大端:11000000 10101000 10111110 10000110

小段:10000110 10111110 10101000 11000000


四.结构体

  • sockaddr结构体:

存放协议族、端口和地址信息,客户端和connect()函数以及服务器的bind()函数需要这个结构体

1
2
3
4
struct sockaddr{
unsigned short sa_family;//协议族,与socket()函数第一个参数相同,填AF_INET
unsigned char sa_data[14];//14字节端口和地址。
}
  • sockaddr_in结构体:

sockaddr结构体是为了统一地址结构表示方法,统一接口函数,但是操作不方便所以定义了等价的sockarrd_in的结构体,他的大小与sockaddr相同,可以强制转化成sockaddr

1
2
3
4
5
6
7
8
9
struct sockaddr_in{
unsigned short sin_family;//协议族,与socket()函数第一个参数相同,填AF_INET。
unsigned short sin_port;//16位端口号,大端序使用htons(整数端口)转换。
struct in_addr sin_addr; //ip地址结构体
unsigned char sin_zero[8];//未使用的部分,保持与sockaddr一样的长度
};
struct in_addr{
unsigned int s_addr; //32位ip地址,大端序
}

其中获取32位IP地址大端序获取有如下方案:

以下API在vs2015即被标记为废弃,不建议在新的代码中使用

新版代码位置:

新API

使用gethostbyname函数:

使用域名/主机名/字符串ip获取大端序ip。通常用于客户端

1
2
3
4
5
6
7
8
9
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name;//主机名
char**h_aliases;//主机所有别名构成的字符串数组,同一个IP可以绑定多个域名。
short h_addrtype;//主机IP地址类型,例如IPv4(AF_INET)还是IPv6(AF_INET6)
short h_length;//主机IP地址长度,IPv4地址为4,IPv6地址为16。
char**h_addr_list;//主机的ip地址,以网络字节序存储。
}
#define h_addr h_addr_list[0]

转换后将大端序地址代码复制到结构体sockaddr_in中的sin_addr成员中

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port_values);//此处填写服务端通信端口
struct hostent* h;
if((h=gethostbyname(/*域名/主机名/字符串格式的ip*/))==nullptr)
{
std::cout << 'failed to get gethost' <<std:;endl;
close(socket);//socket指的是连接
return -1;
}
memcpy(&servaddr.sin_addr,h->addr,h->h_length);//指定服务端的ip

使用getaddrinfo代替gethostbynamegethostbyaddr:

getaddrinfo相比于其他,参数设计非常灵活,支持IPv4/IPv6,多种套接字类型和协议,并且也是线程安全的并且同时也支持跨平台,在Linux中也可以放心替换。

1
2
3
4
5

int getaddrinfo(const char *hostname,
const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
  • hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的十六进制串)

  • service:一个服务名称或者10进制端口号数串

  • hints:可以为空指针,也可以为一个指向某addrinfo结构的指针,调用者在这个结构中填入关于期望返回信息的暗示。如果调用者的服务器支持TCP和UDP,那么调用者可以在这个参数的addrinfo中指定成员ai_socktype设置为SOCK_STREAM使得返回的仅仅适用于数据报套接口的信息。

  • result:返回的一个指向addrinfo结构表数据指针,其定义在头文件netdb.h

  • addrinfo结构体

1
2
3
4
5
6
7
8
9
10
struct addrinfo {
int ai_flags; // 标志位(AI_PASSIVE 等)
int ai_family; // AF_INET / AF_INET6 / AF_UNSPEC
int ai_socktype; // SOCK_STREAM / SOCK_DGRAM
int ai_protocol; // 0 表示自动
size_t ai_addrlen; // 地址长度
struct sockaddr *ai_addr; // 指向 sockaddr_in/in6
char *ai_canonname; // 正式主机名
struct addrinfo *ai_next; // 链表下一节点
};

注意:在使用完addrinfo结构体中一定要手动释放

使用freeaddrinfo(res);

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 向服务器发送连接请求
struct addrinfo server_addr {},*res = nullptr;
server_addr.ai_family = AF_INET;
server_addr.ai_socktype = SOCK_STREAM;

std::string port_str = std::to_string(server_port);
if (getaddrinfo(server_ip.c_str(),port_str.c_str(), &server_addr, &res)) {
::closesocket(m_clientfd);
m_clientfd = -1;
return false;
}

if (::connect(m_clientfd,res->ai_addr,(int)res->ai_addrlen) == -1) {
::closesocket(m_clientfd);
m_clientfd = -1;
return false;
}

freeaddrinfo(res);

五.字符串IP与大端序IP转换

把字符串IP转化为大端序IP,用于网络通讯的服务端中:

c语言提供了几个库函数,用于字符串格式的打IP和大端序IP的相互转化,用于网络通讯服务端程序中。

1
2
3
4
5
6
7
8
9
10
11
typedef unsigned int int_addr_t;

//把转换后的ip赋值给in_addr.s_addr。
in_addr_t inet_addr(const char* cp);

//在函数中,把转换后的ip填充到in_addr.s_addr成员中。
int inet_aton(const char* cp,struct in_addr *inp);

//把大端序IP转化成字符串IP,用于在服务端中解析客户端数据ip地址。
char *int_ntoa(struct in_addr in);

服务端用于通信的IP和端口绑定到socket上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(/*指定服务端口*/);
//如果操作系统有多个ipsuoyouip都用于通信
//只有一个ip则将INADDR_ANY换成ip地址即可
//只有一个地址servaddr.sin_addr.s_addr = inet_addr("192.168.101.138"); 字符串地址转化
//以上形式只支持IP地址不支持主机名,域名
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

//绑定服务端得IP和端口(listenfd)
if(bind(listendfd,(struct sockaddr*)&seraddr/*强制转化为sockaddr*/,sizeof(seraddr))==-1)
{
perror("bind");
close(listenfd);
return -1;
}

//socket设置为可连接(监听)
if(listen(listendfd,5)==-1)
{
perror("listen");
close(listenfd);
return -1;
}

相较于gethostbyname通过主机名,域名.inet_addr是直接指定IP不支持主机名和域名获取IP。所以多用于服务端,gethostbyname而在客户端使用。

在以上工作完成之后客户端可以连接服务端发送请求

1
2
3
4
5
6
//客户端
if(connect(sockfd,(struct sockaddr*)servaddr,sizeof(servaddr))==-1){
perror("connect");
close(sockfd);
return -1;
}