Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

时间:2022-06-02 00:16:51

转载请注明出处: ,谢谢!

内容提要

本节主要学习网络通信基础,主要涉及的内容是:

TCP/IP协议簇基础:两个模型

IPv4协议基础:IP地址分类与表示,子网掩码等

IP地址转换:点分十进制\二进制

TCP/IP协议簇基础

OSI模型

我们知道计算机网络之中,有各种各样的设备,那么如何实现这些设备的通信呢?

显然是通过标准的通讯协议,但是,整个网络连接的过程相当复杂,包括硬件、软件数据封包与应用程序的互相链接等等,如果想要写一支将联网全部功能都串连在一块的程序,那么当某个小环节出现问题时,整只程序都需要改写,非常麻烦!

那怎办?没关系,我们可以将整个网络连接过程分成数个阶层 (layer),每个阶层都有特别的独立的功能,而且每个阶层的程序代码可以独立撰写,因为每个阶层之间的功能并不会互相干扰的。如此一来,当某个小环节出现问题时,只要将该层级的程序代码重新撰写即可。所以程序撰写也容易,整个网络概念也就更清晰!那就是目前你常听到的OSI 七层协定 (Open System Interconnection) 的概念!

如果以图示来说,那么这七个阶层的相关性有点像底下这样:

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇
图 1----- OSI 七层协议各阶层的相关性

依据定义来说,越接近硬件的阶层为底层 (layer 1),越接近应用程序的则是高层 (layer 7)。不论是接收端还是发送端,每个一阶层只认识对方的同一阶层数据。而整个传送的过程就好像人们在寄快递一般,我们透过应用程序将数据放入第七层的包裹,再将第七层的包裹放到第六层的包裹内,依序一直放到第一层的最大的包裹内,然后传送出去给接收端。接收端的主机就得由第一个包裹开始,依序将每个包裹拆开,然后一个一个交给对应负责的阶层来视察.是 OSI 七层协议在阶层定义方面需要注意的特色。

既然说是包裹,那我们都知道,包裹表面都会有个重要的信息,这些信息包括有来自哪里、要去哪里、接收者是谁等等,而包裹里面才是真正的数据。同样的,在七层协议中,每层都会有自己独特的表头数据 (header),告知对方这里面的信息是什么,而真正的数据就附在后头!我们可以使用如下的图示来表示这七层每一层的名字,以及数据是如何放置到每一层的包裹内:

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇
图 2-----OSI 七层协议数据的传递方式

上图中仔细看每个数据报的部分,上层的包裹是放入下层的数据中,而数据前面则是这个数据的表头。其中比较特殊的是第二层,因为第二层 (数据链结层) 主要是位于软件封包 (packet) 以及硬件讯框 (frame) 中间的一个阶层,他必须要将软件包装的包裹放入到硬件能够处理的包裹中,因此这个阶层又分为两个子层在处理相对应的数据。因为比较特殊,所以,第二层的数据格式比较不一样,尾端还出现一个检查码.

每一个阶层所负责的任务是什么呢?简单的说,每一层负责的任务如下:

分层 负责内容
Layer 1
物理层
Physical Layer
由于网络媒体只能传送 0 与 1 这种位串,因此物理层必须定义所使用的媒体设备之电压与讯号等,同时还必须了解数据讯框转成位串的编码方式,最后连接实体媒体并传送/接收位串。
Layer 2
数据链结层
Data-Link Layer
这一层是比较特殊的一个阶层,因为底下是实体的定义,而上层则是软件封装的定义。因此第二层又分两个子层在进行数据的转换动作。在偏硬件媒体部分,主要负责的是 MAC (Media Access Control) ,我们称这个数据报裹为 MAC 讯框 (frame), MAC 是网络媒体所能处理的主要数据报裹,这也是最终被物理层编码成位串的数据。MAC 必须要经由通讯协议来取得媒体的使用权,目前最常使用的则是 IEEE 802.3 的以太网络协议。

