ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

最简单C# TCP/IP例程:同步Socket通信从零到实战

最简单C# TCP/IP例程:同步Socket通信从零到实战 简介面向C#入门开发者的TCP/IP网络通信最小实现例程包基于Socket与TcpListener/TcpClient完整演示服务端-客户端双向通信流程适合刚接触网络编程、想快速跑通第一个通信程序的读者对照学习。包内包含完整的Visual Studio解决方案源码、界面与工程配置齐全可直接编译运行。资源共55个文件以C#源码cs、可执行程序exe、说明文档txt为主另有resx界面资源、dll依赖库及缓存等辅助文件整体仅450KB下载打开解决方案即可查看代码结构并启动调试。目前已有206人学习下载。借助源码与编译好的服务端/客户端可执行程序可直观理解服务端监听端口、接受连接、收发数据以及客户端建立连接、发送请求、接收响应的完整链路便于在此基础上扩展多客户端处理与异常捕获逻辑。1. TCP/IP C#例程为什么说“最简单”反而最实用翻了很久资料你会发现网上九成的 TCP/IP C# 例程都套着异步、粘包拆包、心跳重连、SuperSocket 这类“重型外衣”看起来功能齐全可刚入门的人根本学不会。而真正的“最简单例程”就三样东西一个 Socket一个 Bind/Listen/Accept一个 Send/Receive几十行代码跑通整个链路。这个资源就是按这个思路做的——C# 控制台程序一个服务端、一个客户端同步阻塞式通信没有花哨封装却把 TCP/IP 通信的四板斧全演示完了。它的价值在于让你先把“连接怎么建立、数据怎么流动”这件事看透再去看高并发框架时你才不会被术语吓住。适合刚入门 C# 网络编程的应届生也适合做上位机、工控机通信、写简单协议测试工具的现场工程师——先用最小代码验证链路通不通比什么都重要。2. 先搞懂 Socket 模型同步阻塞例程的选型理由与核心流程很多刚接触网络编程的人上来就写异步结果被 BeginSend、EndReceive 和回调函数搞得一头雾水。这个最简单例程用的是同步阻塞 Socket原因很简单同步模型是 TCP/IP 编程的地基所有异步、事件驱动、高性能框架本质都是在同步收发基础上加入多线程或 I/O 复用。先把同步模型吃透后面看啥都不虚。2.1 同步阻塞 Socket 的通信流程和关键 APITCP 通信的建立遵循“服务端被动等待客户端主动连接”的模型。C# 里最核心的对象是System.Net.Sockets.Socket整个流程可以拆成六步步骤服务端动作客户端动作关键 API1创建 Socket创建 Socketnew Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)2绑定 IP 和端口无需绑定Bind(IPEndPoint)3转为监听状态发起连接服务端Listen(backlog)客户端Connect(IPEndPoint)4接受客户端连接已连接服务端Accept()返回新 Socket5收发数据收发数据Send(byte[])/Receive(byte[])6关闭连接关闭连接Shutdown()Close()SocketType.Stream表示流式套接字对应 TCPProtocolType.Tcp则明确指定传输层协议。注意AddressFamily.InterNetwork是 IPv4 地址族如果要用 IPv6则为InterNetworkV6。这个最简单例程默认只处理 IPv4足够覆盖 99% 的局域网和本机测试场景。服务端Accept()是阻塞方法调用后当前线程会停在那里直到有客户端连进来才返回。返回的新 Socket 表示与客户端之间的独立通道——这是很多初学者最容易忽略的点监听的 socket 不要直接用来收发数据它只负责“接客”真正收发数据的是Accept()返回的“会话 socket”。数据收发层面Receive()同样是阻塞方法它把收到的字节流填充到传入的 byte[] 缓冲区中并返回实际接收到的字节数。如果返回值是 0说明对方已正常关闭连接。这个返回值是后面判断连接是否断开的唯一可靠依据比异常捕获更直观很多第三方库也是基于这个特性做断线检测的。2.2 例程里的连接建立与数据收发代码逐段拆先看服务端完整代码。这个例程的逻辑是绑定本机任意 IP 的 8888 端口循环监听每接受一个客户端就接收一条消息并原样回发然后关闭该会话连接。// Server.cs - 最简单的 TCP 服务端 using System; using System.Net; using System.Net.Sockets; using System.Text; class TcpServer { static void Main(string[] args) { // 1. 创建 IPv4 的 TCP Socket Socket listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 绑定本机所有网卡的 8888 端口。IPAddress.Any 表示监听所有网卡 IPEndPoint localEndPoint new IPEndPoint(IPAddress.Any, 8888); listenSocket.Bind(localEndPoint); // 3. 开始监听backlog10 表示最多有 10 个等待连接的客户端排队的缓冲 listenSocket.Listen(10); Console.WriteLine(服务端已启动监听端口 8888 ...); while (true) { // 4. 阻塞等待客户端连接拿到会话 socket Socket clientSocket listenSocket.Accept(); Console.WriteLine($客户端连接: {clientSocket.RemoteEndPoint}); // 5. 接收数据。缓冲区大小 1024 字节 byte[] buffer new byte[1024]; int receivedCount clientSocket.Receive(buffer); string receivedText Encoding.UTF8.GetString(buffer, 0, receivedCount); Console.WriteLine($收到: {receivedText}); // 6. 原样回发模拟服务端响应 byte[] sendBuffer Encoding.UTF8.GetBytes(服务端已收到: receivedText); clientSocket.Send(sendBuffer); // 7. 关闭会话连接但保留监听 socket 继续接收下一个客户端 clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); } } }逻辑说明和参数说明IPAddress.Any是一个特殊地址0.0.0.0绑定它意味着服务端监听本机所有网卡。如果你只想让某一台局域网机器访问可以改成IPAddress.Parse(192.168.1.100)绑定那个网卡的具体 IP。Listen(10)中的参数是 backlog即等待连接队列的长度。这个例程是串行处理客户端的一次只服务一个连接所以 backlog 只要填 110 都行。如果你填 0在 Windows 上会被系统自动修正为 1 或 2但在 Linux 上可能导致客户端Connect报“连接拒绝”。Receive()是阻塞的如果客户端连接后一直不发数据服务端会卡在这一行永远不会去Accept()下一个客户端。这就是同步模型的最大局限后面避坑章节会讲怎么处理。Shutdown(SocketShutdown.Both)表示发送和接收都禁止然后Close()释放资源。只调Close()而不调Shutdown()有时会导致数据丢失因为系统可能需要时间把缓冲区的数据发完。再看客户端代码。客户端的职责是连接服务端发送一段文本然后接收服务端回显// Client.cs - 最简单的 TCP 客户端 using System; using System.Net; using System.Net.Sockets; using System.Text; class TcpClient { static void Main(string[] args) { // 1. 创建同类型的 Socket Socket clientSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 连接服务端这里的 IP 改成你服务端的实际地址 IPAddress serverIp IPAddress.Parse(127.0.0.1); IPEndPoint serverEndPoint new IPEndPoint(serverIp, 8888); clientSocket.Connect(serverEndPoint); Console.WriteLine(已连接服务端); // 3. 发送数据 string message Hello TCP, 这是最简单的例程; byte[] sendBuffer Encoding.UTF8.GetBytes(message); clientSocket.Send(sendBuffer); Console.WriteLine($发送: {message}); // 4. 接收服务端回显 byte[] receiveBuffer new byte[1024]; int receivedCount clientSocket.Receive(receiveBuffer); string response Encoding.UTF8.GetString(receiveBuffer, 0, receivedCount); Console.WriteLine($服务端回显: {response}); // 5. 关闭连接 clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); } }这里有两个值得展开的细节。Connect()是同步阻塞方法它会完成 TCP 三次握手后才返回。如果服务端没启动Connect()会抛SocketException错误码10061表示目标机器积极拒绝通常就是端口没监听或防火墙拦截。Send()方法把字节流发送出去就立即返回返回值是实际写入系统发送缓冲区的字节数不等于对方“已经收到”——TCP 只保证在传输过程中不丢序不保证应用层立即处理。编码问题也是这个例程特意使用UTF8的原因。Encoding.UTF8是跨平台最通用的文本编码Windows 控制台默认编码可能是 GBK如果你用Console.WriteLine打印中文乱码不代表网络传输乱码而是控制台显示问题。在代码里只要两端都统一用 UTF8 编码就不会出现中文乱码。我见过太多现场问题最后都是两端编码一个 GBK 一个 UTF8 导致的。3. 把例程跑起来从编译到两台电脑联调代码看懂是一回事能跑起来又是另一回事。这一章直接给你操作步骤和参数清单确保在 Visual Studio 或命令行环境下都能顺利运行。3.1 同一台机器回环测试按步骤操作先在本机做回环测试目的有两个一是验证代码本身没有逻辑错误二是熟悉程序运行时的行为特征。步骤如下新建一个控制台项目将服务端代码保存为Server.cs客户端代码保存为Client.cs。如果你用 Visual Studio可以建两个单独的控制台项目分别编译如果用命令行用csc分别编译两个源文件。# 编译服务端和客户端假设已安装 .NET SDK 或 Visual Studio 的开发者命令行 csc Server.cs -out:Server.exe csc Client.cs -out:Client.exe先启动Server.exe控制台会输出“服务端已启动监听端口 8888 ...”。此时服务端阻塞在Accept()。再打开另一个命令行窗口运行Client.exe。正常情况下客户端输出“已连接服务端”“发送: ...”“服务端回显: ...”服务端窗口输出“客户端连接: 127.0.0.1:xxxxx”和“收到: ...”。如果客户端抛SocketException先检查服务端是否真的在运行、端口是否写错、本机是否有其他程序占用了 8888 端口。提示在同一台机器上测试时客户端的IPAddress.Parse(127.0.0.1)是回环地址数据不会经过物理网卡所以即使你没有联网也能跑通。这是验证逻辑最快的路径。参数方面缓冲区byte[1024]足够承载例程里的短消息。如果你测试长文本超过 1024 字节的部分会被截断。这个例程没有处理“接收不完整”的情况属于正常现象——后续避坑章节会说明为什么 TCP 一次接收不一定等于你一次发送。3.2 局域网联调IP、端口、防火墙的配置清单本机跑通后把客户端移到另一台电脑上才能真正理解 TCP/IP 的“IP 地址 端口”定位方式。你需要做三件事配置项服务端客户端说明服务端绑定 IPIPAddress.Any或固定局域网 IP不依赖绑定 Any 最省事客户端连接 IP不依赖填写服务端局域网 IP例如192.168.1.10不要写 127.0.0.1端口88888888两端必须一致Windows 防火墙放行 8888 端口或允许程序通过一般不需要改出站通常默认允许首先用ipconfig查看服务端的 IPv4 地址。客户端代码里IPAddress.Parse(127.0.0.1)要改成这个实际地址。其次防火墙是最大的隐形杀手。Windows 默认防火墙会拦截入站连接第一次运行服务端时系统会弹窗询问是否允许访问如果没有弹窗则要手动添加规则netsh advfirewall firewall add rule nameTCP Test 8888 dirin actionallow protocolTCP localport8888这条命令为入站方向放行 TCP 8888 端口。注意dirin是入站规则因为服务端是“接收连接”的一方客户端作为发起方出站方向默认放行通常不需要额外配置。如果你在公司网络或域环境下可能还要考虑组策略或安全软件拦截。端口选择上8888 这个端口号是我常用的测试端口它不在常见的动态端口范围49152~65535内也不容易被系统随机占用。你可以换成 8080、9000 或其他空闲端口但尽量避免使用 80、22、3389 这类已被知名服务占用的端口否则容易冲突。联调时用 TCP 工具验证链路是一个好习惯。在服务端跑的机器上用telnet 192.168.1.10 8888测试端口通不通。如果 telnet 显示“无法连接”说明网络不通或服务端没在监听如果连上后没有任何提示但也看不到连接被拒绝说明端口通只是协议测试数据不匹配——这能帮你把问题从“网络层”和“应用层”分开不浪费时间去调代码。4. 避坑与排查TCP/IP 例程最常见的六个翻车点写完这个例程后我把它丢给过很多新人跑收集了不少真实翻车现场。这一章把最有共性的六个问题写出来每条都是“现象 → 原因 → 解决”的结构你照着检查就行。4.1 客户端报“目标计算机积极拒绝”或“连接超时”现象运行客户端抛SocketException: 由于目标计算机积极拒绝无法连接或者本机没问题换了一台机器就报“连接超时”。原因积极拒绝通常是服务端没启动或者端口没监听、客户端把端口写错了。连接超时则是网络层不通比如 IP 写错、不在同一网段、防火墙丢包。特别注意同网段和跨网段的表现不一样同网段连不通往往是防火墙跨网段则先查路由。解决先netstat -ano | findstr 8888确认服务端是否在监听再用ping测 IP 连通性最后用 telnet 测端口。把这三步走完就能定位到 70% 的连接问题。4.2 服务端收到了数据但客户端收不到回显现象客户端发送成功服务端打印了收到的内容但客户端Receive()一直阻塞永远等不到响应。原因在同步阻塞模型里如果客户端发送后立即调用Receive()而服务端在Send()之后直接Close()客户端也可能因为时序问题收不到完整的响应。更常见的情况是服务端的Send()抛异常了但客户端没感知。另一种冷门情况服务端用Console.ReadLine()提前退出导致连接被清理。解决在服务端Send()之后加一个Thread.Sleep(100)再关闭或者调用Close()前调用Shutdown(SocketShutdown.Send)。其实从根上讲这个例程是“一问一答”式同步通信只要服务端不主动Close()客户端阻塞在Receive()就不会返回 0。最简单的修复服务端发送完回显后不立即Close()而是等到客户端先断开再清理。4.3 服务端只能接收一个客户端第二个连不上现象第一个客户端通信正常第二个客户端连上后没有收到任何响应甚至报连接失败。原因这个例程的服务端while(true)循环里Accept()之后直接处理单个连接处理完再回到Accept()。但如果第一个连接的处理逻辑中Receive()阻塞了服务端就永远卡住顾不上Accept()新连接。同步串行模型天生只能服务一个客户端。解决把每个客户端的处理放到独立线程或Task.Run()中。这是从“例程”走向“可用程序”的第一步后面进阶章节会完整给出改造方案。4.4 中文收发乱码现象客户端发送“你好”服务端打印出来是乱码或者反过来服务端回显中文在客户端显示乱码。原因两端编码不一致。最常见的组合是服务端默认用 GBK 解码客户端用 UTF8 编码或者控制台显示编码和网络编码混在一起。解决统一使用Encoding.UTF8。代码里严格用Encoding.UTF8.GetBytes()和Encoding.UTF8.GetString()。同时注意 Windows 控制台显示 UTF8 字符可能需要设置字体或使用Console.OutputEncoding Encoding.UTF8;。网络传输层面只要两端字节序列一致显示问题完全不影响通信正确性。4.5 Receive 缓冲区大小没搞对导致数据截断现象客户端发送一段超过 1024 字节的消息服务端只收到前 1024 字节内容被截断。原因Receive()一次最多读取缓冲区容量的数据量。例程里缓冲区为 1024但 TCP 是字节流没有消息边界一次Send的数据可能被拆成多次Receive多次Send也可能合并成一次Receive。所以“每次 Receive 返回的数据等于一次 Send 的数据”是错误的假设。解决先定义协议最常见的是“长度前缀法”。发送时先发 4 字节的消息长度再发消息体接收时先读 4 字节得到长度再循环读完剩余部分。这个方案在进阶章节会给出代码。如果只是测试用短消息把缓冲区扩到 4096 或 8192 能缓解但不治本。4.6 客户端断开后服务端抛异常崩溃现象客户端直接关掉控制台窗口服务端在Receive()处抛SocketException: 远程主机强迫关闭了一个现有的连接。原因TCP 连接断开时对端会发送 FIN 或 RST。如果客户端进程被强制结束系统来不及正常关闭连接服务端的Receive()会收到一个异常。很多新手没有在Receive()周围捕获异常程序直接崩溃。解决Receive()返回 0 表示正常断开抛异常表示异常断开两种情况都必须处理。在服务端代码里用try-catch包住收发逻辑并在catch中记录连接信息和异常原因然后Close()会话 Socket。这是长期运行的服务端程序最基本的健壮性要求。正确的断开判断逻辑是try { int receivedCount clientSocket.Receive(buffer); if (receivedCount 0) { Console.WriteLine(客户端正常断开); } } catch (SocketException ex) { Console.WriteLine($客户端异常断开: {ex.SocketErrorCode}); } finally { clientSocket.Close(); }4.7 防火墙规则没生效的排查顺序现象添加了防火墙规则还是连不上或者第一次能连重启之后又不通了。原因防火墙规则可能只添加到了“单一配置文件”而不是所有配置文件也可能是程序本身被禁止监听端口而端口规则只管入站没管本地监听。解决先netsh advfirewall firewall show rule nameTCP Test 8888确认规则状态。如果Enabled不是Yes重新执行添加命令并加上profileany。如果还是不行临时关闭防火墙测试仅测试环境生产环境不要关确认是不是安全软件或组策略覆盖。5. 进阶从同步例程升级到可用的通信框架最简单例程的价值在于“能跑通”但真实项目中不可能只服务一个客户端。这一章给你三个最实用的升级技巧让你下班前就能把例程改造成勉强能上线的通信模块。5.1 用 Task.Run 实现多客户端并发核心改造点Accept()循环始终只负责接受连接把每个客户端的收发逻辑丢给线程池处理。这样即使某个客户端卡住不发送数据也不会阻塞新连接的接入。改造后的服务端骨架while (true) { Socket clientSocket listenSocket.Accept(); Task.Run(() ProcessClient(clientSocket)); } static void ProcessClient(Socket clientSocket) { try { byte[] buffer new byte[1024]; int receivedCount clientSocket.Receive(buffer); // 业务处理 ... clientSocket.Send(responseBuffer); } catch (SocketException ex) { Console.WriteLine($会话异常: {ex.SocketErrorCode}); } finally { clientSocket.Close(); } }Task.Run把每个连接的处理从主循环挪到后台线程主循环立刻回到Accept()等待下一个客户端。这个模型适合几十个连接的小型场景。如果吞吐量要求高后续可以再改成BeginAccept异步模型或直接上SocketAsyncEventArgs但核心思路不变每个连接独立处理互不干扰。5.2 数据帧协议解决粘包拆包这是每个 TCP 开发者都要过的坎。简便做法是“4 字节长度前缀 消息体”。发送方构造帧byte[] bodyBytes Encoding.UTF8.GetBytes(你好这是一条消息); byte[] lengthBytes BitConverter.GetBytes(bodyBytes.Length); // 4字节小端整数 byte[] frame new byte[4 bodyBytes.Length]; Buffer.BlockCopy(lengthBytes, 0, frame, 0, 4); Buffer.BlockCopy(bodyBytes, 0, frame, 4, bodyBytes.Length); // 一次 Send 整帧 clientSocket.Send(frame);接收方要读取完整的一帧先读 4 字节得到长度再循环读取直到读满该长度。byte[] lengthBytes new byte[4]; int lengthReceived 0; while (lengthReceived 4) { int chunk clientSocket.Receive(lengthBytes, lengthReceived, 4 - lengthReceived, SocketFlags.None); if (chunk 0) return; // 连接断开 lengthReceived chunk; } int bodyLength BitConverter.ToInt32(lengthBytes, 0); // 再根据 bodyLength 循环读 body这里的Receive()重载多了三个参数缓冲区、起始偏移量、要接收的字节数、SocketFlags。注意 TCP 流的特征一次Receive()返回的字节数可能小于你请求的字节数所以必须循环累加直到接收够指定长度。这个写法把“粘包”和“拆包”都一并解决了——无论对方一次发多少帧接收方总是按帧边界读取。5.3 超时和心跳让通信更可靠同步阻塞模型下Connect()在局域网内一般几十毫秒返回但遇到 IP 不可达时可能要等很久。建议设置连接超时避免 UI 卡死clientSocket.ReceiveTimeout 5000; // 接收数据超时 5 秒 clientSocket.SendTimeout 5000; // 发送数据超时 5 秒 clientSocket.Connect(serverEndPoint); // 连接超时不受这两个属性控制连接超时在 .NET 中比较难做同步Connect没有内置超时参数。常见做法是先调用BeginConnect并配合WaitOne(3000)超时再结束异步等待。对最简单例程来说你只要把ReceiveTimeout设上防止客户端无限期等待就已经比 80% 的例程健壮了。心跳是一个更复杂的主题。简单场景下可以定义一个 5 秒定时器定时向服务端发送一个空消息或特定指令。服务端如果连续 3 次心跳超时未收到就判定连接死亡并主动关闭。注意心跳消息必须有别于业务消息通常在协议头中加一个消息类型字段比如 0 表示心跳1 表示业务数据。这一点在例程里不体现但你一旦把它接入真实系统就会理解心跳的作用——很多现场“假死”问题靠心跳能迅速暴露。我自己的习惯是任何 Socket 代码写完之后都要做一个“关闭后重连”的验证客户端连上、发送、断开、再重连、再发送连续循环 10 次不崩溃才敢把代码交到现场。这个验证能一次性暴露资源泄漏、状态残留、端口未释放等一系列问题。从那以后我每次写完 TCP 通信代码都强制走一遍“连 → 发 → 断 → 重连”的流程哪怕是最简单的例程也不例外。希望这套流程和上面的避坑清单能帮到你让你不再被 TCP/IP 的“玄学”问题折磨。本文还有配套的精品资源点击获取
返回列表