TCP/IP网络编程:P3->地址族与数据序列

news/2024/5/2 23:07:22/文章来源:https://blog.csdn.net/InnerPeaceHQ/article/details/126726456

本系列文章为《TCP/IP网络编程----尹圣雨》学习笔记,前面的系列文章链接如下
TCP/IP网络编程:P1->理解网络编程和套接字
TCP/IP网络编程:P2->套接字类型与协议设置

文章目录

  • 前言
  • 一、分配给套接字的IP地址与端口号
    • 1.1 网络地址(Internet Address)
    • 1.2 网络地址分类与主机地址边界
    • 1.3 用于区分套接字的端口号
  • 二、地址信息的表示
    • 2.1 表示IPv4地址的结构体
    • 2.2 结构体sockaddr_in的成员分析
  • 三、网络字节序与地址变换
    • 3.1 字节序(Order)与网络字节序
    • 3.2 字节序转换(Endian Conversions)
  • 四、网络地址的初始化和分配
    • 4.1 将字符串信息转换为网络字节序的整数型
      • 4.1.1 inet_addr函数
      • 4.1.2 inet_aton函数
      • 4.1.3 inet_ntoa函数
    • 4.2 网络地址初始化
    • 4.3 客户端地址信息初始化
    • 4.4 INADDR_ANY
    • 4.5 第1章的hello_server.c、hello_client.c运行过程
    • 4.6 向套接字分配网络地址
  • 五、基于Windows的实现
    • 5.1 函数htons、htonl在Windows中的使用
    • 5.2 函数inet_addr、inet_ntoa在Windows中的使用
    • 5.3 在Wondows环境下向套接字分配网络地址
    • 5.4 WSAStringToAddress & WSAAddressToString
      • 5.4.1 WSAStringToAddress
      • 5.4.2 WSAAddressToString
      • 5.4.3 函数示例


前言

前面一章讨论了套接字的创建方法,如果把套接字比喻为电话,那么目前只安装了电话机。本章着重讲解给电话机分配号码的方法,即给套接字分配IP地址和端口号。


一、分配给套接字的IP地址与端口号

IP是Internet Protocol(网络协议)的简写,是为收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为区分程序中创建的套接字而分配给套接字的序号。


1.1 网络地址(Internet Address)

IP地址

为使计算机连接到网络并收发数据,必须为其分配IP地址。IP地址分为两类。
①IPV4(Internet Protocol version 4)4 字节地址族
②IPV6(Internet Protocol version 6)6 字节地址族
区别: 两者之间的主要差别是IP地址所用的字节数,目前IPV4和IPV6都已经通用。

IPv4

IPV4标准的4字节IP地址分为网络地址主机(指计算机)地址,且分为 A、B、C、D、E 等类型。下图展示了IPv4地址族,一般不会使用已被预约了的E类地址,故省略。
在这里插入图片描述
数据传输过程: 网络地址(网络ID)是为区分网络而设置的一部分IP地址。假设向WWW.SEMI.COM公司传输数据,该公司内部构建了局域网,把所有计算机连接起来。因此,首先应向SEMI.COM网络传输数据,也就是说,并非一开始就浏览所有4字节IP地址进而找到目标主机。而是仅浏览4字节IP地址的网络地址,先把数据传到SEMI.COM的网络。SEMI.COM网络(构成网络的路由器)接收到数据后,浏览传输数据的主机地址(主机ID),并将数据传给目标计算机。
示例: 某主机向203.211.172.103和203.211.217.202传输数据,其中203.211.172和203.211.217为该网络的网络地址。所以,向相应网络传输数据实际上是向构成网络的路由器(Router)或交换机(Switch)传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传递数据。
在这里插入图片描述

路由器与交换机

若想构建网络,需要一种物理设备完成外网与本网主机之间的数据交换,这种设备便是路由器或交换机。它们实际上也是一种计算机,只不过是为特殊目的而设计运行的,因此有了别名。所以,如果在我们使用的计算机上安装适当的软件,也可以将其用作交换机。另外,交换机比路由器功能要简单一些,而实际用途差别不大。


1.2 网络地址分类与主机地址边界

只需通过IP地址的第一个字节即可判断网络地址占用的字节数,因为我们根据IP地址的边界区分网络地址,如下所示:

  • A类地址的首字节范围:0~127
  • B类地址的首字节范围:128~191
  • C类地址的首字节范围:192~223

还有如下这种表述方式:

  • A类地址的首位以0开始
  • B类地址的前2位以10开始
  • C类地址的前3位以110开始

正因如此,通过套接字收发数据时,数据传到网络后即可轻松找到正确的主机。


1.3 用于区分套接字的端口号

端口号的产生背景

IP用于区分计算机,只要有IP地址就能向目标主机传输数据,但仅凭这些无法传输给最终的应用程序。假设各位欣赏视频的同时在网上冲浪,这时至少需要1个接收视频数据的套接字和1个接收网页信息的套接字。问题在于如何区分二者。简言之,传输到计算机的网络数据是发给播放器还是发送给浏览器?

