VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 第一个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,客户端会断开连接,服务器会显示客户端已断开。
六、基础知识拓展

  1. 为什么会出现粘包?
    TCP是“流”协议,数据没有边界,操作系统会把小数据合并成一个数据包发送(减少网络开销),导致粘包。比如客户端连续发送"A"和"B",服务器可能一次收到"AB"。
    解决方法:
    固定长度协议:每个消息的长度固定,比如每个消息100字节,不足的用空格填充;
    分隔符协议:用特殊字符(如\r\n)作为消息的分隔符,服务器收到数据后按分隔符拆分;
    头部长度协议:消息头部包含消息长度,比如前4字节是消息长度,服务器先读取头部,再读取指定长度的消息体。
  2. 如何测试TCP连接?
    telnet命令:telnet 127.0.0.1 8888,如果能连接成功,说明服务器正常监听;
    nc命令:nc 127.0.0.1 8888,功能比telnet更强大,支持发送数据;
    Wireshark抓包:用Wireshark抓取127.0.0.1的TCP数据包,查看三次握手、数据传输、四次挥手的完整流程。
  3. 如何处理多客户端并发?
    线程池:用Task.Run或ThreadPool.QueueUserWorkItem处理客户端,.NET的线程池会自动管理线程数量;
    异步IO:所有IO操作(ReadAsync、WriteAsync)都用异步方法,避免阻塞线程;
    限制并发数:如果服务器性能有限,可以限制最大并发连接数,比如用SemaphoreSlim控制。
  4. TCP与UDP的区别?
特性 TCP UDP
连接方式 面向连接(三次握手) 无连接
可靠性 可靠(重传、有序、校验) 不可靠(不重传、无序)
传输效率 低(有握手、重传开销) 高(无额外开销)
适用场景 文件传输、网页浏览、电商支付 实时通信、视频通话、游戏

七、总结:第一个程序的意义
这是你用C#写的第一个网络程序,虽然简单,但包含了TCP通信的核心流程:绑定→监听→连接→读写→断开;
理解了TcpListener、TcpClient、NetworkStream的用法,就掌握了C#网络编程的基础;
后续的复杂场景(如WebSocket服务器、高并发TCP服务器)都是在这个基础上扩展的。
下一节我们会深入学习TCP协议的底层原理,包括三次握手、四次挥手、滑动窗口等,让你不仅“会用”,更“懂原理”。

 本站原创,转载请注明出处:


相关教程