一. 计算机⽹络背景
⽹络发展
独⽴模式: 计算机之间相互独⽴;

⽹络互联: 多台计算机连接在⼀起, 完成数据共享。

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

⼴域⽹WAN: 将远隔千⾥的计算机都连在⼀起;

所谓 "局域⽹" 和 "⼴域⽹" 只是⼀个相对的概念. ⽐如, 我们有 "天朝特⾊" 的⼴域⽹, 也可以看做⼀个⽐ 较⼤的局域⽹。
初始协议
计算机要实现联通,必须实现协议,比如我们条件不好,你去千里上学,你没手机只能用室友的手机,一直用浪费话费也不合适,就和父亲约定一下,响一声是报平安,响两声是要生活费....,所以两台计算机之间也要约定,接受101,是干什么的,必须约定一下。
计算机之间的传输媒介是光信号和电信号. 通过 "频率" 和 "强弱" 来表⽰ 0 和 1 这样的信息. 要想传递各种不同的信息, 就需要约定好双⽅的数据格式.
思考: 只要通信的两台主机, 约定好协议就可以了么?
定好协议,但是你⽤频率表⽰01,我⽤强弱表⽰01,就好⽐我⽤中国话,你⽤葡萄⽛语⼀样,虽
然⼤家可能遵守的⼀套通信规则,但是语⾔不同,即是订好了基本的协议,也是⽆法正常通信的
所以,完善的协议,需要更多更细致的规定,并让参与的⼈都要遵守。
计算机⽣产⼚商有很多;
计算机操作系统, 也有很多;
计算机⽹络硬件设备, 还是有很多;
如何让这些不同⼚商之间⽣产的计算机能够相互顺畅的通信? 就需要有⼈站出来, 约定⼀个共同的
标准, ⼤家都来遵守, 这就是 ⽹络协议;
⼀般具有定制协议或者标准的资格的组织或者公司都必须是业界公认或者具有江湖地位的组织或者公 司,下⾯是⽂⼼⼀⾔⽣成的标准制定组织,⼤家看⼀下就可以
问:能定制协议标准的组织或者公司
答:能定制协议标准的组织或公司主要有以下⼏类:
- 国际标准化组织:
IEEE(电⽓和电⼦⼯程师协会):这是⼀个由计算机和⼯程领域专家组成的庞⼤技术组织,在
通信协议领域贡献突出。IEEE制定了全世界电⼦、电⽓和计算机科学领域30%左右的标准,包
括IEEE 802系列标准,这些标准涵盖了从局域⽹(LAN)到⼴域⽹(WAN)等多种⽹络技术。
ISO(国际标准化组织):ISO是由多个国家的标准化团体组成的国际组织,它在开放系统互连
(OSI)模型⽅⾯的⼯作尤为著名。OSI模型定义了⽹络通信的七层协议结构,尽管在实际应⽤
中,TCP/IP协议族更为普遍,但OSI模型仍然在学术和理论研究中占有重要地位。
ITU(国际电信联盟):ITU是联合国下属的专⻔机构,负责制定电信领域的国际标准。ITU-T
制定的标准涵盖了电话和⽹络通信,与ISO合作确保了通信技术的全球兼容性和互操作性。
- 区域标准化组织:
ETSI(欧洲电信标准学会):由欧洲共同体各国政府资助,是⼀个由电信⾏业的⼚商与研究机
构参加并从事研究开发到标准制定的组织。
ASTAP(亚洲与泛太平洋电信标准化协会):1998年由⽇本与韩国发起成⽴的标准化组织,旨
在加强亚洲与太平洋地区各国信息通信基础设施及其相互连接的标准化⼯作的协作。
- 公司:
某些公司,如泰凌微,也⾃研各种标准的软件协议栈,包括低功耗蓝⽛、zigbee、thread及
Matter等,并可进⾏定制化改动,这是其核⼼竞争⼒之⼀。泰凌微还计划重点发展智能电⼦价
签、智能遥控、智能家居等市场。
- ⺠间国际团体:
IETF(互联⽹⼯程师任务组):这是⼀个负责开发和推⼴互联⽹协议(特别是构成TCP/IP协议
族的协议)的志愿组织,通过RFC发布新的或者取代⽼的协议标准。
- 官⽅机构:
FCC(联邦通信委员会):美国对通信技术的管理的官⽅机构,主要职责是通过对⽆线电、电
视和有线通信的管理来保护公众利益。也对包括标准化在内的通信产品技术特性进⾏审查和监
督。
以上这些组织或公司都能在⼀定程度上定制协议标准,以满⾜特定需求或推动技术发展。
协议分层
协议本质也是软件,在设计上为了更好的进⾏模块化,解耦合,也是被设计成为层状结构的
软件分层的好处

在这个例⼦中, 我们的"协议"只有两层:语⾔层、通信设备层。
但是实际的⽹络通信协议,设计的会更加复杂, 需要分更多的层
但是通过上⾯的简单例⼦,我们是能理解,分层可以实现解耦合,让软件维护的成本更低
再识协议
其实没有网络的时候,即便是单机情况,计算机内部也会有自己的协议!!
比如,有些内容是如何写入内存的,如何写入磁盘的都是存在一个协议的。
所以计算机内部会走线。
网络通信不就是这个线的距离变长了吗?
距离变长就会存在问题。

所以此时就存在TCP/IP协议的,它的作用就是解决网络通信的具体方案。
OSI七层模型
OSI(Open System Interconnection,开放系统互连)七层⽹络模型称为开放式系统互联参考
模型,是⼀个逻辑上的定义和规范;
把⽹络从逻辑上分为了7层. 每⼀层都有相关、相对应的物理设备,⽐如路由器,交换机;
OSI 七层模型是⼀种框架性的设计⽅法,其最主要的功能使就是帮助不同类型的主机实现数据传
输;
它的最⼤优点是将服务、接⼝和协议这三个概念明确地区分开来,概念清楚,理论也⽐较完整.
通过七个层次化的结构模型使不同的系统不同的⽹络之间实现可靠的通讯;
但是, 它既复杂⼜不实⽤; 所以我们按照TCP/IP四层模型来讲解.