端口号的使用示例

背景: 假设我开发了收发数据的P2P程序,该程序用块单位分割1个文件,从多台计算机接收数据。
问题: 如上所述,若想接收多台计算机发来的数据,则需要相应个数的套接字。那如何区分这些套接字呢?
答案: 计算机中一般配有NIC(Network Interface Card,网络接口卡)数据传输设备。通过NIC向计算机内部传输数据时会用到IP。操作系统负责把传递到内部的数据适当分配给套接字,这时就要利用端口号。也就是说,通过NIC接收的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字,如下图所示。
在这里插入图片描述

端口号的性质

①端口号就是在同一操作系统内为区分不同套接字而设置的,因此无法将1个端口号分配给不同套接字。
②另外,端口号由16位构成,可分配的端口号范围是0-65535。但0-1023是知名端口(Well-known PORT),一般分配给特定应用程序,所以应当分配此范围之外的值。
③最后,虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。例如:如果某TCP套接字使用9190号端口,则其他TCP套接字就无法使用该端口号,但UDP套接字可以使用。
总结: 总之,数据传输目标地址同时包含IP地址和端口号,只有这样数据才会被传输到最终的目的应用程序(应用程序套接字)。


二、地址信息的表示

应用程序中使用的IP地址和端口号以结构体的形式给出了定义。本节以IPv4为中心,围绕此结构体讨论目标地址的表示方法。


2.1 表示IPv4地址的结构体

填写地址信息时围绕提问为线索进行:

  • 提问1:“采用哪一种地址族?”

    回答1:“基于IPv4的地址族。”

  • 提问2:“IP地址是多少?”

    回答2:“211.204.214.76。”

  • 提问3:“端口号是多少?”

    回答3:“2048。”

结构体定义为如下形态就能包含上述信息,此结构体将作为地址信息传递给bind函数。

struct sockaddr_in
{sa_family_t sin_family;  //地址族(Address Family)uint16_t sin_port;       //16 位 TCP/UDP 端口号struct in_addr sin_addr; //32位 IP 地址char sin_zero[8];        //不使用
};

该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址。

struct in_addr
{in_addr_t s_addr; //32位IPV4地址
}

讲解以上2个结构体前先观察一些数据类型。uint16_tin_addr_t等类型可以参考POSIX(Portable Operating System Interface,可移植操作系统接口)。POSIX是为UNIX系列操作系统设立的标准,它定义了一些其他数据类型,如下表所示。

数据类型名称数据类型说明声明的头文件
int 8_tsigned 8-bit intsys/types.h
uint8_tunsigned 8-bit int (unsigned char)sys/types.h
int16_tsigned 16-bit intsys/types.h
uint16_tunsigned 16-bit int (unsigned short)sys/types.h
int32_tsigned 32-bit intsys/types.h
uint32_tunsigned 32-bit int (unsigned long)sys/types.h
sa_family_t地址族(address family)sys/socket.h
socklen_t长度(length of struct)sys/socket.h
in_addr_tIP地址,声明为 uint_32_tnetinet/in.h
in_port_t端口号,声明为 uint_16_tnetinet/in.h

从这些数据类型声明也可掌握之前结构体的含义。额外定义这些数据类型的原因是:这是考虑到扩展性的结果。如果使用int32_t类型的数据,就能保证在任何时候都占用4字节,即使将来用64位表示int类型也是如此。


2.2 结构体sockaddr_in的成员分析

①成员 sin_family

每种协议适用的地址族不同。比如,IPV4使用4字节的地址族,IPV6使用16字节的地址族。

sin_family地址信息如下表所示:

地址族(Address Family)含义
AF_INETIPV4网络协议中用的地址族
AF_INET6IPV6网络协议中用的地址族
AF_LOCAL本地通信中采用的UNIX协议的地址族

注: AF_LOACL只是为了说明具有多种地址族而添加的。

②成员 sin_port

该成员保存16位端口号,重点在于它以网络字节序保存。

③成员 sin_addr

该成员保存32位IP地址信息,且也以网络字节序保存。为理解好该成员,应同时观察结构体in_addr。但结构体in_addr声明为uint32_t,因此只需当作32位整数型即可。

④成员 sin_zero

无特殊含义。只是为结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员。必须填充为0,否则无法得到想要的结果。


sockaddr_in 结构体的使用
在之前的代码中,sockaddr_in结构体变量地址值将以如下方式传递给bind函数。

struct sockaddr_in serv_addr;
....
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)error_handling("bind() error");
....

此处bind第二个参数期望得到sockaddr结构体变量的地址值,包括地址族、端口号、IP地址等。sockaddr结构体的定义如下:

struct sockaddr
{sa_family_t sin_family; //地址族char sa_data[14];       //地址信息
}

注意:

sockaddr是通用的结构体,并非只为IPv4设计,这从保存地址信息的数组sa_data长度为14字节也可看出。因此,结构体sockaddr要求在sin_family中指定地址族信息。
对于包含地址信息来讲使用sockaddr非常麻烦,所以我们使用新的结构体sockaddr_in生成符合bind函数要求的字节流。最后将sockaddr_in型的结构体强制转换为sockaddr型的结构体变量,再传递给bind函数即可。
由于结构体sockaddr并非只为IPv4设计,为了与sockaddr保持一致,所以sockaddr_in结构体中也有地址族信息。


三、网络字节序与地址变换

背景

不同CPU中,4字节整数型值1在内存空间的保存方式是不同的。4字节整数型值1可用2进制表示如下。
00000000 00000000 00000000 00000001
有些CPU以这种顺序保存到内存,另外一些CPU则以倒序保存.
00000001 00000000 00000000 00000000
若不考虑这些就收发数据则会发生问题,因为保存顺序的不同意味着对接收数据的解析顺序也不同。


3.1 字节序(Order)与网络字节序

字节序

CPU向内存保存数据的方式有2种,这意味着CPU解析数据的方式也分为2种。
①大端序(Big Endian):高位字节存放到低位地址。
②小端序(Little Endian):高位字节存放到高位地址。

示例: 不同字节序保存数据的方式

假设在0x20号开始的地址中保存4字节int类型数0x12345678。
①大端序CPU保存方式:
在这里插入图片描述
整数0x12345678中,0x12是最高位字节,0x78是最低位字节。因此,大端序中先保存最高位字节0x12(最高位字节0x12存放到低位地址)。
②小端序CPU保存方式:
在这里插入图片描述
先保存最低位字节0x78
总结: 从以上分析可以看出,每种CPU的数据保存方式均不同。因此,代表CPU数据保存方式的 主机字节序(Host Byte Order) 在不同CPU中也各不相同。目前主流的Intel系列CPU以小端序方式保存数据。


示例: 字节序问题

接下来分析2台字节序不同的计算机之间数据传递过程中可能出现的问题,如下图所示。
在这里插入图片描述
问题: 上图中,大端序系统传输数据0x1234时未考虑字节序问题,直接以0x12、0x34的顺序发送。接收端以小端序方式保存数据,因此小端序接收的数据变成0x3412。
解决方案: 在通过网络传输数据时,将数据组的格式统一约定为大端序,这种约定称为网络字节序(Network Byte Order)。因此,所有计算机接收数据时应识别该数据是网络字节序格式,小端序系统传输数据时应转化为大端序排列方式。


3.2 字节序转换(Endian Conversions)

字节序转换函数

下面介绍帮助转换字节序的函数。
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsined long ntohl(unsigned long);
函数名的含义
①htons中的h代表主机(host)字节序。
②htons中的n代表网络(network)字节序。
③htons中的s指的是short。
④htons中的l指的是long(Linux中long类型占用4个字节,这很关键)。
因此,htons是h、to、n、s的组合,也可以解释为把short型数据从主机字节序转化为网络字节序。ntohs可以解释为把short型数据从网络字节序转化为主机字节序。
端口号转换与IP地址转换
通常,以s作为后缀的函数中,s代表2个字节short,因此用于端口号转换
l作为后缀的函数中,l代表4个字节,因此用于IP地址转换
疑问: 我的系统是大端序的,为sockaddr_in结构体变量赋值前就不需要转换字节序了吧?
回答: 这么说也不能算错。但我认为,有必要编写与大端序无关的统一代码。这样,即使在大端序系统中,最好也经过主机字节序转换为网络字节序的过程。当然,此时主机字节序与网络字节序相同,不会有任何变化。


示例: 字节序转换函数使用案例(Linux)

#include <stdio.h>
#include <arpa/inet.h>int main(int argc, char *argv[])
{unsigned short host_port = 0x1234;unsigned short net_port;unsigned long host_addr = 0x12345678;unsigned long net_addr;net_port = htons(host_port); net_addr = htonl(host_addr);printf("Host ordered port: %#x \n", host_port);printf("Network ordered port: %#x \n", net_port);printf("Host ordered address: %#lx \n", host_addr);printf("Network ordered address: %#lx \n", net_addr);return 0;
}

下面这就是在小端序CPU中运行的结果。如果在大端序CPU中运行,则变量值不会改变。大部分朋友都会得到类似的运行结果,因为Intel和AMD系列的CPU都采用小端序标准。
在这里插入图片描述


思考

问题: 数据在传输之前都要经过转换吗?
答: 也许有人认为:“既然数据传输采用网络字节序,那在传输前应直接把数据转换成网络字节序,接收的数据也需要转换成主机字节序再保存。”实际上,这个过程是自动的。除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题。


四、网络地址的初始化和分配

前面已讨论过网络字节序,接下来介绍以bind函数为代表的结构体的应用。


4.1 将字符串信息转换为网络字节序的整数型

sockaddr_in中保存地址信息的成员为32位整数型。然而,对于IP地址的表示,我们熟悉的是点分十进制表示法(Dotted Decimal Notation),而非整数型数据表示法。幸运的是,有函数会帮我们将字符串形式的IP地址转换成32位整数型数据,在转换类型的同时进行网络字节序转换。


