c#Socket学习,使用Socket创建一个在线聊天,日志笔记(5)
socket是基于TCP/IP 协议的一个实现。TCP/IP不是具体的东西,不能通过代码调用,socket实现了tcp/ip,
Socket 不是协议,而是操作系统提供的编程接口(API)。
Socket和HTTP,Websocket,MQTT都是基于tcp/ip的协议。
Socket属于tcp/ip协议中的,介于应用层和传输层中的一个实现,http是应用层的实现。
Socket实现跨进程/主机的网络通信。
提供可靠的字节流传输TCP或者无链接的数据报传输UDP。
网络通信只有两类:可靠字节流(TCP,SOCK_STREAM)+ 尽最大努力数据报(UDP,SOCK_DGRAM)。
- SOCK_STREAM: 流式, TCP
- SOCK_DGRAM: 广播, UDP
- SOCK_RAW: 原始协议
OSI七层模型
- 应用层:如HTTP, FTP, WebSocket等。
- 表示层:负责数据格式化、加密解密等。
- 会话层:管理应用程序之间的会话。
- 传输层:TCP,UDP。
- 网络层:IP,ICMP等。
- 数据链路层:以太网,网卡等。
- 物理层:物理线路,光纤等。
OSI五层模型
- 应用层:整合了OSI模型中的应用层、表示层和会话层。
- 传输层:TCP,UDP。
- 网络层:IP,ICMP等。
- 数据链路层:网卡,交换机等。
- 物理层:物理媒体,如光纤。
粘包/分包
粘包:多个数据包被合并成一个大的数据包发送或接收
分包:一个数据包被拆分成多个小的数据包发送或接收
长度前缀法(最常用,示例项目中也是这么用的)
// 发送
public static void SendMessage(NetworkStream stream, string message)
{
byte[] data = Encoding.UTF8.GetBytes(message);
byte[] lengthBytes = BitConverter.GetBytes(data.Length);
// 确保网络字节序(大端)
if (BitConverter.IsLittleEndian)
{
Array.Reverse(lengthBytes);
}
// 发送长度前缀
stream.Write(lengthBytes, 0, 4);
// 发送数据
stream.Write(data, 0, data.Length);
}
// 接收
public static string ReceiveMessage(NetworkStream stream)
{
byte[] lengthBytes = new byte[4];
int bytesRead = stream.Read(lengthBytes, 0, 4);
if (bytesRead != 4) return null;
// 转换为主机字节序
if (BitConverter.IsLittleEndian)
{
Array.Reverse(lengthBytes);
}
int length = BitConverter.ToInt32(lengthBytes, 0);
// 接收数据
byte[] buffer = new byte[length];
bytesRead = 0;
while (bytesRead < length)
{
int read = stream.Read(buffer, bytesRead, length - bytesRead);
if (read == 0) return null;
bytesRead += read;
}
return Encoding.UTF8.GetString(buffer);
}
分隔符法
使用 \r\n\r\n,假设数据中有这个怎么办呢?
或者自定义–END–
A5 22 33 44 5A
使用A5作为数据开头,5A作为数据结尾,缺点就是传输的数据里面可能有数据开头或者结尾,这时候需要进行转移,将数据中的结束位和开始位进行转义。
// 发送
void SendWithDelimiter(Socket socket, string message)
{
byte[] data = Encoding.UTF8.GetBytes(message + "|END|");
socket.Send(data);
}
// 接收
string ReceiveByDelimiter(Socket socket)
{
byte[] buffer = new byte[1024];
int bytesRead = socket.Receive(buffer);
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
int endIndex = data.IndexOf("|END|");
return endIndex >= 0 ? data.Substring(0, endIndex) : data;
}
固定长度法
适合固定格式的协议,每次传输的协议长度固定。
每次都发送长度是一样的。
A5 11 22 33 44 5A
每次都发送相同长度的数据,解析的时候,根据不同的位进行解析。例如11是操作符,不同的操作符对数据进行不同的解析。
例如,不同的数据使用使用不同的解析方式,然后空位使用FF 补充,这个需要双方约定。
A5 12 22 43 FF 5A
A5 13 22 FF FF 5A
// 发送
void SendFixedLength(Socket socket, string message)
{
byte[] data = Encoding.UTF8.GetBytes(message.PadRight(FIXED_LENGTH, '\0'));
socket.Send(data);
}
// 接收
string ReceiveFixedLength(Socket socket)
{
byte[] buffer = new byte[FIXED_LENGTH];
int totalRead = 0;
while (totalRead < FIXED_LENGTH)
{
int bytesRead = socket.Receive(buffer, totalRead, FIXED_LENGTH - totalRead, SocketFlags.None);
if (bytesRead == 0) break;
totalRead += bytesRead;
}
return Encoding.UTF8.GetString(buffer).TrimEnd('\0');
}
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect("127.0.0.1", 8888);
// 发送消息
SendWithDelimiter(clientSocket, "Hello Server");
SendFixedLength(clientSocket, "Fixed Data");
连接
Socket ≈ 打电话(建立连接后可以直接说话)
HTTP ≈ 发邮件(一发一收,每次都要写格式)
WebSocket ≈ 打电话,但先发短信确认身份,然后畅聊
socket连接(tcp)
四次握手三次挥手
客户端 服务器
|----SYN----->|
|<---SYN+ACK--|
|----ACK----->|
连接建立,双向通信
|----FIN----->|
|<---ACK------|
|<---FIN------|
|----ACK----->|
- 保持长连接
- 双向实时通信
- 需要手动管理连接状态
http连接
过程:TCP三次握手 → HTTP请求 → HTTP响应 → TCP四次挥手
四次握手三次挥手,因为http是基于socket的,所以也需要实现,请求的时候四次握手,然后发送数据,获取到数据之后,挥手完成请求。
客户端 服务器
|---TCP握手--->|(三次握手)
|---GET/POST-->|(HTTP请求头+体)
|<---200 OK----|(HTTP响应头+体)
|---TCP挥手--->|(四次挥手)
每次请求都重复此过程(HTTP/1.0)
- 短连接(HTTP/1.0)或长连接(HTTP/1.1 Keep-Alive)
- 客户端主动发起,服务器响应
- 无状态,每个请求独立
websocket连接
过程:HTTP握手 → 协议升级 → WebSocket通信
四次握手三次挥手,因为http是基于socket的,所以也需要实现,请求的时候四次握手,然后发送数据,获取到数据之后,确认up头,更新完成成之后转成socket通信。
客户端 服务器
|---TCP握手--->| // 1. 底层TCP三次握手建立连接
| |
|---HTTP握手--->| // 2. WebSocket握手(HTTP协议)
|<--HTTP响应----| // 服务器响应升级协议
| |
|===WebSocket===>| // 3. 升级为WebSocket协议通信
|<==双向数据=====| // 全双工数据传输
| |
|--Close帧----->| // 4. WebSocket关闭握手
|<--Close帧-----| // 双向确认关闭
| |
|---TCP挥手--->| // 5. 底层TCP四次挥手断开连接
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 随机base64编码的16字节值
Sec-WebSocket-Version: 13
Origin: http://example.com
Upgrade: websocket - 请求升级协议
Connection: Upgrade - 连接升级
Sec-WebSocket-Key - 随机密钥,用于安全性验证
Sec-WebSocket-Version: 13 - 协议版本
mqtt连接
过程:TCP连接 → MQTT握手 → 发布/订阅
客户端 服务器
|---TCP SYN--->| // 1. TCP三次握手
|<--SYN+ACK----|
|---TCP ACK--->|
|--CONNECT---->| // 2. MQTT连接请求
|<-CONNACK-----| // 3. 连接确认
|===通信阶段===| // 发布/订阅消息
|--DISCONNECT->| // 4a. 优雅断开(MQTT层)
| |
|---TCP FIN--->| // 5. TCP四次挥手(传输层)
|<---TCP ACK---|
|<---TCP FIN---|
|---TCP ACK--->|
- 轻量级,适合IoT设备
- 基于发布/订阅模式
- 支持QoS(服务质量等级)
- 心跳机制保持连接
心跳
心跳(Heartbeat) 是客户端定期向服务器发送的小数据包,目的是:
- 保活连接:告诉服务器"我还活着"
- 检测连接状态:及时发现断开的连接
- 防止超时断开:避免防火墙/NAT超时清理连接
- 网络质量检测:通过响应时间判断网络状况
客户端 服务器
|---心跳包---->| // 定期发送
|<---响应------| // 服务器确认
(等待一段时间)
|---心跳包---->| // 再次发送
|<---响应------|
...循环...
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
public class HeartbeatManager
{
private Socket socket;
private Timer heartbeatTimer;
private DateTime lastResponseTime;
private Thread receiveThread;
public void StartHeartbeat()
{
// 每30秒发送一次心跳
heartbeatTimer = new Timer(SendHeartbeat, null, 0, 30000);
// 启动接收线程
receiveThread = new Thread(ReceiveMessages);
receiveThread.IsBackground = true;
receiveThread.Start();
}
private void SendHeartbeat(object state)
{
try
{
// 发送心跳包
byte[] heartbeat = Encoding.UTF8.GetBytes("HEARTBEAT");
socket.Send(heartbeat);
// 检查上次响应时间
if ((DateTime.Now - lastResponseTime).TotalSeconds > 90)
{
Reconnect(); // 90秒没响应,重连
}
}
catch (SocketException)
{
Reconnect(); // 发送失败,重连
}
}
private void ReceiveMessages()
{
byte[] buffer = new byte[1024];
while (true)
{
try
{
int bytesRead = socket.Receive(buffer);
if (bytesRead > 0)
{
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
if (message == "HEARTBEAT_RESPONSE")
{
OnHeartbeatResponse(); // 心跳响应
}
else
{
ProcessMessage(message); // 业务消息
}
}
}
catch (SocketException)
{
break; // 连接异常,退出接收循环
}
}
}
public void OnHeartbeatResponse()
{
lastResponseTime = DateTime.Now; // 更新响应时间
}
private void ProcessMessage(string message)
{
lastResponseTime = DateTime.Now; // 收到任何消息都更新响应时间
// 处理业务逻辑...
}
private void Reconnect()
{
// 重连逻辑...
}
}
大端/小端
在网络传输中统一使用大端传输。
什么是高低位字?
32位整数:0x12345678
高位字(High Word):0x1234(高16位)
低位字(Low Word):0x5678(低16位)
64位整数:0x123456789ABCDEF0
高位双字(High Dword):0x12345678(高32位)
低位双字(Low Dword):0x9ABCDEF0(低32位)
大端(Big Endian)
高位字节在低地址,低位字节在高地址
就像正常阅读数字:0x12345678 → 内存:12 34 56 78
网络字节序标准
bool isLittleEndian = BitConverter.IsLittleEndian;
Console.WriteLine($“系统是端序.true大,false小: {isLittleEndian}”);
小端(Little Endian)
低位字节在低地址,高位字节在高地址
x86架构默认:0x12345678 → 内存:78 56 34 12
bool isLittleEndian = BitConverter.IsLittleEndian;
Console.WriteLine($"系统是端序.true大,false小: {isLittleEndian}");
转换方式
using System.Net;
// 系统提供的转换(注意:只支持short/int)
short hostShort = 0x1234;
short networkShort = IPAddress.HostToNetworkOrder(hostShort);
short backToHost = IPAddress.NetworkToHostOrder(networkShort);
int hostInt = 0x12345678;
int networkInt = IPAddress.HostToNetworkOrder(hostInt);
int backToInt = IPAddress.NetworkToHostOrder(networkInt);
有状态和无状态
有状态(statelful)
一个有状态的系统会在执行某个操作时,将当前操作的上下文和状态记录下来。这些上下文和状态信息可以用来支持更复杂的操作,比如说处理多个请求,或者在不同的时间点上执行一系列的操作。在这种系统中,用户的每个请求都会被认为是不同的,并且需要针对每个请求单独维护状态信息。就像是AI 一样会联系上下文。用户的多个请求是相关联的,服务器能识别这是同一个用户的连续请求。
例如
- 传统Session(如PHP Session、Tomcat Session)
- TCP连接
- 数据库连接池
- SSH连接
无状态(stateless)
相反,一个无状态的系统不会维护任何状态信息,它会处理每个请求并给出一个结果。在这种系统中,所有请求都是相同的,并且没有任何请求在上下文上具有优劣之分。这种系统通常更加简单和可扩展,因为它不需要维护额外的状态信息。不会联系上下文,每次请求都是一个独立的请求。
例如
- RESTful API(理想情况)
- HTTP协议(本身是无状态的)
- 静态文件服务器
现实中的混合使用
HTTP的无状态 + 状态管理技巧
Cookie-Session机制:
客户端请求 → 服务器生成Session ID → 存状态到服务器 → 返回Session ID
后续请求携带Session ID → 服务器读取对应状态
JWT(JSON Web Token):
// 无状态认证示例
// 请求头中携带自包含的token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// token包含所有必要信息,服务器无需存储状态
其他
魔数(Magic Number)
在计算机领域,“魔数”(Magic Number)通常指用于标识文件格式、协议类型或数据结构的特定字节序列。它是一种约定俗成的“签名”,用于快速判断一段数据是否符合预期格式。
在编程中,“魔数”一词也有另一层含义——指代码中出现但未加解释的硬编码数字或字符串(如 if (status == 42)),这类“魔法值”应尽量避免,推荐使用具名常量代替。
在自定义通信协议时,我们也可以在消息头部加入一个固定的“魔数”,用于校验消息合法性、防止误解析或抵御非法连接。
文件格式魔数
| 文件格式 | 魔数值(十六进制) | ASCII表示 | 说明 |
|---|---|---|---|
| JPEG图像 | FF D8 | ÿØ | 文件开头两个字节标识JPEG格式 |
| PNG图像 | 89 50 4E 47 0D 0A 1A 0A | .PNG.... | 八个字节的固定签名 |
| GIF图像 | 47 49 46 38 39 61 | GIF89a | GIF89a格式 |
47 49 46 38 37 61 | GIF87a | GIF87a格式 | |
| ZIP压缩文件 | 50 4B 03 04 | PK.. | ZIP文件标准签名 |
| PDF文档 | 25 50 44 46 2D | %PDF- | PDF文件开头标识 |
| BMP图像 | 42 4D | BM | Windows位图文件 |
| ELF可执行文件 | 7F 45 4C 46 | .ELF | Unix/Linux可执行文件格式 |
数据结构魔数
| 结构类型 | 魔数值 | 说明 |
|---|---|---|
| Java类文件 | 0xCAFEBABE | Java虚拟机识别.class文件的标志 |
| Mach-O可执行文件 | 0xFEEDFACE | 32位macOS可执行文件 |
0xFEEDFACF | 64位macOS可执行文件 | |
| SQLite数据库 | 53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 | SQLite format 3 + null终止符 |
特定条件魔数
| 场景 | 位置 | 魔数值 | 作用 |
|---|---|---|---|
| x86 MBR引导扇区 | 第511-512字节 | 0x55 0xAA | 标识有效的主引导记录 |
| FAT文件系统引导扇区 | 偏移0x1FE-0x1FF | 0x55 0xAA | 标识有效的引导扇区 |
| Java对象序列化流 | 开头四个字节 | 0xACED0005 | Java序列化流的起始标识 |
编程语言和框架魔数
| 类型 | 示例值 | 说明 |
|---|---|---|
| Python .pyc文件 | 0x16 0x0D 0x0D 0x0A | Python字节码文件的魔数,用于版本兼容性检查 |
协议魔数
| 协议 | 魔数/标识 | 说明 |
|---|---|---|
| HTTP协议 | HTTP/ | 响应开头标识协议版本,如HTTP/1.1 |
| TLS/SSL协议 | 0x16 0x03 | ClientHello消息的前两个字节,标识TLS版本和握手类型 |
1472

被折叠的 条评论
为什么被折叠?