其实在⽹络⻆度,OSI定的协议7层模型其实⾮常完善,但是在实际操作的过程中,会话层、表⽰层是不可能接⼊到操作系统中的,所以在⼯程实践中,最终落地的是5层协议。
但是要理解上⾯的话,需要我们学习完⽹络才可以理解,这⾥就知道就可以。
TCP/IP五层(或四层)模型
TCP/IP是⼀组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇.
TCP/IP通讯协议采⽤了5层的层级结构,每⼀层都呼叫它的下⼀层所提供的⽹络来完成⾃⼰的需求.
物理层: 负责光/电信号的传递⽅式. ⽐如现在以太⽹通⽤的⽹线(双绞 线)、早期以太⽹采⽤的的同
轴电缆(现在主要⽤于有线电视)、光纤, 现在的wifi⽆线⽹使⽤电磁波等都属于物理层的概念。物
理层的能⼒决定了最⼤传输速率、传输距离、抗⼲扰性等. 集线器(Hub)⼯作在物理层.
数据链路层: 负责设备之间的数据帧的传送和识别. 例如⽹卡设备的驱动、帧同步(就是说从⽹线上
检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就⾃动重发)、数据差错校验等⼯
作. 有以太⽹、令牌环⽹, ⽆线LAN等标准. 交换机(Switch)⼯作在数据链路层.
⽹络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识⼀台主机, 并通过路由表的
⽅式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)⼯作在⽹路层.
传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送
到⽬标主机.
应⽤层: 负责应⽤程序间沟通,如简单电⼦邮件传输(SMTP)、⽂件传输协议(FTP)、⽹络远
程访问协议(Telnet)等. 我们的⽹络编程主要就是针对应⽤层.

物理层我们考虑的⽐较少,我们只考虑软件相关的内容. 因此很多时候我们直接称为 TCP/IP四层模型.
⼀般⽽⾔
对于⼀台主机, 它的操作系统内核实现了从传输层到物理层的内容;
对于⼀台路由器, 它实现了从⽹络层到物理层;
对于⼀台交换机, 它实现了从数据链路层到物理层;
对于集线器, 它只实现了物理层;
但是并不绝对. 很多交换机也实现了⽹络层的转发; 很多路由器也实现了部分传输层的内容(⽐如端⼝转发);
为什么要有TCP/IP协议?
⾸先,即便是单机,你的计算机内部,其实都是存在协议的,⽐如:其他设备和内存通信,会有内
存协议。其他设备和磁盘通信,会有磁盘相关的协议,⽐如:SATA,IDE,SCSI等。只不过我们感 知不到罢了。⽽且这些协议都在本地主机各⾃的硬件中,通信的成本、问题⽐较少。•
其次,⽹络通信最⼤的特点就是主机之间变远了。任何通信特征的变化,⼀定会带来新的问题,有
问题就得解决问题,所以需要新的协议咯。
这就是我们上面说的但计算机内部也会走线。
所以,为什么要有TCP/IP协议?本质就是通信主机距离变远了
什么是TCP/IP协议?
TCP/IP协议的本质是⼀种解决⽅案
TCP/IP协议能分层,前提是因为问题们本⾝能分层
TCP/IP协议与操作系统的关系(宏观上,怎么实现的)

通过这张图来看一下。
我们上面也说了,比较权威的公司或者什么定标准,定出来标准不同的厂商就要着手实现自己的协议了。
不同操作系统按照同一个标准设计自己的协议的。
所以究竟什么是协议?
协议本质是一种约定(计算机OS内如何设定约定)。
OS一般用什么语言写的呢??C语言,协议是很多的。操作系统要不要管理呢??
答案是需要通过结构体管理的,协议本质就是结构体。
这个结构体是定义在协议栈下内部的!!

📌 问题:主机B能识别data,并且准确提取a=10,b=20,c=30吗?
回答:答案是肯定的!因为双⽅都有同样的结构体类型struct protocol。也就是说,⽤同样
的代码实现协议,⽤同样的⾃定义数据类型,天然就具有"共识",能够识别对⽅发来的数
据,这不就是约定吗?就是双方都是相同的结构体变量什么的,我从一个主机发送到另外一个主机,它天然是可以解析的。
关于协议的朴素理解:所谓协议,就是通信双⽅都认识的结构化的数据类型
因为协议栈是分层的,所以,每层都有双⽅都有协议,同层之间,互相可以认识对⽅的协
议。•
⽹络购物,快递单的例⼦
协议的约定这个特点,是怎么做到的呢??
答案是遵守相同的OSI标准。

每个协议对应一个问题,详细的我们后面再谈。
⽹络传输基本流程
局域⽹ 络传输流程图
局域⽹(以太⽹为例)通信原理
⾸先回答,两台主机在同⼀个局域⽹,是否能够直接通信?
原理类似上课
每台主机在局域⽹上,要有唯⼀的标识来保证主机的唯⼀性:mac地址

局域网通信就是每个主机都能收到信息,但是只有指定的主机才能处理信息,那么是如何区别这些主机的呢??
答案是每个主机都存在一个MAC地址。
认识MAC地址
MAC地址⽤来识别数据链路层中相连的节点;
⻓度为 48 ⽐特位, 即 6 个字节. ⼀般⽤ 16 进制数字加上冒号的形式来表⽰(例如:
08:00:27:03:fb:19)
在⽹卡出⼚时就确定了, 不能修改. mac地址通常是唯⼀的(虚拟机中的mac地址不是真实的mac地
址, 可能会冲突; 也有些⽹卡⽀持⽤⼾配置mac地址).
windows>ipconfig /all
后⾯我们详细谈论数据链路层的时候,会谈 mac 帧协议,此处我们做⼀个了解即可