4.1.1 inet_addr函数

inet_addr函数的声明如下:

#include<arpa/inet.h>
in_addr_t inet_addr(const char * string);
//成功时返回32位大端序整数型值,失败时返回INADDR_NONE。

如果向该函数传递类似211.214.107.99的点分十进制格式的字符串,它会将其转换为32位整数型数据并返回。当然,该整数型值满足网络字节序。另外,该函数的返回值类型in_addr_t在内部声明为32位整数型。


示例: inet_addr函数的调用过程

#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{char *addr1 = "1.2.3.4";//1个字节能表示的最大整数是255,所以代码中 addr2 是错误的IP地址。从运行结果看,inet_addr 不仅可以转换地址,还可以检测有效性。char *addr2 = "1.2.3.256";unsigned long conv_addr = inet_addr(addr1);if (conv_addr == INADDR_NONE) //正确printf("Error occured! \n");elseprintf("Network ordered integer addr: %#lx \n", conv_addr);conv_addr = inet_addr(addr2);if (conv_addr == INADDR_NONE) //错误printf("Error occured! \n");elseprintf("Network ordered integer addr: %#lx \n", conv_addr);return 0;
}

运行,可以看出inet_addr函数不仅可以把IP地址转成32位整数型,而且可以检测无效的IP地址。另外,从输出结果可以验证确实转换为网络字节序。
在这里插入图片描述


4.1.2 inet_aton函数

inet_aton函数与inet_addr函数在功能上完全相同,也将字符串形式IP地址转换为32位网络字节序整数并返回。不同的是该函数利用了in_addr结构体,且使用频率更高。

#include<arpa/inet.h>
int inet_aton(const char * string, struct in_addr * addr);
/*
成功时返回1(true),失败时返回0(false)。
参数1:string,含有需转换的IP地址信息的字符串地址值。
参数2:addr,将保存转换结果的in_addr结构体变量的地址值。
*/

调用inet_addr函数,返回转换后的IP地址信息还需保存到sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数则不需此过程,因为该函数会自动把结果存入该结构体变量。


示例: inet_aton函数的调用过程

#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
void error_handling(char * message);int main(int argc, char *argv[])
{char * addr = "127.232.124.79";struct sockaddr_in addr_inet;if(!inet_aton(addr, &addr_inet.sin_addr))//转换后的IP地址信息虚构保存到sockaddr_in的in_addr型变量才有意义。因此inet_aton函数的第二个参数要求得到in_addr型的变量地址值。这就省去了手动保存IP地址信息的过程。error_handling("Conversion error");elseprintf("Network order integer addr: %#x \n",addr_inet.sin_addr.s_addr);return 0;
}void error_handling(char * message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

运行,结果如下。可以看出,已经成功的把转换后的地址放进了addr_inet.sin_addr.s_addr中。
在这里插入图片描述


4.1.3 inet_ntoa函数

还有一个函数,与 inet_aton() 正好相反,它可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式,函数原型如下:

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr adr);

注意:

----该函数将通过参数传入的整数型IP地址转换为字符串格式并返回。
----但要小心,返回值为 char 指针,返回字符串地址意味着字符串已经保存在内存空间,但是该函数未向程序员要求分配内存,而是在内部申请了内存保存了字符串。也就是说调用了该函数候要立即把信息复制到其他内存空间。因为,若再次调用inet_ntoa函数,则有可能覆盖之前保存的字符串信息。
----总之,再次调用 inet_ntoa 函数前返回的字符串地址是有效的。若需要长期保存,则应该将字符串复制到其他内存空间。


示例: inet_ntoa函数的调用过程

#include<stdio.h>
#include<string.h>
#include<arpa/inet.h>int main(int argc, char * argv[])
{struct sockaddr_in addr1, addr2;char *str_ptr;char str_arr[20];addr1.sin_addr.s_addr = htonl(0x1020304);addr2.sin_addr.s_addr = htonl(0x1010101);//把结构体变量addr1中的IP地址信息转换为字符串形式str_ptr = inet_ntoa(addr1.sin_addr);strcpy(str_arr, str_ptr); //浏览并复制上面返回的IP地址信息printf("Dotted-Decimal notation1: %s \n", str_ptr);/*
再次调用inet_ntoa函数。由此得出,第调用inet_ntoa那一行中返回的地址已覆盖了新的IP地址字符串,可通过第打印str_ptr的输出结果进行验证。
*/inet_ntoa(addr2.sin_addr);printf("Dotted-Decimal notation2: %s \n", str_ptr);//因为inet_ntoa返回的IP地址笛福传保存到了str_arr中,因此可以正确输出printf("Dotted-Decimal notation3: %s \n", str_arr); return 0;
}

运行,结果如下。可以看出成功将IP地址转化为字符串。
在这里插入图片描述


4.2 网络地址初始化

下面是服务器端套接字创建过程中常见的网络地址信息初始化方法:

struct sockaddr_in addr;
char *serv_ip = "211.217.168.13";          // 声明 IP 地址字符串
char *serv_port = "9190";                  // 声明端口号字符串
memset(&addr, 0, sizeof(addr));            // 结构体变量 addr 的所有成员初始化为 0,主要是为了将 sockaddr_in 的成员 sin_zero 初始化为 0。
addr.sin_family = AF_INET;                 // 指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的 IP 地址初始化
addr.sin_port = htons(atoi(serv_port));    // 基于字符串的端口号初始化

代码解读

①上述代码中,memset函数将每个字节初始化为同一值:
----第一个参数为结构体变量addr的地址值,即初始化对象为addr
----第二个参数为0,因此初始化值为0
----第三个参数传入addr的长度
因此addr的所有字节均初始化为0。这么做是为了将sockaddr_in结构体的成员sin_zero初始化为0。
②最后一行代码调用的atoi函数把字符串类型的值转换成整数型。
③总之,上述代码利用字符串格式的IP地址和端口号初始化了sockaddr_in结构体变量。
④另外,代码中对IP地址和端口号进行了硬编码,这并非良策,因为运行环境改变就得更改代码。因此,我们运行时传入IP地址和端口号会更好。


4.3 客户端地址信息初始化

上述网络地址信息初始化过程主要针对服务器端而非客户端。给套接字分配IP地址和端口号主要是为下面这件事做准备:

“请把进入IP 211.217.168.13、9190端口的数据传给我!”

反观客户端中连接请求如下:

“请连接到IP 211.217.168.13、9190端口!”

请求方法不同意味着调用的函数也不同:

  • 服务器端的准备工作通过bind函数完成
  • 客户端的准备工作通过connect函数完成

因此函数调用前需准备的地址值类型也不同:

  • 服务器端声明sockaddr_in结构体变量,将其初始化为赋予服务器端IP和套接字的端口号,然后调用bind函数
  • 客户端声明sockaddr_in结构体,并初始化为要与之连接的服务器端套接字的IP和端口号,然后调用connect函数。

4.4 INADDR_ANY

每次创建服务器端套接字都要输入IP地址会有些繁琐,此时可如下初始化地址信息:

struct sockaddr_in addr;
char * serv_port = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
add.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));

与之前方式的区别

与之前方式最大的区别在于,利用常数INADDR_ANY分配服务器端的IP地址。
----若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。
----而且,若同一计算机中已分配多个IP地址(多宿主(Multi-homed)计算机,一般路由器属于这一类),则只要端口号一致,就可以从不同IP地址接收数据。
因此,服务器端中优先考虑这种方式。而客户端中除非带有一部分服务器端功能,否则不会采用。

创建服务器端套接字时需要IP地址的原因

问题: 初始化服务器端套接字时应分配所属计算机的IP地址,因为初始化时使用的IP地址非常明确,那为何还要进行IP初始化呢?
答案: 同一计算机中可以分配多个IP地址,实际IP地址的个数与计算机中安装的NIC的数量相等。即使是服务器端套接字,也需要决定应接收哪个IP地址传来的(哪个NIC传来的)数据。因此,服务器端套接字初始化过程中要求IP地址信息。另外,若只有一个NIC,则直接使用INADDR_ANY。


4.5 第1章的hello_server.c、hello_client.c运行过程

  • 第1章中执行./hserver 9190命令以运行相当于服务器端的hello_server.c。通过代码可知,向main函数传递的9190为端口号。通过此端口创建服务器端套接字并运行程序,但未传递IP地址,因为可以通过INADDR_ANY指定IP地址。

  • 执行./hclient 127.0.0.19198命令以运行相当于客户端的hello_client.c。与服务器端运行方式相比,最大的区别是传递了IP地址信息。

  • 127.0.0.1是回送地址(loopback address),指的是计算机自身IP地址。在第1章的示例中,服务器端和客户端在同一计算机中运行。因此,连接目标服务器端的地址为127.0.0.1。当然,若用实际IP地址代替此地址也能正常运转。如果服务器端和客户端分别在2台计算机中运行,则可以输入服务器端IP地址。

4.6 向套接字分配网络地址

前面了解了sockaddr_in结构体的初始化方法,接下来就把初始化的地址信息分配给套接字。bind函数负责这项操作。

#include<sys/socket.h>
int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen);
/*
成功时返回0,失败时返回-1。
参数1:sockfd,要分配地址信息(IP地址和端口号)的套接字文件描述符
参数2:myaddr,存有地址信息的结构体变量地址值。
参数3:addrlen,第二个结构体变量的长度。
*/

如果此函数调用成功,则将第二个参数指定的地址信息分配给第一个参数中的相应套接字。


示例: 服务器端常见套接字初始化过程

int serv_sock;
struct sockaddr_in serv_addr;
char * srev_port = "9190";/* 创建服务器端套接字(监听套接字)*/
serv_sock = socket(PF_INET, SOCK_STREAM, 0);/* 地址信息初始化 */
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));/* 分配地址信息 */
bind(serv_sock, (struct sockaddr * )&serv_addr, sizeof(serv_addr));

