第3章 地址与数据序列

3.1分配给套接字的IP地址和端口号

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

1.网络地址(Internet Address)

IPv4(Internet Protocol version 4)   4字节地址族
IPv6(Internet Protocol version 6)   16字节地址族

为了应对2010年前后IP地址耗尽提出的IPv6

Ipv4标准的4字节IP地址分为网络地址主机地址,分为A、B、C、D、E等类型(一般不会使用被预约了的E类地址)。

A类

网络ID位

主机ID位

主机ID位

主机ID位

B类

网路ID位

网络ID位

主机ID位

主机ID位

C类

网络ID位

网络ID位

网络ID位

主机ID位

D类

网络ID位

网络ID位

网络ID位

网络ID位

IPv4地址族

如何找到目标主机? 开始仅浏览四字节IP的网络地址,先把数据传送到目标网络。而构成目标网络的路由器接收到数据之后,浏览传输数据的主机地址(主机ID)并将数据传送给目标主机。
所以“向相应的网络传输数据”实际上是向构成网络的“路由器”或者“交换机”传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传递数据。

补充:

想要构建网络,需要一种物理设备完成外网与本网主机之间的数据交换,这种设备便是路由器或者交换机(也属于计算机,只是更专用,两者实际用途差别不大)。

2.网络地址分类与主机地址边界

  • A类地址的首字节范围:0~127(首位以0开始)

  • B类地址的首字节范围:128~191(首位以10开始)

  • C类地址的首字节范围:192~223(首位以110开始)

3.区分套接字的端口号

IP用于区分计算机,只要有IP就能向目标主机传递数据,但是还无法传给最终应用程序。看视频和看网页需要两个套接字。如何区分呢?

计算机有NIC(Network Interface Card,网络接口卡)数据传输设备,操作系统把传送到内部的数据适当分配套接字,这时就需要端口。NIC接受的数据内含有端口号,操作系统参考这个端口号把数据传输给相应端口的套接字总之端口号就是同一操作系统内为区分不同套接字而设置的

  • 端口号为16位(0~65535)

  • 0~1023为知名端口(分配给特定的应用程序)

  • TCP和UDP不共用端口号(两种类型的套接字可以重复端口号)

数据传输目标地址需要同时包含IP地址和端口号,才能最终传送到目标应用程序(应用程序套接字

3.2地址信息的表示

1.表示IPv4地址的结构体

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];    //不使用
}

其中的另一个结构体

struct in_addr
{
    int addr_t      s_addr;     //32位IPv4地址
}

上述uint16_t、in_addr_t等类型可以参考POSIX,这是为UNIX操作系统设的标准。为了确保扩展性,所以额外定义了这些数据类型。

2.结构体sockaddr_in的成员分析

  1. 成员sin_family 每种协议组适用的地址族不同,所以要填写这个

    地址族

    含义

    AF_INET

    IPv4网络协议中使用的地址族

    AF_INET6

    IPv6网络协议中使用的地址族

    AF_LOCAL

    本地通信采用的UNIX协议的地址族

    AF_LOCAL是为了说明有多种地址族设置的。

  2. sin_port 保存16位端口号(以网络字节序保存)

  3. sin_addr 保存32位IP地址信息(以网络字节序保存)

  4. sin_zero 为了和sockaddr结构体保持一致插入的成员,必须为0。

    bind函数第二个参数希望得到sockaddr结构体变量的地址值,但是直接向它填充很麻烦,所以向sockaddr_in填数据容易许多,然后生成符合bind要求的字节流转化成sockaddr型的结构体变量,在给bind函数。

    if(bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(servaddr))==-1)
    {
        error_handling(...);
    }
    struct sockaddr{
        sa_family_t     sin_family;     //地址族
        char            sa_data[14];    //地址信息
    }
    
         其中这个数组保存IP地址和端口号,剩余部分为0,所以前面的sin_zero要全填0。
    

补充: 既然sockaddr_in保存IPv4地址信息的结构体,为什么还要sin_family单独指定?
因为sockaddr并非只是为IPv4设计的,因此sockaddr结构体要求在sin_family中指定地址族信息。