继续看,主机A是存在dst就是表示目的MAC地址的主机,此时你发送的信息全部主机都能看到但是只有目的主机能处理。
局域网就是一个碰撞域,发送数据的主机多了就会发生碰撞,发送的主机都要等一会儿,都要碰撞检测,碰撞避免,此时没有碰撞的主机就可以在这个时间进行处理数据了,所以碰撞域就是基于碰撞检测和碰撞避免的通信模式。
此时为什么联网的人多了就会卡呢??
这是因为此时数据碰撞的概率增加了。
数据碰撞的本质不就是往同一块区域写数据吗??
、 这不就是一个共享区吗??
碰撞避免不就是相当于一把锁吗??
不就每次只允许一个主机发送数据。
数据碰撞了就会卡在数据链路层,不会传给上层。

用户A给用户B发送了一个你好是怎么收到的?
是用户A通过应用层传给传输层,再给网络层,再给数据链路层,然后通过这些我们所谓的线发送过去数据对方的数据链路层收到信息,再给它的网络层给传输层,给应用层再给用户。

我们上面也说过每层都是存在协议的,我们都知道协议的本质就是结构体,那么这个结构体就是我们上面所说的各个层的报头,结构体拼接上我们的发的信息,然后对方的应用层应该和我们拥有一样的报头来解析这个内容。
这个报头的作用就是协议封装的,你比如我们买快递,他肯定是用快递盒子包装起来这个快递的,而不是直接给你这个东西,这个快递盒子就相当于我们的报头,快递单上的名字什么的信息就是我们的结构体中的变量。

我们把发送的内容叫做有效载荷,每层的报头就叫做本层的报头,其它内容是有效载荷。
每层都会封装。
怎么封装的呢??
本质就是开辟一块空间先把发送的消息放进去,然后消息前面再把结构体也就是我们的协议放过去即可。
你发送方封装完之后,再给对方的数据链路层,对方的数据链路层解包,每次都把自己的报头解包即可,解包每个层的叫做自底向上是数据帧,数据报,数据段和请求与报答。

我们发送方每次都是添加一个相应的报头,这个不就是入栈的过程吗,我们把这个封装的内容给接收方,每次接收方都是从最后一个入栈的开始拿,每次解包相应层的数据包,此时不久相当于入栈和出栈的过程吗,所以我们才叫做协议栈的。
共识:任何层协议都要解决两个问题a:将报头和有效载荷进行分离 b:把数据交付给上层哪一种协议的问题。
a叫做解包,b叫做分用,解包过程就是类似于我们快递盒子都是相同的,比如尺寸都是多大的,在这里就是占用的内存大小相同,此时就能找到对应层的数据包进行解包。
b问题的解决方案就是我们本层数据报中存在上一层协议的特定的数字来表示上层协议,所以就能交给上一层了。
重谈局域网通信

局域网通信时主机通信吗??
本质时协议栈之间的通信就是不同层之间的。
跨⽹络传输流程图
⽹络中的地址管理 - 认识IP地址
IP 协议有两个版本, IPv4 和 IPv6 . 我们整个的课程, 凡是提到IP协议, 没有特殊说明的, 默认都是
指 IPv4
IP 地址是在 IP 协议中, ⽤来标识⽹络中不同主机的地址;
对于 IPv4 来说, IP 地址是⼀个 4 字节, 32 位的整数;
我们通常也使⽤ "点分⼗进制" 的字符串表⽰ IP 地址, 例如 192.168.0.1 ; ⽤点分割的每⼀个
数字表⽰⼀个字节, 范围是 0 - 255 ;
跨⽹段的主机的数据传输. 数据从⼀台计算机到另⼀台计算机传输过程中要经过⼀个或多个路由器

那么IP地址和MAC地址有什么区别呢??
IP地址是一直不变的,MAC是一直变化的。
举个例子,唐僧取经,它是从东土大唐而来,去西天拜佛求经的,这个起始和目的都是一直不变的,这就是IP 地址,但是你中途路过的比如车迟国,女儿国,黑风岭什么的,此时你到达一个地址你的起始和下一站的目标就变了,但是一直还是朝着我们最终目标走的。这就是MAC地址。
MAC地址就是我们的中途的路径,我们的IP地址就是我们的目标地方。
同一块局域网的IP地址都是很像的同属于一个子网,我们打个比方,A:198.162.2.2,B:198.162.2.3,你就可以知道和我存在在一个局域网中,找的很快,但是如果你是172.168.2.9,此时你基本可以确定不属于同一个局域网。

看一下这个我们是如何到达我们的目的IP地址的呢??
首先就是在自己的局域网中先给每个主机都收到数据但是发现目的IP不是自己都抛弃数据,最后被路由器拿到,此时结果路由器解包拿到目的IP地址然后再次封装进入到我们新的局域网中去寻找我们的目的IP地址。

看一下这个图,我们从上层向下层封装的时候,到我们的数据链路层的时候就会把我们当前的MAC地址和我们下一个路由的地址给封装起来,其实真正的过程是数据链路层封装成帧的时候是需要先看快速查找这个目的IP地址是否在我们的局域网中的,如果在目的地址就是我们局域网中对应主机的MAC地址,如果不在的话,此时目的地址就是路由器了,此时因为不在你的局域网中,所以我要去其它局域网中找,如图就是不在,首先先发送给局域网中的所有主机,所有主机都抛弃了数据,此时进入到路由器,此时由于你数据链路层封装了一层,我要先解包看看目的IP地址到底在哪好知道往哪走,解包之后拿到目的地址就是解析到我们的网络层之后就直接再次封装,此时数据链路层封装成帧之后就有了新的MAC首地址和目的的MAC地址了,还是查找看看是否在次局域网中,在了不在还是上面的操作,路由器通常不是一次就能找到的,再次重复此过程直到找到即可。
所以在找的过程中我们每个主机拿到的网络层的IP地址都是相同的,所以我们的互联网是建立在IP网络的基础上的,ip协议是消除了我们底层局域网的差异,使得我们看到的都是同样的ip报文。如果没有IP地址你是无法实现跨局域网通信的,正是IP地址的存在使得我们不同局域网之间也是可以实现通信的。

