把OSI参考模型彻底看懂 ── 拆解一个HTTP请求里的七层结构
· 小村 豪 · OSI参考模型, TCP/IP, 网络, Wireshark, Ethernet, TCP, HTTP, C#, .NET, Socket, 技术咨询
OSI参考模型几乎是每一本网络入门书都会最先介绍的内容,但经常能听到「七层的名字都能说出来,但说实话完全没有画面感」这样的心声。物理层、数据链路层、网络层……即便靠死记硬背记住了,往往还是搞不清楚自己写的C#代码,或者眼前发生的通信故障,到底要怎么和那张七层图对应起来。
本博客此前写过像是误以为TCP中Send的每个单位都能对应一次Receive、TCP重传导致工业相机通信中断的原因与排查等探讨L4(传输层)行为的文章,但一直没有一篇文章专门梳理这些内容背后的基础──「层」这个概念本身。
本文的做法很简单:实际组装出一个承载HTTP GET请求的Ethernet帧,从外层开始依次解剖。 七层并不只是概念图上的东西,而是真实地、物理地嵌套在网络上流动的那171个字节之中。只要亲眼通过十六进制转储和Wireshark看过一次,OSI参考模型就不再是需要死记硬背的对象了。
本文中出现的代码,已整理成一套可构建、可运行的示例(帧的组装与解剖库、导出可用Wireshark打开的pcap文件、单元测试),公开在GitHub上。
osi-model-packet-anatomy - komurasoft-blog-samples (GitHub)
1. 先讲结论
- OSI参考模型不是「实现」,而是一种「词汇」。 现实中运行的互联网是TCP/IP协议族(实质上只有四层),OSI的七层协议本身在普及竞争中败下阵来,并没有被真正投入使用。留存下来的,是「用L2来划分问题」「这是L7的问题」这种作为通用语言的模型。12
- 七层是以字节序列的形式物理性地层层嵌套在一起的。 以承载HTTP GET的帧为例,开头14个字节是Ethernet头(L2),接下来20个字节是IPv4头(L3),再接下来20个字节是TCP头(L4),剩下的部分则是HTTP文本内容(L7)。每一层的头部都紧接在前一层之后开始,这一点可以直接通过本文的十六进制转储和Wireshark确认。
- 你直接编写的C#代码,实际上只碰到L7的字节序列。 传给
Socket.Send的只有应用数据,TCP、IP头部是由操作系统的协议栈加上去的,Ethernet头部与电信号则由NIC那一侧负责。正因为如此,选择使用HttpClient、SslStream还是Socket,本质上就是在选择「从哪一层开始把工作交给操作系统」。3 - L5(会话层)、L6(表示层)在现实的协议栈中并不是以独立层存在的。 TLS和字符编码承担了它们的部分角色,但在TCP/IP模型里,这些统统归在应用层之下。「搞不懂L6到底是什么」并不是你的问题,而是模型与现实出现落差的地方本来就在这里。2
- OSI模型在实务中真正派上用场的时刻,是故障排查和沟通的时候。 像「ping能通但HTTP却失败」这种情况,就可以判断L3及以下是正常的,该怀疑的是L4及以上──这种用层的词汇来缩小排查范围的方式,成为网络、基础设施、应用开发人员之间都能通用的共同语言。
2. 为什么OSI参考模型总是止步于死记硬背
许多讲解都是从这样一张表开始的。
| 层 | 名称 | 说明 |
|---|---|---|
| L7 | 应用层 | 为应用程序提供通信服务 |
| L6 | 表示层 | 转换数据的表示形式 |
| L5 | 会话层 | 管理通信的建立与终止 |
| L4 | 传输层 | 提供可靠的数据传输 |
| L3 | 网络层 | 进行路径选择与地址分配 |
| L2 | 数据链路层 | 进行相邻节点间的数据传输 |
| L1 | 物理层 | 把比特转换为电信号 |
这张表本身没有错,但每一行都是用抽象的措辞写成的,光是读一遍并不会在脑中形成具体画面。被告知「转换数据的表示形式」,恐怕没几个人能立刻想到这对应到自己代码里的哪一行。
还有一个应当坦白说清楚的历史事实。OSI参考模型是ISO(国际标准化组织)与ITU-T所制定的标准(ISO/IEC 7498-1、ITU-T X.200),本来是打算为七层各自配上对应的一整套OSI协议、规模宏大的构想。1 但在20世纪90年代的普及竞争中,已经能实际运行的TCP/IP成为了事实上的标准,OSI协议本身几乎没有被真正用起来就落幕了。奠定互联网基础的RFC 1122,是用链路层、网际层(IP)、传输层、应用层这实质上的四层来描述整个体系的,并不存在与L5、L6对应的独立层。2
也就是说,我们如今学习OSI参考模型的意义,并不是「因为现实就是照这个实现的」,而是为了获得「按层思考」这个工具,以及故障排查时的通用词汇。只要立足于这个前提,就不必再为「找不到L5和L6的实体」而纠结,思路一下子就会清晰起来。
3. 正题:拆解一个HTTP请求
前言到此为止,接下来看真正的东西。下面这段十六进制转储,是承载一条GET /index.html HTTP/1.1请求的Ethernet帧(共171字节)。这是开头提到的示例代码组装出来的结果,由于IPv4头部校验和与TCP校验和都经过了正确计算,所以丢给Wireshark也会被识别为一个正常的数据包。
0000 02 00 00 00 00 01 02 00 00 00 00 02 08 00 45 00 ..............E.
0010 00 9d 12 34 40 00 40 06 3b 99 c0 00 02 0a c6 33 ...4@.@.;......3
0020 64 50 cb 84 00 50 00 00 03 e8 00 00 07 d0 50 18 dP...P........P.
0030 ff ff a3 05 00 00 47 45 54 20 2f 69 6e 64 65 78 ......GET /index
0040 2e 68 74 6d 6c 20 48 54 54 50 2f 31 2e 31 0d 0a .html HTTP/1.1..
0050 48 6f 73 74 3a 20 65 78 61 6d 70 6c 65 2e 63 6f Host: example.co
0060 6d 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 4b m..User-Agent: K
0070 6f 6d 75 72 61 53 6f 66 74 44 65 6d 6f 2f 31 2e omuraSoftDemo/1.
0080 30 0d 0a 41 63 63 65 70 74 3a 20 74 65 78 74 2f 0..Accept: text/
0090 68 74 6d 6c 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e html..Connection
00a0 3a 20 63 6c 6f 73 65 0d 0a 0d 0a : close....
看右侧的ASCII显示会发现,从中间某处(偏移量0x36)开始出现了GET /index.html HTTP/1.1这样人类可以读懂的文本。那么,它前面的那54个字节又是什么呢?把这段数据交给示例中的解剖器,会得到下面这样的报告:
[ L2 Ethernet II | offset 0 | 14 bytes | dst=02:00:00:00:00:01 src=02:00:00:00:00:02 type=0x0800 (IPv4)
[ L3 IPv4 | offset 14 | 20 bytes | 192.0.2.10 -> 198.51.100.80 TTL=64 proto=6 (TCP) checksum=OK
[ L4 TCP | offset 34 | 20 bytes | 52100 -> 80 [Psh, Ack] seq=1000 win=65535
[ L7 HTTP | offset 54 | 117 bytes | GET /index.html HTTP/1.1
这正是本文最想让大家仔细看的一张图。要点有三个。
- 每一层都是紧接着「前一层结束的位置」开始的。 Ethernet头是第0〜13字节,IPv4头是第14〜33字节,TCP头是第34〜53字节,HTTP则从第54字节开始。层并不是一种概念上的分类,而是可以直接指出的帧起始处的字节区间。
- 内层是外层的「载荷(payload)」。 从Ethernet的角度看,IPv4之后的内容都只是载荷,它不关心里面是不是TCP。从IP的角度看,TCP之后是载荷;从TCP的角度看,HTTP是载荷。每一层只读取自己的头部,对载荷本身不做任何处理,直接往上层传递。正是这种「不去关心」的结构,构成了封装(Encapsulation)。
- L1(物理层)以及L5、L6并没有出现在这张图里。 L1的工作是把这段字节流转换成电信号、光或无线电波,因此不会出现在转储中;而L5、L6如上一章所述,在明文的HTTP/1.1里并不存在独立的实体。「七层里,实际会出现在转储中的只有四层」──这才是现实的样子。
再次强调,这并不是因为HTTP有什么特殊之处。无论是数据库连接、gRPC,还是工业相机的GigE Vision,只要是Ethernet上的TCP/IP通信,所有数据包都拥有这种相同的嵌套结构。变化的只是L7载荷的具体内容。
4. L2 数据链路层 ── 送到「隔壁」
按解剖结果从外层依次往里看。开头14字节是Ethernet II头。
02 00 00 00 00 01 目的MAC地址(6字节)
02 00 00 00 00 02 源MAC地址(6字节)
08 00 EtherType = 0x0800(载荷为IPv4)
L2的工作是把数据送到同一网段内的相邻节点。目的地址使用MAC地址来指定。MAC地址是分配给NIC的标识符,原则上不会跨越路由器直接送到最终对端手中。从办公室的PC发往Web服务器的帧,其目的MAC并不是Web服务器的MAC,而是默认网关(路由器)的MAC。
这里很多人都会产生一个疑问:「已经有IP地址了,为什么还需要MAC地址」。答案就在于层的分工不同。IP地址指向的是「最终目的地」,MAC地址指向的则是「下一段路要交给谁运送」。用快递来类比的话,IP地址就是运单上的收货地址,MAC地址则是「下一个转运站」的名称。运单(L3)从头到尾都不会变,但收件人名称(L2)在每一段都会被替换。从IP地址查出相邻节点MAC地址的机制就是ARP,可以用arp -a命令查看PC记住的对应表。
从设备的角度说,负责查看L2并转发的是交换机(switching hub)。交换机只根据目的MAC地址来选择端口,并不会去看载荷里的IP地址。
5. L3 网络层 ── 送到世界的尽头
接下来的20字节是IPv4头。摘取主要字段如下。
45 版本=4,头部长度=5个字(20字节)
00 9d 总长度 157字节(IP头+TCP+HTTP。不含Ethernet的14字节)
40 06 TTL=64,协议=6(载荷为TCP)
3b 99 头部校验和
c0 00 02 0a 源IP 192.0.2.10
c6 33 64 50 目的IP 198.51.100.80
L3的工作是无论要跨越多少台路由器,都要把数据送到最终目的地主机。相对于L2只负责一段路(一个hop),L3的目的IP地址从通信开始到结束都不会改变。路由器会查看收到的数据包的目的IP,判断「接下来交给哪台路由器」,并替换掉L2的头部后再转发出去。
TTL(Time To Live)是能让我们直观看到「跨越路由器」这一动作的字段。每跨过一台路由器就会减1,一旦变为0,数据包就会被丢弃,并向源端返回错误。tracert(Windows的路径追踪命令)就是故意把TTL依次设为1、2、3……发出数据包,借此把途中经过的路由器逐一暴露出来。也就是说,tracert输出中排列的每一行,正是「L3的hop」的实物。
6. L4 传输层 ── 送到哪一个进程
接下来的20字节是TCP头。
cb 84 源端口 52100
00 50 目的端口 80
00 00 03 e8 序列号
00 00 07 d0 确认号
50 18 头部长度=5个字,标志位 [PSH, ACK]
ff ff 窗口大小
a3 05 校验和
到L3为止,数据包已经送到了目的地的「主机(机器)」上。然而在那台机器上,Web服务器、数据库以及许多其他进程,可能同时都在进行通信。L4的一项工作,就是利用端口号来判断「应该交给哪一个进程(的Socket)」。目的端口80是「HTTP服务器正在监听的端口号」这一约定,操作系统看到这个端口号,就会把数据放入对应进程的接收缓冲区。
TCP另一项重要工作,是通过序列号、确认应答、重传、流量控制,提供可靠的字节流。4 这方面的行为与应对方式,每一项都足以单独写成一篇文章,这里就交给关于TCP消息边界的文章以及关于TCP重传的文章。这里只需要建立这样的直觉:「从L4往上,已经不再是网络的形态,看起来更像是连接进程与进程之间的一条管道」。
有一个能体现模型的理想与现实之间落差的有趣事实。TCP的校验和并不是仅针对TCP段本身计算,而是被定义为要先加上一段排列了L3信息(源、目的IP地址)的「伪头部」,再进行计算。4 如果各层真的严格独立,L4的计算里就不该混入L3的地址。这正是一个很好的例子,说明OSI参考模型终究只是一张用于梳理概念的地图,而现实中的协议会根据需要跨层交叉。
7. L5、L6 ── 模型与现实出现落差的地方
到目前为止的解剖图中,并没有出现L5(会话层)、L6(表示层)。这正是让人觉得「搞不懂OSI」的最大原因,这里就直接讲清楚。
L5、L6在现实的TCP/IP协议栈中,并不是以独立层的形式存在的。 在RFC 1122的互联网模型里,TCP之上的一切都归为「应用层」。2 OSI模型原本设想的角色,在现实中被分散吸收到了下面这些地方。
| OSI的设想 | 现实中被吸收的场所举例 |
|---|---|
| L5:对话(会话)的建立与管理 | TLS的会话握手、HTTP的Cookie/令牌、应用程序自身的登录管理 |
| L6:数据表示形式的转换、加密 | TLS加密、字符编码(UTF-8)、JSON等序列化格式 |
以HTTPS为例,TCP(L4)之上会夹一层TLS,HTTP在其中流动。「TLS算第几层?」这个问题很容易被当作考题,但非要把它定死成一个答案,并没有实务意义。比起能不能马上答出「是L6」,更重要的是能说明它「在L4之上、L7之下,同时承担相当于L5、L6的角色」。
在.NET代码里,这种关系直接体现在类的叠加方式上。用SslStream把TcpClient.GetStream()得到的L4流包起来之后,Write写入的明文会先被加密成TLS记录,再交给TCP。5
using var client = new TcpClient("example.com", 443);
// 用TLS(相当于L5/L6)包裹L4的字节流
using var ssl = new SslStream(client.GetStream());
await ssl.AuthenticateAsClientAsync("example.com");
// 这里写的是L7(HTTP)的明文。加密是SslStream的工作
await ssl.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n..."));
用流包裹流的这种结构,正是封装的代码版本。在Wireshark里查看443端口的通信时,会发现TCP载荷只显示为一团不透明的Application Data,这也能从反方向确认「L7被包进了相当于L6的封装里」。
8. 你写的C#代码碰到的是哪一层
到这里,把前面的内容对应到Windows应用开发者所使用的工具上。
| 层 | 实物举例 | .NET API举例 | 由谁添加头部 |
|---|---|---|---|
| L7 应用层 | HTTP、gRPC、自定义协议 | HttpClient、Grpc.Net.Client、自行组装的报文 |
你的代码/库 |
| (相当于L5/L6) | TLS、序列化、字符编码 | SslStream、JsonSerializer、Encoding |
库 |
| L4 传输层 | TCP、UDP | Socket、TcpClient、UdpClient(头部由OS生成) |
操作系统协议栈 |
| L3 网络层 | IP、路由、ICMP | Ping、NetworkInterface(仅供查询) |
操作系统协议栈 |
| L2 数据链路层 | Ethernet、Wi-Fi、ARP | (普通应用无法直接接触) | NIC驱动/NIC |
| L1 物理层 | 电信号、光、无线电波 | ── | NIC/线缆/空间 |
从这张表可以看出两件事。
第一,API的选择就是在选择「从哪一层往下交给操作系统」。 使用HttpClient可以把L7的组装也交出去;而使用Socket时,L7全部都要自己写。反过来说,普通的Windows应用几乎不会直接操作L3及以下(raw socket需要管理员权限,并伴随诸多限制)。
第二,传给Socket.Send的,实际上「只有」L7的字节序列。 示例的Part 2是一个通过loopback实际发送HTTP请求的演示,应用程序准备的只有60字节的HTTP文本内容,第3章看到的那54字节头部群,全部由操作系统和NIC负责添加。你应用程序里的网络代码,实际上只负责制作七层里最内侧的那份载荷──这就是OSI模型与你所写代码之间精确的位置关系。
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(IPAddress.Loopback, port);
byte[] request = Encoding.ASCII.GetBytes(
$"GET / HTTP/1.1\r\nHost: localhost:{port}\r\nConnection: close\r\n\r\n");
// 传进去的只有L7的字节序列。TCP/IP/Ethernet的头部
// 是在这次调用的另一侧(操作系统和NIC)被添加上去的
int sent = await socket.SendAsync(request);
9. 动手实践 ── 组装数据帧,并用Wireshark打开
为了把「读懂了」变成「亲眼见证了」,这里介绍一下示例代码的运行方式。
示例的核心,是按照第3章的顺序,从L7往L2一层层包裹组装出数据帧的SampleFrameBuilder。发送时每一层发生的事情,直接对应到方法的排列顺序。
public static byte[] BuildHttpGetFrame()
{
// L7: 应用程序传给Send的「只有」这段字节
byte[] http = Encoding.ASCII.GetBytes(HttpRequest);
// L4: 由OS协议栈在前面添加TCP头
byte[] tcp = WrapInTcp(http);
// L3: 再添加IPv4头
byte[] ip = WrapInIpv4(tcp);
// L2: 在交给NIC驱动之前添加Ethernet头
return WrapInEthernet(ip);
}
运行演示程序时,除了会显示十六进制转储和嵌套结构的视图之外,还会把这个帧导出成一个pcap文件。
dotnet run --project samples/Demo
请试着用Wireshark打开生成的sample-http-get.pcap。6 中间的窗格会纵向排列出Ethernet II/Internet Protocol Version 4/Transmission Control Protocol/Hypertext Transfer Protocol这四行,点击其中任意一行,下方十六进制转储中对应的字节区间就会被高亮显示。第3章的嵌套结构图,相当于被业界标准工具重新呈现了一次。确认到这里之后,接下来可以试试真实流量。启动Wireshark开始抓包,在过滤器里输入tcp.port == 443之类的条件,然后用浏览器打开任意一个网站,就能确认真实通信全部都是由这种嵌套结构组成的。
另外,在解剖器(PacketDissector)本身的实现中,也埋了一些值得学习的实务要点。例如在切出IPv4载荷时,不能直接切出接收缓冲区剩下的全部内容,而必须信任头部里的TotalLength字段来决定切出的长度。这是因为Ethernet存在最小帧长(60字节)的限制,过短的数据包末尾会被补上没有意义的填充字节。「用内层的长度信息,去除外层带来的干扰(填充)」──这也是层的分工体现在实现上的一个例子,并已通过单元测试加以验证。
10. 在实务中使用OSI模型 ── 用层的词汇来排查故障
前文提到「OSI是作为词汇留存下来的」。这里举两个这种词汇真正发挥作用的场景。
故障排查。 「连不上服务器」这样的报告,一旦转换成层的词汇,就可以系统性地缩小范围。通常的做法是从下层开始依次确认。
| 确认项 | 使用的工具 | 能确定存活的层 |
|---|---|---|
| 连接指示灯、Wi-Fi连接状态 | 目视 | L1〜L2 |
| 对同一网段的网关执行ping | ping 192.168.x.1 |
自身周边的L3 |
| 对目标主机执行ping | ping <目标> |
整条路径的L3 |
| 对目标端口建立TCP连接 | Test-NetConnection <目标> -Port 443 |
L4(加上途中的防火墙) |
| 发送HTTP请求 | curl -v或应用程序本身 |
L7(加上TLS) |
举例来说,如果「ping能通,但Test-NetConnection失败」,就说明L3及以下是正常的,嫌疑会集中在堵住L4端口的某个因素上(服务停止、防火墙、端口配置错误)。如果「TCP能连上,但HTTP返回400」,那就不是网络问题,而是L7(请求内容本身)的问题。与其盲目地反复插拔网线,逐层确认到底存活到哪一层──这才是OSI模型在实务中的用法。
沟通中的共同语言。 像「感觉是L2的问题,麻烦看一下交换机端口」「那是L7的事,该找应用团队」这类对话,能在网络人员、基础设施人员、应用开发人员之间准确传达。层的编号,是用于简洁表达责任边界的行业通用坐标。
11. 纠正常见的误解
最后,统一纠正一些关于OSI参考模型常见的误解。
- 「互联网是靠OSI的七层在运行的」 ── 并非如此。实际被实现出来的是TCP/IP(实质上四层),OSI是用于说明与沟通的参考模型。2
- 「TCP/IP的四层和OSI的七层能干净地对应起来」 ── L5〜L7的对应本质上是模糊的。「TCP/IP的应用层 = OSI的L5+L6+L7」这种表格经常能见到,但正如第7章所述,L5、L6的角色在现实中是分散在TLS或序列化格式里的。
- 「TLS是第6层(或第5层)的协议」 ── 非要把它定死成某一层并没有意义。准确的说法是:它在L4之上、L7之下,同时承担相当于L5、L6的角色。
- 「看端口号就能知道是什么协议」 ── 端口80并不代表就一定是HTTP。端口号只是一种约定,实际传输的内容是什么,要看载荷本身才能知道。示例中的解剖器之所以不是靠端口号,而是靠内容开头的字符串来判断是否为HTTP,正是出于这个原因。
- 「交换机是L2、路由器是L3的设备」 ── 作为出发点是对的,但现实中还普遍存在L3交换机、按L4分流的负载均衡器、检查L7的WAF等跨越多层的设备。用「这台设备能读到哪一层」来理解,才是准确的方式。
12. 总结
- OSI参考模型不是实现,而是一种按层思考的词汇。现实中运行的是TCP/IP(实质上四层)
- 七层不是概念图,而是真实地、物理地嵌套在一个数据帧的字节序列里(L2:第0〜13字节,L3:第14〜33字节,L4:第34〜53字节,L7:第54字节及以后)
- 每一层只读取自己的头部,对载荷(内层)不做任何处理──这就是封装
- L5、L6在现实的协议栈中并不是独立存在的,TLS和编码吸收了它们的角色
- 你的C#代码所写的只有L7的字节序列。TCP/IP头部由操作系统负责,Ethernet由NIC负责
- 实务中的用途,是从下层开始逐层确认存活状态的故障排查,以及团队之间的共同语言
- 只要用示例代码组装出数据帧并用Wireshark打开,本文的内容就都能亲眼确认
相关文章
- 误以为TCP中Send的每个单位都能对应一次Receive ── 把数据当作字节流来设计接收端
- TCP重传导致工业相机通信中断的原因与排查
- C# async/await实务判断表 ── Task.Run与ConfigureAwait
相关咨询领域
小村软件有限责任公司承接进行TCP/IP通信的Windows应用程序的设计与实现、在与工业设备通信时出现「偶尔断线、变慢」等通信异常的原因排查,以及使用数据包抓取进行故障排查的支持工作。
参考链接
-
ITU-T, X.200 : Information technology - Open Systems Interconnection - Basic Reference Model: The basic model. OSI基本参考模型的原始文档(内容与ISO/IEC 7498-1相同)。关于七层各自的定义。 ↩ ↩2
-
IETF, RFC 1122 - Requirements for Internet Hosts – Communication Layers. 规定互联网主机应实现的要求的RFC。关于用链路层、IP层、传输层、应用层这四层来梳理协议族。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Socket Class (System.Net.Sockets). .NET中的Socket API。关于应用程序只传递载荷,协议头部的生成由操作系统的协议栈负责。 ↩
-
IETF, RFC 9293 - Transmission Control Protocol (TCP). TCP现行规范。关于通过序列号、确认应答提供可靠性,以及校验和计算中会用到包含IP地址的伪头部。 ↩ ↩2
-
Microsoft Learn, SslStream Class (System.Net.Security). 关于包裹既有流(通常是TCP的
NetworkStream)以提供TLS加密与身份验证的类。 ↩ -
Wireshark Foundation, Wireshark User’s Guide. 关于抓包文件的打开方式、数据包详情窗格与字节序列窗格的对应显示,以及显示过滤器的使用方法。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
在 C# 中安全调用 Win32 API —— P/Invoke 实务指南(DllImport / LibraryImport / CsWin32)
本文整理在 C# 中通过 P/Invoke 调用 Win32 API 或原生 DLL 时的实务要点:DllImport 与 LibraryImport 的区别、用 CsWin32 自动生成签名、字符串封送的陷阱、用 SafeHandle 管理句柄、SetLastError ...
在 WinForms/WPF 应用中集成 Entra ID 认证 —— MSAL.NET 与 WAM Broker 的实务架构
本文以实务视角整理在 WinForms/WPF 桌面应用中集成 Entra ID(原 Azure AD)认证的步骤:公共客户端的思路、ROPC 被弃用的现状、应用注册、MSAL.NET 的 AcquireTokenSilent 模式、WAM Broker、令牌缓存的持久化,...
业务应用的日期时间与时区 ── 从 DateTime 的陷阱到 UTC 存储原则、测试设计
服务器迁移后时间整整差了 9 个小时、只有海外分支机构的日期会变成前一天——本文从 DateTime 的 Kind 与隐式转换梳理日期时间事故的根本原因。内容涵盖与 DateTimeOffset 的取舍、UTC 存储与 ISO 8601 原则、TimeZoneInfo 与夏...
常见问题
汇总了咨询这一主题时常见的问题。
- OSI参考模型的七层,实际的互联网真的在用吗?
- 并没有在用。现实中运行的互联网是TCP/IP协议族(实质上只有四层),OSI的七层协议本身在20世纪90年代的普及竞争中败下阵来,几乎没有真正被大规模使用过就落幕了。奠定互联网基础的RFC 1122,是用链路层、网络层(IP)、传输层、应用层这四层来描述整个体系的。留存下来的,是「用L2来划分问题」「这是L7的问题」这种用于故障排查与团队沟通的通用词汇模型。
- 会话层(L5)和表示层(L6)到底体现在哪里?
- 在现实的TCP/IP协议栈中,它们并不是以独立层的形式存在的。OSI模型原本设想的角色,在现实中被分散吸收到了其他机制里:L5(对话的建立与管理)对应的是TLS的握手、HTTP的Cookie/令牌;L6(数据表示形式的转换、加密)对应的是TLS加密、字符编码、JSON之类的序列化格式。非要把「TLS算第几层」定死并没有实务意义,更重要的是能说明它是「在L4之上、L7之下,同时承担L5、L6角色」的存在。
- 既然已经有IP地址了,为什么还需要MAC地址?
- 因为两者在层级上的分工不同。IP地址(L3)是指向最终目的地的地址,从通信开始到结束都不会改变。MAC地址(L2)则是指向「下一段路要交给谁运送」的收件人名称,每跨越一台路由器就会被替换一次。从办公室的PC发往Web服务器的帧,其目的MAC并不是Web服务器的MAC,而是默认网关(路由器)的MAC。从IP地址查出相邻节点MAC地址的机制就是ARP,可以用arp -a命令查看对应表。
- OSI参考模型在实务中到底有什么用?
- 用在故障排查和团队之间的沟通上。像「连不上服务器」这样的报告,可以通过从下层开始逐层确认存活状态来系统性地缩小范围:目视连接指示灯(L1〜L2)、对网关执行ping(L3)、确认能否对目标端口建立TCP连接(L4)、发送HTTP请求(L7)。举例来说,如果ping能通但TCP连接失败,嫌疑就会集中在堵住L4端口的某个因素上(服务停止、防火墙等)。另外,像「这是L2的问题,麻烦看一下交换机」这种说法,也可以作为业界通用的坐标,用简短的语言传达责任边界。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。