Socket详解
1、基础概念
(1)概念
(2)通信协议支持
| 特性 |
TCP Socket |
UDP Socket |
| 连接方式 |
面向连接(可靠传输) |
无连接(不可靠) |
| 数据保证 |
有序、不丢失 |
可能乱序、丢失 |
| 适用场景 |
文件传输、网页访问(HTTP) |
视频流、实时游戏 |
| 复杂度 |
高(需维护连接状态) |
低(轻量级) |
(3)应用场景
- 即时通讯工具(如微信、QQ):基于TCP保证消息可靠送达,通过长连接实现实时双向通信。
- Web服务器(HTTP服务):HTTP协议底层依赖TCP Socket,服务器监听80端口处理请求。
- 物联网设备控制:设备作为Socket客户端定时上报数据,服务器远程发送指令。
- 实时数据推送:服务端主动向客户端推送消息(如股票行情),需WebSocket等基于Socket的扩展。
2、通信流程
(1)流程图
(2)服务端
csharp
复制代码
// 初始化Socket
Socket serverSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
// 绑定端口并监听
IPEndPoint localEP = new IPEndPoint(IPAddress.Any, 9000);
serverSocket.Bind(localEP);
serverSocket.Listen(10); // 允许10个连接排队
// 异步接受连接
serverSocket.BeginAccept(new AsyncCallback(OnClientConnected), null);
// 处理客户端连接
private void OnClientConnected(IAsyncResult ar) {
Socket clientSocket = serverSocket.EndAccept(ar);
// 启动新线程处理通信
Thread clientThread = new Thread(HandleClient);
clientThread.Start(clientSocket);
}
(3)客户端
csharp
复制代码
Socket clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
// 连接服务器
clientSocket.Connect("127.0.0.1", 9000);
// 异步接收数据
byte[] buffer = new byte[1024];
clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None,
new AsyncCallback(OnDataReceived), buffer);
(4)关键技术
- 异步通信模型
- 原理:使用
BeginAccept/BeginReceive 避免阻塞主线程,通过回调函数处理事件。
- 优势:支持高并发,适用于服务端处理多客户端请求。
- 数据边界处理
- TCP粘包问题:需自定义协议(如消息头声明长度)。
- UDP数据报:天然有边界,但需处理丢包和乱序。
- 跨线程UI更新 :WinForm中必须通过
Control.Invoke 避免线程冲突:
csharp
复制代码
this.Invoke((MethodInvoker)delegate {
txtChatBox.Text += "收到消息: " + message;
});
(5)注意事项
csharp
复制代码
socket.Shutdown(SocketShutdown.Both);
socket.Close();
- 端口占用问题:服务端关闭后需等待2MSL(约1-4分钟)才能复用端口,可通过设置选项解决:
csharp
复制代码
socket.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress, true);
- 数据编码与解析:网络字节序需统一(大端序),文本数据建议用JSON/Protobuf格式化。
- 调试工具推荐
- Wireshark:抓包分析协议细节。
- 日志记录:关键操作(连接/断开/异常)写入日志文件。
- 性能优化
- 使用缓冲区池(Buffer Pool)减少内存分配开销。
- 异步IO配合
SocketAsyncEventArgs 替代APM模型(更高性能)。
3、聊天室功能
(1)项目概述
(2)服务端(控制台)
csharp
复制代码
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Server
{
internal class Program
{
static List<Socket> clientSockets = new List<Socket>();
static readonly object lockObj = new object();
static void Main()
{
const int PORT = 3000;
Socket serverSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
// 绑定端口并监听
IPEndPoint localEP = new IPEndPoint(IPAddress.Any, PORT);
serverSocket.Bind(localEP);
serverSocket.Listen(10);
Console.WriteLine($"服务器启动,监听端口 {PORT}...");
// 异步接受客户端连接
while (true)
{
Socket clientSocket = serverSocket.Accept();
lock (lockObj) clientSockets.Add(clientSocket);
Console.WriteLine($"客户端接入: {clientSocket.RemoteEndPoint}");
// 为每个客户端创建独立线程
Thread clientThread = new Thread(() => HandleClient(clientSocket));
clientThread.Start();
}
}
static void HandleClient(Socket clientSocket)
{
byte[] buffer = new byte[1024];
try
{
while (true)
{
int bytesRead = clientSocket.Receive(buffer);
if (bytesRead == 0) break; // 客户端断开连接
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到消息: {message}");
// 广播消息给所有客户端
BroadcastMessage(message, clientSocket);
}
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
}
finally
{
lock (lockObj) clientSockets.Remove(clientSocket);
clientSocket.Close();
Console.WriteLine($"客户端断开: {clientSocket.RemoteEndPoint}");
}
}
static void BroadcastMessage(string message, Socket senderSocket)
{
byte[] data = Encoding.UTF8.GetBytes(message);
lock (lockObj)
{
foreach (Socket client in clientSockets)
{
if (client != senderSocket && client.Connected)
{
try
{
client.Send(data);
}
catch { /* 忽略发送失败的客户端 */ }
}
}
}
}
}
}
(3)客户端(控制台)
csharp
复制代码
using System.Net.Sockets;
using System.Text;
namespace Client
{
internal class Program
{
static Socket clientSocket;
static void Main()
{
const string SERVER_IP = "127.0.0.1";
const int PORT = 3000;
clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
try
{
clientSocket.Connect(SERVER_IP, PORT);
Console.WriteLine("已连接到服务器,输入消息开始聊天 (输入exit退出)");
// 启动接收线程
Thread receiveThread = new Thread(ReceiveMessages);
receiveThread.IsBackground = true;
receiveThread.Start();
// 主线程处理输入发送
while (true)
{
string input = Console.ReadLine();
if (input.ToLower() == "exit") break;
byte[] data = Encoding.UTF8.GetBytes(input);
clientSocket.Send(data);
}
}
catch (Exception ex)
{
Console.WriteLine($"连接错误: {ex.Message}");
}
finally
{
clientSocket?.Close();
}
}
static void ReceiveMessages()
{
byte[] buffer = new byte[1024];
try
{
while (true)
{
int bytesRead = clientSocket.Receive(buffer);
if (bytesRead == 0) break;
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到: {message}");
}
}
catch
{
Console.WriteLine("与服务器断开连接");
}
}
}
}
(4)核心功能说明
- 服务端机制
- 使用
Accept()阻塞监听客户端连接
- 为每个客户端创建独立线程处理通信
- 通过客户端列表
clientSockets实现消息广播
- 线程锁
lockObj保证多线程安全
- 客户端机制
- 主线程处理用户输入和发送
- 后台线程持续接收服务器消息
- 使用
Encoding.UTF8解决中文乱码问题
- 广播策略
- 遍历所有客户端Socket发送消息
- 跳过消息发送者自身(senderSocket)
- 异常处理避免断开客户端导致崩溃
(5)使用步骤
- 启动多个客户端程序(连接127.0.0.1:3000)
4、大文件传输
(1)项目概述
- 本Socket文件传输程序实现了高效、可靠的文件传输功能,包含三个核心特性:基于SHA256的分片校验机制、断点续传功能以及传输速度优化。
(2)接收端(控制台)
csharp
复制代码
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
namespace FileReceiver
{
internal class Program
{
private const int BufferSize = 64 * 1024; // 64KB 分片大小
private const int Port = 4000;
private static readonly string SavePath = @"D:\ReceivedFiles\";
static void Main()
{
Directory.CreateDirectory(SavePath);
// 创建监听Socket
TcpListener listener = new TcpListener(IPAddress.Any, Port);
listener.Start();
Console.WriteLine($"文件接收服务已启动,监听端口: {Port}");
while (true)
{
// 接受客户端连接
using (TcpClient client = listener.AcceptTcpClient())
using (NetworkStream stream = client.GetStream())
{
Console.WriteLine($"客户端已连接: {client.Client.RemoteEndPoint}");
// 传输速度优化
client.NoDelay = true; // 禁用Nagle算法
client.ReceiveBufferSize = 64 * 1024; // 64KB缓冲区
// 接收文件头信息(文件名+文件大小)
byte[] headerBuffer = new byte[512];
int headerSize = stream.Read(headerBuffer, 0, headerBuffer.Length);
string header = Encoding.UTF8.GetString(headerBuffer, 0, headerSize);
// 解析文件元数据
string[] headerParts = header.Split('|');
if (headerParts.Length < 3)
{
Console.WriteLine("无效文件头格式");
continue;
}
string fileName = headerParts[1];
long fileSize = long.Parse(headerParts[2]);
string filePath = Path.Combine(SavePath, fileName);
// 处理断点续传请求
bool isResume = false;
long resumePosition = 0;
if (headerParts[0] == "RESUME")
{
isResume = true;
resumePosition = long.Parse(headerParts[3]);
Console.WriteLine($"收到断点续传请求: {fileName},从 {resumePosition} 字节处继续接收");
// 检查文件是否存在且大小正确
if (File.Exists(filePath))
{
FileInfo fileInfo = new FileInfo(filePath);
if (fileInfo.Length == resumePosition)
{
// 发送确认消息
byte[] confirmBytes = Encoding.UTF8.GetBytes("OK");
stream.Write(confirmBytes, 0, confirmBytes.Length);
Console.WriteLine($"开始接收: {fileName} ({fileSize}字节),继续从 {resumePosition} 字节处接收");
}
else
{
// 文件大小不匹配,重新开始传输
byte[] rejectBytes = Encoding.UTF8.GetBytes("REJECT");
stream.Write(rejectBytes, 0, rejectBytes.Length);
Console.WriteLine("文件大小不匹配,将重新开始传输");
isResume = false;
}
}
else
{
// 文件不存在,重新开始传输
byte[] rejectBytes = Encoding.UTF8.GetBytes("REJECT");
stream.Write(rejectBytes, 0, rejectBytes.Length);
Console.WriteLine("文件不存在,将重新开始传输");
isResume = false;
}
}
else if (headerParts[0] == "FILE")
{
Console.WriteLine($"开始接收: {fileName} ({fileSize}字节)");
}
else
{
Console.WriteLine("无效的文件头类型");
continue;
}
// 分片接收文件内容
FileMode fileMode = isResume ? FileMode.Append : FileMode.Create;
using (FileStream fs = new FileStream(filePath, fileMode))
using (SHA256 sha256 = SHA256.Create())
{
byte[] buffer = new byte[BufferSize];
long totalReceived = isResume ? resumePosition : 0;
// 如果是断点续传,记录当前文件大小作为起始接收位置
if (isResume)
{
fs.Seek(0, SeekOrigin.End);
}
while (totalReceived < fileSize)
{
// 读取校验和长度
byte[] checksumLengthBytes = new byte[sizeof(int)];
int checksumLengthRead = stream.Read(checksumLengthBytes, 0, sizeof(int));
if (checksumLengthRead != sizeof(int))
{
Console.WriteLine("接收校验和长度失败");
break;
}
int checksumLength = BitConverter.ToInt32(checksumLengthBytes, 0);
// 读取校验和
byte[] receivedChecksum = new byte[checksumLength];
int checksumRead = stream.Read(receivedChecksum, 0, checksumLength);
if (checksumRead != checksumLength)
{
Console.WriteLine("接收校验和失败");
break;
}
// 读取数据长度
byte[] dataLengthBytes = new byte[sizeof(int)];
int dataLengthRead = stream.Read(dataLengthBytes, 0, sizeof(int));
if (dataLengthRead != sizeof(int))
{
Console.WriteLine("接收数据长度失败");
break;
}
int dataLength = BitConverter.ToInt32(dataLengthBytes, 0);
// 读取实际数据
int bytesRead = stream.Read(buffer, 0, dataLength);
if (bytesRead != dataLength)
{
Console.WriteLine("接收数据失败");
break;
}
// 验证校验和
byte[] computedChecksum = sha256.ComputeHash(buffer, 0, bytesRead);
bool checksumMatch = receivedChecksum.SequenceEqual(computedChecksum);
if (!checksumMatch)
{
Console.WriteLine("校验和不匹配,传输错误");
break;
}
// 写入文件
fs.Write(buffer, 0, bytesRead);
totalReceived += bytesRead;
// 显示进度
Console.Write($"\r进度: {totalReceived * 100 / fileSize}%");
}
fs.Flush();
}
Console.WriteLine($"\n文件接收完成: {filePath}");
}
}
}
}
}
(3)发送端(控制台)
csharp
复制代码
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
namespace FileSender
{
internal class Program
{
private const int BufferSize = 64 * 1024; // 64KB 分片大小
private const int Port = 4000;
private const string ServerIP = "127.0.0.1";
static void Main()
{
string filePath = "test_file.txt";
if (!File.Exists(filePath))
{
Console.WriteLine("文件不存在");
return;
}
Console.WriteLine($"使用测试文件: {filePath}");
FileInfo fileInfo = new FileInfo(filePath);
string fileName = fileInfo.Name;
long fileSize = fileInfo.Length;
// 计算分片数量
int totalChunks = (int)Math.Ceiling((double)fileSize / BufferSize);
try
{
using (TcpClient client = new TcpClient(ServerIP, Port))
using (NetworkStream stream = client.GetStream())
{
// 传输速度优化
client.NoDelay = true; // 禁用Nagle算法
client.SendBufferSize = 64 * 1024; // 64KB缓冲区
// 检查是否需要断点续传
string tempFilePath = Path.Combine(Path.GetTempPath(), $"{fileName}.temp");
long startPosition = 0;
if (File.Exists(tempFilePath))
{
// 读取上次传输的位置
using (BinaryReader reader = new BinaryReader(File.OpenRead(tempFilePath)))
{
startPosition = reader.ReadInt64();
}
if (startPosition < fileSize)
{
Console.WriteLine($"检测到未完成的传输,将从 {startPosition} 字节处继续传输");
// 发送断点续传请求
string resumeHeader = $"RESUME|{fileName}|{fileSize}|{startPosition}";
byte[] resumeHeaderBytes = Encoding.UTF8.GetBytes(resumeHeader);
stream.Write(resumeHeaderBytes, 0, resumeHeaderBytes.Length);
// 等待服务器确认
byte[] confirmBuffer = new byte[10];
stream.Read(confirmBuffer, 0, confirmBuffer.Length);
string confirm = Encoding.UTF8.GetString(confirmBuffer).Trim();
if (confirm != "OK")
{
Console.WriteLine("服务器不支持断点续传,将重新开始传输");
startPosition = 0;
}
}
else
{
Console.WriteLine("文件已传输完成,无需再次传输");
return;
}
}
// 如果不是断点续传,则发送完整的文件头
if (startPosition == 0)
{
string header = $"FILE|{fileName}|{fileSize}";
byte[] headerBytes = Encoding.UTF8.GetBytes(header);
stream.Write(headerBytes, 0, headerBytes.Length);
}
Console.WriteLine($"开始发送: {fileName} ({fileSize}字节)");
// 分片发送文件内容
using (FileStream fs = File.OpenRead(filePath))
using (SHA256 sha256 = SHA256.Create())
{
// 如果是断点续传,跳转到上次传输的位置
if (startPosition > 0)
{
fs.Seek(startPosition, SeekOrigin.Begin);
}
byte[] buffer = new byte[BufferSize];
int bytesRead;
long totalSent = startPosition;
// 创建临时文件记录传输进度
using (BinaryWriter progressWriter = new BinaryWriter(File.Open(tempFilePath, FileMode.Create)))
{
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// 增加分片校验机制
byte[] checksum = sha256.ComputeHash(buffer, 0, bytesRead);
stream.Write(BitConverter.GetBytes(checksum.Length), 0, sizeof(int));
stream.Write(checksum, 0, checksum.Length);
stream.Write(BitConverter.GetBytes(bytesRead), 0, sizeof(int));
stream.Write(buffer, 0, bytesRead);
totalSent += bytesRead;
// 保存传输进度
progressWriter.Seek(0, SeekOrigin.Begin);
progressWriter.Write(totalSent);
progressWriter.Flush();
// 显示进度
Console.Write($"\r进度: {totalSent * 100 / fileSize}%");
}
stream.Flush();
}
// 传输完成后删除临时文件
if (totalSent == fileSize && File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
}
Console.WriteLine("\n文件发送完成");
}
}
catch (Exception ex)
{
Console.WriteLine($"传输错误: {ex.Message}");
}
}
}
}
(4)核心功能说明
- 分片校验机制
- 功能说明:使用SHA256加密算法对每个数据分片进行校验,确保文件传输的完整性和可靠性。
- 发送端(FileSender)实现 :
- 在发送每个数据分片前,使用SHA256算法计算该分片的校验和
- 按照固定格式发送数据:先发送校验和长度,然后是校验和本身,接着是数据长度,最后是实际数据内容
- 使用64KB作为分片大小,既保证了传输效率,也确保了校验准确性
- 接收端(FileReceiver)实现 :
- 按照发送端定义的格式接收数据:先接收校验和长度,然后接收校验和,接着接收数据长度,最后接收实际数据
- 使用相同的SHA256算法计算接收到的数据分片的校验和
- 将计算得到的校验和与接收到的校验和进行比对,确保数据完整性
- 如果校验失败,可触发重传机制(当前版本未实现自动重传)
- 代码实现关键点:
csharp
复制代码
// 发送端计算和发送校验和
byte[] checksum = sha256.ComputeHash(buffer, 0, bytesRead);
stream.Write(BitConverter.GetBytes(checksum.Length), 0, sizeof(int));
stream.Write(checksum, 0, checksum.Length);
stream.Write(BitConverter.GetBytes(bytesRead), 0, sizeof(int));
stream.Write(buffer, 0, bytesRead);
// 接收端接收和验证校验和
// 读取校验和长度、校验和、数据长度和实际数据
// 计算接收到数据的校验和并与发送的校验和比对
- 断点续传功能
- 功能说明:在文件传输过程中如果发生中断(如网络故障、程序崩溃等),支持从中断位置继续传输,而无需重新开始。
- 发送端(FileSender)实现 :
- 使用临时文件记录每次传输的进度位置
- 程序启动时检查是否存在未完成的传输任务
- 如果存在未完成传输,发送特殊的RESUME头信息给接收端,包含文件名、总大小和期望的起始位置
- 等待接收端确认是否可以继续传输
- 如果接收端同意继续,从指定位置开始读取和发送文件内容
- 传输完成后自动删除临时进度文件
- 接收端(FileReceiver)实现 :
- 解析接收到的文件头信息,区分普通传输请求(FILE)和断点续传请求(RESUME)
- 对于断点续传请求,检查目标文件是否存在且大小与请求的起始位置匹配
- 根据检查结果,向发送端发送确认(OK)或拒绝(REJECT)响应
- 如果确认继续传输,使用FileMode.Append模式打开文件,并从文件末尾开始写入数据
- 代码实现关键点:
csharp
复制代码
// 发送端发送断点续传请求
string tempFilePath = Path.Combine(Path.GetTempPath(), $"{fileName}.temp");
if (File.Exists(tempFilePath))
{
using (BinaryReader reader = new BinaryReader(File.OpenRead(tempFilePath)))
{
startPosition = reader.ReadInt64();
}
string resumeHeader = $"RESUME|{fileName}|{fileSize}|{startPosition}";
// 发送断点续传请求并等待确认
}
// 接收端处理断点续传请求
if (headerParts[0] == "RESUME")
{
resumePosition = long.Parse(headerParts[3]);
// 检查文件状态并发送确认/拒绝响应
FileMode fileMode = isResume ? FileMode.Append : FileMode.Create;
}
- 传输速度优化
- 功能说明:通过一系列配置优化,显著提高文件传输速度。
- 增大缓冲区大小 :
- 将原有的4KB缓冲区增大到64KB,减少网络交互次数
- 在发送端和接收端统一使用相同的缓冲区大小,确保最佳匹配
- 禁用Nagle算法 :
- Nagle算法会延迟小数据包的发送以提高吞吐量,但会增加延迟
- 在文件传输场景中,禁用Nagle算法可以减少延迟,提高实时性
- 优化Socket缓冲区设置 :
- 显式设置发送和接收缓冲区大小为64KB,确保与分片大小匹配
- 减少操作系统层面的缓冲区管理开销
- 代码实现关键点:
csharp
复制代码
// 传输速度优化配置
client.NoDelay = true; // 禁用Nagle算法
client.SendBufferSize = 64 * 1024; // 64KB发送缓冲区
// 接收端类似设置client.ReceiveBufferSize
(5)技术架构与流程
- 整体架构
- 发送端(FileSender):负责读取本地文件,计算校验和,发送文件数据,并支持断点续传
- 接收端(FileReceiver):负责监听连接,接收文件数据,验证校验和,保存文件,并支持断点续传
- 通信协议:基于TCP协议实现可靠传输,自定义简单协议头格式
- 传输流程
- 连接建立阶段 :
- 接收端启动并监听指定端口
- 发送端连接到接收端
- 双方配置Socket参数(禁用Nagle算法,设置缓冲区大小等)
- 文件头传输阶段 :
- 发送端确定使用普通传输(FILE头)还是断点续传(RESUME头)
- 发送端发送文件头信息
- 接收端解析文件头并做相应处理
- 对于断点续传请求,接收端发送确认/拒绝响应
- 文件内容传输阶段 :
- 发送端以64KB为单位分片读取文件
- 对每个分片计算SHA256校验和
- 按顺序发送校验和长度、校验和、数据长度和数据内容
- 接收端接收并验证每个分片
- 发送端实时保存传输进度
- 传输完成阶段 :
- 发送端完成所有数据发送
- 发送端删除临时进度文件
- 双方关闭连接
(6)使用说明
- 配置与依赖
- 开发环境:.NET Framework/.NET Core
- 依赖库:System.Net.Sockets、System.Security.Cryptography、System.IO
- 端口配置:默认使用4000端口(可在代码中修改Port常量)
- 保存路径:接收端默认保存到D:\ReceivedFiles\(可在代码中修改SavePath常量)
- 运行方法
plain
复制代码
dotnet run --project FileReceiver\FileReceiver.csproj
复制代码
- 然后启动发送端程序:
* 发送端会自动使用项目目录下的test_file.txt作为测试文件进行传输
plain
复制代码
dotnet run --project FileSender\FileSender.csproj
(7)扩展建议
- 实现自动重传机制,在校验失败时自动请求重传
- 添加用户认证机制,增强安全性
- 支持多文件并发传输
- 添加图形用户界面,提高易用性
- 实现传输限速功能,避免占用过多网络带宽