这就是我们上面所讲的过程,简单看一下很简单。
Socket编程预备
1. 理解源IP地址和⽬的IP地址
IP 在⽹络中,⽤来标识主机的唯⼀性
注意:后⾯我们会讲 IP 的分类,后⾯会详细阐述 IP 的特点
但是这⾥要思考⼀个问题:数据传输到主机是⽬的吗?不是的。因为数据是给⼈⽤的。⽐如:聊天是⼈在聊天,下载是⼈在下载,浏览⽹⻚是⼈在浏览?
但是⼈是怎么看到聊天信息的呢?怎么执⾏下载任务呢?怎么浏览⽹⻚信息呢?通过启动的 qq,迅雷,浏览器。
⽽启动的 qq,迅雷,浏览器都是进程。换句话说,进程是⼈在系统中的代表,只要把数据给进程,⼈就相当于就拿到了数据。
所以:数据传输到主机不是⽬的,⽽是⼿段。到达主机内部,在交给主机内的进程,才是⽬的。
但是系统中,同时会存在⾮常多的进程,当数据到达⽬标主机之后,怎么转发给⽬标进程?这就要在⽹络的背景下,在系统中,标识主机的唯⼀性。
网络间通信的本质就是进程间通信。

怎么把数据交给特定的进程呢??
我们的进程有这么 多呢??
就要引入一个端口号的概念了。
2. 认识端⼝号
端⼝号( port )是传输层协议的内容.
端⼝号是⼀个 2 字节 16 位的整数;
端⼝号⽤来标识⼀个进程, 告诉操作系统, 当前的这个数据要交给哪⼀个进程来处理;
IP地址 + 端⼝号能够标识⽹络上的某⼀台主机的某⼀个进程;
⼀个端⼝号只能被⼀个进程占⽤.

进程都存在pid,为什么还要端口号呢??
为了减少耦合度,解耦合,因为如果我们的pid出现了问题,比如后面发展这个pid删除了不用了,你此时就要大改,但是如果你有端口号,即使pid出现问题,照样不影响我。
一个进程可以占用几个端口号啊??
可以的,但是一个端口号只能被一个进程占用。
我该如何理解,通过端口号找到进程呢?
通过端口号查哈希表找到。
端⼝号范围划分
0 - 1023 : 知名端⼝号, HTTP, FTP, SSH 等这些⼴为使⽤的应⽤层协议, 他们的端⼝号都
是固定的.
1024 - 65535 : 操作系统动态分配的端⼝号. 客⼾端程序的端⼝号, 就是由操作系统从这个范
围分配的.
理解 "端⼝号" 和 "进程ID"我们之前在学习系统编程的时候, 学习了 pid 表⽰唯⼀ 个进程; 此处我们的端⼝号也是唯⼀表⽰⼀个 进程.
那么这两者之间是怎样的关系?
10086例⼦
另外, ⼀个进程可以绑定多个端⼝号; 但是⼀个端⼝号不能被多个进程绑定;
进程 PID 属于系统概念,技术上也具有唯⼀性,确实可以⽤来标识唯⼀的⼀个进程,但是这样
做,会让系统进程管理和⽹络强耦合,实际设计的时候,并没有选择这样做。
理解源端⼝号和⽬的端⼝号
传输层协议( TCP 和 UDP )的数据段中有两个端⼝号, 分别叫做源端⼝号和⽬的端⼝号. 就是在描述 "数 据是谁发的, 要发给谁";
理解socket
综上, IP 地址⽤来标识互联⽹中唯⼀的⼀台主机, port ⽤来标识该主机上唯⼀的⼀个⽹络进程
IP+Port 就能表⽰互联⽹中唯⼀的⼀个进程
所以,通信的时候,本质是两个互联⽹进程代表⼈来进⾏通信,{srcIp,srcPort,dstIp,dstPort}
这样的4元组就能标识互联⽹中唯⼆的两个进程
所以,⽹络通信的本质,也是进程间通信
我们把 ip+port 叫做套接字 socket
3. 传输层的典型代表
如果我们了解了系统,也了解了⽹络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通
过⽹络协议栈进⾏通信,必定调⽤的是传输层提供的系统调⽤,来进⾏的⽹络通信

