017-网络基础概念
1. 网络发展背景
在计算机被发明出来时,一般都是用来做科学研究等计算,此时计算机不与其他计算机进行通信,这时的计算机就是独立模式。
独立模式:计算机之间相互独立。

随着计算机科学的发展,人们发现有很多信息资源都是需要共享的,然后就慢慢发展出来了网络互连,以便计算机之间的通信。
网络互联:多台计算机连接在一起,完成数据共享。

局域网LAN:计算机数量更多了,通过路由器和交换机连接在一起。

广域网WAN:将远隔千里的计算机都连接在一起。

所谓"局域网"和"广域网"只是一个相对的概念,广域网也可以看作一个范围更大的局域网。
2. 网络协议初识
2.1 认识"协议"
"协议"是一种约定。
计算机之间的传输媒介是电信号和光信号。通过"频率"和"强弱"来表示0和1这样的信息。想要传递各种不同的信息,就需要约定好双方的数据格式。

但是,计算机的生产商有很多,计算机操作系统也有很多,计算机网络硬件设备还是有很多,要让这些不同厂商之间生产的的计算机能够相互顺畅的通信,就需要一个公共的标准,大家都来遵守,这就是网络协议。
2.2 协议分层
我们在打电话时,我们和对方约定好使用汉语进行沟通,这是一种汉语语言协议,我们使用的通信设备,比如我们都使用电话机进行通话,两台电话机之间的通信是被事先设定好的,就是一种电话机通信协议。
在打电话时,我们可以使用不同的语言进行沟通,比如中国人在电话中使用汉语沟通,美国人在电话中使用英语沟通,但是他们都可以使用一样的电话机进行通话,在语言进行切换时,电话机可以不做任何改变。
我们使用汉语沟通时,我们不仅可以使用电话机,还可以使用对讲机,虽然通信设备进行了更换,但是我们的语言协议并没有变化,此时我们也可以进行通话。

这里我们就是将语言协议和通信协议分成了两层,像这样将一个复杂的通信分成多层有利于将每一层的内容进行模块化,便于维护和切换。
在实际的网络通信会更加复杂,需要分更多的层次。
分层最大的好处在于"封装",像我们使用面向对象的语言(比如C++),我们可以将一个程序分成调用层和函数实现层,这样就可以帮助我们更好的维护代码。
2.3 OSI七层模型
OSI(Open System Interconnection,开放系统互联)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。
把网络从逻辑上分为了7层,每一层都有相关、相对应的物理设备,比如路由器、交换机。
OSI七层模型是一种框架性的设计方法,其最主要的功能就是帮助不同类型的主机实现数据传输。


它最大的优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论一起比较完整,通过提供层次化结构模型使不同的系统不同的网络之间实现可靠的通讯。
在网络角度,OSI7层协议是非常完善的,但是在实际上,会话层和表示层是不可能接入到操作系统中的,最终实际落地的其实是5层协议。
2.4 TCP/IP五层(或四层)模型
TCP/IP是一组协议的代名词,它还包括了许多协议,组成了TCP/IP协议簇。


TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
- 物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。
- 数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重)、数据差错校验等工作。有以太网、令牌环网, 无线LAN等标准。交换机(Switch)工作在数据链路层。
- 网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。
- 传输层/运输层:负责两台主机之间的数据传输。如传输控制协议(TCP),能够确保数据可靠的从源主机发送到目标主机。
- 应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层。
物理层我们考虑到比较少,因此很多时候可以曾为TCP/IP四层模型。
一般而言:
- 对于一台主机,它的操作系统内核实现了从传输层到物理层的内容。
- 对于一台路由器,它实现了从网络层到物理层。
- 对于一台交换机,它实现了从数据链路层到物理层。
- 对于一台集线器,它只实现了物理层。
但并不绝对,很多交换机也实现了网络层的转发,很多路由器也实现了部分传输层的内容(比如端口转发)。
3. 再识协议
3.1 为什么要有TCP/IP协议
在只有独立的计算机下,硬件于与硬件之间只需要完成自身硬件与硬件的传输,并不需要与外界的其他硬件进行通信,他们之间只需要维护一些自身的协议。
但当不同的计算机需要进行通信时,计算机与计算机之间有不同的差别,并且两台计算机可能相隔很远的地方,他们需要形成新的通信体系来进行通信。
两台主机距离变远后,就出现了一系列的问题:
- 我要发给远方的主机,就要先将信息发送给路由器,这个过程怎么办?
- 网络上有很多主机,应该如何定位到我想要发送到的那个主机?
- 如果我的数据在发送过程中丢失了怎么办?
- 发送数据不是目的,只是手段,让对方读取数据并用上数据才是目的,那么对方怎么知道我的数据经过什么样的处理,应该怎么样使用。
着些不同性质、不同种类的问题就通过分层的协议,一层一层进行解决。
所以为什么要有TCP/IP协议?本质就是因为通信主机距离变远了。
3.2 什么是TPC/IP协议
- TCP/IP协议的本质是一种解决方案。
- TCP/IP协议能分层,前提是因为问题本身能分层。
3.3 TCP/IP协议与操作系统的关系

- 物理层:网卡。
- 数据链路层:驱动程序。
- 网络层(IP)、传输层(TCP):操作系统内部自主实现。
- 应用层:在操作系统上层,由用户实现。
操作系统与操作系统之间不同,但是他们都需要根据一样的标准实现网络层和传输层的功能,这样,在不同计算机的不同操作系统上也可以通过一样的标准进行通信。
3.4 究竟什么是协议
- 截止到目前,我们并未接触任何协议,但是如何朴素的理解协议,我们就可以试试了。
- 虽然操作系统与操作系统之间大部分代码都是不一样的,但在TCP/IP的实现都是类似的,假设Win和Linux,在Win中定义一个C语言结构体,然后通过网络将这个结构体的内容传给Linux,此时Linux只需要用C语言定义同一个类型的结构体就可以读取到Win发来的内容。
- 对协议的朴素理解:所谓协议,就是通信双方都认识的结构化的数据。
- 因为协议栈是分层的,所以每层双方都有协议,同层之间,互相可以认识对方的协议。
举个例子:
当我们网购或者发快递,每个快递都会有一个快递单,快递单上会有各种信息,比如我们买了一个键盘,我们收到的除了我们的键盘,在键盘的包装盒外还会有一张快递单,我们收到快递以后,拿到键盘后,通常都会把快递单和外包装盒丢掉,但是,这个快递单就是寄件人和收件人所做的一个约定,通过这个快递单,才能知道它应该寄给谁,它是谁寄出的,而这个约定是快递公司定的,收件方和寄件方遵守这个约定,这个约定就是收寄快递的"协议"。
我们在网络之间的传输也是类似,比如有两个计算机A和B,A发送给B的不仅仅是它想要发送的内容,而是包含了"协议报头"和"有效载荷"两个部分的报文,协议报头就相当于我们上面的快递单,有效载荷就相当于上面的键盘。
4. 网络传输的基本流程
4.1 局域网通信原理
-
两台主机在同一个局域网里,是能够直接通信的。
-
就像所有同学在同一个教室里上课,老师说了一句"张三同学,站起来,你为什么不交作业",这时老师向整个教室中,说了一条这样一句话,这时我们所有人都听到了,但是只有张三同学站起来了,此时张三同学说"我的作业早就交了,只是你没看到",所有人也听到这句话。虽然大家都听到了这两句话,但是大家都没有任何反应,只有老师和张三在互相对话。在这里,教室就相当于一个局域网,每个人相当于这个局域网的一台主机,老师讲消息发送到局域网中,虽然大家都能听到,但是大家分析了这个消息的标识发现不是发给自己的,所以只有张三进行了处理。每台主机都要有唯一的标识,在上面的标识中,每个人的标识就是自己的名字。
-
每台主机在局域网上,要有唯一的标识来保证主机的唯一性:MAC地址。
-
MAC地址用来识别数据链路层中相连的节点。
-
MAC地址长度为48位,即6个字节,一般用16进制加上冒号的形式来表示(如:08:00:27:03:0b:16)。
-
MAC地址在网卡出厂的时候就决定了,不可修改,MAC地址通常是唯一的。