至于偏向软件的部分则是由逻辑链接层 (logical link control, LLC) 所控制,主要在多任务处理来自上层的封包数据 (packet) 并转成 MAC 的格式,负责的工作包括讯息交换、流量控制、失误问题的处理等等。
Layer 3
网络层
Network Layer
这一层是我们最感兴趣的,因为我们提及的 IP (Internet Protocol) 就是在这一层定义的。同时也定义出计算机之间的联机建立、终止与维持等,数据封包的传输路径选择等等,因此这个层级当中最重要的除了 IP 之外,就是封包能否到达目的地的路由 (route) 概念了!
Layer 4
传送层
Transport Layer
这一个分层定义了发送端与接收端的联机技术(如 TCP, UDP 技术),同时包括该技术的封包格式,数据封包的传送、流程的控制、传输过程的侦测检查与复原重新传送等等,以确保各个数据封包可以正确无误的到达目的端。
Layer 5
会谈层
Session Layer
在这个层级当中主要定义了两个地址之间的联机信道之连接与挂断,此外,亦可建立应用程序之对谈、提供其他加强型服务如网络管理、签到签退、对谈之控制等等。如果说传送层是在判断资料封包是否可以正确的到达目标,那么会谈层则是在确定网络服务建立联机的确认。
Layer 6
表现层
Presentation Layer
我们在应用程序上面所制作出来的数据格式不一定符合网络传输的标准编码格式的!所以,在这个层级当中,主要的动作就是:将来自本地端应用程序的数据格式转换(或者是重新编码)成为网络的标准格式,然后再交给底下传送层等的协议来进行处理。所以,在这个层级上面主要定义的是网络服务(或程序)之间的数据格式的转换,包括数据的加解密也是在这个分层上面处理。
Layer 7
应用层
Application Layer
应用层本身并不属于应用程序所有,而是在定义应用程序如何进入此层的沟通接口,以将数据接收或传送给应用程序,最终展示给用户。

事实上, OSI 七层协议只是一个参考的模型 (model),目前的网络社会并没有什么很知名的操作系统在使用 OSI 七层协定的联网程序代码.

TCP/IP 协议

TCP/IP网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层。如下图所示,如果没有特别说明,一般引用的图都出自《TCP/IP详解 卷一》。

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

图3 -----TCP/IP协议栈

其实 TCP/IP 也是使用 OSI 七层协议的观念,所以同样具有分层的架构,只是将它简化为四层,在结构上面比较没有这么严谨,程序撰写会比较容易些。

既然 TCP/IP 是由 OSI 七层协议简化而来,那么这两者之间有没有什么相关性呢?它们的相关性可以图示如下,同时这里也列出目前在这架构底下常见的通讯协议、封包格式与相关标准:

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

图 4 -----OSI 与 TCP/IP 协议之相关性

从上图中,我们可以发现 TCP/IP 将应用、表现、会谈三层整合成一个应用层,在应用层上面可以实作的程序协议有 HTTP, SMTP, DNS 等等。传送层则没有变,不过依据传送的可靠性又将封包格式分为连接导向的 TCP 及非连接导向的 UDP 封包格式。网络层也没有变,主要内容是提供了 IP 封包,并可选择最佳路由来到达目标 IP 地址。数据链结层与物理层则整合成为一个链结层,包括定义硬件讯号、讯框转位串的编码等等,因此主要与硬件 (不论是区网还是广域网) 有关。

那 TCP/IP 是如何运作的呢?我们就拿常常连上的 google入口网站来做个说明,整个联机的状态可以这样看:

  1. 应用程序阶段:妳打开浏览器,在浏览器上面输入网址列,按下 [Enter]。此时网址列与相关数据会被浏览器包成一个数据,并向下传给 TCP/IP 的应用层;
  2. 应用层:由应用层提供的 HTTP 通讯协议,将来自浏览器的数据报起来,并给予一个应用层表头,再向传送层丢去;
  3. 传送层:由于 HTTP 为可靠联机,因此将该数据丢入 TCP 封包内,并给予一个 TCP 封包的表头,向网络层丢去;
  4. 网络层:将 TCP 包裹包进 IP 封包内,再给予一个 IP 表头 (主要就是来源与目标的 IP ),向链结层丢去;
  5. 链结层:如果使用以太网络时,此时 IP 会依据 CSMA/CD 的标准,包裹到 MAC 讯框中,并给予 MAC 表头,再转成位串后,利用传输媒体传送到远程主机上。