认识TCP协议
此处我们先对 TCP ( Transmission Control Protocol 传输控制协议)有⼀个直观的认识; 后
⾯我们再详细讨论TCP的⼀些细节问题.
传输层协议
有连接
可靠传输:报文丢失了就重传了,这也意味着要做更多工作,维护成本会更高
⾯向字节流:对方发的快递数我是不确定的,只能确定收到的快递数。
认识UDP协议
此处我们也是对 UDP ( User Datagram Protocol ⽤⼾数据报协议)有⼀个直观的认识; 后⾯再详
细讨论.
传输层协议
⽆连接
不可靠传输:报文丢失和缺失什么的传输出去我不管,做更少工作,维护成本更低。
⾯向数据报:收到的都是完整的,比如你收快递快递一定是一个或者多个,不可能发半个,也能确定对方发了几个快递我买了几个快递。
因为我们暂时还没有深⼊了解 tcp 、 udp 协议,此处只做了解即可
4. ⽹络字节序
我们已经知道,内存中的多字节数据相对于内存地址有⼤端和⼩端之分, 磁盘⽂件中的多字节数据相对于 ⽂件中的偏移地址也有⼤端⼩端之分, ⽹络数据流同样有⼤端⼩端之分. 那么如何定义⽹络数据流的地 址呢?
两种"高低"的区别
-
数据本身的高低 (位权)
- 这指的是数字本身的大小。就像数字
123,1代表一百,是高位 ;3代表三,是低位。 - 对于十六进制数
0x12345678,0x12是最高位字节,0x78是最低位字节。这个顺序是固定的,不会改变。
- 这指的是数字本身的大小。就像数字
-
内存地址的高低 (门牌号)
- 这指的是内存空间的编号。我们可以把内存想象成一排从左到右的门牌号。
- 左边是低地址 (例如门牌号
0x4000)。 - 右边是高地址 (例如门牌号
0x4003)。 - 这个顺序也是固定的,地址总是从低到高增长。
大小端要解决的问题,就是决定把数据的"高位"和"低位",分别放进哪个"门牌号"里。
数据从左到右是从高位到地位的,但是我们的内存从左到右是1~100,所以右边是高位,左边是低位。
想象内存是一排房间,房间号(地址)是从左到右递增的(001, 002, 003...)。
现在来了一个"大户人家"------数字 0x1234(这是一个十六进制数,占2个字节)。
- 高位字节(大头/大端) :
0x12(因为它代表数值大,像家族的族长)。 - 低位字节(小头/小端) :
0x34(因为它代表数值小,像家族的小弟)。
这两个字节要住进 001 和 002 号房间,有两种住法:
-
大端模式 (Big-Endian) :"族长住小号房间"
- 族长
0x12住进 001 号房(低地址)。 - 小弟
0x34住进 002 号房(高地址)。 - 特点 :这符合人类阅读习惯,从左到右看就是
12 34。
- 族长
-
小端模式 (Little-Endian) :"小弟住小号房间"
- 小弟
0x34住进 001 号房(低地址)。 - 族长
0x12住进 002 号房(高地址)。 - 特点 :这在内存里是反着存的,读出来是
34 12。
- 小弟
所以我们大端发送数据如果小端解析就会反过来了。
网络统一了规定就是必须通过大端通信。
为什么使用大端??
因为可读性是比较好的,比如我们发送数据0x1234abcd,根据下面的规则数据是从低地址到高地址发送的,大端就是高地址放数据的低地址就是最右边放cd依次类推发送的还是0x1234abcd可读性很好。
发送主机通常将发送缓冲区中的数据按内存地址从低到⾼的顺序发出;
接收主机把从⽹络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到⾼的顺序保存;•
因此,⽹络数据流的地址应这样规定: 先发出的数据是低地址,后发出的数据是⾼地址.
TCP/IP协议规定,⽹络数据流应采⽤⼤端字节序,即低地址⾼字节.
不管这台主机是⼤端机还是⼩端机, 都会按照这个TCP/IP规定的⽹络字节序来发送/接收数据;
如果当前发送主机是⼩端, 就需要先将数据转成⼤端; 否则就忽略, 直接发送即可;
为使⽹络程序具有可移植性,使同样的C代码在⼤端和⼩端计算机上编译后都能正常运⾏,可以调⽤以下 库函数做⽹络字节序和主机字节序的转换。

这些函数名很好记, h 表⽰ host , n 表⽰ network , l 表⽰ 32 位⻓整数, s 表⽰ 16 位短整
数。
•
例如 htonl 表⽰将 32 位的⻓整数从主机字节序转换为⽹络字节序,例如将IP地址转换后准备发
送。
•
如果主机是⼩端字节序,这些函数将参数做相应的⼤⼩端转换然后返回;
•
如果主机是⼤端字节序,这些函数不做转换,将参数原封不动地返回。
5. socket编程接⼝

你存在本地通信和网络通信,操作系统并没有各自给自己设置一份函数而是通用一份函数来实现,那么它如何区分自己是网络通信还是本地通信呢??
此时我们发现它们都存在前十六位地址代表不同的类型,只需要把十六位地址的内容提取出来即可区分。

我们可以发现这个struct sockaddr 是我们要使用的类型,那么我们如何通过这个相同的类型实现不同类型的通信呢??
答案是多态,这个结构体为基类,下面两个结构体都是子类了,此时就可以实现我们想要的了。
6.相关函数的认识
socket
这个函数的作用:给你的程序开一个「网络通信的接口 / 通道」 ,所有后续的网络发送、接收、连接、绑定等操作,都必须基于这个函数创建出来的套接字进行。
这是三个函数需要传递的参数。

返回值就是我们之前讲的fd就是文件描述符。
bind
将创建好的套接字,绑定到一个固定的「IP 地址 + 端口号」上

socket+bind的作用:
- socket () 做什么
创建一个空的套接字
-
只是向操作系统申请:我要一个能上网的通道
-
此时这个套接字没有 IP、没有端口、没人认识它
-
好比:买了一个空插座,啥都没接
- bind () 做什么
给这个套接字绑定 固定 IP + 固定端口
-
给套接字分配地址,固定下来
-
让外界(客户端)能精准找到你的程序
-
好比:给插座装上门牌 + 房间号,别人才能找到你
串联理解(服务器视角)
-
**socket()**开辟网络通信工具 → 有了干活的工具
-
**bind()**给工具绑定地址端口 → 定点站岗,等待别人连接
bzero

这个函数的两个参数分别是第一个可以传入任意类型。第二个则是要传入你这个数据的大小。