3.3网络字节序与地址变换

1.字节序与网络字节序

  • 大端序(Big Endian):高字节存放地位地址

  • 小端序(Little Endian):高位字节存放在高位地址

代表CPU数据保存方式的主机字节序,在不同CPU有不同的排序方式。(Intel就是小端序)

大端序系统传输数据0X1234未考虑字节排序问题,从低位开始传输也就是说0X12、0X34顺序传输,而小端序系统收到之后就成了0X3412(低位存储在低地址),而非0X1234。所以网络数据传输约定统一方式,这种约定称为网络字节序——大端序。所以计算机接收数据前先识别网络字节序,小端序传输数据前转化成大端序。

2.字节序转换

  • unsigned short htons(unsigned short)

  • unsigned short ntohs(unsigned short)

  • unsigned long htonl(unsigned long)

  • unsigned long ntohl(unsigned long)

其中的h代表主机字节序,n代表网络字节序,s代表短整型,l代表长整型 s的短整型2字节16位,用来转换端口号排序 l的长整型4字节32位,用来转换IP地址排序

3.4网络地址的初始化与分配

1.将字符串信息转化成网络字节序的整数型

#include<arpa/inet.h>
in_addr_t inet_addr(const char * string);

成功时返回32位大端序整数型值,失败返回INADDR_NONE

例如“211.214.107.99”的点分十进制字符串会转换成整数型并返回,而且是网络字节序。in_addr_t在内部声明为32位整型。而且inet_addr有自动检测错误功能,不合法的地址会返回INADDR_NONE。

#include<arpa/inet.h>
int inet_aton(const char *string,struct in_addr *addr)

string 需要转换的字符串型IP地址的地址
addr   保存转换结果的in_addr结构体变量的地址
成功返回1(true),失败返回0(false)

inet_addr转换的地址需要手动带入,sockaddr_in结构体内的in_addr结构体变量,但是inet_aton可以自动填入该结构体变量

#include<arpa/inet.h>
char * inet_ntoa(struct in_addr adr);
成功返回转换出的字符串地址值,失败返回-1

既然是返回字符指针类型,说明,这个字符串已经保存到内存,说明这个函数内部申请了内存并保存了字符串,所以要立即将这个地址的字符串信息拷贝到其他内存空间,再次调用这个函数会覆盖原来的字符串信息

2.网络地址初始化

struct sockaddr_in addr;
char * serv_ip = "211.217.168.13";          //声明IP地址字符串
char * serv_port = "9190";                  //声明端口号字符串
memset(&addr , 0 , sizeof(addr));           //结构体变量addr的所有成员初始化为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对应地址的结构体变量没一个字节都初始化成0,这主要是为了将sockaddr_in结构体里的sin_zero初始化为0,atoi()是将字符串类型的值(指的是那些数字9190而不是字符串二进制对应的整型)转换成整型。代码对IP地址和端口号进行了硬编码,运行环境改变就得改变,所以不是良策。所以示例用main函数传入IP地址和端口号。

3.客户端地址信息初始化

上述主要是针对服务端做的初始化。服务端准备工作通过bind函数完成,客户端通过connect函数完成。服务端声明sockaddr_in结构体,为其分配服务端的IP和套接字端口号,然后调用bind();客户端声明sockaddr_in结构体,并初始化为要与之连接的服务端套接字的主机IP和端口号

4.INADDR_ANY

struct sockaddr_in addr;
char * serv_port = "9190";
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
每次手动输入主机的IP过于繁琐。所以用INADRR_ANY分配服务端IP。
这个方式会自动获取服务端计算机IP地址。
如果同一个计算机配备多个IP地址(多宿主计算机,一般为路由器这种),端口号只要一致,就可以从不同的IP地址(不同的NIC)接收数据。