服务器端代码结构默认如上,当然还有未显示的异常处理代码。


五、基于Windows的实现

Windows中同样存在sockaddr_in结构体及各种变换函数,而且名称、使用方法及含义都相同。也就无需针对Windows平台进行太多修改或改用其他函数。接下来将前面几个程序改成Windows版本。


5.1 函数htons、htonl在Windows中的使用

示例: Windows平台下调用htons函数和htonl函数,这两个函数的用法与Linux平台下的使用并无区别。

#include <WinSock2.h>
#include <stdio.h>int main(int argc, char *argv[])
{WSADATA wsa_data;unsigned short host_port = 0x1234;unsigned short net_port;unsigned long host_addr = 0x12345678;unsigned long net_addr;if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)printf("WSAStarup() error!");net_port = htons(host_port);net_addr = htonl(host_addr);printf("Host ordered port: %#x \n", host_port);      // 打印结果:0x1234printf("Network ordered port: %#x \n", net_port);    // 打印结果:0x3412printf("Host ordered address: %#x \n", host_addr);   // 打印结果:0x12345678printf("Network ordered address: %#x \n", net_addr); // 打印结果:0x78563412WSACleanup();return 0;
}

该程序多了进行库初始化的WSAStartup函数调用和winsock2.h头文件的#include语句,其他部分没有区别。运行,结果如下:
在这里插入图片描述


5.2 函数inet_addr、inet_ntoa在Windows中的使用

前面分别给出了Linux中这两个函数的调用示例,而Windows中不存在inet_aton函数,故这里省略inet_aton函数的调用。

示例: inet_addr函数和inet_ntoa函数的调用过程