等到google收到你的包裹后,在依据相反方向拆解开来,然后交给对应的层级进行分析,最后就让google 的 WWW 服务器软件得到你所想要的数据,该服务器软件再根据你的要求,取得正确的资料后,又依循上述的流程,一层一层的包装起来,最后传送到你的电脑页面.

各层功能和协议的简单解释

1)网络接口层:模型的基层,负责数据帧的发送和接收(帧是网络信息的独立传输单元).该层将帧发送到网络或从网络取下.

2)互联层:互联协议将数据包封装成IP数据包,并进行必要的路由算法,有效地找到目的主机最优的路径树.这里有四种协议:

①网际协议IP:负责在主机和网络之间寻找路径和路由数据包.

②地址解析协议ARP:获得同一网络中的主机硬件地址.

③ 网际控制信息协议ICMP:发送信息,并报告有关数据包的错误信息

④互联组管理协议IGMP:同来实现本地多路组播路由器报告.

3)传输层:传输协议在主机之间提供通信会话,传输协议的选择因数据传输方式而定:主要有两种传输协议:

TCP传输控制协议:为应用程序提供可靠的通信连接,适合于一次传输大量数据的情况,并适用于要求得到响应的应用程序.

UDP用户数据包协议:提供了无连接通信,部队传送包进行可靠确认,适用于小量数据且不需确认得到响应的情况

4)应用层:应用程序通过这一层访问网络,主要常见的协议有FTP,HTTP,DNS,TELNET.等.


TCP/IP协议簇体系中,在网络接口层,最重要的信息是主机的MAC地址,在物理上唯一的标示一台主机,IP层的IP地址在逻辑上唯一标示某台主机,如果一台主机有多个IP地址,则其在网络上有多个身份.在主机内部,传输层的端口对应唯一的应用服务.

IPv4协议基础

一台连接到网络的主机必须要有IP才能与其他主机进行通信

IP地址的表示形式和分类

1.ip地址有两种表示形式:

点分十进制和点分二进制.

十进制的表示形式中每个IP地址长度位四个字节,由4~8位组成,通常称为八位体.一个IP地址的4个域分别标明了网络号和主机号,即是每个每个IP地址由两部分组成:网络号Net_ID和主机号Host_ID.

<span style="font-family:SimSun;font-size:12px;">IPv4 结构:

struct in_addr {
in_addr_t s_addr; // 32-bit IPv4 address, 网络字节序
};</span>
其中网络ID:标示一个物理网络,同一个网络上所有的主机使用同一个网络号,拥有相同网络主机ID且在物理上连接的主机之间通信不需要路由设备,即他们在同一个局域网内.

主机ID:确定网络中的一个工作端\服务器\路由器或者其他的TCP/IP主机.对于同一个网络号而言,主机号是唯一的.每个主机由一个逻辑IP地址确定网络号和主机号.

IP 在同一网域的意义

那么同一个网域该怎么设定,与将 IP 设定在同一个网域之内有什么好处呢?

  • Net_ID 与 Host_ID 的限制:
    在同一个网段内,Net_ID 是不变的,而 Host_ID 则是不可重复,此外,Host_ID 在二进制的表示法当中,不可同时为 0 也不可同时为 1 ,因为全为 0 表示整个网段的地址 (Network IP),而全为 1 则表示为广播的地址 (Broadcast IP)


  • 在区网内透过 IP 广播传递数据
    在同物理网段的主机如果设定相同的网域 IP 范围 (不可重复),则这些主机都可以透过 CSMA/CD 的功能直接在区网内用广播进行网络的联机,亦即可以直接网卡对网卡传递数据 (透过 MAC );

  • 设定不同区网在同物理网段的情况
    在同一个物理网段之内,如果两部主机设定成不同的 IP 网段,则由于广播地址的不同,导致无法透过广播的方式来进行联机。此时得要透过路由器 (router) 来进行沟通才能将两个网域连结在一起。

  • 网域的大小
    当 Host_ID 所占用的位越大,亦即 Host_ID 数量越多时,表示同一个网域内可用以设定主机的 IP 数量越多。

