IP(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)并将数据传送给目标主机。
所以“向相应的网络传输数据”实际上是向构成网络的“路由器”或者“交换机”传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传递数据。
补充:
想要构建网络,需要一种物理设备完成外网与本网主机之间的数据交换,这种设备便是路由器或者交换机(也属于计算机,只是更专用,两者实际用途差别不大)。
A类地址的首字节范围:0~127(首位以0开始)
B类地址的首字节范围:128~191(首位以10开始)
C类地址的首字节范围:192~223(首位以110开始)
IP用于区分计算机,只要有IP就能向目标主机传递数据,但是还无法传给最终应用程序。看视频和看网页需要两个套接字。如何区分呢?
计算机有NIC(Network Interface Card,网络接口卡)数据传输设备,操作系统把传送到内部的数据适当分配套接字,这时就需要端口。NIC接受的数据内含有端口号,操作系统参考这个端口号把数据传输给相应端口的套接字。总之端口号就是同一操作系统内为区分不同套接字而设置的
端口号为16位(0~65535)
0~1023为知名端口(分配给特定的应用程序)
TCP和UDP不共用端口号(两种类型的套接字可以重复端口号)
数据传输目标地址需要同时包含IP地址和端口号,才能最终传送到目标应用程序(应用程序套接字)
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操作系统设的标准。为了确保扩展性,所以额外定义了这些数据类型。
成员sin_family 每种协议组适用的地址族不同,所以要填写这个
地址族 | 含义 |
---|---|
AF_INET | IPv4网络协议中使用的地址族 |
AF_INET6 | IPv6网络协议中使用的地址族 |
AF_LOCAL | 本地通信采用的UNIX协议的地址族 |
AF_LOCAL是为了说明有多种地址族设置的。
sin_port 保存16位端口号(以网络字节序保存)
sin_addr 保存32位IP地址信息(以网络字节序保存)
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中指定地址族信息。
大端序(Big Endian):高字节存放地位地址
小端序(Little Endian):高位字节存放在高位地址
代表CPU数据保存方式的主机字节序,在不同CPU有不同的排序方式。(Intel就是小端序)
大端序系统传输数据0X1234未考虑字节排序问题,从低位开始传输也就是说0X12、0X34顺序传输,而小端序系统收到之后就成了0X3412(低位存储在低地址),而非0X1234。所以网络数据传输约定统一方式,这种约定称为网络字节序——大端序。所以计算机接收数据前先识别网络字节序,小端序传输数据前转化成大端序。
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地址排序
#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
既然是返回字符指针类型,说明,这个字符串已经保存到内存,说明这个函数内部申请了内存并保存了字符串,所以要立即将这个地址的字符串信息拷贝到其他内存空间,再次调用这个函数会覆盖原来的字符串信息。
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地址和端口号。
上述主要是针对服务端做的初始化。服务端准备工作通过bind函数完成,客户端通过connect函数完成。服务端声明sockaddr_in结构体,为其分配服务端的IP和套接字端口号,然后调用bind();客户端声明sockaddr_in结构体,并初始化为要与之连接的服务端套接字的主机IP和端口号
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)
前面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));
函数几乎和Linux没啥别,不过Windows没 int inet_aton(const char* string,struct in_addr * addr); 其他几乎没啥区别和Linux
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插件快速发布