#pragma execution_character_set("utf-8")#include <stdio.h>
#include <string.h>
#include <WinSock2.h>
void ErrorHandling(char* message);int main(int argc, char* argv[])
{WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)printf("WSAStarup() error!");// inet_addr 函数调用示例{char* addr = "127.212.124.78";unsigned long conv_addr = inet_addr(addr);if(conv_addr == INADDR_NONE)printf("Error occured!\n");elseprintf("Network ordered integer addr: %#lx \n", conv_addr);}// inet_ntoa 函数调用示例{struct sockaddr_in addr;char* strPtr;char strArr[20];addr.sin_addr.s_addr = htonl(0x1020304);strPtr = inet_ntoa(addr.sin_addr);strcpy(strArr, strPtr);printf("Dotted-Decimal notation3 %s \n", strArr);}WSACleanup();return 0;
}void ErrorHandling(char* message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

运行,结果如下
在这里插入图片描述


5.3 在Wondows环境下向套接字分配网络地址

Windows中向套接字分配网络地址的过程与Linux中完全相同,因为bind函数的含义、参数及返回类型完全一致。

sOCKET servSock;
struct sockaddr_in servAddr;
char * servPort = "9190";/*创建服务器端套接字*/
servSock = socket(PF_INET,SOCK_STREAM,0);/*地址信息初始化*/
memset(&servAddr, 0, sizeof( servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = hton1( INADDR_ANY);
servAddr.sin_port = htons(atoi(serv_port));/*分配地址信息*/
bind(servSock,(struct sockaddr * )&servAddr,sizeof( servAddr));

这与Linux平台下套接字初始化及地址分配过程基本一致,只不过改了一些变量名。


5.4 WSAStringToAddress & WSAAddressToString

这两个转换函数的优缺点

下面介绍Winsock2中增加的2个转换函数,它们在功能上与inet_ntoa和inet _addr完全相同。
①优点: 支持多种协议,在IPv4和IPv6中均可适用。
②缺点: 使用inet _ntoa、inet _addr可以很容易地在Linux和Windows之间切换程序。而将要介绍的这2个函数则依赖于特定平台,会降低兼容性。因此本书不会使用它们,介绍的目的仅在于让各位了解更多函数。


5.4.1 WSAStringToAddress

WSAStringToAddress函数将地址信息字符串适当填入结构体变量。

#include <winsock2.h>
INT WSAStringToAddress(LPTSTR AddressString, INT AddressFamily, LPwSAPROTOCOL_INFO lpProtocolInfo,LPSOCKADDR lpAddress, LPINT lpAddressLength
);
//成功时返回0,失败时返回SOCKET_ERROR。
/*
AddressString    含有IP和端口号的字符串地址值。
AddressFamily    第一个参数中地址所属的地址族信息。
lpProtocollnfo   设置协议提供者(Provider),默认为NULL。
lpAddress        保存地址信息的结构体变量地址值。
lpAddressLength  第四个参数中传递的结构体长度所在的变量地址值。
*/

上述函数中新出现的各种类型几乎都是针对默认数据类型的typedef声明。后面的示例主要通过默认数据类型向该函数传递参数。


5.4.2 WSAAddressToString

WSAAddressToString与WSAStringToAddress在功能上正好相反,它将结构体中的地址信息转换成字符串形式。

#include <winsock2.h>
INT wSAAddressToString(LPSOCKADDR lpsaAddress, DWORD dwAddressLength,LPWSAPROTOCOL_INFO lpProtocolInfo, LPSTR lpszAddressString,LPDWORD lpdwAddressstringLength
);
//成功时返回0,失败时返回SOCKET__ERROR。
/*
lpsaAddress              需要转换的地址信息结构体变量地址值。
dwAddressLength          第一个参数中结构体的长度。
lpProtocollnfo           设置协议提供者(Provider),默认为NULL。
lpszAddressString        保存转换结果的字符串地址值。
lpdwAddressStringLength  第四个参数中存有地址信息的字符串长度。
*/

5.4.3 函数示例

示例: WSAAddressToString与WSAStringToAddress函数的使用

#pragma execution_character_set("utf-8")/*
#undef用于取消之前定义的宏。根据项目环境,VC++会自主声明这2个宏,
这样在第18行和第22行调用的函数中,参数就将转换成unicode形式,给出错误的运行结果。所以插入了这2句宏定义。
*/
#undef UNICODE
#undef _UNICODE
#include <stdio.h>
#include <winsock2.h>int main(int argc, char* argv[]) {char* strAddr = "203.211.218.102:9190";char strAddrBuf[50];SOCKADDR_IN servAddr; int size;WSADATA wsaData;WSAStartup(MAKEWORD(2, 2), &wsaData);size = sizeof(servAddr);//前面给出了需转换的字符串格式的地址。这一行调用WSAStringToAddress函数转换成结构体, 保存到声明的变量servAddr中。WSAStringToAddress(strAddr, AF_INET, NULL, (SOCKADDR*)&servAddr, &size);size = sizeof(strAddrBuf);//上面第二行代码的逆过程,调用WSAAddressToString函数将结构体转换成字符串。WSAAddressToString((SOCKADDR*)&servAddr, sizeof(servAddr), NULL, strAddrBuf, &size);printf("Second conv result: %s \n", strAddrBuf);WSACleanup();return 0;
}

运行,效果如下:
在这里插入图片描述
注: Linux环境下地址初始化过程中声明了sockaddr_in变量,而示例则声明了SOCKADDR_IN类型的变量。实际上二者完全相同,只是为简化变量定义添加了typedef声明。

typedef struct sockaddr_in SOCKADDR_IN;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_15713.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CSP2022 J/S 游寄

9.18 A.m. 自己学校考,但只能睡到7点不到,就很无语。 来了好多同学,关系也不错,聊了一会天就去考试了。 至于考试没什么好说的,J也就那样。 P.m. 上午对了一下答案,貌似 \(92\) ? 中午机房太吵了,没怎么睡就去考试了。 考什么大家应该都有数,宇宙射线?秒表?做尼玛呢…

卷积神经网络的应用实例,卷积神经网络可解释性

神经网络激活函数与损失函数的作用 谷歌人工智能写作项目&#xff1a;神经网络伪原创 深度学习之损失函数与激活函数的选择 深度学习之损失函数与激活函数的选择在深度神经网络&#xff08;DNN&#xff09;反向传播算法(BP)中&#xff0c;我们对DNN的前向反向传播算法的使用做…

单片机原理与应用以及C51编程技术——硬件体系结构梳理

文章目录一、单片机的结构原理1.1 主要性能和特点1.2 内部框图1.3 CPU1.3.1 运算器1.3.2 控制器1.4 几个主要的特殊功能寄存器SFR说明1.4.1 程序指针PC1.4.2 累加器A1.4.3 寄存器B1.4.4 数据指针DPTR1.4.5 程序状态字PSW介绍例子1.4.6 堆栈指针SP堆栈的介绍堆栈的作用堆栈操作的…

Verilog学习笔记

sky视频笔记&#xff1a;数字逻辑回顾&Hello World_哔哩哔哩_bilibili 一、数电基础 1.组合逻辑 电路逻辑输出值只和当前的输入有关比如&#xff1a;AND/OR/XOR/NAND/NOR/MUX/Adder/Multiplier 2.时序逻辑 电路逻辑输出值跟当前的输入和电路的当前状态有关保存当前状态的…

用Python生成Hilbert矩阵

代码放在了最后&#xff0c;前面是解题思路 目录 1.什么是Hilbert矩阵矩阵&#xff1a; 2.找规律 1.第一种思路&#xff1a;先从值出发&#xff08;找规律&#xff09; 2.第二种思路&#xff1a;先从下标索引出发&#xff08;找规律&#xff09; 三、代码展示 四、输出展…

WPF 界面打不开提示 System.ArithmeticException Overflow or underflow in the arithmetic operation 异常

本文告诉大家如何解决界面打不开,抛出 System.ArithmeticException: Overflow or underflow in the arithmetic operation 异常的修复方法本文告诉大家如何解决界面打不开,抛出 System.ArithmeticException: Overflow or underflow in the arithmetic operation 异常的修复方…

某IOT设备漏洞分析

申明&#xff1a;本文章所分享内容仅用于网络安全技术讨论&#xff0c;切勿用于违法途径&#xff0c;所有渗透都需获取授权&#xff0c;违者后果自行承担&#xff0c;与本文及作者无关&#xff0c;请谨记守法. 设备名称: DLINK DIR-818l 固件包: d-link DIR818L_FW105b01 A1 环…

Rust学习笔记:简单练习

最近一个月比较闲&#xff0c;忙碌大半年终于有自己短暂的休息时间。如果不写大的程序&#xff0c;偶尔写写一些小东西&#xff0c;其实用起来也很自在&#xff0c;前提是如果没有太多生活压力。看电视变成奢侈的事情。有时候&#xff0c;我会羡慕老外享受福利待遇非常好&#…

javaweb|JSTL的下载、配置与原理,解决uri导入时报错的问题

今天在配置jstl时&#xff0c;发现在引入uri时出现了问题&#xff0c;地址直接报红。在尝试了几种方法后&#xff0c;最后成功解决了。 JSTL一、下载1、https://tomcat.apache.org/2、找到taglib标准库3、下载前2个jar包二、配置1、放入web-inf的lib文件夹里2、将Jar包放入tomc…

windows下 解决PHP-CGI 进程崩溃502

PHP是世界上最好的语言&#xff0c;但需要PHP解析器&#xff1b;Apachephp,需要通过mod_php.so和php相连&#xff1b;nginxphp 需要转发给 cgi程序 关于FastCGI&#xff1a; 全称 FastCGI Process Manager&#xff0c;是一种进程管理器&#xff0c;管理 cgi&#xff0c;市面上…

1474_AURIX TC275 WDT的运行模式

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 1. 前面的内容中其实已经看到了&#xff0c;这个看门狗的时钟其实是固定的&#xff0c;SPB的时钟。这样&#xff0c;后面了解时钟树的时候需要注意一下。其实&#xff0c;在功能安全的失效…

ES6--》一文搞懂JS中的Promise

目录 Promise Promise使用 Promise封装Ajax请求 Promise封装读取文件 Promise.prototype.then 方法 Promise多文件读取 Promise.prototype.catch() Promise.prototype.finally() Promise.all() Promise.race() Promise.allSettled() Pomise.any() Promise.resolve…

微信小程序开发入门与实战(Behaviors使用)

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; 微信小程序 &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f4…

【原创】基于JavaWeb的社区疫情防控管理系统(疫情防控管理系统毕业设计)

项目介绍&#xff1a;后端采用JspServlet。前端使用的是Bootstrap的一个网站模板。开发一个在线的社区疫情防控管理系统。从角色的划分&#xff0c;包括用户、社管员、管理员。功能模块上包括了社区公告发布、高风险地区记录、地区感染信息管理、社区出入登记管理、行程信息管理…

关于maven生命周期的理解

晚上有点无聊&#xff0c;看到了一些东西引发了自己的思路&#xff0c;就想将maven的一些东西总结总结&#xff0c;有从网上抄的&#xff0c;也有自己的思路。 一、生命周期是指什么&#xff08;lifecycle&#xff09; Maven的生命周期就是对所有的构建过程进行抽象和统一。包…

Posix与System V IPC

Posix与System V IPC一、Posix IPC1.概述2.IPC名字3.px_ipc_name函数3.创建与打开IPC通道4.IPC权限二、System V IPC1.概述2.key_t键和ftok函数3.ipc_perm结构4.创建与打开IPC通道5.IPC权限6.标识符重用7.ipcs和ipcrm程序8.内核限制一、Posix IPC 1.概述 三种类型的IPC合称为…

Redux的基本使用过程详解

文章目录Redux的使用过程Redux测试项目的搭建Redux的基本使用步骤Redux目录的结构划分React的三大原则Redux的使用过程 Redux测试项目的搭建 1.创建一个新的项目文件夹:learn-redux # 执行初始化操作 npm init -y或yarn init -y # 安装redux:npm install redux --save或yarn …

自定义View 布局过程(Layout)

目录一、作用二、layout过程详解2.1单一View的layout过程具体使用具体流程源码分析总结2.2ViewGroup的layout过程具体使用具体流程源码分析总结三、细节问题&#xff1a;getWidth() &#xff08; getHeight()&#xff09;与 getMeasuredWidth() &#xff08;getMeasuredHeight(…

Java高级——编译JDK

编译JDKJDK是什么&#xff1f;编译环境搭建JDK下载编译参数编译在IDE中调试源码JDK是什么&#xff1f; Java Development Kit&#xff0c;用于执行和开发java程序 编译环境搭建 本文采用Ubantu 18.04 执行以下命令安装依赖 apt-get install build-essential apt-get instal…

Transformer

参考 https://www.ylkz.life/deeplearning/p12158901/ https://zhuanlan.zhihu.com/p/396221959 模型结构 Input Embedding 将文本中词汇的数字表示转变为向量表示, 希望得到其在高维空间中的特征表示向量。 # 导入必备的工具包 import torch import torch.nn as nn import …