0.关注博主有更多知识
操作系统入门知识合集
目录
1.理解IP地址和MAC地址
2.认识端口号
3.感性认识TCP协议和UDP协议
4.网络字节序
5.socket编程接口
1.理解IP地址和MAC地址
事实上在上一篇博客当中粗浅了介绍一个IP地址MAC地址,其中我们知道IP地址提供方向,MAC地址提供可行性。那么到底应该怎么理解方向和可行性呢?我们以一个简单的例子来理解:我们知道四大名著中的《西游记》是唐僧一行人去西天取经的故事,那么唐僧会经过很多地方并且遇到很多人,假设现在唐僧走到了女儿国,女儿国国王问:你从哪里来?你要干什么去?唐僧回答道:贫僧从东土大唐而来,要去西天拜佛求取真经。女儿国国王又问:那你上一次上一次是从哪里来的呀?唐僧回答道:贫僧从车迟国而来。女儿国国王又说道:那你要去西天取经,那你下一个国家就要去祭赛国。然后唐僧一行人收拾行李出发前方祭赛国......到了祭赛国,祭赛国国王问道:你们从哪里来?要干什么去?唐僧回答道:贫僧从东土大唐而来,要去西天拜佛求取真经。祭赛国国王又说:你要去西天拜佛求经,那你现在要先去朱紫国。然后唐僧一行人又收拾行李赶路......那么在这个例子中,唐僧口中的地址就有两种地址,我们这里把它称为绝对目标和相对目标,绝对目标指的是唐僧要从东土大唐到西天拜佛求经;相对目标指的是求取真经的路上途径的各个国家,例如从车迟国前往女儿国、女儿国前往祭赛国。
在上面的例子当中,我们不难分析出什么是IP地址,什么是MAC地址,即IP地址就是东土大唐和西天,那么要从东土大唐到西天,东土大唐就是源IP地址,西天就是目的IP地址;MAC地址就是途径的各个国家,其中从某个国家出发,这个国家称为源MAC地址,到哪个国家去,这个国家称为目的MAC地址。
那么由此可得,IP地址在网络通信当中,即数据传输当中,是保持长期不变的;而MAC地址是经常变化的。所以数据传输的过程就好像唐僧拜佛求经的过程,即数据从源IP地址出发,它的目标是达到目的IP地址;而数据在传输的过程中不是嗖的一下就过去,而是要脚踏实地、顺序地经过源IP和目的IP当中的各个机器,而这些各个机器是通过直接相连而组成局域网的,那么在局域网当中通信需要的地址为MAC地址,所以MAC地址在数据传输的过程中是经常变化的。这也就印证了IP地址提供方向,MAC地址提供可行性的说法。
2.认识端口号
那么既然IP地址既然能够确定方向,那么也能推测出IP地址在全网当中唯一(暂时这么理解),也就是说,IP地址确定一台唯一的主机。而我们之前说过,数据从源主机传输到目的主机不是通信的目的,而是通信的手段,通信的目的是为了解决人的需求,而人的需求需要通过软件、程序来表达。那么也就是说,网络通信的实质就是进程间通信!
通信的进程之间需要具有唯一性(总不可能让QQ的消息推送到抖音里面去吧?)。我们在介绍Linux进程实现的时候知道,进程创建的时候操作系统会为该进程分配一个可用的PID,那么这个PID是用来描述进程的唯一标识符。事实上在网络通信当中用PID来表示通信双方进程的唯一性是可以的,至少在技术上是可以的,但实际上网络通信当中并不采用这种方案,原因如下:
1.使用PID描述通信双方的进程就会给操作系统和网络加上一层耦合关系,因为具有耦合关系,那么操作系统、网络任何一方都不能出错
2.通常通信的两个进程一个为客户端,一个为服务端,那么客户端在使用的时候需要知道服务端的IP地址和唯一的进程标识,也就是说客户端希望绑定一个长期不变、甚至永久不变的唯一进程标识。那么如果进程的唯一标识使用PID来表示的话,如果服务端的机器一旦出现异常而引发进程退出、重启等等,那么进程就要重新加载,进程重新加载就会造成PID的变化
3.并不是所有的进程都需要网络服务
所以在网络通信当中,通信双方的进程使用端口号来标识进程的唯一性。由此可以得出,IP地址+端口号能够确定全网当中唯一的一台主机上的唯一一个进程。端口号(port)是传输层协议的内容,也就是说端口号是操作系统提供的,它是一个2字节16位的数字,能用来标识一个进程,并且值得注意的是:一个端口号只能被一个进程绑定,但是一个进程可以绑定多个端口号。因为进程本身就具有PID,那么进程也不一定需要网络服务,所以绑定了端口号的进程一定就是网络服务进程。
那么操作系统在网络通信当中,一定是根据端口号去找到进程的PCB,寻找的方式就是通过哈希表。那么此时就会产生一个问题,我们说过进程控制块(PCB)是被链入一个双向循环链表的数据结构当中的,怎么这里是通过哈希表去查找的呢?事实上我们以前介绍的概念没有错误,而是因为操作系统内部实现非常复杂,PCB不单单被链入了双向循环链表当中,而且还被链入了哈希表这样的数据结构。也就是说,在操作系统当中,某一份资源不仅仅只占用了一个数据结构,还同时占用了多个数据结构。我们通过一张图来理解:
既然网络通信的实质是进程间通信,那么进程间通信需要有一个必要前提,那便是看到同一份资源,那么很显然,这个同一份资源就是网络。通信双方的进程通过网络进行数据传输,那么由此可得,网络通信就是在做I/O操作,所以我们人的上网行为无外乎就两种:要么发数据,要么接收数据。其中,我们需要认识到网络通信是相互的,所以客户端给服务端发送数据时,服务端接收到消息后,很有可能会向客户端回复一些消息,既然需要发送数据,那么就必须知道目标主机的IP地址和对应进程的端口号,所以无论是通信双方的哪个进程,只要向对方发送数据了,那么一定会多发一部分数据,这个多出来的数据就是自己进程本身的IP地址和端口号,而多出来的IP地址和端口号以协议的形式呈现。
3.感性认识TCP协议和UDP协议
TCP协议和UDP协议都是传输层的协议,也就是说我们在编写应用层的代码时,必须使用传输层提供的接口,虽然可以跳过传输层而使用更底层的接口,但我们不那么做。
TCP协议:TCP(Transmission Control Protocol,传输控制协议):
1.TCP协议是传输层协议
2.使用TCP协议进行网络通信时,必须建立连接。例如打电话的过程,当用户A想跟用户B通话时不是直接对着电话一顿输出,而是要先把电话拨通,用户B再接听,此时才能正常通话
3.TCP协议是可靠传输协议,数据在网络的传输过程中,一定会产生各种各样的错误,例如丢包、多发、少发等等,那么TCP协议有传输控制策略,即TCP协议有办法、有策略应对网络通信时发生错误的情况
4.面向字节流传输
UDP协议:(User Datagram Prorocol,用户数据报协议):
1.UDP协议是传输层的协议
2.使用UDP协议进行网络通信时,无需建立建立链接。例如发邮件的过程,用户A向用户B发送一封邮件时,用户B不需要实时在线等待该邮件的到来
3.UDP协议是不可靠传输协议,也就是说网络错误发生时UDP协议不会做出任何动作
4.面向数据报传输
那么上面介绍到了可靠传输和不可靠传输,在这里需要强调的是,可靠传输不是一个褒义词,不可靠传输也不是一个贬义词。这叫特性。那么TCP协议是可靠传输协议,这就意味着TCP协议需要实现更多的策略、机制来应对网络发生错误的情况,所以TCP协议的实现会复杂,并且难以维护和使用;而UDP协议是不可靠传输协议,这就意味着传输速度一定比TCP协议快,并且实现机制简单,易于维护和使用。
TCP协议和UDP协议都有特定的使用场景,谁也不能取代谁。TCP协议的使用是非常广泛的,例如应用于银行系统、电商系统等等;而UDP协议因为其传输速度更快,所以更适合于视频直播这种类似的场景。
4.网络字节序
我们知道数据都是存储在内存当中的,而内存当中的每个数据单元都有对应的地址,既然有地址,那么就一定有高地址和低地址之分,那么数据就有两套不同的存储方式:数据的高权值位存放到内存的高地址处,我们称为大端存储模式;数据的高权值位存放到内存的低地址处,我们称为小端存储模式。因为有大端和小端的存在,所以在网络通信时,主机与主机之间使用的存储模式都是不一样的,这就意味着网络必须屏蔽掉这种差异。解决这种差异的手段有很多,例如在报文当中添加一个标记位用来标记发送数据的机器是大端还是小端,接收数据的机器根据该标记位来确定是否需要将收到的数据进行转化......解决的手段非常多,在技术上也一定能够实现。但是网络采用了一种非常暴力的解决方案,即规定在网络当中的数据都是大端字节序。这就意味着无论通信双方的主机是大端还是小端、是接收方还是发送方,都要遵守以下规则:
1.发送数据的主机通要将缓冲区当中的数据按内存地址由低到高发送
2.接收数据的主机要把接收到的数据保存在缓冲区中,并且按内存地址由低到高的顺序存储
3.如果发送数据的主机是小端机,那么发送到网络之前需要将其转换为大端字节序;如果主机已是大端机,那么跳过转换,直接发送到网络
4.接收数据的主机如果是小端机,那么接收到的数据一定是大端字节序,所以使用之前必须将其转换为小端字节序;反之直接使用
那么大小端字节序之间的转换在网络当中确实比较麻烦,所以<arpa/inet.h>库为我们做好了转换函数的封装:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htonl(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t htonl(uint16_t netshort);
这些接口看似并不能满足我们的使用需求,这是因为我们还没有开始编程,具体事情放到具体的情况再介绍。这些函数的命名是有道理的,"h"表示host,即主机;"to"直接翻译,即向...干嘛;"n"表示network,即网络,"l"表示long,即长整型;"s"表示short,即短整型,所以"htonl"表示主机将32位的长整数从主机序列转换为网络字节序(大端字节序),然后在函数内部实现转换,其返回值就是转换之后的结果。
5.socket编程接口
首先我们要知道什么是socket,socket翻译过来就是插座的意思,就好像数据从源主机发送到目的主机上,像是一个插头寻找插座的过程。那么"插头"我们认为是源IP和源端口号,"插座"我们认为是目的IP和目的端口号,IP和端口号我们称为套接字。所以套接字编程实际上就是在为IP和端口号编程。那么socket编程接口如下:
// 创建 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);
这些接口我们一个都不认识,但是我们能够发现一个共性,即大多数接口都需要一个参数,即"struct sockaddr *address",那么这个参数是什么东西呢?
我们需要知道,套接字编程分为很多种类,常见的种类就有三种:网络套接字编程、原始套接字编程、Unix域间套接字编程。其中,网络套接字编程直接使用传输层提供的接口,既可以跨网络通信也可以本地通信;而原始套接字编程可以跳过传输层而使用更底层的接口;Unix域间套接字编程只能本地通信,与本地进程间使用命名管道通信的效果差不多。那么这么多种类的套接字编程,就需要多套编程接口,而设计多套编程接口无论在维护、使用上面都是非常麻烦的,所以接口的设计者设采用了一种非常高明的手段,即通过传递不同的参数来确定套接字编程的种类,这就是一种多态的表现。
那么网络套接字对应的结构体为struct sockadd_in,其中的"in"不要理解为里面,而要理解为inet,即IP地址;Unix域间套接字对应的结构体为struct sockaddr_un,其中"un"要理解为Unix:
因为这些结构体的类型不同,这就注定了我们在使用套接字接口时需要做强制类型转换。那么套接字接口接收到我们传递的参数之后(强制类型转换成统一的struct sockaddr *)之后,接口内部先检查前两个字节以确定套接字的种类,然后再将struct sockaddr *强转为对应的套接字类型的结构体:
那么我们知道,void *类型可以接收任何类型的指针,但是为什么接口的设计者不采用这种方案呢?答案一定意想不到,因为那个时候还没有void *这个语法。那么现在支持了,为什么不从新设计以下这些接口呢?因为软件的设计需要保证向前兼容,不排除有的编译器仍然很老旧,不排除有的用户、公司、企业的业务逻辑已经默认采用原始的方案,如果接口一旦从新设计,那么操作系统的代码需要重新编排,所以修改接口的代价是非常大的。
那么对于这些结构体的具体内容,我们到真正编程的时候再来一个一个分析。