一顺序
OSI七层模型 → TCP/IP四层模型 → 物理层/数据链路层基础 → IP协议 → ARP协议 → ICMP协议 → TCP协议
二OSI七层模型与TCP/IP四层模型
2.1OSI七层模型(理论模型)
| 层级 | 名称 | 核心功能 | 典型设备 / 协议 |
|---|---|---|---|
| 7 | 应用层 | 为用户应用程序提供网络服务 | HTTP、HTTPS、FTP、SMTP、DNS |
| 6 | 表示层 | 数据格式转换、加密解密、压缩解压缩 | SSL/TLS、JPEG、ASCII |
| 5 | 会话层 | 建立、管理和终止会话 | 会话控制协议 |
| 4 | 传输层 | 端到端的通信控制 | TCP、UDP |
| 3 | 网络层 | 路由选择、分组转发 | IP、ICMP、ARP、RIP、OSPF |
| 2 | 数据链路层 | 帧的封装与解封装、差错检测 | 以太网、PPP、MAC 地址、交换机 |
| 1 | 物理层 | 比特流的传输 | 网线、光纤、集线器、网卡 |
2.2TCP/IP四层模型(实际使用模型)
OSI 模型过于理想化,实际互联网使用的是 TCP/IP 模型,它将 OSI 的七层合并为四层:
| TCP/IP 四层 | 对应 OSI 七层 | 核心功能 |
|---|---|---|
| 应用层 | 应用层 + 表示层 + 会话层 | 为应用程序提供服务 |
| 传输层 | 传输层 | 端到端通信 |
| 网际层 | 网络层 | 路由选择和分组转发 |
| 网络接口层 | 数据链路层 + 物理层 | 比特流和帧的传输 |
2.3数据封装与解封装过程
- 发送方 :数据从应用层向下传递,每经过一层就加上该层的首部(Header),这个过程叫封装
- 接收方 :数据从物理层向上传递,每经过一层就去掉该层的首部,这个过程叫解封装
三IP协议(网际层核心)
3.1IP地址
- 作用:唯一标识互联网中的一台主机
- IPv4 地址:32 位,通常用点分十进制表示,如
192.168.1.1 - 分类:
- A 类:0.0.0.0 ~ 127.255.255.255(大型网络)
- B 类:128.0.0.0 ~ 191.255.255.255(中型网络)
- C 类:192.0.0.0 ~ 223.255.255.255(小型网络)
- D 类:224.0.0.0 ~ 239.255.255.255(组播)
- E 类:240.0.0.0 ~ 255.255.255.255(保留)
- 特殊 IP 地址:
127.0.0.1:回环地址,用于本机测试0.0.0.0:表示所有网络接口255.255.255.255:广播地址
3.2子网划分
- 问题:IP 地址分类过于死板,导致 IP 地址浪费
- 解决:通过子网掩码将一个大网络划分为多个小网络
- 子网掩码:32 位,连续的 1 表示网络位,连续的 0 表示主机位
- 示例:
- C 类默认子网掩码:
255.255.255.0(/24) - 划分为 4 个子网:
255.255.255.192(/26)
- C 类默认子网掩码:
3.3路由
- 作用:当源主机和目的主机不在同一个网络时,将 IP 数据报从源主机转发到目的主机
- 路由器:工作在网络层,根据路由表转发 IP 数据报
- 路由表:包含目的网络地址、下一跳地址、出接口等信息
3.4IP数据报结构
IP 数据报首部固定为 20 字节,可选字段最长 40 字节,因此 IP 首部最大长度为 60 字节。
| 协议 | 它的角色 | 负责的核心问题 | 类比生活 |
|---|---|---|---|
| TCP 协议 | 内层包装(大主管) | 负责质量。确保数据不丢、不乱、完整。 | 公司里的商务总监,负责跟客户死磕合同,确保一字不差。 |
| IP 协议 | 外层包装(跑腿员) | 负责地址和路线。负责把包裹送到地方。 | 公司里的外卖小哥,只管照着地址开车送货,不关心盒子里装的是啥。 |
四ARP协议(地址解析协议)
IP 协议只知道对方的"名字(IP地址)",而 ARP 协议是帮 IP 协议查到对方的"身份证号(MAC地址)",从而让数据真正能在物理网线里传过去。
-
IP 地址(虚拟的、暂时的): 就像你的收件地址 (比如"北京路1号3网段5号工位")。它会随着你搬家(换网络)而改变。IP 协议只认这个地址。
-
MAC 地址(物理的、永久的): 就像你的身份证号 。由网卡生产厂商烧录在芯片里,全球唯一,走到哪都不变。网线、网卡、交换机这些物理硬件,只认 MAC 地址。
六ICMP协议(互联网控制消息协议)
还记得我们之前说的"套娃"吗?TCP 是装在 IP 大箱子外壳里面的"内层小盒子"。
这就导致了一个致命的逻辑问题:如果外层的 IP 箱子在路上直接被路由器给拆了、丢了,里面的 TCP 盒子根本连重传的机会都没有!
-
TCP 是"端到端"的: 它只住在起始端(你的电脑)和终点端(服务器)。路上的路由器根本不运行 TCP 协议。
-
ICMP 是"网络层"的: 它和 IP 协议是一伙的,专门管马路上的事(路由器和路由器之间、路由器和你的网卡之间)。
当包裹在马路上出事时,远在终点的 TCP 根本不知道,这时候必须靠沿途的路由器用 ICMP 协议 大喊一声:"出事啦!
七TCP
八C#中写通信
- SerialPort 走串口线(COM口)
- Socket 走 TCP/IP 网络(网口、WiFi、光纤)
1.TCP
在 C# 的 System.Net 和 System.Net.Sockets 命名空间里,你主要打交道的是以下三大家族:
① 传输层双雄:TCP vs UDP (最底层的控制)
在 C# 中,如果你想做底层通信,你会直接使用:
-
TcpClient/TcpListener(或者更底层的Socket): * 为什么选它? 因为正如前面所说,它是"金牌大管家"。你用 C# 写的聊天软件、联机游戏网关、ERP 系统,必须保证数据一个字都不能错、不能丢、顺序不能乱。- 代价: 速度稍微慢一点点,因为要建立连接(三次握手)。
-
UdpClient:- 为什么选它? UDP 是 TCP 的死对头("马大哈"协议),它只管发,丢不丢无所谓。在 C# 中写实时视频通话、网络语音、FPS 实时竞技游戏(如射击位置同步)时,会选择 UDP。因为这时候"快"比"百分之百准确"更重要。
② 应用层霸主:HTTP / WebSocket (现代开发最常用)
绝大多数 C# 程序员在日常工作中,其实根本不需要直接去摆弄 TCP 的 Socket。大家用的是基于 TCP 封装好的高级玩具:
-
HttpClient: 用来调用 Web API(获取 JSON 数据)。你不需要管 TCP 怎么握手,你只需要给它一个 URL。 -
WebSocket/ SignalR: 用来实现网页、客户端的即时大屏刷新、消息推送。它解决了传统 TCP 粘包问题,直接给你处理好了一个个"消息帧"。
8.1. TcpClient 的三大核心部件
在 C# 里用 TcpClient 写通信,你其实只需要掌握三个东西:
-
Connect()(踩油门): 负责向服务器发起 TCP 三次握手。你只要给它一个 IP 和 端口(Port),它只要不报错,就说明你和服务器之间的"专属管道"已经铺设成功了。 -
GetStream()(接通水管): 这是TcpClient最灵魂的方法。它会返回一个NetworkStream对象。在 C# 里,"流(Stream)"就代表着一条畅通无阻的水管。 -
Read()/Write()(接水/灌水): * 你想发数据?往 Stream 里Write(写)二进制字节。- 你想收数据?从 Stream 里
Read(读)二进制字节。
- 你想收数据?从 Stream 里
8.2实例
cs
using System;
using System.Net.Sockets;
using System.Text;
class Program
{
static void Main()
{
try
{
// 1. 创建客户端并连接服务器(这一步操作系统自动帮你完成了 TCP 三次握手!)
// 自动把对方的 IP 填入外层的 IP 协议大箱子
TcpClient client = new TcpClient("127.0.0.1", 8888);
Console.WriteLine("连接服务器成功!");
// 2. 获取网络水管(NetworkStream)
NetworkStream stream = client.GetStream();
// 3. 发送消息:把大白话字符串转成计算机认的字节数组(Byte[])
string message = "你好,服务器!";
byte[] dataToSend = Encoding.UTF8.GetBytes(message);
stream.Write(dataToSend, 0, dataToSend.Length); // 灌水,数据发射出去!
Console.WriteLine($"已发送: {message}");
// 4. 接收服务器的回应
byte[] buffer = new byte[1024]; // 准备一个 1024 字节的脸盆接水
int bytesRead = stream.Read(buffer, 0, buffer.Length); // 阻塞等待,直到水流进来
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到服务器回复: {response}");
// 5. 关机,拆除管道
stream.Close();
client.Close();
}
catch (Exception e)
{
Console.WriteLine($"发生错误: {e.Message}");
}
}
}
TcpClient 对象有什么用
连,发,收
- 连接相关
new TcpClient(ip, port):直接构造并自动发起连接(三次握手)。client.Connect(ip, port):先创建对象,再手动连接。client.Connected:判断是否连上。client.Close()/Dispose():断开连接、释放端口Microsoft Learn。
- 数据收发(最重要)
NetworkStream stream=client.GetStream():拿到读写流 ,用来发数据、收数据Microsoft Learn。
stream.Write(字节数组):发数据给服务器Microsoft Learn。stream.Read(字节数组):读服务器发过来的数据Microsoft Learn。
- 辅助属性
Available:当前有多少数据可读。Client:拿到底层 Socket,做更底层操作。简单说:TcpClient = 连接管理器 + 数据收发器。
8.3两大2问题
因为 TCP 的本质特性 会在应用层(你的 C# 代码里)引发两个著名的现象:
🕳️ 巨坑一:面向字节流 ------ 著名的"粘包"与"分包"
这是初学者 99% 会崩溃的地方。 假设你在客户端连续调用了两次 stream.Write():
-
第一次发:
"Hello" -
第二次发:
"World"
你在服务器端用 stream.Read() 去接水的时候,你一厢情愿地以为会收到两次,第一次是 Hello,第二次是 World。
然而现实是: 服务器可能一次性接到了 "HelloWorld"(粘包 );或者第一次接到了 "Hell",第二次接到了 "oWorld"(分包)。
原因: 因为底层的 TCP 协议是面向字节流的。它就像一根水管,你灌进去两杯水,水在管子里融为一体了,流出来的时候就是一股水流,它根本不知道哪滴水属于第一杯,哪滴属于第二杯!
解决办法: 你必须在 C# 代码里自己定规则。比如**"定长法"(规定每个消息必须是 100 字节,不够用空格补齐),或者"带包头法"**(每次发正文前,先雷打不动地发 4 个字节,告诉对方后面正文有多长)。
🕳️ 巨坑二:Read() 是个"老实人" ------ 阻塞(Blocking)
看这句代码:int bytesRead = stream.Read(buffer, 0, buffer.Length); 如果服务器此时开小差,没有给你发任何数据,你的 C# 程序运行到这一行时,会死死卡在这里,整个线程一动不动。
-
如果你在 UI 界面(比如 WinForm/WPF)的主线程里这么写,你的软件界面会直接卡死未响应。
-
解决办法: 在现代 C# 开发中,我们一律推荐使用它的异步版本 :
await stream.ReadAsync(...)和await stream.WriteAsync(...)。利用 C# 的async/await机制,让线程在等水流进来的时候去干别的事,网页和界面就不会卡顿
2.UDP
2.1特点
-
没有三次握手(省心):
TcpClient发数据前,必须先死等Connect()成功。而UdpClient不需要连接 !你想给谁发,把数据打包好,填上对方的 IP 和端口,一脚油门直接Send发射出去。至于对方开没开机、能不能收到,它一概不管。 -
没有连贯的水管(数据报形式): TCP 是管道里的"水流",会粘包。而 UDP 发送的是"大石块"(数据报)。你
Send一个"Hello",对方Receive到的绝对就是一个完完整整的"Hello",天然绝不粘包! -
缺点(提心吊胆): 它在物理马路上被路由器挤掉了就是真掉了,它不会自动重传。
2.2UDP简易通信
因为 UDP 不需要握手,所以在 C# 里,你甚至不需要像 TCP 那样分出严格的"服务端"和"客户端"结构,大家都是对等的 UdpClient。
我们直接用一个程序(用多线程模拟接收和发送)
cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
// 创建一个 UDP 客户端,绑定本机 9999 端口(专门用来接收别人发给 9999 的数据)
UdpClient udpNode = new UdpClient(9999);
Console.WriteLine("UDP 节点已启动,正在监听 9999 端口...");
// 1. 开启一个线程,死循环【接收】数据
Task.Run(() =>
{
while (true)
{
// IPEndPoint 用来记录"这块石头是谁扔给我的"
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);
// 核心:Receive 直接接住一整块"数据石头",并把对方的地址存入 remoteEP
byte[] receivedData = udpNode.Receive(ref remoteEP);
string msg = Encoding.UTF8.GetString(receivedData);
Console.WriteLine($"\n[收到来自 {remoteEP} 的导弹]: {msg}");
}
});
// 2. 主线程用来【发送】数据
Console.WriteLine("按下回车,给本机的 9999 端口发射一颗 UDP 导弹...");
Console.ReadLine();
string message = "乌拉!这是一颗 UDP 导弹!";
byte[] dataToSend = Encoding.UTF8.GetBytes(message);
// 核心:直接 Send!直接指定目的地 IP 和端口,不需要提前 Connect!
udpNode.Send(dataToSend, dataToSend.Length, "127.0.0.1", 9999);
Console.WriteLine("导弹已发射(不管对方接没接住)!");
Console.ReadLine();
}
}