-
第一个C#网络程序:TCP客户端与服务器
第一部分:网络编程基础
第1章 网络编程核心概念
1.4 第一个C#网络程序:TCP客户端与服务器
一、为什么从TCP开始?
我第一次写网络程序时,先试了UDP,以为简单,结果发现在丢包、乱序问题上卡了一周——后来才明白,TCP是网络编程的“基础款”,它帮你解决了可靠传输、有序、重传等核心问题,适合新手入门。这节就带你从零开始写一个TCP客户vb.net教程C#教程python教程SQL教程access 2010教程端和服务器,从代码到原理,一步步拆解,让你不仅能跑通程序,更能理解背后的逻辑。
二、TCP通信的核心流程
在写代码之前,先搞懂TCP通信的完整流程,就像做菜前先看菜谱:
1.服务器启动:绑定IP地址和端口,开始监听客户端连接;
2.客户端连接:向服务器的IP地址和端口发起连接请求;
3.三次握手:TCP协议自动完成三次握手,建立可靠连接;
4.数据传输:客户端和服务器互相发送数据,TCP保证数据可靠、有序;
5.四次挥手:客户端或服务器主动断开连接,TCP协议自动完成四次挥手;
6.资源释放:关闭Socket,释放网络资源。
三、TCP服务器:等待客户端连接的“守门人”
先写服务器,它是整个通信的“被动方”,需要先启动,等待客户端连接。
代码实现(.NET 6+ 控制台程序)
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace TcpServerDemo;
class Program
{
// 服务器监听的IP地址和端口
private const string ListenIp = "0.0.0.0"; // 监听所有本地IP地址
private const int ListenPort = 8888;
static async Task Main(string[] args)
{
// 1. 创建TcpListener实例,绑定IP和端口
var ipAddress = IPAddress.Parse(ListenIp);
var listener = new TcpListener(ipAddress, ListenPort);
try
{
// 2. 启动监听
listener.Start();
Console.WriteLine($"[服务器] 已启动,监听端口:{ListenPort}");
Console.WriteLine($"[服务器] 等待客户端连接...");
// 3. 循环接受客户端连接(异步非阻塞)
while (true)
{
// AcceptTcpClientAsync会阻塞当前线程,直到有客户端连接
var client = await listener.AcceptTcpClientAsync();
var remoteEndPoint = client.Client.RemoteEndPoint as IPEndPoint;
Console.WriteLine($"[服务器] 客户端已连接:{remoteEndPoint?.Address}:{remoteEndPoint?.Port}");
// 4. 用Task.Run开启新线程处理客户端,避免阻塞主线程
_ = Task.Run(() => HandleClientAsync(client));
}
}
catch (Exception ex)
{
Console.WriteLine($"[服务器] 异常:{ex.Message}");
}
finally
{
// 5. 停止监听,释放资源
listener.Stop();
Console.WriteLine($"[服务器] 已停止");
}
}
/// <summary>
/// 处理单个客户端的通信
/// </summary>
private static async Task HandleClientAsync(TcpClient client)
{
// 用using自动释放TcpClient资源
using (client)
{
// 获取网络流,用于读写数据
var stream = client.GetStream();
// 定义接收缓冲区,大小4KB(可根据实际调整)
var buffer = new byte[4096];
try
{
// 循环接收客户端数据
while (true)
{
// ReadAsync会阻塞当前线程,直到有数据可读
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
// 如果bytesRead为0,说明客户端已断开连接
if (bytesRead == 0)
{
var remoteEndPoint = client.Client.RemoteEndPoint as IPEndPoint;
Console.WriteLine($"[服务器] 客户端已断开:{remoteEndPoint?.Address}:{remoteEndPoint?.Port}");
break;
}
// 把字节数组转换成字符串(UTF-8编码)
string receivedMsg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"[服务器] 收到客户端消息:{receivedMsg}");
// 构造响应消息
string responseMsg = $"服务器已收到:{receivedMsg}";
byte[] responseBytes = Encoding.UTF8.GetBytes(responseMsg);
// 发送响应给客户端
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
Console.WriteLine($"[服务器] 发送响应:{responseMsg}");
}
}
catch (Exception ex)
{
var remoteEndPoint = client.Client.RemoteEndPoint as IPEndPoint;
Console.WriteLine($"[服务器] 客户端通信异常:{remoteEndPoint?.Address}:{remoteEndPoint?.Port},错误:{ex.Message}");
}
}
}
}
代码逐行讲解
IP地址与端口绑定
csharp
var ipAddress = IPAddress.Parse(ListenIp);
var listener = new TcpListener(ipAddress, ListenPort);
1.IPAddress.Parse("0.0.0.0"):监听所有本地IP地址(包括IPv4的127.0.0.1、192.168.1.100等),如果只需要本地测试,可以用IPAddress.Loopback(即127.0.0.1);
2.TcpListener:.NET官方封装的TCP服务器类,底层基于Socket,简化了TCP服务器的开发。
启动监听
csharp
listener.Start();
1.启动后,服务器会在指定端口上监听客户端的连接请求,此时端口处于“监听状态”,可以用netstat -ano | findstr :8888命令查看。
接受客户端连接
csharp
var client = await listener.AcceptTcpClientAsync();
1.AcceptTcpClientAsync是异步方法,会释放当前线程,直到有客户端连接,避免阻塞主线程;
2.返回的TcpClient对象代表与客户端的连接,包含客户端的IP地址、端口、网络流等信息。
处理客户端通信
csharp
_ = Task.Run(() => HandleClientAsync(client));
1.用Task.Run开启新线程处理单个客户端,这样主线程可以继续接受其他客户端连接,支持多客户端并发;
2.注意:_ = 表示忽略返回的Task,不需要等待它完成,避免阻塞主线程。
网络流读写
csharp
var stream = client.GetStream();
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
1.NetworkStream是TCP通信的核心流,用于读写数据,底层自动处理TCP的可靠传输;
2.ReadAsync会阻塞当前线程,直到有数据可读,返回读取到的字节数;
3.如果返回0,说明客户端已断开连接(TCP协议会发送一个长度为0的数据包表示断开)。
编码转换
csharp
string receivedMsg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
byte[] responseBytes = Encoding.UTF8.GetBytes(responseMsg);
1.网络传输的是字节数组,需要转换成字符串,这里用UTF-8编码(注意客户端和服务器要统一编码,否则会出现乱码);
2.如果传输的是二进制数据(如文件),不需要转换成字符串,直接读写字节数组即可。
28.
资源释放
csharp
using (client) { ... }
listener.Stop();
1.using语句会自动释放TcpClient资源,关闭Socket连接;
2.listener.Stop()会停止监听,释放服务器的端口资源。
四、TCP客户端:主动发起连接的“访客”
再写客户端,它是通信的“主动方”,需要主动连接服务器。
代码实现(.NET 6+ 控制台程序)
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace TcpClientDemo;
class Program
{
// 服务器的IP地址和端口
private const string ServerIp = "127.0.0.1"; // 本地测试用回环地址
private const int ServerPort = 8888;
static async Task Main(string[] args)
{
// 1. 创建TcpClient实例
var client = new TcpClient();
try
{
// 2. 连接服务器(异步非阻塞)
await client.ConnectAsync(ServerIp, ServerPort);
Console.WriteLine($"[客户端] 已连接到服务器:{ServerIp}:{ServerPort}");
// 获取网络流
var stream = client.GetStream();
// 3. 循环发送消息给服务器
while (true)
{
Console.Write("[客户端] 请输入消息(输入exit退出):");
string input = Console.ReadLine() ?? string.Empty;
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("[客户端] 正在断开连接...");
break;
}
// 把字符串转换成字节数组(UTF-8编码)
byte[] sendBytes = Encoding.UTF8.GetBytes(input);
// 发送数据给服务器
await stream.WriteAsync(sendBytes, 0, sendBytes.Length);
Console.WriteLine($"[客户端] 发送消息:{input}");
// 4. 接收服务器的响应
var buffer = new byte[4096];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
Console.WriteLine("[客户端] 服务器已断开连接");
break;
}
string responseMsg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"[客户端] 收到服务器响应:{responseMsg}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[客户端] 异常:{ex.Message}");
}
finally
{
// 5. 关闭连接,释放资源
client.Close();
Console.WriteLine("[客户端] 已断开连接");
}
}
}
代码逐行讲解
连接服务器
csharp
await client.ConnectAsync(ServerIp, ServerPort);
1.ConnectAsync是异步方法,会发起TCP连接请求,底层自动完成三次握手;
2.如果服务器未启动或网络不通,会抛出SocketException(错误码10061表示服务器拒绝连接)。
发送消息
csharp
byte[] sendBytes = Encoding.UTF8.GetBytes(input);
await stream.WriteAsync(sendBytes, 0, sendBytes.Length);
1.把用户输入的字符串转换成字节数组,用WriteAsync发送给服务器;
2.注意:TCP是“流”协议,没有消息边界,多次发送的小数据可能被合并成一个数据包发送(粘包),如果需要区分消息,需要自己定义协议(如固定长度、分隔符)。
接收响应
csharp
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
1.发送消息后,等待服务器的响应,ReadAsync会阻塞当前线程,直到收到数据;
2.如果服务器断开连接,会返回0,此时客户端需要关闭连接。
关闭连接
csharp
client.Close();
1.关闭TcpClient,底层会发送TCP断开请求,完成四次挥手,释放Socket资源。
五、运行测试:让客户端和服务器“对话”
1.启动服务器:运行TcpServerDemo,控制台输出[服务器] 已启动,监听端口:8888;
2.启动客户端:运行TcpClientDemo,控制台输出[客户端] 已连接到服务器:127.0.0.1:8888;
3.发送消息:在客户端输入Hello Server,服务器会收到消息并返回响应,客户端会显示服务器的响应;
4.退出程序:在客户端输入exit,客户端会断开连接,服务器会显示客户端已断开。
六、基础知识拓展
-
为什么会出现粘包?
TCP是“流”协议,数据没有边界,操作系统会把小数据合并成一个数据包发送(减少网络开销),导致粘包。比如客户端连续发送"A"和"B",服务器可能一次收到"AB"。
解决方法:
固定长度协议:每个消息的长度固定,比如每个消息100字节,不足的用空格填充;
分隔符协议:用特殊字符(如\r\n)作为消息的分隔符,服务器收到数据后按分隔符拆分;
头部长度协议:消息头部包含消息长度,比如前4字节是消息长度,服务器先读取头部,再读取指定长度的消息体。 -
如何测试TCP连接?
telnet命令:telnet 127.0.0.1 8888,如果能连接成功,说明服务器正常监听;
nc命令:nc 127.0.0.1 8888,功能比telnet更强大,支持发送数据;
Wireshark抓包:用Wireshark抓取127.0.0.1的TCP数据包,查看三次握手、数据传输、四次挥手的完整流程。 -
如何处理多客户端并发?
线程池:用Task.Run或ThreadPool.QueueUserWorkItem处理客户端,.NET的线程池会自动管理线程数量;
异步IO:所有IO操作(ReadAsync、WriteAsync)都用异步方法,避免阻塞线程;
限制并发数:如果服务器性能有限,可以限制最大并发连接数,比如用SemaphoreSlim控制。 - TCP与UDP的区别?
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(重传、有序、校验) | 不可靠(不重传、无序) |
| 传输效率 | 低(有握手、重传开销) | 高(无额外开销) |
| 适用场景 | 文件传输、网页浏览、电商支付 | 实时通信、视频通话、游戏 |
七、总结:第一个程序的意义
这是你用C#写的第一个网络程序,虽然简单,但包含了TCP通信的核心流程:绑定→监听→连接→读写→断开;
理解了TcpListener、TcpClient、NetworkStream的用法,就掌握了C#网络编程的基础;
后续的复杂场景(如WebSocket服务器、高并发TCP服务器)都是在这个基础上扩展的。
下一节我们会深入学习TCP协议的底层原理,包括三次握手、四次挥手、滑动窗口等,让你不仅“会用”,更“懂原理”。
本站原创,转载请注明出处:










