news 2026/11/2 11:39:07

C# TCP/IP 最简单例程:用 TcpListener 与 TcpClient 实现可靠通信

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# TCP/IP 最简单例程:用 TcpListener 与 TcpClient 实现可靠通信

简介:面向C#初学者或刚接触网络编程的开发者,这份压缩包提供了最简的TCP/IP客户端与服务端实现,演示了从创建Socket、指定IPv4地址族与流式套接字,到绑定端口、启动监听、接受连接,以及客户端通过Connect建立连接后利用Send/Receive交换数据的完整链路,适合作为入门练习或后续项目脚手架。包内共55个文件,整体约450KB,以cs源码、exe可执行程序、txt说明和项目配置文件为主,客户端与服务端均包含可直接运行的exe,另附pdb调试文件、resources资源及图标文件,解压后即可对照源码验证通信效果,无需额外配置环境。目前已有206人学习该资源。源码中同时给出了TcpListener与TcpClient两种常用写法,并附有Visual Studio解决方案文件,便于在工程中直接打开调试;对于想进一步理解网络编程的读者,还可参考描述中提到的多线程连接处理、异常捕获与数据编码等扩展方向,从而快速掌握基础通信模块的搭建方法。

1. TCP/IP C# 最简单例程到底解决什么问题:一条跨机器的可靠文本通道

做上位机、自动化设备或者办公小工具的时候,最常遇到的问题不是逻辑复杂,而是两台机器之间传一句话都传不踏实。工控机采集到数据要发给上位机,或者上位机要下发一条指令给设备,很多人的第一个念头就是“TCP/IP”。这个方向没错,TCP/IP 在 C# 里做客户端和服务端,靠内置的TcpListener和TcpClient就能搭出一个最小可用通道,不需要引入第三方框架,也不依赖复杂的通信中间件。

这篇例程针对的就是这类诉求:你手里有一台 Windows 主机,想用它做服务端监听端口,另一台机器(也可能是本机另一个进程)做客户端来连它,互相收发字符串或小数据块。适合刚接触 C# 上位机开发的人,也适合需要快速验证“服务端接口测试”是不是通的从业者。读完你能独立写出一个可运行的客户端和服务端,并且知道连接不上、数据收不全时先查哪里。

2. 先分清 TCP/IP 与 C# 的 API:为什么这块用 TCP,最小验证怎么做

2.1 为什么这类例程默认选 TCP 而不是 UDP

TCP/IP 协议族里,应用层最常用的两个传输层协议是 TCP 和 UDP,但“客户端-服务端传数据”这件事,默认选 TCP。原因是它给出了三个硬承诺:数据按序到达,不丢,不重复。对于控制指令、状态上传这类场景,指令丢了可能造成产线误动作,重复了可能造成重复执行,这两件事的代价都比几十毫秒延迟高得多。

UDP 不是不能做通信,它更适合视频流、游戏同步这种允许丢帧重发的场景。你要做的是一个“最简单例程”,目标是把通信链路跑通,不是调低延迟,所以选 TCP 是稳妥的。C# 里实现 TCP server 和 client 至少有四类 API 可选:最底层的Socket、包装好的TcpListener/TcpClient、更上层的NetworkStream,以及像 ASP.NET Core 那样的承载层框架。最简单例程里我用TcpListener搭档TcpClient,它们把 socket 的创建、绑定、监听、连接过程收敛得很干净,如果你日后要自己控制缓冲区或 IO 行为,随时可以用Socket替换。

2.2 用本机回环跑通最小链路的验证命令

写代码之前,先确认操作系统层面 TCP 通信环境是好的。这一步很重要,因为很多时候代码没问题,是端口被占或者防火墙拦了,结果你在代码里瞎找半天,这属于 TCP 编程最常见的“黑匣子”错觉。先在服务端开一个真实监听端口程序,再用另一条命令去连它,就能把“操作系统网络栈正常”和“C# 代码正常”两件事分开验证。

# 在 PowerShell 中检查端口 9000 是否在监听 netstat -ano | findstr 9000 # 从本机发起一个 TCP 连接到 9000 端口 Test-NetConnection 127.0.0.1 -Port 9000

netstat输出里看到LISTENING说明有进程在 9000 端口等待连接;Test-NetConnection的TcpTestSucceeded输出为True说明 TCP 握手成功。这两条命令可以作为 C# 代码的参照物:命令行能通,代码连不通,问题基本出在代码;命令行也不通,先查端口占用和防火墙。做服务端接口测试时,这套命令比写完整客户端快得多,我一般先用它确认端口状态再动手调试。