主要的作用是对这几个内部变量的赋值,就是先清空我们的结构体,然后给这几个变量进行赋值。
-
htons() :把端口号 变成网络能识别的格式
-
inet_addr() :把字符串 IP(如 "192.168.1.1")变成网络能识别的数字格式
二、htons () 到底干嘛?为什么要用?
作用
把主机字节序 → 转换成网络字节序 专门给端口号用。
为什么必须用?
因为:
-
电脑存储数字的顺序(主机序)
-
网络传输数字的顺序(网络序)
不一样!
如果不转,端口号会变成乱的,客户端根本连不上。
例子
你想绑定端口 8080必须写:
htons(8080)
三、inet_addr () 干嘛?为什么要用?
作用
把字符串格式的 IP → 转成 32 位整数格式 IP
"192.168.1.100" → 0xC0A80164(网络能识别)
因为:
-
人看得懂:
192.168.1.100(字符串) -
电脑 / 网络看不懂字符串,只看得懂数字
所以必须用 inet_addr() 转换。
-
htons() :给端口号做格式转换(电脑序 → 网络序)
-
inet_addr() :给IP 地址做格式转换(字符串 → 网络数字)
不使用这两个函数 → 服务器绑定失败 / 客户端连不上
local.sin_addr.s_addr = htonl(INADDR_ANY);这个代码什么作用呢??
**让服务器监听本机所有网卡(所有 IP)**也就是:
- WiFi 来的连接 → 收
- 网线来的连接 → 收
- 虚拟机网卡 → 收
- 本地回环 127.0.0.1 → 收
一句话:不管哪个 IP 找到我,我都接收。
- INADDR_ANY 是什么?
它就是 0.0.0.0 的常量意思:本机所有可用的 IP 地址
- 为什么要用 htonl ()?
因为:
-
INADDR_ANY 是一个 32 位整数
-
网络传输必须用 网络字节序(大端)
-
所以必须用 htonl () 转换
重点:htons vs htonl 区别
你一定要记住这个:
-
htons :给 端口(16 位) 用
-
htonl :给 IP 地址(32 位) 用
s 代表 short(16 位)l 代表 long(32 位)
看一下这几个类封装的。

这个我们使用的这个sockaddr_in内部就张这个样子,有端口号和我们的IP地址。

这个in_port_t就是一个无符号16位的整数。

我们使用的in_addr就是一个结构体这个结构体中封装了一个in_addr_t的变量,这个变量就是32位无符号整数类型的。
我们怎么没有看到sin_family呢??

其实就是我们结构体中的第一个参数,只不过被define了一下。
前面就是一个宏函数,它会自动在结构体中展开,这个##就表示的是拼接的意思。
sa_family_t sin_family宏展开,结构体中的第一行就变成了这个样子了,因为你传入了sin_它会自动拼接到后面,所以这就是我们的这个sin_family了。

这个sa_family_t类型就是我们的无符号短整数了。
recvfrom

它的核心作用是:从网络上接收数据,同时获取发送方的 IP 地址和端口号。
-
接收网络数据:从指定的 socket 缓冲区中读取对方发送过来的数据
-
获取发送方信息 :自动返回 发送数据的客户端 / 服务端的IP 地址 和端口号(UDP 必须靠它识别发送方,因为 UDP 无连接)
-
支持阻塞 / 非阻塞:可以设置等待数据(阻塞),或直接返回(非阻塞)
-
> 0 :成功接收到的字节数
-
= 0:连接关闭(TCP 场景,UDP 几乎不会出现)
-
= -1 :接收失败,可通过
errno查看错误原因
为什么 UDP 必须用 recvfrom?
-
TCP 是面向连接的 :一旦建立连接,通信双方固定,用
recv就够了 -
UDP 是无连接的 :谁都可以给你发数据,必须用
recvfrom获取发送方的 IP 和端口,才能知道数据是谁发的,也才能给对方回消息
下面来看一下它的几个参数。
int sockfd
作用:你要从哪个 "网络通道" 收数据
-
就是你用
socket()创建出来的那个文件描述符 -
相当于门牌号,告诉系统:我要从这个网络接口收消息
void *buf
作用:接收数据的 "篮子"
-
一块内存空间,用来存放收到的消息
-
你传一个数组 / 指针进去,收到的数据会直接写到这里
size_t len
作用:你的篮子最多能装多少字节
-
一般直接填
sizeof(buffer) -
告诉系统:别写超了,会溢出
int flags
作用:特殊接收模式(99% 情况直接填 0)
-
0 = 普通模式,阻塞等待数据到来
-
不用管其他复杂标志,新手永远写 0
struct sockaddr *src_addr
作用:存储【谁给我发的消息】
-
这是输出参数
-
调用完后,里面会自动填上发送方的 IP + 端口
-
你必须定义一个变量传进去,不然不知道消息是谁发的
socklen_t *addrlen
作用:告诉系统上面那个地址结构体有多大
-
这是输入输出参数
-
输入:结构体大小
-
输出:系统返回的实际大小
必须传地址 &

这是我们使用的一个实例,现在有一个问题,为什么第五个函数不直接使用struct sockaddr_in作为参数呢??
网络地址有很多种:
- IPv4 →
struct sockaddr_in - IPv6 →
struct sockaddr_in6 - 本地域 →
struct sockaddr_un
系统不可能为每种地址写一个 recvfrom 函数。
所以就设置了一个基类,传入的对象是我们的子类对象,这是类似于多态的一种方式。
定义为struct socketaddr类型的就是为了能传多种类型的网络地址的结构体,类似于我们C语言的多态的方式。
ntohs

ntohs = 把「网络字节序」转成「主机字节序」(专门用来转端口号)
-
网络发过来的端口号 → 看不懂(大端)
-
用
ntohs()一转 → 变成你电脑能看懂的正常数字(小端)
为什么要用它?
因为:
-
网络传输统一用:大端字节序
-
你的电脑大概率用:小端字节序
-
两者不兼容,端口号直接读会是乱的、颠倒的数字
所以必须用 ntohs 转换。
inet_ntoa

inet_ntoa 就是专门把二进制的 IP 地址 → 转成我们能看懂的字符串 IP (比如 192.168.1.100)。
它和 ntohs 是黄金搭档 ,在 recvfrom 之后必用!
这两个函数的作用不就是把网络中的看不懂的转化为我们机器能看懂的,和上面将的两个函数正好反过来了。
sendto

