计算机网络
1. OSI七层模型
1.1 模型介绍
| 名称 | 概述 |
|---|---|
| 物理层 | 底层数据传输,如网线;网卡标准。 |
| 数据链路层 | 定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。 |
| 网络层 | 定义IP编址,定义路由功能;如不同设备的数据转发。 |
| 传输层 | 端到端传输数据的基本功能;如TCP、UDP。 |
| 会话层 | 控制应用程序之间会话能力;如不同软件数据分发给不同软件。 |
| 表示层 | 数据格式标识,基本压缩加密功能。 |
| 应用层 | 各种应用软件;包括Web应用。 |

1.2 说明
- 物理层:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。
- 数据链路层:接收来自物理层的位流形式的数据,并封装成帧,传送到上一层
- 网络层:将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路径。
- 传输层:在源端与目的端之间提供可靠的透明数据传输。
- 会话层:负责在网络中的两节点之间建立、维持和终止通信。
- 表示层:处理用户信息的表示问题,数据的编码,压缩和解压缩,数据的加密和解密。
- 应用层:为用户的应用进程提供网络通信服务。
1.3 总结
- 传输层数据又被称作TCP报文段或UDP用户数据报(Segments)
- 网络层数据被称作包(Packages)
- 数据链路层的数据被称作帧(Frames)
- 物理层的数据被称作比特流(Bits)
- 网络七层模型是一个标准,而非实现
- 网络四层模型是一个实现的应用模型
2. 三次握手

3. 四次挥手

完整HTTP请求包括哪些内容
- 客户端域名解析
- TCP的3次握手
- 客户端发起HTTP请求
- 服务器响应HTTP请求
- 客户端浏览器接收得到HTML代码
- 浏览器解析HTML代码,并请求HTML代码中的资源(如js、css、图片)
- 浏览器对页面进行渲染呈现给用户
4. DNS的工作原理
- DNS将主机名转换为IP地址,属于应用层协议,使用UDP传输。
- 当用户输入域名时,浏览器先检查自己的缓存中是否包含这个域名映射的ip地址,1)有解析结束。 2)若没命中,则检查操作系统缓存(如Windows的hosts)中有没有解析过的结果,有解析结束。 3)若无命中,则请求本地域名服务器解析(LDNS)。 4)若LDNS没有命中就直接跳到根域名服务器请求解析。根域名服务器返回给LDNS一个 主域名服务器地址。 5)此时LDNS再发送请求给上一步返回的gTLD( 通用顶级域), 接受请求的gTLD查找并返回这个域名对应的Name Server的地址 。6)Name Server根据映射关系表找到目标ip,返回给LDNS。
5. RPC
5.1 总结
- 用于实现不同服务间的远程调用,如gRPC、Thrift和Dubbo。它涉及服务寻址、序列化和网络传输,简化了跨平台调用。RPC常用于大型网站的微服务架构,通过注册中心进行服务发现,并提供安全性和服务治理。

5.2 概念
- RPC(Remote Procedure Call Protocol) 远程过程调用协议。
- RPC是一种通过网络从远程计算机程序上请求服务,不需要了解底层网络技术的协议。
- RPC主要作用就是不同的服务间方法调用就像本地调用一样便捷。
5.3 常用RPC技术或框架
- 应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud。
- 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
- 通信框架:MINA 和 Netty
5.4 主流的gRPC、Thrift、Dubbo
- gRPC:gRPC是Google开源软件,gRPC是基于HTTP2.0协议,而HTTP2.0是基于二进制的HTTP协议升级版本,底层使用Netty框架支持。
- Thrift:Thrift是Facebook开源项目,其是一个跨语言的服务开发框架。用户只需在进行二开即可,对底层的RPC通讯透明。
- Dubbo:Dubbo是阿里开源组件协议和序列化框架都可以插拔,依托Spring框架开发,远程接口是基于Java接口,适用于微服务架构。
5.5 RPC作用
- 服务化:微服务化,跨平台的服务之间远程调用;
- 分布式系统架构:分布式服务跨机器进行远程调用;
- 服务可重用:开发一个公共能力服务,供多个服务远程调用。
- 系统间交互调用:两台服务器A、B,服务器A上的应用a需要调用服务器B上的应用b提供的方法,而应用a和应用b不在一个内存空间,不能直接调用,此时,需要通过网络传输来表达需要调用的语义及传输调用的数据。
5.6 架构