2.3 C# 四类 API 的取舍,以及什么时候放弃“最简单”

Socket最灵活但代码量最大,要自己处理Bind、Listen、Accept和异步收发的细节;TcpListener和TcpClient是Socket的友好封装,适合 90% 的业务通信;NetworkStream是TcpClient.GetStream()得到的流对象,负责读写字节;ASP.NET Core 那套承载模型适合做 HTTP/HTTPS 服务,TCP 例程用它有点重。

如果你要传文件、做大并发网关,或者要精确控制每个连接的发送缓冲区,那就别追求最简单,改用Socket并自己管理缓冲区。但如果你只是让客户端和服务端互传文本、小报文,TcpListener加TcpClient是性价比最高的组合。下面两章就给这个组合的最小可运行实现,代码基于 .NET 6 及以上版本的 C# 控制台项目,不需要额外安装 NuGet 包。

3. 服务端怎么写:监听循环、接收连接和第一版消息边界处理

3.1 服务端程序骨架:监听、接受连接、逐个处理

服务端的核心逻辑是“监听一个端口,循环接受客户端连接,为每个连接开一个独立任务处理收发”。下面这段代码是最简版的服务端,它监听0.0.0.0:9000,每来一个客户端就启动一个异步处理任务,收到的消息以文本形式打印出来,并原样回显一条确认文本。

using System.Net; using System.Net.Sockets; using System.Text; TcpListener listener = new TcpListener(IPAddress.Any, 9000); listener.Start(); Console.WriteLine($"服务端已启动,监听 0.0.0.0:9000"); while (true) { TcpClient client = await listener.AcceptTcpClientAsync(); Console.WriteLine($"客户端接入:{client.Client.RemoteEndPoint}"); _ = HandleClientAsync(client); // 不等待,立刻接受下一个连接 } async Task HandleClientAsync(TcpClient client) { using (client) using (NetworkStream stream = client.GetStream()) { byte[] buffer = new byte[1024]; while (true) { int readCount = await stream.ReadAsync(buffer); if (readCount == 0) { Console.WriteLine("客户端主动断开"); break; } string message = Encoding.UTF8.GetString(buffer, 0, readCount); Console.WriteLine($"收到消息:{message}"); byte[] ack = Encoding.UTF8.GetBytes($"ack:{message}"); await stream.WriteAsync(ack); } } }

这段代码里有两个关键点。第一,IPAddress.Any表示服务端监听本机所有网卡 IP,这样局域网内任意一台机器都能通过服务端 IP 访问;如果你只想允许本机连接,改成IPAddress.Loopback(127.0.0.1)即可。第二,AcceptTcpClientAsync配合while (true)构成一个持续接收连接的循环,而_ = HandleClientAsync(client)的意思是“启动一个异步任务,不在当前循环里等它结束”,这是保证服务端能同时服务多个客户端的基础。

3.2 关于半包、粘包:为什么不能把一次 Read 当成一条消息

上面这个例程能跑通,但它有一个明显隐患:ReadAsync返回的字节数不保证就是一条完整消息。TCP 是字节流协议,它的底层只保证字节顺序,不保证“一次写入就是一个完整消息”。客户端一次发送 100 字节,服务端可能一次读回 100 字节,也可能分两次读回 60 和 40;客户端连续发送两条消息,服务端也可能一次读回两条的合并体。常见的术语管前者叫“半包”,后者叫“粘包”,这不是 C# 的问题,是 TCP 本身的传输特性。

如果要做到“收发消息内容准确”,最简单的办法是约定消息边界,比如每条消息以换行符结尾,或者像第 6 章那样加一个固定长度的消息头。在当前这个最简例程里,数据是短文本,一次ReadAsync大概率能覆盖完整一条,但你要知道这只是碰巧够用,不是设计保证。做完这个例程,下一步就该考虑消息帧。

3.3 超时参数:为什么服务端要设置 ReceiveTimeout

网络编程最怕“连接挂着不吐数据”,线程一直阻塞在ReadAsync上。给NetworkStream设置超时时间可以避免无限等待,这是服务端稳定性的重要参数。C# 里和超时相关的属性有两个:ReadTimeout和WriteTimeout,它们作用于同步读写,对异步读写不生效。

TcpClient client = await listener.AcceptTcpClientAsync(); client.ReceiveTimeout = 5000; // 5 秒收不到数据就抛异常 client.SendTimeout = 5000; // 5 秒发不出去就抛异常 client.NoDelay = true; // 关闭 Nagle 算法,降低小包延迟

我一般还会配一个client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true),让 TCP 层定期发送保活探测包,这样客户端异常断电时服务端能在两分钟左右感知连接失效,而不是一直把连接挂在列表里。对最少代码的例程来说,ReceiveTimeout和NoDelay是性价比最高的两个参数,前者防止死等,后者让交互式小消息发得更及时。

