TCP/IP网络编程系列之三(初级)

摘要:
TCP/IP网络编程系列之三-地址族与数据序列分配给套接字的IP地址和端口IP是InternetProtocol的简写,是为首发网络数据而分配给计算机的值。端口号是不能重复,但TCP套接字和UDP套接字不会公用端口号,所以允许重复。地址信息的表示应用程序中使用的IP地址和端口号以结构体的形式给出了定义,我们主要以IPV4为中心。sin_addr保存32位的IP地址信息,且以网络字节序保存,结构体in_addr声明为uint32_t,一次只需要保存32位整数类型即可。
TCP/IP网络编程系列之三-地址族与数据序列

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

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

网络地址

网络地址分为IPV4和IPV6,分别你别为4个字节地址簇和6个字节地址簇。IPV4标准的4个字节的地址分为网络地址和主机地址,且分为A、B、C、D、E 等类型。一般很少用到E类型。如下图所示:net-id指网络ID,host-id指主机ID

TCP/IP网络编程系列之三(初级)第1张

说明:

A类地址的首字节范围是:0-127

B类地址的首字节范围是:128-191

C类地址的首字节范围是:192-223

或者也可以这样分

A类地址的首位是以0开头

B类地址的前两位是以10开头

C类地址的前三位是以110开头

概述

网络ID是为区分网络而设置的一部分IP地址。比如你向www.baidu.com公司传输数据,该公司内部构建了局域网,把所有的计算机连接起来。因此,首相向baidu.com网络传输数据,也就是说,并非一开始就浏览所有4字节的IP地址,进而找到目标主机;而是仅浏览4字节IP地址的网络地址,先把数据传到baidu.com的网络。baidu.com网络接收到数据之后,浏览传输数据的主机地址并将数据传输给目标地址。一般的网络都会有路由器和交换机,所以实际上是向路由器或交换机传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传送数据。

用于区分套接字的端口号

IP用于区分计算机,只要有IP地址就能想目标主机传输数据,但是仅凭这些数据无法传输给最终的应用程序。假设在欣赏音乐的同时在听音乐或者上网浏览网页,这时至少需要1个接受视频数据的套接字和1个接受网页信息的套接字。但是如何区分它们呢,也就是说传输到计算机的网络数据是发送给视频播放器还是音乐播放器?假设我们开发了迅雷等应用程序,该程序用块单位分割一个文件,从多台计算机接受数据。那如何区分这些套接字呢?

计算机中一般都会有NIC(NetWork Interface Card,网络接口卡)数据传输设备。通过NIC向计算机内部传输数据时会用到IP。OS负责把传递到内部的数据适当的分配给套接字,这时就要利用端口号。通过NIC接受的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字。端口号就是在同一操作系统内为区分不同套接字而设置的,因此无法将一个端口号分配给不同的套接字。并且,端口号由16位构成,可分配的端口号的范围是0-65535,0-1023是知名端口号(Well-Know PORT),一般分配给特定应用程序,所以应当分配此范围之外的值。端口号是不能重复,但TCP套接字和UDP套接字不会公用端口号,所以允许重复。比如:某TCP套接字使用9190端口号,则其他TCP无法就无法使用端口号,但是UDP套接字就可以使用。

总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最总的目的应用程序(应用程序套接字)。

地址信息的表示

应用程序中使用的IP地址和端口号以结构体的形式给出了定义,我们主要以IPV4为中心。

复制代码
struct sockaddr_in
{
      sa_family_t          sin_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位IP地址
};
复制代码

uint16_t in_addr_t等类型可以参考POSIX,我这边简单说一下

POSIX中定义的数据类型
数据类型数据类型说明声明的头文件
int8_tsigned 8-bit intsys/types.h
uint8_tunsigned 8-bit intsys/types.h
int16_tsigned 16-bit intsys/types.h
uint16_tunsigned 16-bit intsys/types.h
int32_tsigned 32-bit int

sys/types.h

这主要是考虑到扩展性,如果使用int32_t类型的数据,就能保证在任何时候都占用4字节,即使将来用64位的来存储也是一样。

结构体sockaddr_in的成员分析

  • 成员sin_family

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

地址簇        含义

AF_INET       IPV4网络协议中使用的地址族

AF_INET6      IPV6网络协议中使用的地址族

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

  • 成员sin_port

该成员保存16位端口号,具体在下面讲解。

  • sin_addr

保存32位的IP地址信息,且以网络字节序保存,结构体in_addr声明为uint32_t,一次只需要保存32位整数类型即可。

  • sin_zero