- 在以太网中,任何时刻,只允许一台机器向网络中发送数据。
- 如果有多台同时发送,会发生数据干扰,我们称之为数据碰撞。
- 所有发送数据的主机要进行碰撞检测和碰撞避免。
- 从系统的角度理解:这里可以用之前的临界资源来理解,一个以太网就是一个临界资源,主机发送消息就是在使用这一份临界资源,不过这里在使用之前并没有进行类似加锁的操作,而是在使用中发现了数据碰撞,然后直接进行处理。
- 没有交换机的情况下,一个以太网就是一个碰撞域。
- 局域网通信的过程中,主机对收到的报文确认是否是发给自己的,是通过目标MAC地址判定。
4.2 网络传输流程图

其中每层都有协议,所以我们进行上述传输流程的时候,每层都要封包和解包。

每一层收到上一层的信息都看成"有效载荷",再加上自己的"报头",再交给下一层。
每一层收到的上一层的信息,不关心内容是什么,只需要封装上自己的报头,传给下一层即可。
所以虽然物理上看,信息要进行层层封装和转发,但是相对的每一层发出和收到的信息都是一样的,所以我们可以看作每一层都在和对方的同层进行直接通信。
在上图中可以把信息看成一个栈,封装时每一层都入栈一个报头,解包时每一层都弹出一个报头,我们可以把这样一整个过程看成入栈和出栈的过程,所以我们也把TCP/IP叫做网络协议栈。
每一层对于自己的报头都包含着一些信息,包括载荷有多长、上层协议是什么等。
不同的协议层对数据包有不同的称谓,在传输层叫段,在网络层叫数据报,在链路层叫帧。
网络协议的共性:
- 报头和有效载荷分离的问题---解包。
- 因为每层都有很多个协议,所以解包时存在一个问题,除了应用层,每一层协议都必须解决一个问题,自己的有效载荷,应该要交给上层的哪一个协议---分用。为了解决这个问题,在封装时,每个报头都要添加上,自己是从上上层的哪个协议接受的有效载荷。
4.3 跨网络传输流程图
上面谈到的都是在同一个局域网中的情况,下面谈一下在不同局域网中的情况。

网络层(IP层)向上(包括网络层)看到的所有报文都是一样的,也就是主机1拿到的网络层报文、传输层报文、应用层报文都是一模一样的。这里我们可以得出一个结论:IP可以屏蔽底层网络的差异。
4.4 网络中的地址管理-IP地址
IP协议有两个版本,IPv4和IPv6,在后面的学习中,如果没有特殊说明,都是指IPv4。
- IP地址是在IP协议中,用来标识网络中不同主机的地址。
- 对于IPv4来说,IP地址是一个4字节,32位的整数。
- 我们通常使用点分十进制来表示IP地址,例如:192.168.0.1,每个数字的范围是0~255。
跨网段的主机数据传输,数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器。

如上图,当报文从用户A到第一个路由器时,其中带目标IP的就是用户B的IP,目标Mac就是第一个路由器的Mac,从第一个路由器到第二个路由器时,目标IP仍然是用户B的IP,目标Mac则变成了第二个路由器的Mac,以此类推。

上图中展示了网络层接收到上层信息后,继续封装然后发往目标网络层的过程,当目标网络层解包后,就可以拿到对方的信息,对于不同的主机来说,使用了相同的协议后,从IP层(网络层)开始往上拿到的数据就是一模一样的。
所以对于上层来说,所有的网络都是IP网络,IP网络可以屏蔽底层网络的差异。
IP地址和Mac地址的区别:
- IP地址:最终目标。
- Mac地址:下一目标。