4. 客户端怎么写:连接、重试和断开时怎么优雅收尾

4.1 客户端最小代码:连接服务端并发送一条消息

客户端的职责比服务端简单直接:指定服务端 IP 和端口,建立连接,写入数据,读取回包。下面的代码就是完整可运行的最小客户端,服务端跑在上面那个程序上,这个客户端连上去发一条 "hello tcp"。

using System.Net; using System.Net.Sockets; using System.Text; TcpClient client = new TcpClient(); try { await client.ConnectAsync(IPAddress.Parse("127.0.0.1"), 9000); Console.WriteLine("连接成功"); NetworkStream stream = client.GetStream(); byte[] sendData = Encoding.UTF8.GetBytes("hello tcp"); await stream.WriteAsync(sendData); Console.WriteLine("消息已发送"); byte[] buffer = new byte[1024]; int readCount = await stream.ReadAsync(buffer); string response = Encoding.UTF8.GetString(buffer, 0, readCount); Console.WriteLine($"服务端回包:{response}"); } catch (SocketException ex) { Console.WriteLine($"连接失败:{ex.Message}"); } finally { client.Dispose(); }

ConnectAsync会完成三次握手,握手期间如果目标端口没监听,或者防火墙拦了数据包,这里会抛SocketException。注意IPAddress.Parse的地址要和服务端监听地址对应:服务端监听Any,客户端可以写服务端主机的实际 IP,如192.168.1.10;服务端监听Loopback,客户端就只能写127.0.0.1。写死 IP 适合最简单的测试,正式项目中 IP 和端口一般放配置文件里。

4.2 断线重连:连接不在建立那一刻,连接是持续的过程

真实生产环境里没有“连接一次用一辈子”这种好事。服务端重启、网线拔出、网络切换都会让已有连接失效。刚才那个最简客户端在连接成功后不再管断线,这在开机演示里够用,但作为上位机客户端不够。我一般会加一个“断线检测 + 自动重连”的循环,让客户端始终尝试恢复连接。