无特殊含义,只是未使用结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员,必须填充为0,否则无法得到想要的结果。之前在服务端bind函数的时候,

struct sockaddr_in serv_addr;

if(bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr)==-1),其中第二个参数,是由sockaddr_in结构体转化而来的,并且希望得到sockaddr结构体变量地址,包括地址簇、端口号、IP地址等。但是我们直接向sockaddr结构体填充这些信息会带来麻烦。

struct sockaddr

{

sa_family_t  sin_family;//地址族

char      sa_data[14];//地址信息

};

sa_data保存地址信息,包括IP地址和端口号,剩余部分应填充为0,这也是bind函数的要求。这对于包含地址信息来讲非常麻烦,从而有了新的结构体sockaddr_in。

网络字节序和地址转换

  • 字节序和网络字节序

cpu向内存保存数据有两种,这意味着cpu解析数据的方式也是两种分别为大端序和小端序。

  1. 大端序(Big Endian):高位字节存放到低位地址。
  2. 小端序(Little Endian):高位字节存放到高位地址。

比如0x00000001
大端序:内存低比特位 00000000 00000000 00000000 00000001 内存高比特位
小端序:内存低比特位 10000000 00000000 00000000 00000000 内存高比特位

还可以如下图表示:TCP/IP网络编程系列之三(初级)第4张

所以出现了一个问题如果两台计算机的cpu的数据保存方式不同,但是他们是如何传送数据的呢?如何进行网络传输的呢?所以就规定了一个标准,在通过网络传输的过程中统一按照大端序。即先把数据数组转化为大端序格式然后进行传输。因此,所有计算机接受数据时应识别该数据的字节格式,小端序系统传输数据时应该转化为大端字节序的排列方式。

  • 字节序转换(Endian Conversions)

所以我们懂应该知道为何在填充sockadr_in结构提前将数据转化为网络字节序。转换字节的函数:

unsigned short htons (unsigned short);

unsigned short ntohs(unsigned short);

unsigned long htons (unsigned long);

unsigned long ntohs (unsigned long);

通过函数名可以知道他的功能,只要了解一下细节。htons中的h代表主机(host)字节序,n代表网络字节序。s指的是short,l指的是long(linux 中long类型占用4个字节),可以解释为"把short类型数据从主机字节序转化为网络字节序"。所以ntohs也就知道了吧。我们通过例子来看一下效果:

TCP/IP网络编程系列之三(初级)第5张运行结果之后会看到如下图结果:TCP/IP网络编程系列之三(初级)第6张

这就是小端序cpu运行结果,如果在大端值中是不会发生变化的。Intel和AMD系列的cpu都采用小端序标准。数据在传输过程中需要经过转换吗?实际上没有必要,这个过程是自动的。除了想sockaddr_in结构体变量填充数据外,其他情况不需要考虑字节序问题。

网络地址的初始化与分配

sockaddr_in 中保存地址信息的成员为32位整数型。因此,为了分配IP地址将其转化为32位整数型数据。对我们而言并非易事。对于IP地址的的表示,我们熟悉的是点分十进制,而非整型数据表示法。幸运的是,有个函数可以帮我们将字符串形式的IP地址转化为32位的整型数据。此函数在转换类型的同时进行网络字节序转换。

#include <arpa/inet.h>

in_addr_t inet_addr(const char* string)
成功时返回32位大端序整型数据,失败时返回INADDR_NONE

下面是测试代码:

TCP/IP网络编程系列之三(初级)第7张运行结果如下图所示:TCP/IP网络编程系列之三(初级)第8张从运行结果可以看出,inet_addr函数不仅可以把ip地址转换为32位整数,而且还可以检测无效的ip地址。并且输出的确实是网络字节序。还有一个函数与inet_addr函数功能完全相同,只不过该函数利用了in_addr结构体,且使用频率更高。

#include <arpa/inet.h>

int inet_aton(const char *string,struct in_addr *addr)
成功时返回1,失败时返回0;

实际编程中若要调用inet_addr函数,需要将转化后的IP地址信息代入sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数不需要此过程。原因在于,若传递in_addr结构体变量地址值,函数会自动把结果填入该结构体变量。ok,下面再讲解一个把网络字节序整数型IP地址转换成我们熟悉的字符串形式。

#include <arpa/inet>

char *inet_ntoa(struct in_addr adr);
成功时返回转换的字符串地址,失败时返回-1。