5. Socket编程预备
5.1 理解源IP地址和目的IP地址
- IP在网络中,用来标识主机的唯一性。
数据传输到主机是目的吗?并不是。
数据是要呈现给人看的,而人要看到这些数据就要通过主机中正在执行的进程。
所以,数据传输到主机不是目的,而是手段。数据到达主机的内部,再交给主机的进程,这才是目的。
但是在系统中,同时会存在非常多的进程,当数据达到目标主机之后,怎么转发到目标进程?这就要在网络的背景下,在系统中,标识主机和进程的唯一性。

5.2 认识端口号
5.2.1 端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。

5.2.2 端口号划分
- 0-1203:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,它们的端口号都是固定的。
- 1024-65535:操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配的。
- 就类似于电话号码,知名的电话号码都是固定的,如:110、119、120等,这些电话都是固定给警察局、火警、医院等地方所使用的,而我们自己的电话号码,则是在11位电话号码中进行分配的。
5.2.3 理解端口号和进程ID
在操作系统中,进程pid代表唯一的进程,此处端口号也代表唯一的进程,这两者之间有什么样的关系?为什么不能直接使用进程pid,而是要多创建一个端口号的概念?
进程pid是系统的概念,确实可以标识意义的进程,但是如果这样做,就会让系统进程管理和网络模块强耦合,则意味着如果一边有改动,另一边有可能也需要跟着改动,这样分离是为了解耦,让系统是系统,网络是网络。就像我们本来就有身份证号可以标识我们这个人,为什么到学校还要有学号?公司还要有工号?这也是为了解耦和方便管理。
另外的,一个进程可以绑定多个端口号,但是一个端口号只能对应一个进程,只有需要网络通信的进程才需要绑定端口号,没有绑定端口号的进程不需要进行网络通信。
5.2.4 理解socket
IP地址用来标识互联网中唯一的主机,port用来标识该主机上唯一的网络进程,所以我们只需要通过IP+port就能确定互联网中的唯一一个进程,我们在网络中通信的时候就可以使用源IP、源Port、目标IP、目标Port这个四元组就能标识网络中唯二的两个进程。
综上所述,数据传输是进程间的传输,所以网络通信实质上就是进程间通信。
我们把基于IP+port这种进程间通信的方式叫做Socket通信。
5.3 传输层的经典代表
如果我们了解了系统, 也了解了网络协议栈, 我们就会清楚, 传输层是属于内核的, 那么我们要通过网络协议栈进行通信, 必定调用的是传输层提供的系统调用, 来进行的网络通信。

对下面两种协议,只做简单的认识,后面再详细讨论。
5.3.1 UDP协议
UDP(User Datagram Protocol 用户数据报协议):
- 传输层协议
- 无连接:通信前无需建立连接
- 不可靠传输:传输过程中,如果数据丢失或损坏,什么都不做,不保证数据可靠
- 面向数据报
5.3.2 TCP协议
TCP(Transmission Control Protocol 传输控制协议):
- 传输层协议
- 有连接:通信前需要先建立连接
- 可靠传输:传输过程中,如果数据丢失或损坏,会有对应的策略,如重传等
- 面向字节流
5.4 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略,直接发送即可
对于大小端:

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

上面的接口中,如果调用:
- 小端机器,将会进行大小端转换,然后返回
- 大端机器,将什么都不做,然后返回
5.5 Socket编程接口
5.5.1 socket常见API
c
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
5.5.2 sockaddr 结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及后面要讲的 UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。

- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4 地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6,这样只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
- socket API 可以都用 struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in,这样的好处是程序的通用性,可以接收IPv4、IPv6、以及UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数
对于同一个接口,根据传入指向不同结构体的指针做出不同的处理,这就是C语言版的多态。
sockaddr结构:

sockaddr_in结构:

虽然socket API的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in,这个结构里主要有三部分信息:地址类型、端口号、IP 地址。
in_addr结构:

in_addr用来表示一个IPv4的IP地址,其实就是一个32位的整数。