补充: 初始化套接字时应分配所属计算机的IP地址,因为初始化使用IP非常明确,那为何还要IP初始化?因为,同一个计算机可以有多个IP,实际上IP地址的个数和计算机的NIC数量相等。所以即使是服务器套接字也需要决定接收哪个IP(哪个NIC传来的)传来的数据。所以要求IP地址信息初始化。如果只有一个NIC就直接用INADDR_ANY

127.0.0.1是回送地址(loopback address)

5.向套接字分配网络地址

前面sockaddr_in已经初始化,那要把地址信息分配给套接字。

#include<sys/socket.h>
int bind(int sockfd,struct sockaddr * myaddr,socklen_t addrlen);

sockfd      要分配地址信息的套接字文件描述符
myaddr      存有地址信息的结构体变量地址
addrlen     结构体变量的长度
成功返回0,失败返回-1
  • 套接字常见初始化步骤

    int serv_sock;
    struct sockaddr_in serv_addr;
    char * serv_port = "9190";
    /*创建服务器套接字(监听套接字)*/
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    /*地址信息初始化*/
    memset(&serv_addr,0,sizeof(aerv_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));
    
    

3.5基于Windows的实现

函数几乎和Linux没啥别,不过Windows没 int inet_aton(const char* string,struct in_addr * addr); 其他几乎没啥区别和Linux

Windows新增的函数

WSAStringToAddress 和 WSAAddressToString

#include<winsock2.h>

INT WSAStringToAddress(LPTSTR AddressString,INT AddressFamily,LPWSAPROTOCOL_INFO lpProtocolInfo,LPSOCKADDR lpAddress,LPINT lpAddressLength)

成功返回0,失败时返回SOCKET_ERROR
  • AddressString 含有IP和端口号的字符串地址值

  • AddressFamily 第一个参数中地址所属的地址族信息

  • lpProtocolInfo 设置协议提供者,默认为NULL

  • lpAddress 保存地址信息的结构体变量地址值

  • lpAddressLength 第四个参数中传递的结构体长度所在的变量地址值

#include<winsock2.h>
INT WSAAddressToString(LPSOCKADDR lpsaAddress,DWORD dwAddressLength,LPWSAPROTOCOL_INFO lpProtocolInfo,LPSTR lpszAddressString,LPDWORD lpdwAddressStringLength)

成功返回0,失败返回SOCKET_ERROR
  • lpsaAddress 需要转换的地址信息结构体变量地址值

  • dwAddressLength 第一个参数中结构体长度

  • lpProtocolInfo 设置协议提供者,默认为NULL

  • lpszAddressString 保存转换结果的字符串地址值

  • lpdwAddressStringLength 第四个参数中存有地址信息的字符串长度

#undef UNICODE
#undef _UNICODE
//上面两行用于取消之前定义的宏,根据项目环境,VC++会自主声明这两个宏,这样在下面的函数调用,参数会转换成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);
    //前面给出了需要转换的字符串的地址。下面调用代码,将其转换成结构体,保存到前面的servAddr结构体中
	WSAStringToAddress(
		strAddr, AF_INET, NULL, (SOCKADDR*)&servAddr, &size);

	size=sizeof(strAddrBuf);
    //是前面函数的逆过程,将servAddr结构体转换成字符串信息
	WSAAddressToString(
		(SOCKADDR*)&servAddr, sizeof(servAddr), NULL, strAddrBuf, &size);

	printf("Second conv result: %s \n", strAddrBuf);
  	WSACleanup();
	return 0;
}

正确输出:

不加#undef UNICODE、#undef _UNICODE

htons、htonl的windows应用:

#include <stdio.h>
#include <winsock2.h>
void ErrorHandling(char* message);

int main(int argc, char *argv[])
{
	WSADATA	wsaData;
	unsigned short host_port=0x1234;
	unsigned short net_port;
	unsigned long host_addr=0x12345678;
	unsigned long net_addr;

	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 
	
	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);
	WSACleanup();
	return 0;
}

void ErrorHandling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

inet_addr、inet_ntoa在windows的应用

#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)
		ErrorHandling("WSAStartup() 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");
		else
			printf("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);
}

本文章使用limfx的vscode插件快速发布