2.IP地址的分类

为了 IP 管理与发放注册的方便性,以及方便管理主机号和网络号大小的关系,InterNIC 将整个 IP 网段分为五种等级,每种等级的范围主要与 IP 那 32 bits 数值的前面几个位有关,基本定义如下:

以二进制说明 Network 第一个数字的定义:
Class A : 0xxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D 的开头是 0,占一个字节
|--net--|---------host------------|

Class B : 10xxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D 的开头是 10,占两个字节
|------net-------|------host------|

Class C : 110xxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D 的开头是 110,占三个字节
|-----------net-----------|-host--|

Class D : 1110xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D 的开头是 1110,整个地址是多点传送地址
Class E : 1111xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D 的开头是 1111,整个地址是保留地址

五种分级在十进制的表示:
Class A : 0.xx.xx.xx ~ 127.xx.xx.xx
Class B : 128.xx.xx.xx ~ 191.xx.xx.xx
Class C : 192.xx.xx.xx ~ 223.xx.xx.xx
Class D : 224.xx.xx.xx ~ 239.xx.xx.xx
Class E : 240.xx.xx.xx ~ 255.xx.xx.xx

根据上表的说明,我们可以知道,你只要知道 IP 的第一个十进制数,就能够约略了解到该 IP 属于哪一个等级,以及同网域 IP 数量有多少。不过,上表中你只要记忆三种等级,亦即是 Class A, B, C 即可,因为 Class D 是用来作为群播 (multicast) 的特殊功能之用 (最常用在大批计算机的网络还原),至于 Class E 则是保留没有使用的仅供实验的网段。因此,能够用来设定在一般系统上面的,就只有 Class A, B, C 三种等级的 IP.

A类地址可以有很大数量的主机,最高位0,紧跟的七位标示Net_ID.一共允许126个网络.最后一个网段的127作为本机回环测试.广播地址是X.255.255.255

B类地址最高两位置为10,前16位网络号,后6位主机号,允许有16384个网络.广播地址是:X.X.255.255

C类地址:高三位置为110,前24位值网络号,后8位主机号,允许有约200万个网络.广播地址是X.X.X.255

在分配网络号和主机号的时候遵守以下原则:

  • 网络号不能为127,该表示是保留为本地回路及诊断功能
  • 不能将网络号和主机号均置为1.如果每一位都是1,就会被解释为网内广播而不是一个主机号.
  • 各位均不能置0,否则该地址被解释为"就是本网络"
  • 对于本网络来说,主机号唯一,否则出现IP地址已分配或有冲突的问题

子网掩码(NetMask)

TCP/IP的每台主机都需要用一个子网掩码.是一个有4字节的地址.用来封装或屏蔽IP地址的一部分,以区分网络号和主机号.

子网掩码中,对应的IP地址网络号的位置被置为1,对应主机号的位置置为0.即是网络地址可以由IP地址和子网掩码做与运算求得

子网掩码的另一个重要作用就是进行子网划分.....不再赘述.

IP地址转换

inet_addr()函数的语法如下:

#include <sys/socket.h>
#include <neiinet/in.h>

#include <arpa/inet.h>

unsigned long inet_addr(const char * string);
这个函数使用string作为输入参数,并将这个点分十进制的IP地址转换为32位的二进制表示法,函数的返回值就是这个32位的二进制的网络字节序。当然如果string不是一个有效的点分十进制IP地址,函数返回INADDR_NONE。另外需要注意的是,当inet_addr函数返回INADDR_NONE的时候,它并没有建立一个有效的errno值,所以当函数返回错误的时候,不要去测试errno的值。

       在新程序中避免使用inet_addr函数,而应该使用inet_aton函数作为代替。因为对于inet_addr函数来说,即使输入的参数是有效的IP地址:255.255.255.255,他的返回值仍然是INADDR_NONE。

inet_aton函数

     inet_aton函数是将字符串形式的IP地址转换为网络字节序的32位IP地址的改进形式。语法如下:

#include <sys/socket.h>
#include <neiinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char* string, struct in_addr*addr);