sendto 是 UDP 专用发送函数 ,和 recvfrom 是一对!
简单说:recvfrom 用来收数据 + 拿对方地址 sendto 用来发数据 + 指定发给谁
把数据发送给 指定 IP + 端口 的主机(UDP 必须用它)
因为 UDP 没有连接,所以每次发消息都必须告诉系统:发给谁?IP 多少?端口多少?
sockfd
你创建的 UDP socket 文件描述符(和 recvfrom 用同一个)
buf
你要发送的数据缓冲区
len
数据长度(一般 strlen (buf))
flags
永远填 0
dest_addr
发给谁? 传对方的 IP + 端口 (struct sockaddr_in)*传参时要强转成 (struct sockaddr)**
addrlen
地址长度:sizeof(struct sockaddr_in)
sendto 和 send 的区别
-
send:TCP 用(已经建立连接,不用指定地址) -
sendto:UDP 用(必须指定目标 IP + 端口)
-
> 0 ✅ 成功 返回的数字 = 实际收到多少字节
-
= 0 TCP 里表示连接关闭UDP 基本不会出现
-
= -1 ❌ 出错了比如网络断了、socket 被关闭等

哪些LOG相关的内容可以不看就是我们自己实现的日志打印功能。
这是我们服务端的,创建绑定,拿到客户端的IP和端口号,然后发送数据。

那么我们的客户端需不需要显示绑定呢??
答案是不需要的,因为我们系统会隐式的绑定。
为什么呢??
因为服务器的IP和端口号都是公开的,但是我们客户端的IP和端口号不能是一样的,因为你打开抖音和淘宝,如果是程序员手动的绑定端口号的话,可能抖音和淘宝的程序员设置的端口号都是一样的,此时你打开抖音和打开淘宝都是同一个端口号的话,此时就会出现问题了,因为端口号是唯一表示我们进程的。你这明显是两个进程啊,所以我们就要借助操作系统帮助我们生成一个端口号,此时这就不可能会冲突了。
介绍了这么多的函数,我们下面自己来设计一个聊天的功能吧。

这是我们的Udp_server.hpp文件,实现我们服务端所要用到的方法的。
首先来分析一下这个方法,首先头文件是我们自己通过查找得到的各个函数的头文件的,可以先看一下几个私有成员,第一个是存放我们socket函数创建出来的返回的文件描述符的,判断是否创建成功,然后我们创建了这个sockaddr_in它是我们sockaddr的一个子类,类似子类,它代表的是我们要通过什么通信的一种通信方式,我们上面也说过通信方式依然有很多种的,通过这个bzero函数初始化一下我们的这个结构体变量,然后给这个结构体中的几个变量赋值,要先给family赋值,调用的htons这几个函数我们上面也讲过作用,这里就不说了,此时我们就拿到了我们服务端的ip地址和端口号了,此时我们绑定一下这个文件描述符和ip和端口号,这样子我们初始化工作就完成了,此时我们完成了服务端的绑定工作。
下面的Start函数,进入循环之后,我们先创建了一个缓冲区,然后初始化一下继续定义我们的这个结构体变量,这个变量的作用是来获取我们客户端的ip和端口号的,方便我们再次给服务端发送回去数据,函数调用我们上面讲的也很详细,就不说了,这个recvfrom函数,此时就会拿到我们客户端给我们发送的数据了,放在了buffer中,然后我们把它打印出来,打印出来之后,继续把第n位置为0就是我们的/0吗,因为我们的n-1位此时都存放有数据,此时我们的peer结构体中也拿到了我们的客户端的ip地址和端口号等信息,然后我们定义一个变量,继续把我们接收到的信息继续发送回我们的客户端。此时就完成了这个操作。

大部分工作我们在上面完成了,这时候我们的服务端的主函数代码就很简单了,就是一个智能指针,创建这个结构体的对象,我们的ip地址和端口号都是通过命令行参数拿到的,然后传递给我们的这个UdpServer结构体初始化变量,其它的没啥了。

这个使我们的客户端的实现,首先我们也是通过传入命令行参数,然后我们拿到传入的ip地址和端口号,此时我们也是先创建我们通信的端口就是通过socket函数,然后不用我们显示绑定,原因我上面也说了,还是创建一个sockaddr_in结构体对象,它的作用就是拿到我们命令行传入的ip和端口号,然后传入到我们的sendto函数的,此时你发送的信息存在line里面,通过sendto函数发送给我们服务端的recvfrom函数收到信息,然后创建我们的这个temp的作用就是通过recvfrom函数继续拿到我们服务端sendto函数发送回来的消息放在不同的变量中,此时也能拿到我们服务端的ip和端口号。通过temp.sin_port的形式拿到。
此时我们运行一下。

这个127.0.0.1 8080这个ip和端口号是帮助我们测试使用的,此时我们就完成了这个功能,但是存在一个问题,就是我们一个主机可以由多个ip地址表示,如果我们的ip地址不同,即使我们的端口号相同也是无法连接上接收到消息的,这该怎么办呢??
我们只需要改一点点代码即可。

只需把绑定固定的ip改成我们任意ip即可,此时只要是你这个主机的ip都能连上这个服务端。

我们通过这个127.0.0.1 8080此时还能连上通信。
此时别人也能通过你的公网ip+端口号也能连接上你的服务器端。
以上都是UDP相关的一些函数和设计的代码。
TCP相关函数
socket

这些相关的函数这里不过多讲了,下面只讲一些和UDP不同的。
listen

这个函数的作用就是监听我们连接的到来。
主要看第二个参数,你可以理解位你允许几个在这等,比如设置4,就是允许五个排队。
accept
处于listen状态要调用这个函数连接,没有新连接就阻塞到这个accept函数这里。

最后两个参数类似于我们上面udp圈起来的那两个参数。
重点看一下这个返回值,成功了就返回文件描述符,难道会产生新的文件描述符来返回吗??
答案是是的。
下面就理解一下我们传入的文件描述符和我们返回的文件描述符 。
举个例子来理解一下吧,首先就是我们饭店现在都很内卷,所以门口一般都会占有拉客的人,这个拉客的人把客人拉进来之后,自己就会出去继续拉客,进来的人交给我们的其它服务员来服务,这里的拉客的人就相当于我们的listen的socketfd,就是我们传入的文件描述符,只有一个,而我们其它服务员就是我们返回的文件描述符,可以有多个,服务不同的人,这里就是服务器相当于饭店,这个传入的文件描述符相当于拉客的通过listen函数一直监听看着人,这个返回值的文件描述符就是服务员就是服务我们客户端的请求的。