5.7 调用过程
具体调用过程: 1. 客户端(Client)通过本地调用的方式调用服务(以接口方式调用); 2. 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息进行组装序列化成能够进行网络传输的消息体(将消息体对象序列化为二进制流); 3. 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端(通过sockets发送消息); 4. 服务端存根(Server Stub)收到消息后进行反序列化操作,即解码(将二进制流反序列化为消息对象); 5. 服务端存根(Server Stub)通过解码结果调用本地的服务进行相关处理; 6. 服务端(Server)本地服务业务处理; 7. 服务端(Server)将处理结果返回给服务端存根; 8. 服务端存根(Server Stub)序列化处理结果(将结果消息对象序列化为二进制流); 9. 服务端存根(Server Stub)将序列化结果通过网络发送至客户端(通过sockets发送消息); 10. 客户端存根(Server Stub)接收到消息,进行反序列化解码(将结果二进制流反序列化为消息对象); 11. 客户端得到最终的结果。
5.8 核心功能
- 客户端:Client,服务调用方。
- 客户端存根:Client Stub,存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
- 服务端存根:Server Stub,接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
- 服务端:Server,服务的真正提供者。
- newtwork service:底层传输,tcp或http
5.9 功能实现
功能实现主要分为服务寻址、序列化和反序列化、网络传输功能。
5.9.1 服务寻址功能
- 本地:在本地方法调用中,函数体是直接通过函数指针来指定的,但是在远程调用中,由于两个进程的地址空间完全不一样,函数指针不起作用。
- 远程:RPC中所有函数或方法都有自己的一个ID,在所有进程中都唯一。客户端在做远程过程调用时,必须附上这个ID,即客户端会查一下表,找出相应的Call ID,然后传给服务端,服务端也会查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
- Call ID映射表一般是一个哈希表。
- 需要通过服务注册中心去查询对方服务有哪些实例。
5.9.2 序列化和反序列化功能
- 序列化:将消息对象转换为二进制流。
- 反序列化:将二进制流转换为消息对象。
- 远程调用涉及到数据的传输,在本地调用中,只需要将数据压入栈中,然后让函数去栈中读取即可。
- 但远程的数据传输,由于客户端和服务端不在同一个服务器上,涉及不同的进程,不能通过内存传递参数,此时就需要将客户端先将请求参数转成字节流(编码),传递给服务端,服务端再将字节流转为自己可读取格式(解码),这就是序列化和反序列化的过程。反之,服务端返回值也逆向经历序列化和反序列化到客户端。
- 序列化的优势:将消息对象转为二进制字节流,便于网络传输。可跨平台、跨语言。如Python编写的客户端请求序列化参数传输到Java编写的服务端进行反序列化。
5.9.3 网络传输功能
- 客户端将Call ID和序列化后的参数字节流传输给服务端。
- 服务端将序列化后的调用结果回传给客户端。
- 协议:主要有TCP、UDP、HTTP协议。
- 基于TCP协议
- 客户端和服务端建立Socket连接。
- 客户端通过Socket将需要调用的接口名称、方法名称及参数序列化后传递给服务端。
- 服务端反序列化后再利用反射调用对应的方法,将结果返回给客户端。
- 基于HTTP协议
- 客户端向服务端发送请求,如GET、POST、PUT、DELETE等请求。
- 服务端根据不同的请求参数和请求URL进行方法调用,返回JSON或者XML数据结果。
- TCP和HTTP对比:
- 基于TCP协议实现的RPC调用,由于是底层协议栈,更佳灵活的对协议字段进行定制,可减少网络开销,提高性能,实现更大的吞吐量和并发数。但,底层复杂,实现代价高。
- 基于HTTP协议实现的RPC调用,已封装实现序列化,但HTTP属于应用层协议,HTTP传输所占用的字节数比TCP更高,传输效率对比TCP较低。
6. mqtt
6.1 总结
- MQTT是一种轻量级的物联网通信协议,基于发布/订阅模式,支持QoS级别。它具有精简的协议设计,开放的消息协议,以及广泛应用于物联网、M2M通信、消息推送和智能设备等领域。MQTT协议涉及发布者、订阅者和消息代理(Broker)的角色,以及连接、订阅、发布消息的过程,并包含会话保持和心跳机制,确保消息的可靠传输。
- MQTT(Message Queuing Telemetry Transport, 消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为远程连接设备提供实时可靠的消息服务,作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用
- MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(loT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
6.2 MQTT特点
- 基于Publish/Subscribe(发布/订阅)模式的物联网通信协议
- 简单易实现
- 支持Qos(服务质量)
- 报文精简
- 基于TCP/IP
6.3 设计规范
- 精简,不添加可有可无的功能;
- 发布/订阅(Pub/Sub)模式,方便消息在传感器之间传递,解耦Client/Server模式,带来的好处在于不必预先知道对方的存在(ip/port), 不必同时运行
- 允许用户动态创建主题(不需要预先创建主题),零运维成本;
- 把传输量降到最低以提高传输效率
- 把低带宽、高延迟、不稳定的网络等因素考虑在内;
- 支持连续的会话保持和控制(心跳协议)
- 理解客户端计算能力可能很低
- 提供服务质量( quality of service level: QoS)管理:
- 不强求传输数据的类型与格式,保持灵活性(指的使应用层业务数据)
6.4 主要特性
- 开放消息协议,简单易实现。
- 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
- 对负载(协议携带的应用数据)内容屏蔽的消息传输。
- 基于TCP/IP网络连接,提供有序,无损,双向连接。主流的MQTT是基于TCP连接进行数据推送的,但是同样有基于UDP的版本,叫做MQTT-SN。这两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。由于基于不同的连接方式,优缺点自然也就各有不同了。
- 消息服务质量(QoS)支持,可靠传输保证;有三种消息发布服务质量:
- QoS0:“至多一次”,消息发布完全依赖底层TCP/IP网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。这一种方式主要普通APP的推送,倘若你的智能设备在消息推送时未联网,推送过去没收到,再次联网也就收不到了。
- QoS1:“至少—次”,确保消息到达,但消息重复可能会发生。
- QoS2:“只有一次”,确保消息到达一次。在一些要求比较严格的计费系统中,可以使用此级别。在计费系统中,消息重复或丢失会导致不正确的结果。这种最高质量的消息发布服务还可以用于即时通讯类的APP的推送,确保用户收到且只会收到一次。
- 1字节固定报头,2字节心跳报文,最小化传输开销和协议交换,有效减少网络流量。这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集,要知道嵌入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了。
- 在线状态感知:使用Last Will和Testament特性通知有关各方客户端异常中断的机制。
- Last Will:即遗言机制,用于通知同一主题下的其他设备,发送遗言的设备已经断开了连接。
- Testament:遗嘱机制,功能类似于Last Will。
6.5 主题订阅

6.5.1 发布订阅模式

客户端只需要订阅这个主题,当有其他客户端向这个服务端发布消息时,这个客户端就可以收到这个消息
6.5.2 请求响应模式

请求响应模式: 客户端向服务端发送请求,服务端收到请求后,向客户端返回响应
6.6 协议原理
6.6.1 实现方式
- 实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:
- 发布者(Publish)
- 代理(Broker)(服务器)
- 订阅者(Subscribe)
- 其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。

- MQTT传输的消息分为: 主题(Topic) 和 负载(payload)两部分:
- Topic: 可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload)\
- payload: 可以理解为消息的内容,是指订阅者具体要使用的内容
6.6.2 网络传输与应用消息
- MQTT会构建底层网络传输: 它将建立客户端到服务器的连接,提供两者之间的一个有序的、无损的、基于字节流的双向传输。
- 当应用数据通过MQTT网络发送时,MQTT会把与之相关的服务质量(QoS)和主题名(Topic)相关联
6.6.3 MQTT客户端
- 一个使用MQTT协议的应用程序或者设备,它总是建立到服务器的网络连接。客户端可以
- 发布其他客户端可能会订阅的信息
- 订阅其他客户端发布的消息
- 退定或删除应用程序的消息
- 断开与服务器的连接
6.6.4 MQTT服务器端
- MQTT服务器以称为"消息代理"(Broker), 可以是一个应用程序或一台设备,它是位于消息发布者和订阅者之间,它可以:
- 接受来自客户的网络连接
- 接受客户发布的应用信息
- 处理来自客户端的订阅和退订请求
- 向订阅的客户转发应用程序消息
6.6.5 发布/订阅、主题、会话
- MQTT是基于发布(Publish)/订阅(Subscribe)模式来进行通信及数据交换的,与HTTP的请求(Request)/**应答(Response)**的模式有本质的不同
- **订阅者(Subscriber)**会向 消息服务器(Broker)订阅一个主题(Topic)。成功订阅后,消息服务器会将该主题下的消息转发给所有订阅者
- 主题(Topic)以’/‘为分隔符区分不同的层级,包含通配符’+’ 或 ‘#’的主题又称为主题过滤器(Topic Filters); 不含通配符的成为主题名(Topic Names) 例如:
sensor/10/temperature
sensor/+/temperature
$SYS/broker/metrics/packets/received
$SYS/broker/metrics/#
'+' : 表示通配一个层级, 例如a/+,匹配a/x, a/y
- 发布者(Publisher)只能向主题名发布消息,订阅者(Subscriber)则可以通过订阅主题过滤器来通配多个主题名称
- 会话(Session) :每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
6.6.6 MQTT协议中的方法
- MQTT协议中定义了一些方法(也被称为动作),表示对确定资源进行操作。这个资源可以代表预先存在的数据或动态生成数据,这取决于服务器的实现。通常来说,资源指服务器上的文件或输出的主要方法有:
- CONNECT: 客户端连接到服务器
- CONNACK: 连接确认
- PUBLISH: 发布消息
- PUBACK: 发布消息确认
- PUBREC: 发布的消息已接收
- PUBREL: 发布的消息已释放
- PUBCOMP: 发布完成
- SUBSCRIBE: 订阅请求
- SUBACK: 订阅确认
- UNSUBSCRIBE: 取消订阅
- UNSUBACK: 取消订阅确认
- PINGREQ: 客户端发送心跳
- PINGRESP: 服务端心跳响应
- DISCONNECT: 断开连接
- AUTH: 认证
6.7 数据包结构
- 在MQTT协议中,一个MQTT数据包由: 固定头(Fixed header)、可变头(Variable header)、消息体(payload)三部分构成。
- 固定头(Fixed header)。存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识,如连接,发布,订阅,心跳等。其中固定头是必须的,所有类型的MQTT协议中,都必须包含固定头。
- 可变头(Variable header)。存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容。可变头部不是可选的意思,而是指这部分在有些协议类型中存在,在有些协议中不存在。
- 消息体(Payload)。存在于部分MQTT数据包中,表示客户端收到的具体内容。与可变头一样,在有些协议类型中有消息内容,有些协议类型中没有消息内容。


6.7.1 固定头

固定头存在于所有MQTT数据包中,固定头包含两部分内容:
- 首字节(字节1)
- 剩余消息报文长度(从第二个字节开始,长度为1-4字节)。
数据包类型: 第一个字节(Byte 1)中的7-4个bit位(Bit[7-4]),标识4位无符号值

标志位: 第一个字节中的0-3个bit位(Bit[3-0])。字节位Bit[3-0]用作报文的标识。

其中Bit[3]为DUP字段,如果该值为1,表明这个数据包是一条重复的消息;否则该数据包就是第一次发布的消息
Bit[2-1]为QoS字段:
- 如果Bit 1 和 Bit 2都为0,表示QoS 0: 至多一次;
- 如果Bit 1为1, 表示QoS 1: 至少一次;
- 如果Bit 2为1,表示QoS 2:只有一次;
- 如果同时将Bit 1 和 Bit 2都设置成1,那么客户端或服务器认为这是一条非法的消息,会关闭当前连接。
QoS: 服务质量是指 客户端和服务端之间的服务质量
MQTT消息的QoS :MQTT发布消息服务质量保证(QoS)不是端到端的,是客户端与服务端之间的。订阅者收到MQTT消息的QoS级别,最终取决于发布消息的QoS和主题订阅的QoS

QoS消息订阅(至多一次):

QoS1消息发布订阅(至少一次)

QoS2消息发布订阅(只有一次)

6.7.2 可变头
- 可变头的意思是可变化的消息头部。有些报文类型包含可变头部有些报文则不包含。可变头部在固定头部和消息内容之间,其内容根据报文类型不同而不同
6.7.3 消息体
- 有些报文类型是包含Payload(消息载体),如PUBLISH的Payload就是指消息内容(应用程序发布的消息内容)。而CONNECT的Payload则包含Client Identifier, Will Topic, Will Message, Username, Password等信息。
- Payload只在某些报文类型中出现,其内容和格式也根据报文类型不同而不同

7. 网络连接
7.1 基本定义
- 四个参数:IP,子网掩码,默认网关,DNS
- IP地址:一种逻辑地址,用来标识网络中的一个主机。
- IP地址=网络地址+主机地址
- IP地址是一个4*8bit的数字串(IPv4协议)
- 子网掩码NETMASK:功能是将IP分为网络地址和主机地址
- 子网掩码可以用来判断两台电脑是否处于同一子网内
- 默认网关
- 连接两个不同的网络的设备都可以叫做网关设备
- 网关的作用就是实现两个网络之间的通讯与控制
- 网关地址就是网关设备的IP地址
- DNS域名服务器
- DNS是域名服务器,用来解析域名(域名和IP之间的解析)
- 如果没有DNS登录网站时就必须输入网址的IP地址,有了DNS就可以输入网址
- 0和1和255一般不做普通ip用,.0是给子网的,.1一般是给网关用,.255是广播地址。
[!NOTE] IP,子网掩码,子网ip,网关地址,广播地址,DNS
7.2 网络分类
根据网络覆盖范围主要分为3类:
- 局域网(Local Area Network,LAN)是指范围在几百米到十几公里内办公楼群或校园内的计算机相互连接所构成的计算机网络。
- 城域网(Metropolitan Area Network,MAN)所采用的技术基本上与局域网相类似,只是规模上要大一些。城域网既可以覆盖相距不远的几栋办公楼,也可以覆盖一个城。
- 广域网(Wide Area Network,WAN)通常跨接很大的物理范围,如一个国家。
网络还可以按照所有者分为公网、私网这两种Internet的接入方式。
公网接入方式:上网的计算机得到的IP地址是Internet上的非保留地址,公网的计算机和Internet上的其他计算机可随意互相访问。私网则反之。
IP是英文Internet Protocol的缩写,意思是“网络之间互连的协议”,也就是为计算机网络相互连接进行通信而设计的协议。
IP地址类型分为:公有地址、私有地址
公有地址(Public address):由Inter NIC(Internet Network Information Center因特网信息中心)负责。这些IP地址分配给注册并向Inter NIC提出申请的组织机构。通过它直接访问因特网。
| 类别 | 最大网络数 | IP地址范围 | 最大主机数 | 私有IP地址范围 |
|---|---|---|---|---|
| A | 126(2^7-2) | 1.0.0.0-127.255.255.255 | 16777214 | 10.0.0.0-10.255.255.255 |
| B | 16384(2^14) | 128.0.0.0-191.255.255.255 | 65534 | 172.16.0.0-172.31.255.255 |
| C | 2097152(2^21) | 192.0.0.0-223.255.255.255 | 254 | 192.168.0.0-192.168.255.255 |
- 私有地址:私有地址(Private address)属于非注册地址,专门为组织机构内部使用。以下列出留用的内部私有地址:
7.3 网络设备
网卡是一个网络组件,属于硬件范畴,主要负责计算机之间数据的封装和解封。
MAC地址:网卡的物理地址,网卡设备的编号,默认情况是全球唯一的(16进制)。
MAC与IP对比
- 长度不同。IP地址为32位,MAC地址为48位。
- 分配依据不同。
- 网络寻址方式不同。OSI参考模型,ip地址是基于第三层工作(网络层),mac地址是第二层(数据链路层)
网线是连接局域网必不可少的。在局域网中常见的网线主要有双绞线(RJ45接口)、铜轴电缆、光缆三种。
交换机(Switch)意为“开关”,是一种用于电(光)信号转发的网络设备,交换机它可以为接入交换机的任意两个网络节点提供独享的电信号通路。
路由器(Router)又称网关设备(Gateway)是用于连接多个逻辑上分开、相对独立的网络。
7.4 网络拓扑结构图
结构类型:
- 星型拓扑结构
- 总线型拓扑结构
- 环型拓扑结构
- 树型拓扑结构
- 网络拓扑结构
- 混合型拓扑结构
- 蜂窝型结构
8. ARP
- ARP:地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取(MAC)物理地址的协议。

[!note] 当一个主机发送数据时,首先查看本机MAC地址缓存中有没有目标主机的MAC地址, 如果有就使用缓存中的结果;如果没有,ARP协议就会发出一个广播包,该广播包要求查询目标主机IP地址对应的MAC地址,拥有该IP地址的主机会发出回应,回应中包括了目标主机的MAC地址,这样发送方就得到了目标主机的MAC地址。如果目标主机不在本地子网中,则ARP解析到的MAC地址是默认网关的MAC地址。
9. 加密算法
9.1 不可逆加密算法
- 可以通过数据计算加密后的结果,但是通过结果是无法计算出原加密数据
- 应用场景
- Hash算法用在不可还原的密码存储、信息完整性校验。
- 音视频文件、软件安装包对比新旧版本是否一样。
- 用户名和密码加密后数据库的存储(密码不能找回,只能重置)。
9.2 对称加密算法

- 加密和解密使用相同的密钥
- 生成密钥的算法公开,计算量小,加密速度快,加密效率高,密钥较短。
- 双方共同拥有同一套密钥,有一方被窃取密钥,双方都受影响
- 如果为每个客户都生成不同的密钥,则密钥的数量巨大,密钥的管理就有压力
- 应用场景
- 代表算法:DES,3DES,AES,RC4,RC5
- 登录信息用户名和密码加密,传输加密、指令加密
9.3 非对称加密算法

- 需要一对密钥(2个密钥),分别是公开密钥(publickey,公钥)和私有密钥(privatekey,私钥)
- 安全系数高,一般长度大的是私钥,长度小的是公钥
- 加密和解密速度相对略慢,密钥长,计算量大,效率略低
- 想收到谁发来的消息,把公钥给谁;私钥要自己揣着。
- 应用场景
- 代表算法:RSA、ECC、DSA、EI Gamal
- HTTPS(SSL)证书制作、CRS请求证书、金融通信加密、蓝牙等硬件信息加密配对传输
10. Linux Socket
10.1 服务端TCP
10.1.1 服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
const char *response = "Hello from server";
// 1. 创建 TCP Socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置地址结构
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 3. 绑定Socket
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 4. 监听连接,最大等待队列长度3
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 5. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
printf("Client connected\n");
// 6. 读取客户端数据
read(new_socket, buffer, BUFFER_SIZE);
printf("Received from client: %s\n", buffer);
// 7. 向客户端发送响应
send(new_socket, response, strlen(response), 0);
printf("Response sent\n");
// 8. 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
10.1.2 函数说明
unistd.h:Unix系统调用(如 read, write, close)。注意:这是POSIX标准,在Windows上不原生支持。
sys/socket.h 和 netinet/in.h:网络编程相关的套接字API(如 socket, bind, listen 等)。同样,这些是Unix/Linux的头文件。
server_fd = socket(AF_INET, SOCK_STREAM, 0)中AF_INET:使用IPv4地址族。SOCK_STREAM:使用TCP协议(流式套接字)。0:协议类型,0表示自动选择(对于TCP就是IPPROTO_TCP)。
bind() 系统调用的作用是为套接字分配一个本地地址(IP + Port)。当你指定一个具体的IP地址(如 192.168.1.100)时,你是在说:“我只接受发往这个特定IP的连接”。当你使用 INADDR_ANY (0.0.0.0) 时,你是在说:“我接受发往这台机器任何一个IP地址的连接,只要端口是8080”。
close() 是 POSIX 标准定义的一个系统调用 (system call)。来自于#include <unistd.h>。它不仅可以关闭文件,还可以关闭套接字 (socket)、管道 (pipe)、设备等任何通过 open()、socket()、pipe() 等系统调用创建的资源(Linux一切皆文件)。close() 是一个通用的资源释放函数,而 socket 只是它能操作的一种资源类型。
sin_addr 是 struct sockaddr_in 中的一个成员,它的类型是 struct in_addr。所以,sin_addr 本身是一个结构体,而 sin_addr.s_addr 才是真正的32位IP地址整数。
10.1.3 监听Socket
- 服务器监听套接字 (server_fd)
- 创建方式:server_fd = socket(AF_INET, SOCK_STREAM, 0);这个套接字是由 socket() 系统调用直接创建的。
- 作用:它是一个监听套接字 (listening socket)。它的唯一职责是监听指定的IP地址和端口(在您的代码中是 0.0.0.0:8080)上的连接请求。它本身不用于发送或接收应用数据。 生命周期:在服务器启动时创建。调用 bind() 绑定到地址和端口。调用 listen() 进入监听状态。它会持续存在,等待并接受多个客户端的连接请求
10.1.4 通信Socket
- 客户端通信套接字 (new_socket) 创建方式:new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);这个套接字是由 accept() 系统调用返回的。accept() 的参数 server_fd 正是那个监听套接字。
- 作用:它是一个已连接套接字 (connected socket) 或通信套接字。它代表了一个具体的、已经建立的 TCP 连接。它用于与特定的客户端进行双向数据通信。read() 和 send() 都是操作这个 new_socket,用来收发实际的应用数据。
- 生命周期:当一个新的客户端发起连接请求(connect())时,accept() 被唤醒,并为这个新的连接创建一个新的 new_socket。服务器可以为每个连接的客户端创建一个独立的 new_socket。当与该客户端的通信结束后,服务器调用 close(new_socket) 来关闭这个特定的连接,而不影响监听套接字 server_fd 继续监听其他连接。
10.1.5 建立连接
- 从服务器的连接请求等待队列中,取出一个已经完成三次握手的客户端连接请求,并为这个新连接创建一个新的套接字(socket),以便服务器可以与该客户端进行独立的数据通信。简单来说,它完成了“从监听到建立连接”的最后一步。
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 是一个阻塞调用(除非套接字被设置为非阻塞模式)。如果当前没有客户端连接请求,accept() 会一直等待,直到有客户端成功连接(即完成TCP三次握手)。一旦有连接到来,accept() 就会返回。
- 创建新套接字:它不会使用原来的监听套接字(server_fd)来收发数据。而是创建并返回一个全新的套接字(new_socket),专门用于与这个特定的客户端通信。原来的监听套接字(server_fd)则继续回到监听状态,等待下一个连接。
- 完成连接建立:当客户端调用 connect() 发起连接时,TCP 三次握手在内核层面完成。握手成功后,这个连接会被放入服务器的“已完成连接队列”(completed connection queue)。accept() 就是从这个队列中取出一个连接。
- accept() 会覆盖 address 结构体中的 sin_port 和 sin_addr,将其设置为客户端的端口和IP。原始的服务器地址信息确实会丢失,因为 address 被重用作输出缓冲区。但是这不是问题,因为服务器地址是程序自己配置的,应该由程序自己记住。accept() 的设计目的就是获取客户端信息,而不是维护服务器信息。
10.1.6 内核socket
阶段 1:服务器调用 listen()
- listen(server_fd, 5);这是一个系统调用,会从用户空间陷入内核空间。
- 内核根据 server_fd 找到对应的内核套接字结构。
- 内核将该套接字状态设置为 LISTEN。
- 内核在内核的 TCP 协议栈中注册:“端口 8080 现在有进程在监听”。
- 内核创建半连接队列和全连接队列。
- 此时,内核已经准备好处理发往 8080 端口的任何 TCP 包。
阶段 2:客户端发送 SYN 包
- 客户端内核发送一个 SYN 包到 服务器IP:8080。
- 服务器网卡收到这个数据包。
- 网络驱动程序将数据包交给内核的 IP 层,再交给 TCP 层。
- TCP 层查找:有没有进程在监听 8080 端口?
- 找到:是的!有一个套接字状态为 LISTEN。
- 内核 TCP 协议栈自动响应:将这个连接放入半连接队列。构造一个 SYN-ACK 包。通过网卡发送回客户端。
阶段 3:客户端回复 ACK 包
- 客户端发送 ACK 包。
- 服务器内核收到 ACK。
- TCP 层再次查找:8080 端口的监听套接字。
- 找到半连接队列中的条目。
- 内核 TCP 协议栈自动处理:将连接从半连接队列移到全连接队列。连接状态变为 ESTABLISHED。
- 此时,三次握手完成。整个过程服务器程序(用户空间)毫不知情。
阶段 4:accept() 取走连接
- 程序调用 accept()。
- 系统调用进入内核。
- 内核检查全连接队列。
- 发现有连接!
- 内核:从队列中取出连接。为这个连接创建一个新的内核套接字结构(用于数据传输)。返回一个新的文件描述符(new_socket)给程序。
数据包收发需要“socket”,但不是程序代码中的socket,是内核的socket。
10.1.7客户端连接不变
客户端完全不会察觉服务器内部创建了一个新的 socket 文件描述符。对客户端来说,连接始终是同一个,因为它只关心 TCP 连接的“四元组”(源IP、源端口、目的IP、目的端口),而这个四元组在整个通信过程中从未改变。
四元组未变:
- 客户端看到的始终是 (自己的IP:端口 -> 服务器IP:8080)。
- 服务器 IP 和端口没变,客户端 IP 和端口也没变。
- 所以对客户端 TCP 栈来说,这是同一个连接。
数据包内容不变:
- 当通过 new_socket 调用 send() 发送数据时:数据进入内核。内核根据 new_socket 找到对应的 struct sock。内核使用该 struct sock 中的连接信息(序列号、确认号等)封装 TCP 包。发送出去的包仍然是 (服务器IP:8080 -> 客户端IP:54321)。
- 客户端收到的包与握手完成后收到的第一个包没有任何区别。
TCP 状态机连续:
- 从客户端视角,连接状态从 SYN_SENT → ESTABLISHED,然后一直保持 ESTABLISHED。
- 服务器内部如何管理这个连接(用哪个文件描述符),对客户端状态机完全没有影响。
过程分解:
- 三次握手完成(内核层面):客户端发送 SYN。服务器内核回复 SYN-ACK。客户端回复 ACK。
- 此时,TCP 连接已在内核层面建立,状态为 ESTABLISHED。
- 内核已经为这个连接维护了一个完整的 struct sock(内核套接字结构),包含了序列号、窗口大小、缓冲区等所有状态。
- accept() 被调用:程序调用 accept()。系统调用进入内核。
- 内核从全连接队列中取出那个已经存在的连接。即:找到该连接对应的内核 struct sock。创建一个新的文件描述符(如 new_socket = 4)。将这个文件描述符与已有的内核 struct sock 关联起来。
- 返回 new_socket 给您的程序。
accept() 并没有“创建一个新的网络连接”,它只是为已经存在的、已完成握手的连接,在用户空间创建了一个用于通信的句柄。
accept() 返回的 new_socket 只是一个用户空间的整数句柄,它指向的是内核中早已存在的、代表该连接的 struct sock。内核中的连接实体并没有“新建”,只是被“移交”给了一个可用于读写的文件描述符。
10.1.8 并发
上面的循环是串行处理:必须处理完一个客户端,才能接受下一个。如果想同时处理多个客户端(并发),可以用多进程/多线程/IO复用
10.1.9 多进程改进
while(1) {
new_socket = accept(server_fd, ...); // 取出一个连接
pid_t pid = fork();
if (pid == 0) {
// 子进程代码空间
close(server_fd); // 子进程不需要监听套接字
// 处理客户端请求(可能耗时)
char buffer[1024];
read(new_socket, buffer, 1024);
send(new_socket, "Hello", 5, 0);
close(new_socket);
exit(0); // 子进程结束
}
else if (pid > 0) {
// 父进程代码空间
close(new_socket); // 父进程关闭已交给子进程的连接描述符
// 继续 while 循环,accept 下一个
}
else {
perror("fork failed");
}
}
- fork() 一次调用后,父进程和子进程都会从该点继续执行。系统会给父进程返回子进程的进程号(正整数),给子进程返回 0。通过 if (pid == 0) 可以让子进程执行处理客户端请求的代码,而父进程则回到 accept() 继续监听新的连接,从而实现并发处理。
10.1.10 多线程改进
#include <pthread.h>
void *handle_client(void *arg) {
int new_socket = *(int*)arg;
free(arg); // 释放动态分配的内存
// 处理请求
char buffer[1024];
read(new_socket, buffer, 1024);
send(new_socket, "Hello", 5, 0);
close(new_socket);
pthread_exit(NULL);
}
int main() {
// ... socket, bind, listen ...
while(1) {
int *new_sock_ptr = malloc(sizeof(int)); // 必须动态分配
*new_sock_ptr = accept(server_fd, ...);
pthread_t thread_id;
if (pthread_create(&thread_id, NULL, handle_client, (void*)new_sock_ptr) != 0) {
perror("pthread_create failed");
free(new_sock_ptr);
}
// 分离线程,避免僵尸线程
pthread_detach(thread_id);
}
}
- 传递给线程的 new_socket 必须动态分配( malloc ),否则主线程修改时,线程可能读到错误值。
// 错误示例:不要这样做!
while (1) {
int new_socket = accept(server_fd, ...);
pthread_create(&tid, NULL, handle_client, &new_socket); // 传递局部变量地址
}
- 主线程的 new_socket 是一个局部变量,存储在主线程的栈上。当主线程循环到下一次 accept 时,会复用同一块栈内存,导致 new_socket 的值被覆盖。而此时,子线程可能还在使用这个地址读取 new_socket 的值,就会读到已经被主线程修改的新值,而不是创建线程时的旧值,从而引发错误。
10.1.11 多路复用改进
#include <sys/epoll.h>
#define MAX_EVENTS 64
int epfd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
// 将监听套接字加入 epoll
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
while(1) {
// 阻塞等待事件
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
// 新连接到来
int new_socket;
while ((new_socket = accept(server_fd, NULL, NULL)) > 0) {
// 将新连接也加入 epoll 监听(边缘触发 ET 模式)
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = new_socket;
epoll_ctl(epfd, EPOLL_CTL_ADD, new_socket, &ev);
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 正常情况:连接队列已空
} else {
perror("accept");
}
}
else {
// 已有连接有数据可读
int sockfd = events[i].data.fd;
char buffer[1024];
int n = read(sockfd, buffer, sizeof(buffer));
if (n <= 0) {
// 连接关闭或错误
close(sockfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
} else {
// 处理数据(如 echo)
send(sockfd, buffer, n, 0);
}
}
}
}
多路复用的本质是“把多条数据流合并到一条物理信道上传输,接收端再把它们拆开”,这样就能用更少的硬件资源同时服务更多用户。核心思路可以拆成三步。为了防止各路信号互相干扰,系统会给每路信号分配互不重叠的资源——要么是不同的频率(FDM)、不同的时间片(TDM)、不同的波长(WDM),或者不同的空间路径(SDM)。
- 发端把多路信号“打包”:把每个用户的信号按某种规则(时间、频率、波长等)切成小片,然后把这些小片拼成一条高速流。
- 在一条线路上传:这条线路可以是铜线、光纤或无线频段,只要带宽足够,就能把打包后的信号一起发出去。收端把信号“拆包”
- 按发端约定的规则,把收到的高速流重新还原成原来的多路信号,送给对应的用户。
此处本质是让内核帮你同时监视多个文件描述符,一旦有就绪的就立刻通知你,这样你就可以用一个线程处理成百上千个连接,而不用为每个连接都创建线程或进程。
核心思想
- 问题:传统方式中,如果要同时处理多个客户端连接,通常需要为每个连接创建一个线程或进程。当连接数很多时,系统资源消耗巨大,效率低下。
- 解决方案:多路复用技术允许一个线程同时监视多个文件描述符(如套接字),一旦某个文件描述符就绪(可以读或写),就立即通知应用程序进行处理。
实现机制
- select:将文件描述符集合传递给内核,内核轮询检查每个文件描述符是否就绪。select 使用固定大小的位图来表示文件描述符集合,因此有最大文件描述符数量的限制(通常是 1024)。每次调用 select 都需要将整个集合从用户空间复制到内核空间,效率较低。
- poll:与 select 类似,但使用动态数组来表示文件描述符集合,没有最大文件描述符数量的限制。同样需要将整个集合从用户空间复制到内核空间。
- epoll(Linux 特有):使用事件驱动的方式,内核维护一个就绪事件的链表,只将就绪的事件返回给应用程序,避免了轮询和复制整个集合的开销。epoll 支持水平触发和边缘触发两种模式,效率更高。
工作流程
- 注册:应用程序将需要监视的文件描述符及其感兴趣的事件(如读就绪、写就绪)注册到内核的多路复用器中。
- 等待:应用程序调用多路复用系统调用(如 select、poll、epoll_wait),进入阻塞状态,等待内核通知。
- 通知:内核监视所有注册的文件描述符,一旦有文件描述符就绪,就将就绪的文件描述符及其事件返回给应用程序。
- 处理:应用程序根据返回的就绪事件,对相应的文件描述符进行读写操作,然后继续等待下一次通知。
当一个监听 socket(server_fd)变得“可读”时,表示:有新的客户端连接请求到达了!也就是说,“可读”在这里不是指收到了数据,而是“可以 accept 了”。
因为 Linux 把 网络连接、文件、socket 都抽象成“文件描述符”(file descriptor),并统一用“可读/可写”来表示事件。
- 普通文件:可读 = 有数据可以读取。
- 监听 socket:可读 = 有新连接可以 accept。
- 已连接 socket:可读 = 客户端发来了数据。
每次调用 epoll_wait 时,内核会把“哪些 socket 就绪了”写进这个数组。旧内容会被完全覆盖。
10.2 客户端TCP
10.2.1 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from client";
// 1. 创建 TCP Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("invalid address");
exit(EXIT_FAILURE);
}
// 3. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
// 4. 向服务器发送数据
send(sock, message, strlen(message), 0);
printf("Message sent\n");
// 5. 读取服务器响应
read(sock, buffer, BUFFER_SIZE);
printf("Received from server: %s\n", buffer);
// 6. 关闭连接
close(sock);
return 0;
}
10.2.2 函数说明
- inet_pton(int af, const char *src, void *dst): 把“人类可读的文本格式”IP 地址(如 "127.0.0.1")转换成“机器能用的二进制格式”。不能这样写serv_addr.sin_addr.s_addr = 127.0.0.1; // 编译都通不过!127.0.0.1 是一个“点分十进制表示法”,它是给人看的,不是 C 语言的合法整数常量。htonl(INADDR_ANY) 是规范的服务端写法。