参数string表示点分十进制IP地址的ASCII表示。输出参数addr是一个被新的IP地址跟新的结构。函数调用成功返回非0值。失败返回0.当然他也没有建立一个有效的errno值。

下面我们来看看inet_ntoa函数

 有时候当用户连接到你的服务器的时候,需要知道他的IP地址,系统提供了inet_ntoa函数将32位的二进制IP地址表示转换为点分十进制的字符串形式:

#include <sys/socket.h>
#include <neiinet/in.h>
#include <arpa/inet.h>

char* inet_ntoa(struct in_addr addr);

       需要注意的是inet_ntoa函数的返回值直到下次调用前一直有效。所以如果在线程中使用inet_ntoa的时候,一定要确保每次只有一个线程调用本函数。否则一个线程的返回的结构可能被其他线程返回的结果所覆盖。

inet_network函数

        当我们需要用网络掩码将IP地址中的网络位或者主机位提取出来的时候,如果能将点分十进制的IP地址转换为主机字节序的32位二进制IP地址形式就方便了,而inet_network函数的作用就是如此、语法如下:

#include <sys/socket.h>
#include <neiinet/in.h>
#include <arpa/inet.h>

unsigned long inet_network(const char* addr);

函数的输入参数是存储在字符串addr中的点分十进制IP地址,返回值是主机字节序的32位二进制地址,但是如果输入参数不正确,返回值是0xFFFFFFFF.

    以主机字节序的形式返回结果可以保证用户安全的使用网络掩码,因为如果返回值是网络字节序的话,那么不同的cPu平台所使用的网络掩码和程序代码就会有差异。

inet_lnaof函数

        函数inet_lnaof函数是将套接口地址中的IP地址(网络字节序)转换为没有网络位的主机ID(主机字节序),这个函数为我们省去了很多的麻烦,因为我们不需要对IP地址进行分类,再将主机为从IP地址中提取出来。函数语法如下:

#include <sys/socket.h>
#include <neiinet/in.h>
#include <arpa/inet.h>

unsigned long inet_lnaof(struct in_addr addr);

下面的表提供了一些可以作为inet_lnaof函数输入的典型例子和返回值:

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

inet_netof函数

      inet_lnaof函数返回的是主机ID,而inet_netof函数返回的是网络ID,函数语法如下:

#include <sys/socket.h>
#include <neiinet/in.h>
#include <arpa/inet.h>

unsigned long inet_netof(struct in_addr addr);

下表展示了一些例子:

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

inet_makeaddr函数

根据之前的内容我们知道使用inet_netof函数和inet_lnaof函数,我们就可以把IP地址中的主机为和网络位分别提取出来,有时候我们还需要根据提取出来的主机位和网络为合并为一个新的IP地址。这个时候我们就可以使用inet_makeaddr函数。

#include <sys/socket.h>
#include <neiinet/in.h>
#include <arpa/inet.h>

struct in_addr inet_makeaddr(int net , int host);

函数inet_pton

该函数可以根据地址协议类型进行转换,即支持IPv6的转换.

inet_ntop函数也支持

下面是IPv4地址转换的示例:

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
int main(int argc,char *argv[])
{
in_addr_t net;
struct in_addr net_addr,ret;
char buf[128];
memset(buf,'\0',128);
net=inet_addr("192.168.68.128");
net_addr.s_addr=net;

printf("inet_addr(192.168.68.128)=0x%x\n",inet_addr("192.168.68.128"));
printf("inet_network(192.168.68.128)=0x%x\n",inet_network("192.168.68.128"));

printf("inet_ntoa(net)%s\n",inet_ntoa(net_addr));
inet_aton("192.168.68.128",&ret);
printf("test inet_aton,then inet_ntoa(ret)%s\n",inet_ntoa(ret));
}

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇


#include<arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
int main(int argc,char *argv[])
{
in_addr_t net;
struct in_addr net_addr,ret;
char buf[128];
inet_pton(AF_INET,"192.168.68.128",&ret);
inet_ntop(AF_INET,&ret,buf,128);
printf("buf=%s\n",buf);
}

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