所以即使我们拉客失败了小于0了,但是还是继续拉客,而不是直接结束。
客户端不需要listen因为客户端不需要别人连接它,也不需要显示bind,还是os去bind,什么时候绑定呢??
当你调用connect函数的时候。
connect

使用这个函数发送连接服务器的请求。
read


这是返回值和几个参数的使用,返回成功就是返回实际读取的字节数,另外两种情况如图。
下面我们也来实现一个服务端和客户端之间的网络通信tcp版的我们来区分一下区别。
InetAddr类

这个类的作用也是很简单的,就是把我们经常重复的操作,比如网络转主机,主机转网络这些重复的工作我们把它封装到一个类里面了,方便我们的使用,注意我们定义的那个宏的使用。

这个很简单就是我们枚举的几个错误的类型方便我们观察和使用。

这是我们实现的线程相关的函数,可能我们版本一实现的单进程的tcp通信用不上。

这个函数就非常重要了,我们来分析一下,首先就是我们定义的两个变量,一个是我们的listensockfd,我们上面也说过了它的特殊之处,就是这个listensockfd只有一个,我们的这个fd的作用就是告诉我们的accept函数去哪个端口号里面去检查是否有人连接。
构造和析构函数是必须有的,看一下即可,下面看一下这个初始化函数有什么不同,首先还是需要调用socket方法建立我们网络通信的管道,返回给我们的就是我们的listensockfd了,然后我们创建了这个InetAddr对象,此时我们调用这个构造的时候,因为我们此时的port拿到的都是主机认识的数字,我们需要转换为网络认识的
直接调用了上面的这个函数,帮我们省了再次书写的步骤,此时我们绑定了这个对象,此时多了一个步骤,因为tcp是有连接的,所以我们调用了listen函数来让内核时时刻刻检测到是否有连接过来,有了就下去调用accept函数,此时我们通过_listensockfd此时就能拿到客户端的ip地址和端口号信息,看下面的Start函数,就是调用了我们的accept函数,此时就能接收到我们客户端的请求了,此时我们通过_listensockfd这个文件描述符能拿到客户端要连接哪个端口号,此时我们的这个accept函数去哪个端口号里去接受连接,然后下面我们通过peer创建了我们的clientaddr对象,此时我们的这个clientaddr中就存放着我们客户端的一些信息。

调用这个函数把网络序号转化成我们主机能看懂的数字。
然后调用我们的IO方法,看IO方法。
和我们UDP调用resvfrom和sendto函数不同的是,这里调用的是read和write函数,我们的服务端肯定是先调用read来读取客户端的信息存放到我们的buff中,然后n>0,这个n代表我们读到的字节数,此时就代表我们读到了字节或是说数据,此时就把我们客户端想发送回客户端的内容写入到我们的字符串中,然后调用write函数发送回我们的客户端,此时就完成了这个操作了。

此时这是我们服务端的代码,很简单,看一下即可。

这是我们客户端的代码,还是系统自动帮我们绑定端口号和tcp一样,此时我们也是先创建网络通信管道,然后创建对象,需要注意的是我们需要调用connect函数连接我们的服务端,就这一个不同,其它的很简单,就是发送收信息即可,不需要accept,因为我们不需要接受谁的连接。
此时我们写完了一个简单的服务,但是此时这个服务是存在一个问题的,我们来看一下。

我们如果两台客户端同时连接我们的服务端的话,此时我们的服务端只能服务一个客户端,另外一个客户端无法响应,这是因为我们是单进程的,一次只能服务一个客户端,因为它们都有不同的sockfd,你传进来了一个用户的sockfd的话,只能服务该客户,另外一个客户虽然能连接上,但是收不到响应,我们的udp为什么可以接受多个客户端的访问呢??
这是因为我们的udp是无连接的,所有人用同一个sockfd,所有人都能使用,消息都能被响应。
如图所示,我们该怎么解决这个问题呢??
先说两种方案吧。
相信很容易想到使用进程或者是线程了。

可以使用多进程处理,但是有个问题,就是父进程需要一直等待子进程,此时你父进程还是无法去招待其它客人,还是需要等待子进程的,这不又是串行了吗??可以使用信号量忽略子进程给父进程发送的结束信息,把子进程交给内核处理结束,自动回收,但是我们不使用这个方案,这里我们使用如图方案,就是子进程创建完自己的子进程,自己就结束,此时你的父进程又不需要等待孙子进程。
此时我们就能完成我们的需求了。

大概实现的方案就是如图,就是我们创建子进程,然后让子进程创建孙子进程,子进程关闭自己不需要的_listensockfd防止非法进行一些操作,子进程就是需要拿到我们客户端的sockfd然后去进行服务的,一个子进程对应一个客户端去服务,此时孙子进程继承子进程去执行,而我们的父进程则是关闭我们不需要服务的客户端的sockfd,此时下次子进程继承的时候还是会拿到我们相同的文件描述符,此时就不用担心文件描述符不够用的情况了,父进程则是通过while循环继续回去监听等待,来了继续创建子进程给它服务。
继续来看下面的方法。


这个就是我们的多线程,这个结构体的作用就是为了让我们能拿到TcpEchoServer中的内容,因为我们的Routine这个函数是在结构体中的会存在一个this指针,这个指针的存在会导致我们调用函数老是失败,所以我们把它设置为了static的,但是又该面临一个问题,static不属于我们的结构体中的函数,我们该如何能调用到我们的这个IO操作呢??
此时这个ThreadData这个结构体就诞生了,很好的帮助我们解决了问题。
这个线程分离的作用就是我们的线程结束了不需要让主线程等我,我自己回收,此时就可以了。