简介:本资源是一套面向C#初学者与网络编程入门者的TCP/IP通信实战例程,聚焦客户端与服务端双向通信的核心实现,帮助开发者快速掌握Socket编程基础。压缩包共55个文件,包含12个核心C#源码文件(如Program.cs、ClientFrm.cs、ServerFrm.cs)、8个可直接运行的exe程序(客户端.exe、服务端.exe等)、2个Visual Studio解决方案(.sln)及配套项目文件(.csproj、.resx、.settings等),另有txt说明文档与图标资源,结构完整,开箱即用。资源包仅450KB,轻量简洁,便于本地快速编译调试。已有206人学习下载,所有代码均基于System.Net命名空间原生Socket与TcpClient/TcpListener封装,涵盖IP地址绑定、连接监听、数据收发、异常处理等关键环节,并体现典型WinForm界面交互逻辑,适合作为教学示例、课程实验或自学练手项目。

1. 一个能立刻编译、运行、抓包验证的 TCP 通信最小闭环:不依赖任何框架,纯 .NET 原生 Socket,3 分钟跑通客户端 ↔ 服务端双向收发
你有没有试过:在 Visual Studio 里新建一个控制台项目,敲完 TcpClient 和 TcpListener 代码,一运行就卡在 Connect() 或 AcceptTcpClient() 死等?或者好不容易连上了,发过去的数据在服务端 Read() 出来全是乱码、截断、粘包,调试半天发现 NetworkStream.Read() 返回值没判零、缓冲区没复位、线程没同步……最后干脆扔进角落吃灰?这个「TCP/IP C#最简单例程」就是为这种场景而生的——它不是教学 PPT 里的伪代码,也不是 GitHub 上动辄上千行的“完整示例”,而是一个 删掉所有业务逻辑、只保留 socket 生命周期核心路径 的可执行体。它用最直白的 byte[] 缓冲区、显式 Encoding.UTF8.GetBytes() / GetString() 、手动 stream.Read() 循环 + bytesRead > 0 判定、 client.Close() 后立即 GC.Collect() 防资源泄漏,把 TCP 连接建立、数据发送、接收解析、连接关闭这四个动作,压缩进两个不到 100 行的 .cs 文件里。适合刚学完《C# 网络编程》第 5 章、想亲手看到 SYN → SYN-ACK → ACK 在 Wireshark 里跳出来的开发者;也适合需要快速搭个本地测试桩(mock server)验证前端 HTTP 客户端行为的全栈同学。它不讲三次握手原理,但你跑起来后抓包一看就懂;它不封装异步模型,但每一行 BeginAccept / EndAccept 调用都带着注释说明为什么这里不能用 await (因为这是最简同步模型)。现在,打开 VS,新建项目,复制粘贴,F5 —— 你离第一个真正“活”的 TCP 连接,只剩一次编译。
2. 项目结构与技术选型:为什么是 .NET Framework 4.7.2 + 同步 Socket 模型?
这个例程没有 csproj 文件里花里胡哨的 <PackageReference> ,没有 Program.cs 外的任何 .dll 引用,甚至不需要安装 .NET SDK —— 只要你的系统装了 Windows 10 自带的 .NET Framework 4.7.2(或更高),就能双击 build.bat 直接生成可执行文件。这不是怀旧,而是刻意为之:同步 Socket 模型是理解 TCP 底层行为的“透明玻璃罩”。当你用 await client.ConnectAsync() 时,异常堆栈会藏在 Task 内部;而用 client.Connect("127.0.0.1", 8080) ,一旦超时, SocketException 的 ErrorCode (比如 10061)直接打在控制台,你马上知道是服务端没启、端口被占、还是防火墙拦了。本项目采用最朴素的“一连接一线程”模型(非 I/O Completion Port),原因有三:第一,它让 Thread.Sleep(100) 这种“玄学调试法”变得合法且有效——你可以在线程里加断点,亲眼看着 stream.Read() 怎么从阻塞到返回 5 字节;第二,它规避了 async/await 在控制台程序中容易引发的 SynchronizationContext 陷阱(比如 Console.WriteLine() 在后台线程报错);第三,它和 Wireshark 抓包行为完全对齐:你每 Write() 一次,Wireshark 就抓到一个独立的 TCP segment,而不是被 SendAsync() 合并成的大包。项目目录极简,只有 4 个文件:
/TCP-Simple-Example/
├── Client.cs // 主程序入口,含 Main(),创建 TcpClient,连接、发送、接收
├── Server.cs // 主程序入口,含 Main(),创建 TcpListener,监听、接受、读写
├── build.bat // 一行命令调用 csc.exe 编译,无 MSBuild 依赖
└── README.md // 仅两行:如何运行、常见端口冲突提示
build.bat 内容如下,关键在于 /target:exe 和 /reference 指向 GAC 中的 System.dll :
@echo off
csc /target:exe /out:Client.exe /reference:"C:WindowsMicrosoft.NETFrameworkv4.0.30319System.dll" Client.cs
csc /target:exe /out:Server.exe /reference:"C:WindowsMicrosoft.NETFrameworkv4.0.30319System.dll" Server.cs
echo 编译完成!运行 Server.exe 后,再运行 Client.exe
pause
提示:如果你用的是 .NET 5+,请将
csc替换为dotnet build -c Release -r win-x64,但注意TcpListener构造函数参数需从IPAddress.Any改为IPAddress.Loopback(因 .NET 5+ 默认禁用0.0.0.0绑定)。本例程默认适配传统桌面环境,避免新手陷入跨版本兼容性黑匣子。
2.1 Client.cs 核心逻辑:三次握手后的第一声“Hello”
Client.cs 的 Main() 方法是典型的“连接 → 发送 → 接收 → 关闭”四步链。我们逐段拆解其关键代码,重点看那些教科书不会写、但实际必踩的坑:
static void Main(string[] args)
{
try
{
// 【关键1】显式指定 IP 和端口,不依赖 DNS 解析,避免 "host not found" 异常
var client = new TcpClient();
Console.WriteLine("正在连接到 127.0.0.1:8080...");
client.Connect(IPAddress.Loopback, 8080); // ← 这里必须用 IPAddress.Loopback,不是 "localhost"
Console.WriteLine("连接成功!");
// 【关键2】获取 NetworkStream 并设置写入超时,防止服务端崩溃时客户端无限 hang
using (var stream = client.GetStream())
{
stream.WriteTimeout = 5000; // 单位毫秒
// 【关键3】构造 UTF8 字节数组,长度 + 数据一起发,解决粘包最原始方案
string message = "Hello from Client";
byte[] data = Encoding.UTF8.GetBytes(message);
byte[] lengthPrefix = BitConverter.GetBytes(data.Length); // 4字节 int32
if (BitConverter.IsLittleEndian) Array.Reverse(lengthPrefix); // 网络字节序:大端
// 【关键4】先发长度,再发内容,服务端按此协议解析
stream.Write(lengthPrefix, 0, lengthPrefix.Length);
stream.Write(data, 0, data.Length);
// 【关键5】读取响应:先读4字节长度,再按长度读内容
byte[] recvLenBuf = new byte[4];
int totalRead = 0;
while (totalRead < 4)
{
int bytesRead = stream.Read(recvLenBuf, totalRead, 4 - totalRead);
if (bytesRead == 0) throw new IOException("服务端提前关闭连接");
totalRead += bytesRead;
}
int responseLength = BitConverter.ToInt32(recvLenBuf, 0);
if (BitConverter.IsLittleEndian) Array.Reverse(recvLenBuf);
responseLength = BitConverter.ToInt32(recvLenBuf, 0);
byte[] responseData = new byte[responseLength];
totalRead = 0;
while (totalRead < responseLength)
{
int bytesRead = stream.Read(responseData, totalRead, responseLength - totalRead);
if (bytesRead == 0) throw new IOException("服务端响应不完整");
totalRead += bytesRead;
}
string response = Encoding.UTF8.GetString(responseData);
Console.WriteLine($"服务端回复:{response}");
}
}
catch (SocketException ex) when (ex.ErrorCode == 10061)
{
Console.WriteLine($"错误:无法连接到服务端。请先运行 Server.exe。错误码:{ex.ErrorCode}");
}
catch (Exception ex)
{
Console.WriteLine($"运行异常:{ex.Message}");
}
finally
{
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
}
这段代码的“血泪经验”在于: 它把 TCP 的不可靠性全部暴露给你 。 stream.Read() 不保证一次读完所有数据,所以必须用 while 循环累加 bytesRead ; BitConverter 默认小端,而网络协议要求大端,所以必须 Array.Reverse() ; client.Connect() 的地址不能写 "localhost" ,因为某些系统 hosts 会把它解析成 ::1 (IPv6),而服务端只监听 IPv4 的 127.0.0.1 ,导致连接失败。这些细节,不是“应该知道”,而是“不写就会翻车”。
2.2 Server.cs 核心逻辑:监听、接受、解析、回写,一个都不能少
Server.cs 的 Main() 方法是服务端的完整生命周期。它比客户端多一层 AcceptTcpClient() 的阻塞等待,这也是新手最容易卡住的地方——你以为程序“卡死”了,其实是它在安静地等连接。我们聚焦其核心循环:
static void Main(string[] args)
{
TcpListener listener = null;
try
{
// 【关键1】绑定到 127.0.0.1:8080,明确指定 IPv4,避免 IPv6 冲突
listener = new TcpListener(IPAddress.Loopback, 8080);
listener.Start();
Console.WriteLine("服务端已启动,监听 127.0.0.1:8080...");
while (true) // 持续接受新连接
{
Console.WriteLine("等待客户端连接...");
TcpClient client = listener.AcceptTcpClient(); // ← 这里阻塞,直到有连接
Console.WriteLine($"收到连接:{client.Client.RemoteEndPoint}");
// 【关键2】为每个连接开新线程处理,避免阻塞主监听线程
Thread handlerThread = new Thread(() => HandleClient(client));
handlerThread.Start();
}
}
catch (Exception ex)
{
Console.WriteLine($"服务端异常:{ex.Message}");
}
finally
{
listener?.Stop();
Console.WriteLine("服务端已停止。");
}
}
static void HandleClient(TcpClient client)
{
try
{
using (client) // 确保 client.Close() 在作用域结束时调用
using (var stream = client.GetStream())
{
stream.ReadTimeout = 10000;
// 【关键3】按客户端协议:先读4字节长度
byte[] lenBuf = new byte[4];
int totalRead = 0;
while (totalRead < 4)
{
int bytesRead = stream.Read(lenBuf, totalRead, 4 - totalRead);
if (bytesRead == 0) return; // 客户端断开
totalRead += bytesRead;
}
int msgLength = BitConverter.ToInt32(lenBuf, 0);
if (BitConverter.IsLittleEndian) Array.Reverse(lenBuf);
msgLength = BitConverter.ToInt32(lenBuf, 0);
// 【关键4】再读指定长度的消息体
byte[] msgBytes = new byte[msgLength];
totalRead = 0;
while (totalRead < msgLength)
{
int bytesRead = stream.Read(msgBytes, totalRead, msgLength - totalRead);
if (bytesRead == 0) return;
totalRead += bytesRead;
}
string receivedMsg = Encoding.UTF8.GetString(msgBytes);
Console.WriteLine($"收到消息:{receivedMsg}");
// 【关键5】构造响应:"Echo: " + 原消息,并同样加长度前缀
string echoMsg = $"Echo: {receivedMsg}";
byte[] echoBytes = Encoding.UTF8.GetBytes(echoMsg);
byte[] echoLen = BitConverter.GetBytes(echoBytes.Length);
if (BitConverter.IsLittleEndian) Array.Reverse(echoLen);
stream.Write(echoLen, 0, echoLen.Length);
stream.Write(echoBytes, 0, echoBytes.Length);
}
}
catch (IOException ex) when (ex.InnerException is SocketException se && se.ErrorCode == 10054)
{
// 客户端强制关闭连接,忽略
Console.WriteLine("客户端异常断开。");
}
catch (Exception ex)
{
Console.WriteLine($"处理客户端时出错:{ex.Message}");
}
}
这里的关键设计是: 每个 TcpClient 实例由独立线程处理,且 using 语句确保 client.Close() 和 stream.Close() 必然执行 。很多初学者把 HandleClient 写成同步方法却忘记 Thread.Start() ,结果整个服务端只能服务一个客户;或者忘了 using ,导致 client 对象一直占用 socket 句柄,重启服务端时报“地址已在使用中”。这段代码还特意捕获 ErrorCode 10054 (Connection reset by peer),这是客户端突然关机或 kill -9 时的典型错误,不处理会导致服务端线程崩溃退出。
2.3 避坑:新手必踩的 4 个 TCP 同步模型雷区
注意:以下问题均来自真实调试记录,非理论假设。每一条都对应一个
Console.WriteLine("我在这里卡了3小时")的深夜。
现象 1: client.Connect() 报错 “No connection could be made because the target machine actively refused it”(错误码 10061)
原因 :服务端进程未运行,或运行了但监听地址不是 127.0.0.1 (比如用了 IPAddress.Any 但防火墙阻止了外部访问,或用了 "localhost" 导致 IPv6 解析失败)。
解决 :先运行 Server.exe ,确认控制台输出 “服务端已启动,监听 127.0.0.1:8080…”;检查 Server.cs 中 new TcpListener(IPAddress.Loopback, 8080) 是否写成了 IPAddress.Any 或 "localhost" 。
现象 2:客户端 stream.Read() 一直阻塞,Wireshark 显示只有 SYN 包,没有后续 ACK
原因 :服务端 listener.AcceptTcpClient() 未被调用,或调用后未进入 HandleClient 的 stream.Read() 循环(比如线程没 Start() ,或 HandleClient 方法体为空)。
解决 :在 listener.AcceptTcpClient() 后加 Console.WriteLine("已接受连接"); ;在 HandleClient 开头加 Console.WriteLine("开始处理客户端"); ;确保 Thread handlerThread = new Thread(...); handlerThread.Start(); 这行代码确实执行了。
现象 3:服务端收到消息是乱码,或长度读出来是极大负数(如 -858993460)
原因 : BitConverter.ToInt32() 时未处理字节序。 BitConverter 默认按本机字节序(x86/x64 是小端),而网络传输要求大端(Big-Endian), Array.Reverse() 忘了加,或加错了位置(比如只 reverse 了长度数组,没 reverse 回写时的响应长度)。
解决 :统一在 BitConverter.GetBytes() 后、 stream.Write() 前加 if (BitConverter.IsLittleEndian) Array.Reverse(byteArray); ;在 BitConverter.ToInt32() 前,先对读到的 4 字节数组 Array.Reverse() 。
现象 4:客户端运行一次后,再次运行报错 “Only one usage of each socket address is normally permitted”(错误码 10048)
原因 :上次运行的 Client.exe 或 Server.exe 进程未完全退出,socket 处于 TIME_WAIT 状态(默认 4 分钟),端口被占用。
解决 :任务管理器结束所有 Client.exe 和 Server.exe 进程;或修改 Server.cs 中端口号为 8081 ,重新编译运行;长期开发建议在 finally 块中加 listener?.Server?.Close() (但本例程为求极简,未包含)。
3. 编译与运行全流程:从零开始,5 分钟见证第一个 TCP 数据包
别急着写代码,先确保环境干净。本流程基于 Windows 10/11,默认已安装 .NET Framework 4.7.2+(几乎所有现代 Windows 都自带)。全程无需管理员权限,无需修改注册表,不碰防火墙设置。
3.1 准备工作:创建项目目录,确认编译器可用
打开任意文件夹(比如 D:tcp-demo ),新建文本文件,重命名为 Client.cs ,用记事本或 VS Code 打开,粘贴上文 Client.cs 全部代码。同目录下再建 Server.cs ,粘贴 Server.cs 代码。最后建 build.bat ,粘贴其内容。完成后,目录应如下:
D:tcp-demo
├── Client.cs
├── Server.cs
└── build.bat
然后, 以管理员身份运行 CMD 或 PowerShell 是不必要的,但必须确认 csc.exe 在系统 PATH 中 。打开 CMD,输入:
csc /?
如果看到 C# 编译器帮助信息,说明 OK;如果提示“不是内部或外部命令”,说明 .NET Framework 未正确注册 PATH。此时有两种选择:一是去 C:WindowsMicrosoft.NETFrameworkv4.0.30319 目录下直接运行 csc.exe (把 build.bat 里 csc 改成完整路径);二是更简单的办法——用 Visual Studio Installer 安装 “.NET desktop development” 工作负载,它会自动修复 PATH。
3.2 一键编译:执行 build.bat,生成两个 .exe
双击 build.bat ,或在 CMD 中进入 D:tcp-demo 目录后运行:
build.bat
你会看到类似输出:
Microsoft (R) Visual C# Compiler version 4.8.4084.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
Microsoft (R) Visual C# Compiler version 4.8.4084.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
编译完成!运行 Server.exe 后,再运行 Client.exe
请按任意键继续. . .
此时目录下会出现 Client.exe 和 Server.exe 两个文件,大小各约 4KB。这就是全部产物,没有 bin/ 、 obj/ 文件夹,没有 NuGet 缓存,干净得像刚格式化过的硬盘。
3.3 启动服务端:观察监听状态与连接日志
双击 Server.exe ,或在 CMD 中运行:
Server.exe
控制台输出:
服务端已启动,监听 127.0.0.1:8080...
等待客户端连接...
此时,服务端已进入 listener.AcceptTcpClient() 的阻塞状态,它在安静地等待 TCP 连接请求。你可以用 netstat 验证:
netstat -ano | findstr :8080
应看到一行类似:
TCP 127.0.0.1:8080 0.0.0.0:0 LISTENING 12345
其中 12345 是 Server.exe 的 PID。这证明端口已被成功绑定。
3.4 启动客户端:触发连接、发送、接收全过程
保持 Server.exe 窗口开着, 另开一个 CMD 窗口 ,进入同一目录,运行:
Client.exe
客户端控制台输出:
正在连接到 127.0.0.1:8080...
连接成功!
服务端回复:Echo: Hello from Client
按任意键退出...
同时, Server.exe 窗口会追加输出:
收到连接:127.0.0.1:54321
收到消息:Hello from Client
(端口号 54321 是客户端随机分配的临时端口,每次不同)
至此,一个完整的 TCP 请求-响应闭环已完成。你不需要任何第三方库,不需要配置文件,甚至不需要 Visual Studio IDE —— 记事本 + CMD 就够了。
3.5 抓包验证:用 Wireshark 看清 SYN/SYN-ACK/ACK 与数据载荷
这是理解 TCP 的终极验证。下载 Wireshark(官网免费),安装后启动,选择 Loopback: Microsoft KM-TEST Loopback Adapter (或 Npcap Loopback Adapter ,如果没看到 Loopback 选项,请先安装 Npcap)。点击左上角蓝色鲨鱼图标开始捕获。
然后,按上述步骤运行 Server.exe 和 Client.exe 。Wireshark 会实时显示数据包。过滤只看本例程流量,在过滤栏输入:
ip.addr == 127.0.0.1 && tcp.port == 8080
你会清晰看到 3 个关键包:
-
No.1:TCP 54321 → 8080 [SYN] Seq=0 Win=65535 Len=0 MSS=65495 SACK_PERM=1 TSval=123456789 TSecr=0 WS=64 -
No.2:TCP 8080 → 54321 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=65495 SACK_PERM=1 TSval=123456790 TSecr=123456789 WS=64 -
No.3:TCP 54321 → 8080 [ACK] Seq=1 Ack=1 Win=65535 Len=0
这就是三次握手。之后你会看到:
一、分层排障模型(从物理层到应用层)
网络故障排查最忌”乱试”。按 OSI 自底向上:物理层(网卡/线缆/光模块/端口灯)→ 数据链路层(VLAN、MAC、STP、协商速率)→ 网络层(IP、网关、路由、ARP)→ 传输层(TCP/UDP 端口、ACL)→ 应用层(DNS、代理、证书)。先确认链路通断(网卡 up、协商速率正常),再 ping 网关,再 traceroute 看断点在第几跳,最后查应用端口与日志。
二、排障工具箱
- ping / pathping / mtr:看连通性与逐跳丢包延迟。
- traceroute / tracert:定位故障跳点,区分运营商侧还是内网侧。
- Wireshark:抓包看 TCP 重传、RST、DNS 超时、ARP 风暴,可复现 NAT 穿透与三次握手异常。
- 交换机端口:err-disable、CRC 错包、双工不匹配、loop guard 是端口故障高频原因,show interface 看 counters 是第一步。
- 防火墙:iptables/firewalld/nftables/ufw 规则顺序与默认策略决定放行,改前先备份规则、确认 SSH 不会被锁死。
三、工程经验
交换机端口频繁 err-disable 多因 BPDU guard 或线缆接触不良;无线与有线混排时优先固化上联端口;变更防火墙前在测试环境验证并保留回滚命令。运维类故障的闭环是:复现 → 定位 → 临时恢复 → 根因修复 → 复盘沉淀。
相关阅读
本文为转载作品。版权归原作者所有;本站仅作技术学习与方案参考,如涉侵权请联系我们删除。



评论 0
还没有评论,来抢沙发吧。