using System.Net; using System.Net.Sockets; using System.Text; string serverIp = "127.0.0.1"; int port = 9000; TcpClient? client = null; while (true) { try { client ??= new TcpClient(); if (!client.Connected) { Console.WriteLine("正在连接服务端..."); await client.ConnectAsync(IPAddress.Parse(serverIp), port); Console.WriteLine("已连接"); } NetworkStream stream = client.GetStream(); byte[] data = Encoding.UTF8.GetBytes($"heartbeat:{DateTime.Now:HH:mm:ss}"); await stream.WriteAsync(data); byte[] buffer = new byte[256]; int n = await stream.ReadAsync(buffer); if (n == 0) { Console.WriteLine("连接被服务端关闭"); client.Dispose(); client = null; } else { Console.WriteLine($"回包:{Encoding.UTF8.GetString(buffer, 0, n)}"); } } catch (SocketException ex) { Console.WriteLine($"网络异常: {ex.Message},5 秒后重试"); client?.Dispose(); client = null; await Task.Delay(5000); } await Task.Delay(3000); // 每 3 秒发一次心跳 }

这里有个细节:client.Connected只能反映 TCP 连接在操作系统层面是否建立,不能反映对端是否还活着。判断连接是否有效的可靠依据是ReadAsync返回 0 或者写入时抛异常。所以循环里每次先尝试写心跳,再尝试读回包,任一步异常就销毁旧连接,重新走连接流程。重试间隔建议做退避,比如第一次 3 秒、第二次 6 秒,最多到 30 秒停一下,避免服务端恢复时被一大波重连请求冲垮。

4.3 客户端最常见的报错和处理思路

客户端连不上时最常见的现象是抛SocketException,提示内容五花八门:由于目标计算机积极拒绝,无法连接、连接尝试失败,因为连接方在一段时间后未正确答复或建立连接。前者说明 IP 能到但端口没监听,后者说明数据包发出去了但没收到响应,多半是防火墙或服务端没监听。

这类问题排查有个固定套路:先用Test-NetConnection验证端口通不通;通,就抓代码问题;不通,检查服务端是否在监听、监听的是哪个 IP、防火墙是否放行。很多客户端程序报“请检查网络设置”,其实网络设置没问题,是服务端挂了。分清“网络不可达”和“端口拒绝”是 TCP 调试的基本功,也决定了你该看哪一端的日志。

5. 避坑:TCP C# 例程最常见的四个坑,现象、原因和解法

5.1 服务端本机能连,局域网其他机器连不上

现象:服务端程序跑在本机,从这台机器上用127.0.0.1连接一切正常,换一台局域网电脑用服务端 IP 连接,直接超时。

原因:第一,服务端监听的地址是IPAddress.Loopback,只在回环接口上监听,局域网网卡根本没有监听端口;第二,即便监听的是IPAddress.Any,Windows 防火墙默认会拦截入站 TCP 连接,本机回环地址通常不受防火墙限制,但局域网访问会被拦。

解决:服务端用IPAddress.Any监听,并在防火墙入站规则中放行对应端口。放行操作在控制面板的“Windows Defender 防火墙”里添加“入站规则”,选择 TCP 和特定本地端口,填 9000。改完再用netstat -ano | findstr 9000确认监听地址是0.0.0.0:9000而不是127.0.0.1:9000。

5.2 客户端一次发送两条消息,服务端读到一条拼接数据

现象:客户端连发“A”和“B”两条消息,服务端收到的是“AB”一条,拆分逻辑因此错乱。

原因:TCP 是基于字节流的,Nagle 算法和内核缓冲区可能把多个WriteAsync的数据合并进同一个 TCP 段,接收方一次ReadAsync就拿到了两条消息的字节拼接。

解决:为消息定义边界。最简单的做法是每条消息以\n结尾,服务端读到一个完整行才处理;更通用的是用固定长度头加负载的帧格式,这一步代码见第 6 章。收到数据后先在内存缓冲区里做“拆帧”,不要拿一次ReadAsync的返回值当成一条业务消息。

5.3 服务端关闭程序后客户端立刻抛“远程主机强迫关闭了一个现有的连接”

现象:服务端正常退出后,客户端下一次ReadAsync或WriteAsync抛SocketException (10054)。

原因:TCP 是四步挥手协议,服务端调用Dispose关闭连接后,客户端继续读写时内核感知到对端已关闭,上报异常错误码。10054 表示远程主机强制关闭连接,10053 表示软件中止了连接。

解决:这是正常现象不是 bug。客户端要把这两种错误码视为“连接已失效”,统一走重连逻辑,不要试图让本次读写“恢复”。另外服务端不要直接Environment.Exit,先Stop()监听,再依次关闭客户端连接,这样多数情况下能走正常挥手序列;但不要依赖它,因为对端断网的情况下无论如何都会变成强制关闭。

5.4 发小包延迟明显,零碎交互响应慢

现象:客户端每发一条小消息,服务端要等几十毫秒才收到,交互感明显不好。

原因:Nagle 算法默认开启,它会把多个小包攒到一批再发送,目的是减少小包数量。在交互式场景里,消息本身小于 MSS,等攒包的时间就变成了额外的延迟。

解决:在TcpClient上设置client.NoDelay = true,关闭 Nagle 算法。代价是网络里的小包数量变多,在千兆内网里这是可接受的。另一个相关坑是WriteAsync之后立刻ReadAsync,对端还没把数据发送完,这边就超时了,解决方法是给ReadTimeout留 3 到 5 秒的余量,而不是设成 500 毫秒“硬等”。TCP 不是实时总线,超时设太短只会得到一堆误报。

6. 进阶:用 4 字节长度头做消息帧,顺带验证整个传输链路

上面代码能跑通,但消息边界问题悬而未决。这一章给出一个能直接照用的帧协议:每条消息开头用 4 字节表示负载长度,大端序,后面跟着真正的文本数据。服务端收消息时不断积累缓冲,截出完整帧再解析。这层处理做完,TCP C# 例程才算是一个可以面对真实数据流的最小可靠实现。

发送端组帧代码如下:

static byte[] BuildFrame(string message) { byte[] payload = Encoding.UTF8.GetBytes(message); byte[] frame = new byte[4 + payload.Length]; frame[0] = (byte)(payload.Length >> 24); frame[1] = (byte)(payload.Length >> 16); frame[2] = (byte)(payload.Length >> 8); frame[3] = (byte)payload.Length; Buffer.BlockCopy(payload, 0, frame, 4, payload.Length); return frame; }

接收端维护一个List<byte>接收缓冲,每次ReadAsync后先尝试解析帧。解析逻辑要点:缓冲区不足 4 字节就先攒着;够 4 字节能算出负载长度,但总长度不足则继续等待,拆到一条完整消息就处理并从缓冲区移除已消费部分。这个过程本质是“字节流转消息”,写完这一层,再也不会出现把两条消息拼接当成一条的消息。

我平时验证这个帧协议的方式很简单:客户端循环发送 100 条编号消息,服务端每收到一条完整消息就计数,最后比对总数;如果出现计数小于 100,说明有半包没被正确拼接,需要检查缓冲积累逻辑。验证表可以这样列:数据量 100 条,每条 50 字节,预期收到 100 条,实测必须也是 100 条;故意把发送间隔压到 1 毫秒以内再测一次,看粘包下是否仍然拆帧正确。TCP 调试第一步永远是“先证明字节没丢”,再谈业务逻辑。

这个方向还有一个值得做的进阶动作:把服务端的连接对象和业务处理拆开。因为一旦消息帧能可靠截出来,下一步自然会想到“一个连接收多种消息,按消息类型分发到不同处理函数”,这时候你会需要消息头里再加一个类型字段。接口留好,后面扩展就顺畅了,而不是把if/else写满整个处理循环。做 TCP 例程最容易翻车的地方是想一次性写一个“万能通信框架”,我的教训是从一条消息一个连接做起,确认字节流可靠,再谈架构。希望帮到你,按这套步骤先跑通客户端和服务端,你会比大多数把ReadAsync当一次消息读的人更快上手。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 6:49:04

【人工智能】Token 计费机制详解:从词元到缓存命中

【人工智能】Token 计费机制详解&#xff1a;从词元到缓存命中摘要&#xff1a;本文深入浅出地讲解了大模型计费的核心概念——Token&#xff08;词元&#xff09;。文章从 Token 的定义出发&#xff0c;介绍了分词器如何将输入拆分为 Token 的基本原理&#xff0c;并区分了输入…

作者头像 李华
网站建设 2026/10/30 20:37:34

C-ReD 解读:中文AIGC检测基准里哪种最难检测

来源&#xff1a;论文首页 先说结论&#xff1a;中文 AIGC 检测基准 C-ReD 今年发了&#xff0c;数据量不小&#xff08;12.8 万条&#xff09;&#xff0c;覆盖了新闻、问答、影评、作文、学术写作五个域和九个模型。论文里最有意思的三条发现——新闻和学术写作最难检测、Dee…

作者头像 李华
网站建设 2026/10/19 5:19:11

揭秘!互联网上那些知名的SEO优化企业究竟啥样?

痛点深度剖析我们团队在实践中发现&#xff0c;众多企业在SEO优化领域面临诸多困境。一方面是流量获取难题&#xff0c;SEO见效慢&#xff0c;很多企业做了半年优化&#xff0c;关键词排名却毫无变动&#xff1b;SEM烧钱快&#xff0c;谷歌广告点击成本不断攀升&#xff0c;ROI…

作者头像 李华
网站建设 2026/10/30 22:19:01

Python ai-function-helper 包实战案例详解

1. 引言在 Python 开发中&#xff0c;函数调用是最高频的操作之一。无论是参数校验、默认值处理&#xff0c;还是函数签名检查、调用结果格式化&#xff0c;开发者往往需要编写大量重复代码。ai-function-helper 是一个专注于函数调用辅助能力的轻量级工具包&#xff0c;它把函…

作者头像 李华
网站建设 2026/10/30 8:38:25

10.6笔记

一.浮动简介&#xff1a;一开始是用来实现文字环绕图片效果的&#xff0c;现在浮动是主流的页面布局方式之一float元素浮动后的特点&#xff1a;1.脱离文档流&#xff08;好比新建一个图层&#xff09; 2.无论浮动前是什么元素&#xff0c;浮动后的默认宽与高都是被内容撑开&am…

作者头像 李华
网站建设 2026/10/30 15:19:45

STM32-OLED显示屏与EXTI外部中断

OLED介绍&#xff1a;有机发光二极管OLED硬件电路&#xff1a;4针脚的右边两个针脚提供电源&#xff0c;剩下两个是I2C通信引脚&#xff0c;这里会使用GPIO口模拟I2C通信&#xff0c;因此可以接任意GPIO口。7针脚也同理&#xff0c;两个供电&#xff0c;其余是SPI通信引脚&…

作者头像 李华