但在调用时小心,返回值是char类型的指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完函数后,应该立即将字符串信息复制到其他内存空间。因为在此调用该函数,则有可能覆盖之前保存的字符串信息。总之,再次调用该函数前返回的字符串地址值是有效的。如要长期保存,则应将字符串复制到其他内存空间。示例:

TCP/IP网络编程系列之三(初级)第9张

运行结果如下:

TCP/IP网络编程系列之三(初级)第10张

下面我把之前的代码完全重新组合一下。

服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void ErrorMessage(char *message);

int main(int argc,char *argv[])
{
int serv_sock;
int client_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in client_addr;
char *serverIP= "127.0.0.1";
char *servPort = "9190";
char message[]="Hi,TCPIP";
socklen_t clnt_addr_size;
serv_sock = socket(PF_INET,SOCK_STREAM,0);
if(serv_sock==-1)
{
ErrorMessage("Sock Error!");
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(serverIP);
serv_addr.sin_port = htons(atoi(servPort));
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
{
ErrorMessage("Bind() Error");
}
if(listen(serv_sock,5)==-1)
{
ErrorMessage("listen() error");
}
clnt_addr_size = sizeof(client_addr);
client_sock = accept(serv_sock,(struct sockaddr*)&client_addr,&clnt_addr_size);
if(client_sock==-1)
{
ErrorMessage("accept() error");
}
write(client_sock,message,sizeof(message));
close(client_sock);
close(serv_sock);
return 0;
}
void ErrorMessage(char *message)
{
fputs(message,stderr);
fputc(' ',stderr);
exit(1);
}

客户端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void ErrorMessage(char *message);

int main(int argc,char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
char *serv_port = "9190";
int str_len;
sock = socket(PF_INET,SOCK_STREAM,0);
if(sock==-1)
{
ErrorMessage("socket() error");
}
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));
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
{
ErrorMessage("connect() error");
}
str_len = read(sock,message,sizeof(message)-1);
if(str_len==-1)
{
ErrorMessage("read() error");
}
printf("Message from server:%s ",message);
close(sock);
return 0;
}
void ErrorMessage(char *message)
{
fputs(message,stderr);
fputc(' ',stderr);
exit(1);

}

运行结果如下图所示:

TCP/IP网络编程系列之三(初级)第11张

免责声明:文章转载自《TCP/IP网络编程系列之三(初级)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇javascript正则表达式jQuery之前端国际化jQuery.i18n.properties下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Oracle to_date()函数的用法

在Oracle数据库中,Oracle to_date()函数是我们经常使用的函数,下面就为您详细介绍Oracle to_date()函数的用法,希望可以对您有所启迪。 to_date()与24小时制表示法及mm分钟的显示: 一、在使用Oracle的to_date函数来做日期转换时,很多Java程序员也许会直接的采用“yyyy-MM-dd HH:mm:ss”...

网上购物车数据库设计

一、概述 网上购物店的数据模型,它主要模式有产品:product ,帐户:Account,定单:Order。和产品相关的表有category ,product,item, inventory, supplier;和用户相关表有的account ,signon ,profile;和定单相关的表有orders,orderstatus,lineitem FK:...

深度分析:高并发系统架构设计原理,史上最全系列!

架构设计是一系列相关的抽象模式,是人们对一个结构内的元素及元素间关系的一种主观映射的产物。 一、计算机网络基础 A. OSI模型 OSIOpen System Interconnection,简称OSI模型或七层模型。开放系统互连参考模型,是国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合制定的开放系统互连参考模型,为开放式互连信息系统...

你不知道的 Blob

来自公众号:全栈修仙之路 如果你允许用户从你的网站上下载某些文件,那你可能会遇到 Blob 类型。为了实现上述的功能,你可以很容易从网上找到相关的示例,并根据实际需求进行适当的调整。对于部分开发者来说,在完成上述功能之后,他们并不会继续思考 Blob 是什么? 这就导致了一些开发者,还是停留在熟练使用 API 的层面,当遇到比较棘手的问题时,就束手无策...

2、Java数据类型框图和自动转化框图

Java数据类型 自动类型转化    特别注意:byte和char类型之间不能相互转换 1 C:Userslenovo>jshell 2 | 欢迎使用 JShell -- 版本 11.0.2 3 | 要大致了解该版本, 请键入: /help intro 4 5 jshell> byte a=1 6 a ==> 1 7...

关于登录随机验证码的实现

一、验证码相关知识 Python生成随机验证码,需要使用PIL模块. 安装: pip3 install pillow 基本使用 1. 创建图片 from PIL import Image img = Image.new(mode='RGB', size=(120, 30), color=(255, 255, 255)) # 在图片查看器中打开 # i...