c#软件开发学习笔记--TCP 粘包拆包、SqlSugar 框架
·
学习总结:TCP 粘包拆包 与 SqlSugar 框架
一、TCP 粘包与拆包
1.1 问题背景
TCP 是面向字节流的协议,发送方写入的数据在接收方看来是一条连续不断的字节流,没有边界。因此会出现以下三种现象:
| 现象 | 说明 |
|---|---|
| 粘包 | 多次发送的消息被合并成一次接收(例如两条短消息挤在一个数据包中到达) |
| 拆包 | 一条消息被拆成多次接收(例如大消息被网络层切成多个数据包) |
| 混合 | 既有粘包又有拆包同时发生 |
注意:粘包拆包不是 TCP 的 Bug,而是字节流协议的固有特性。UDP 是面向报文的协议,不存在此问题。
1.2 常见解包方案
- 消息定长:每条消息固定长度,不足补齐。
- 特殊分隔符:例如以
\r\n结尾(FTP、HTTP 头部采用此方式)。 - 头部长度字段(本案例采用):在消息体前加一个固定长度的头部,记录消息体的字节长度。
1.3 协议设计(本案例)
本案例采用 “4 字节头 + 变长消息体” 的方案:
+------------+-------------------+
| Header | Body |
| 4 bytes | N bytes |
| (body长度) | (UTF8 文本) |
+------------+-------------------+
- 头部:4 字节整数,存储消息体的字节长度
- 字节序:网络字节序(大端),使用
IPAddress.HostToNetworkOrder/NetworkToHostOrder转换 - 消息体:UTF-8 编码的字符串
1.4 关键代码
1.4.1 编码:消息 → 字节流
/// <summary>
/// 将字符串编码为「4字节长度头 + 消息体」的字节包
/// </summary>
private byte[] EncodePackage(string msg)
{
byte[] data = Encoding.UTF8.GetBytes(msg);
// 将消息体长度转为网络字节序(大端),保证跨平台一致
byte[] headLength = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(data.Length));
byte[] package = new byte[4 + data.Length];
Array.Copy(headLength, package, headLength.Length); // 头部
Array.Copy(data, 0, package, 4, data.Length); // 消息体
return package;
}
1.4.2 解码:从字节流中循环抽取完整包
/// <summary>
/// 从缓冲区 bsList 中循环解析出完整消息
/// 粘包:一次能解析出多条;拆包:长度不足时跳出等待下次数据
/// </summary>
private void TryParsePackage()
{
while (bsList.Count >= 4) // 至少要有 4 字节头部
{
byte[] headBytes = bsList.GetRange(0, 4).ToArray();
int bodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(headBytes, 0));
int totalLength = 4 + bodyLength;
if (bsList.Count < totalLength) break; // 拆包:包不完整,等下一次
byte[] bodyData = bsList.GetRange(4, bodyLength).ToArray();
string msg = Encoding.UTF8.GetString(bodyData);
AppendLog("收到消息:" + msg);
bsList.RemoveRange(0, totalLength); // 移除已解析部分,继续解析下一个
}
}
1.4.3 接收数据 + 缓冲
/// <summary>
/// 异步循环读取网络流,累积到 bsList 后尝试解析
/// </summary>
private void ReceiveData(TcpClient client)
{
cts = new CancellationTokenSource();
Task.Run(async () =>
{
byte[] buffer = new byte[1024];
while (!cts.IsCancellationRequested)
{
NetworkStream stream = client.GetStream();
int len = await stream.ReadAsync(buffer, cts.Token);
if (len == 0) { AppendLog("客户端断开连接"); break; }
byte[] realData = new byte[len];
Array.Copy(buffer, realData, len);
bsList.AddRange(realData); // 关键:累积到缓冲区
TryParsePackage();
}
}, cts.Token);
}
1.5 流程示意
发送端: msg ──EncodePackage──> [4字节头][body] ──> stream.Write
接收端: stream.Read ──> bsList.AddRange ──> TryParsePackage (while循环)
│
┌─────────────┴─────────────┐
│ bsList.Count >= 4 ? │
│ 取出头部 → bodyLength │
│ 总长 4+bodyLength │
│ 不足 → break (拆包等待) │
│ 足够 → 取body → 处理 → 移除 │
└───────────────────────────┘
1.6 要点小结
- ✅ 用
List<byte>作为接收缓冲区,累积所有到达字节 - ✅ 用
while循环解析,一次可拆出多个包(解决粘包) - ✅ 包不完整时
break等待下次数据(解决拆包) - ✅ 头部使用网络字节序保证收发双方字节序一致
- ✅ 解析完一个包后必须
RemoveRange移除已处理部分
二、SqlSugar 第三方 ORM 框架
2.1 框架简介
SqlSugar 是一款老牌国产 .NET ORM 框架,由果糖大数据科技团队维护,开箱即用、易于上手。
2.2 两种使用模式
| 模式 | 类 | 特点 |
|---|---|---|
| 原生模式 | SqlSugarClient | 高性能,每次 new 新对象,禁止单例 |
| 单例模式 | SqlSugarScope | 对新手友好,解决大部分线程安全问题,必须单例 |
2.3 核心配置(本案例采用原生模式)
/// <summary>
/// 窗体加载时初始化 SqlSugarClient
/// </summary>
private void Form1_Load(object sender, EventArgs e)
{
db = new SqlSugarClient(new ConnectionConfig()
{
ConnectionString = "server = localhost,64345; database = sugarDemo.db; uid = sa; pwd = 123123;Encrypt=True;TrustServerCertificate=True",
DbType = DbType.SqlServer,
IsAutoCloseConnection = true, // 自动关闭连接,省去手动 using
},
db =>
{
// AOP:执行 SQL 时打印原生 SQL,方便调试
db.Aop.OnLogExecuting = (sql, pars) =>
{
Console.WriteLine(UtilMethods.GetNativeSql(sql, pars));
};
});
}
2.4 实体定义
[SugarTable] // 标记为数据库表
public class Student
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)] // 主键 + 自增
public int Id { get; set; }
public string Name { get; set; }
public int? Age { get; set; } // 可空类型对应数据库可空字段
}
2.5 数据库与表初始化
// 创建数据库(若不存在)
db.DbMaintenance.CreateDatabase();
// CodeFirst:根据实体类自动生成/同步表结构
db.CodeFirst.InitTables(typeof(Student));
2.6 核心 API(CRUD)
| 操作 | API | 示例 |
|---|---|---|
| 增 | db.Insertable<T>(entity).ExecuteCommand() | db.Insertable(list).ExecuteCommand() |
| 删 | db.Deleteable<T>(entity).ExecuteCommand() | db.Deleteable<Student>(stu).ExecuteCommand() |
| 改 | db.Updateable<T>(entity).ExecuteCommand() | db.Updateable<Student>(stu).ExecuteCommand() |
| 查 | db.Queryable<T>()...ToList() | db.Queryable<Student>().Single(s => s.Id == id) |
2.6.1 批量插入
List<Student> list = new List<Student>()
{
new Student(){Name = "张三1", Age = 100},
new Student(){Name = "张三2", Age = 200},
new Student(){Name = "张三3", Age = 300},
};
int row = db.Insertable(list).ExecuteCommand();
2.6.2 条件查询 + 动态拼接
使用
Expressionable<T>动态拼接 where 条件,避免写大量 if-else 分支 SQL。
/// <summary>
/// 根据界面输入条件动态拼接查询条件并分页绑定 DataGridView
/// </summary>
private void BindView()
{
if (db == null) return;
var express = Expressionable.Create<Student>();
// AndIF:仅当条件成立时追加 AND 过滤
express.AndIF(!string.IsNullOrWhiteSpace(textBox1.Text), s => s.Name.Contains(textBox1.Text));
express.AndIF(!string.IsNullOrWhiteSpace(textBox2.Text), s => s.Age >= int.Parse(textBox2.Text));
express.AndIF(!string.IsNullOrWhiteSpace(textBox3.Text), s => s.Age <= int.Parse(textBox3.Text));
var list = db.Queryable<Student>()
.Where(express.ToExpression())
.ToPageList(pageNumber, pageSize, ref totalCount, ref totalPages)
.ToList();
dataGridView1.DataSource = list;
label2.Text = $"{pageNumber}/{totalPages}";
}
2.6.3 修改与删除(按主键定位)
// 修改:先查后改
int id = int.Parse(dataGridView1.SelectedRows[0].Cells["Id"].Value!.ToString()!);
Student stu = db.Queryable<Student>().Single(s => s.Id == id);
stu.Name = "三生三世";
stu.Age = 100;
db.Updateable<Student>(stu).ExecuteCommand();
// 删除
Student stu = db.Queryable<Student>().Single(s => s.Id == id);
db.Deleteable<Student>(stu).ExecuteCommand();
2.7 分页查询要点
ToPageList(pageNumber, pageSize, ref totalCount, ref totalPages)一次返回当前页数据 + 总数 + 总页数- 翻页只需修改
pageNumber后重新调用BindView() - 每页条数变更时,应重置
pageNumber = 1,避免越界
2.8 要点小结
- ✅
IsAutoCloseConnection = true让连接管理自动化,避免资源泄漏 - ✅
Aop.OnLogExecuting是调试 SQL 的利器 - ✅
Expressionable<T>适合动态条件查询场景 - ✅
CodeFirst.InitTables实现"代码先行",实体变更直接同步表结构 - ✅ CRUD 全部链式调用,统一以
ExecuteCommand()(增删改)或ToList()/Single()(查)收尾
三、关联知识点
- ADO.NET 五大对象:
SqlConnection、SqlCommand、SqlDataAdapter、SqlDataReader、DataSet,SqlSugar 底层即基于此封装。 - IOC / DI:SqlSugarClient 推荐使用 Scope 或瞬发注入,禁止单例。
- 网络字节序:TCP 自定义协议应统一使用大端序(
HostToNetworkOrder/NetworkToHostOrder)。
更多推荐

所有评论(0)