简介:网络通信是构建分布式系统的重要组成部分,C#通过丰富的网络库支持TCP通信开发。本文详细介绍C#中基于 System.Net.Sockets 命名空间构建TCP服务器的方法,涵盖 TcpListener 监听连接、 TcpClient 建立通信、 NetworkStream 数据传输、异步处理、异常处理及资源释放等关键环节。通过示例代码讲解服务器与客户端的基本交互流程,并探讨多线程、异步任务、数据读写操作等核心技术,帮助开发者掌握构建稳定高效TCP通信程序的方法。
1. TCP通信协议概述
TCP(Transmission Control Protocol)作为互联网中最常用的传输层协议之一,以其 面向连接、可靠传输、流量控制和拥塞控制 等特性,广泛应用于需要数据完整性和顺序保证的场景。本章将从TCP协议的基本结构入手,逐步解析其在客户端-服务器通信模型中的核心作用。
在TCP通信中, 三次握手 (Three-way Handshake)是建立连接的关键步骤,它确保双方都具备发送与接收数据的能力。如下图所示:
sequenceDiagram
客户端->>服务器: SYN=1 (同步标志)
服务器->>客户端: SYN=1, ACK=1 (确认同步)
客户端->>服务器: ACK=1 (确认连接)
通过三次握手,TCP确保了连接的可靠性,防止无效连接长期占用资源。而在通信结束时,TCP通过 四次挥手 (Four-way Wavehand)断开连接,保障数据传输的完整性。
此外,TCP还依赖于 IP地址 与 端口号 来唯一标识通信的两端。IP地址用于定位网络中的主机,而端口号则用于区分主机上运行的不同应用程序。标准端口号范围为0~65535,其中0~1023为系统端口,如HTTP(80)、HTTPS(443)等。
TCP与UDP的主要区别如下:
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接 | 无连接 |
| 可靠性 | 可靠传输 | 不保证送达 |
| 传输速度 | 相对较慢 | 快速但不可靠 |
| 流量控制 | 支持 | 不支持 |
| 应用场景 | 网页浏览、文件传输、邮件等 | 视频流、在线游戏、DNS查询等 |
通过本章的讲解,读者将建立起对TCP协议的宏观理解,为后续基于C#的TCP编程实践打下坚实基础。
2. C#网络编程命名空间System.Net.Sockets
在进行TCP通信开发时,C# 提供了丰富的网络编程支持,其中 System.Net.Sockets 命名空间是实现 TCP/IP 协议栈通信的核心组件。该命名空间封装了底层的 Socket 操作,提供了高级的抽象类和方法,使得开发者可以更加便捷地构建客户端-服务器架构的应用程序。本章将深入探讨 System.Net.Sockets 命名空间的结构、核心类的功能定位、Socket 的操作流程、网络地址解析机制以及异常处理策略,帮助读者掌握 C# 网络编程的基础与进阶知识。
2.1 System.Net.Sockets命名空间概述
System.Net.Sockets 是 .NET Framework 和 .NET Core 中用于处理网络通信的核心命名空间之一。它提供了对 TCP、UDP、IP 等协议的支持,封装了底层操作系统提供的 Socket 接口,使开发者能够以面向对象的方式进行网络编程。
2.1.1 命名空间的核心类与功能分类
该命名空间中包含多个核心类,各自承担不同的功能角色:
| 类名 | 功能描述 |
|---|---|
Socket | 提供对底层网络通信的最直接访问,支持 TCP 和 UDP 协议 |
TcpListener | 封装 TCP 服务器端监听功能,简化服务器端编程 |
TcpClient | 封装 TCP 客户端连接,提供更高层次的抽象 |
UdpClient | 封装 UDP 协议通信 |
IPAddress | 表示 IP 地址 |
IPEndPoint | 表示网络端点(IP 地址 + 端口号) |
NetworkStream | 提供基于流的网络数据传输接口 |
SocketException | 处理底层 Socket 操作异常 |
这些类通过组合使用,可以构建出完整的 TCP 或 UDP 通信模型。例如:
- 使用
TcpListener启动一个监听服务器; - 使用
TcpClient连接到远程服务器; - 通过
Socket实现更灵活的自定义通信逻辑; - 利用
IPAddress和IPEndPoint定位目标主机。
2.1.2 TcpListener与TcpClient类的作用定位
TcpListener 和 TcpClient 是该命名空间中两个最常用的类,它们分别用于实现服务器端和客户端的通信逻辑。
TcpListener 类
TcpListener 主要用于监听客户端连接请求。它封装了创建监听 Socket、绑定地址和端口、启动监听等操作。典型的使用流程如下:
TcpListener listener = new TcpListener(IPAddress.Any, 8080);
listener.Start();
Console.WriteLine("服务器已启动,正在监听端口 8080...");
TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("客户端已连接");
逐行解读:
- 第1行:创建
TcpListener实例,绑定到本地所有 IP 地址(IPAddress.Any)和端口 8080。 - 第2行:调用
Start()方法启动监听。 - 第3行:输出服务器启动信息。
- 第4行:调用
AcceptTcpClient()方法阻塞等待客户端连接。 - 第5行:客户端连接成功后输出提示信息。
TcpClient 类
TcpClient 是客户端类,用于建立与服务器的连接并进行数据通信。其典型使用方式如下:
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
string message = "Hello, Server!";
byte[] data = Encoding.UTF8.GetBytes(message);
stream.Write(data, 0, data.Length);
Console.WriteLine("消息已发送至服务器");
逐行解读:
- 第1行:创建
TcpClient实例,尝试连接 IP 地址为127.0.0.1、端口为 8080 的服务器。 - 第2行:获取网络流,用于数据读写。
- 第3行:定义要发送的消息内容。
- 第4行:将字符串转换为字节数组。
- 第5行:向服务器发送数据。
- 第6行:输出发送成功提示。
2.2 Socket类详解
Socket 类是 System.Net.Sockets 命名空间中最核心的类之一,它直接与操作系统提供的 Socket 接口对接,提供了最底层的网络通信能力。使用 Socket 类可以实现比 TcpListener 和 TcpClient 更灵活的通信控制。
2.2.1 Socket的创建与绑定操作
创建一个 Socket 实例需要指定地址族、Socket 类型和协议类型。例如,创建一个用于 TCP 通信的 IPv4 Socket:
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
参数说明:
-
AddressFamily.InterNetwork:表示使用 IPv4 地址族; -
SocketType.Stream:表示使用流式 Socket(TCP); -
ProtocolType.Tcp:指定使用 TCP 协议。
绑定地址和端口:
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 8080);
serverSocket.Bind(endPoint);
代码分析:
-
IPEndPoint表示一个网络端点(IP + Port); -
Bind()方法将 Socket 绑定到特定的地址和端口上; -
IPAddress.Any表示监听本地所有网络接口。
2.2.2 Socket的连接与监听模式
服务器端:监听连接
服务器端调用 Listen() 方法进入监听状态:
serverSocket.Listen(10);
Console.WriteLine("正在监听客户端连接...");
Socket clientSocket = serverSocket.Accept();
说明:
-
Listen(10):设置最大连接队列长度为 10; -
Accept():阻塞等待客户端连接,返回客户端 Socket。
客户端:主动连接
客户端通过 Connect() 方法发起连接:
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect("127.0.0.1", 8080);
Console.WriteLine("已连接到服务器");
2.2.3 数据发送与接收的基本方法
一旦建立连接,就可以通过 Send() 和 Receive() 方法进行数据传输。
发送数据:
string message = "Hello, Server";
byte[] buffer = Encoding.UTF8.GetBytes(message);
clientSocket.Send(buffer);
接收数据:
byte[] receiveBuffer = new byte[1024];
int bytesReceived = clientSocket.Receive(receiveBuffer);
string response = Encoding.UTF8.GetString(receiveBuffer, 0, bytesReceived);
Console.WriteLine("收到服务器消息:" + response);
流程图说明:
graph TD
A[创建Socket] --> B[绑定地址]
B --> C[监听/连接]
C --> D[发送数据]
D --> E[接收数据]
E --> F[关闭连接]
2.3 网络地址与端口解析
在网络通信中,IP 地址和端口号是定位通信目标的关键信息。 System.Net.Sockets 提供了多个类来处理地址解析与端口管理。
2.3.1 IPAddress与IPEndPoint类的使用
IPAddress 类
IPAddress 类用于表示一个 IP 地址。可以通过字符串构造 IP 地址:
IPAddress ip = IPAddress.Parse("192.168.1.1");
也可以使用预定义常量:
IPAddress localIP = IPAddress.Loopback; // 127.0.0.1
IPAddress anyIP = IPAddress.Any; // 0.0.0.0
IPEndPoint 类
IPEndPoint 是 IP 地址和端口号的组合,用于标识网络通信的端点:
IPEndPoint endPoint = new IPEndPoint(ip, 8080);
示例:结合 Socket 使用:
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 8080));
2.3.2 DNS解析与主机名转换
在实际网络通信中,经常需要将主机名(如 www.example.com)解析为对应的 IP 地址。 Dns 类提供了域名解析功能:
IPHostEntry hostEntry = Dns.GetHostEntry("www.example.com");
foreach (IPAddress ip in hostEntry.AddressList)
{
Console.WriteLine("IP地址:" + ip.ToString());
}
逐行解读:
-
Dns.GetHostEntry():获取指定主机的 IP 地址列表; -
hostEntry.AddressList:返回所有解析到的 IP 地址; - 遍历输出每个 IP 地址。
DNS解析流程图:
graph LR
A[输入主机名] --> B[Dns.GetHostEntry()]
B --> C[解析IP地址列表]
C --> D[选择IP进行通信]
2.4 网络编程中的常见异常类型
在网络通信中,可能会遇到各种异常情况,如连接失败、端口占用、网络中断等。 System.Net.Sockets 提供了多个异常类用于捕获和处理这些问题。
2.4.1 SocketException与IOException的区别
SocketException
SocketException 是当底层 Socket 操作失败时抛出的异常,通常与网络硬件、协议或连接状态有关。
常见错误码示例:
| 错误码 | 含义 |
|---|---|
| 10061 | 连接被拒绝 |
| 10048 | 地址已在使用中 |
| 10054 | 远程主机强制关闭连接 |
使用示例:
try
{
clientSocket.Connect("127.0.0.1", 8080);
}
catch (SocketException ex)
{
Console.WriteLine("Socket异常:" + ex.Message);
}
IOException
IOException 通常与流操作失败有关,比如网络流中断、写入失败等。
使用示例:
try
{
stream.Write(data, 0, data.Length);
}
catch (IOException ex)
{
Console.WriteLine("IO异常:" + ex.Message);
}
2.4.2 异常捕获与程序健壮性设计
为了提高程序的健壮性,建议在所有网络操作中加入异常处理机制,并对不同类型异常进行分类处理:
try
{
// 网络通信操作
}
catch (SocketException ex)
{
Console.WriteLine("Socket错误:" + ex.SocketErrorCode);
// 根据错误码进行重试或断开处理
}
catch (IOException ex)
{
Console.WriteLine("IO错误:" + ex.Message);
// 关闭流、重连或退出程序
}
catch (Exception ex)
{
Console.WriteLine("未知错误:" + ex.Message);
}
异常处理流程图:
graph TD
A[开始通信] --> B[尝试操作]
B --> C{是否出错?}
C -->|是| D[捕获异常]
D --> E[判断异常类型]
E --> F[SocketException处理]
E --> G[IOException处理]
E --> H[其他异常处理]
C -->|否| I[通信成功]
通过本章的讲解,读者可以清晰地掌握 C# 中 System.Net.Sockets 命名空间的主要类及其用途,理解 Socket 编程的核心流程,并掌握异常处理的基本方法。这些知识将为后续章节中实现 TCP 服务器与客户端通信打下坚实基础。
3. TcpListener监听端口配置与实现
在构建TCP服务器时, TcpListener 类是C#中用于监听客户端连接请求的核心类。它提供了简便的接口来初始化监听、绑定端口、接收连接等操作。本章将深入探讨 TcpListener 的使用方式,包括同步与异步处理机制、多线程和 Task 模型的应用,帮助开发者构建稳定、高效的网络服务器程序。
3.1 TcpListener类的核心功能
TcpListener 类封装了底层Socket操作,使得开发者可以更便捷地构建TCP服务器。它主要负责监听特定端口上的连接请求,并为每个连接创建对应的 TcpClient 对象。
3.1.1 初始化与端口绑定流程
要创建一个 TcpListener 实例,可以通过以下几种方式:
- 使用IP地址和端口号构造
- 直接绑定到本地所有IP地址(即
IPAddress.Any) - 使用主机名和端口解析绑定
示例代码:初始化并绑定端口
using System;
using System.Net;
using System.Net.Sockets;
class Program
{
static void Main()
{
int port = 8888;
IPAddress localAddr = IPAddress.Parse("127.0.0.1");
TcpListener listener = new TcpListener(localAddr, port);
// 开始监听
listener.Start();
Console.WriteLine("Server started on port 8888...");
// 停止监听
listener.Stop();
}
}
代码逻辑分析:
-
IPAddress.Parse("127.0.0.1"):指定监听的本地IP地址。 -
new TcpListener(localAddr, port):创建监听实例,绑定到指定IP和端口。 -
listener.Start():启动监听服务。 -
listener.Stop():停止监听,释放资源。
参数说明:
| 参数名 | 类型 | 描述 |
|---|---|---|
localAddr | IPAddress | 要监听的本地IP地址 |
port | int | 监听的端口号,范围为0~65535 |
表格:常见端口绑定问题及解决方法
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 端口被占用 | 其他程序占用了目标端口 | 更换端口号或关闭占用程序 |
| 拒绝访问 | 没有管理员权限 | 以管理员身份运行程序 |
| 地址无效 | IP地址格式错误 | 确保使用正确的IP地址格式 |
3.1.2 开启监听与停止监听操作
在服务器启动后, TcpListener 开始监听客户端连接请求。当不再需要监听时,应调用 Stop() 方法关闭监听。
示例代码:监听与停止
TcpListener listener = new TcpListener(IPAddress.Any, 8888);
listener.Start();
Console.WriteLine("Listening...");
// 假设监听持续10秒后停止
System.Threading.Thread.Sleep(10000);
listener.Stop();
Console.WriteLine("Stopped.");
逻辑分析:
-
IPAddress.Any表示监听所有网络接口。 -
Thread.Sleep(10000)模拟服务器运行一段时间后关闭。 -
Stop()方法会关闭监听,并释放相关资源。
3.2 同步方式下的连接处理
在同步模式下, TcpListener 通过 AcceptTcpClient() 方法阻塞等待客户端连接,适用于简单场景。
3.2.1 AcceptTcpClient方法的应用
该方法会阻塞当前线程,直到有客户端连接成功,返回一个 TcpClient 对象。
示例代码:同步接受连接
TcpListener listener = new TcpListener(IPAddress.Any, 8888);
listener.Start();
Console.WriteLine("Waiting for a connection...");
TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("Connected.");
NetworkStream stream = client.GetStream();
// 接收数据
byte[] buffer = new byte[256];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: " + data);
stream.Close();
client.Close();
代码逻辑分析:
-
AcceptTcpClient():同步等待客户端连接。 -
GetStream():获取与客户端通信的数据流。 -
Read()方法读取客户端发送的数据。
mermaid 流程图:同步连接处理流程
graph TD
A[启动监听] --> B[等待连接]
B --> C{客户端连接?}
C -->|是| D[接受连接]
D --> E[读取数据]
E --> F[处理数据]
F --> G[关闭连接]
C -->|否| H[继续等待]
3.2.2 多客户端连接的基本逻辑
为了支持多个客户端连接,通常在一个循环中不断调用 AcceptTcpClient() 方法。
示例代码:支持多个客户端连接
while (true)
{
TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("New client connected.");
// 简单响应客户端
NetworkStream stream = client.GetStream();
string response = "Hello from server!";
byte[] data = Encoding.UTF8.GetBytes(response);
stream.Write(data, 0, data.Length);
stream.Close();
client.Close();
}
逻辑分析:
- 使用
while(true)循环持续监听。 - 每次连接后发送一条消息,然后关闭连接。
表格:同步方式优缺点对比
| 优点 | 缺点 |
|---|---|
| 实现简单 | 无法处理并发请求 |
| 适合教学演示 | 不适用于生产环境 |
3.3 异步监听机制设计
异步监听可以提高服务器的并发处理能力,避免主线程阻塞,适用于高并发场景。
3.3.1 BeginAcceptTcpClient与EndAcceptTcpClient方法
这两个方法实现了异步连接处理,常用于 IAsyncResult 模式。
示例代码:异步接受连接
TcpListener listener = new TcpListener(IPAddress.Any, 8888);
listener.Start();
Console.WriteLine("Async listening...");
listener.BeginAcceptTcpClient(new AsyncCallback(OnClientConnect), listener);
Console.ReadLine(); // 防止主线程退出
private static void OnClientConnect(IAsyncResult ar)
{
TcpListener listener = (TcpListener)ar.AsyncState;
TcpClient client = listener.EndAcceptTcpClient(ar);
Console.WriteLine("Async client connected.");
// 处理客户端通信
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[256];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: " + data);
stream.Close();
client.Close();
// 继续监听
listener.BeginAcceptTcpClient(new AsyncCallback(OnClientConnect), listener);
}
代码逻辑分析:
-
BeginAcceptTcpClient():异步开始监听。 -
EndAcceptTcpClient():在回调中获取连接。 -
AsyncCallback:定义回调函数,处理连接事件。
3.3.2 使用回调函数处理连接请求
通过定义回调函数,可以实现非阻塞的连接处理,提高服务器响应能力。
衍生讨论:
- 回调函数中可以处理数据读写、身份验证、协议解析等任务。
- 若需并发处理多个连接,可在回调中启动新线程或使用
Task。
3.4 多线程与Task在监听中的应用
为了提升服务器的并发性能,通常将每个客户端连接分配给独立线程或 Task 进行处理。
3.4.1 为每个连接分配独立线程
示例代码:使用线程处理连接
while (true)
{
TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("Client connected.");
Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClient));
clientThread.Start(client);
}
private static void HandleClient(object obj)
{
TcpClient client = (TcpClient)obj;
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[256];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received from client: " + data);
stream.Close();
client.Close();
}
逻辑分析:
- 每个连接启动一个线程处理通信。
- 线程之间互不影响,提高并发能力。
3.4.2 使用Task实现异步连接处理
相比线程, Task 提供了更高效的异步编程模型。
示例代码:使用Task处理连接
while (true)
{
TcpClient client = await listener.AcceptTcpClientAsync();
Console.WriteLine("Client connected.");
_ = Task.Run(() => HandleClientAsync(client));
}
private static async Task HandleClientAsync(TcpClient client)
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[256];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received from client: " + data);
stream.Close();
client.Close();
}
逻辑分析:
-
AcceptTcpClientAsync():异步接受连接。 -
Task.Run():将连接处理交给线程池。 -
await ReadAsync():异步读取数据,避免阻塞。
表格:线程与Task方式对比
| 比较维度 | 线程 | Task |
|---|---|---|
| 资源开销 | 较大 | 较小 |
| 管理复杂度 | 高 | 低 |
| 并发能力 | 一般 | 强 |
| 适用场景 | 简单并发 | 高并发、异步编程 |
小结
通过本章的学习,我们了解了 TcpListener 的核心功能,掌握了同步与异步监听的实现方式,并探讨了多线程与 Task 在连接处理中的应用。这些内容为构建高性能、稳定运行的TCP服务器奠定了坚实基础,也为后续章节中数据通信流程的实现提供了技术支持。
4. 数据通信流程与流式操作
在TCP通信中,数据的传输是通过流(Stream)机制完成的。C#中的 NetworkStream 类是TCP通信中最核心的流操作对象,它负责在客户端与服务器之间进行可靠的数据读写操作。本章将深入解析 NetworkStream 的使用方式,并结合 StreamReader 、 StreamWriter 、 BinaryReader 、 BinaryWriter 等类,介绍如何实现文本与二进制数据的通信。此外,还将探讨通信协议的设计与实现,包括消息头定义、数据分包与粘包问题的处理等关键内容。
4.1 NetworkStream数据流操作
NetworkStream 是 System.Net.Sockets 命名空间下的核心类之一,它封装了TCP连接的数据流,提供了同步与异步的读写接口。它是TCP通信中实现数据传输的基础。
4.1.1 获取与关闭NetworkStream
在C#中, NetworkStream 通常通过 TcpClient 或 Socket 对象获取。以下是一个通过 TcpClient 获取并使用 NetworkStream 的示例:
using System;
using System.Net.Sockets;
class Program
{
static void Main()
{
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
// 使用完成后关闭流和客户端
stream.Close();
client.Close();
}
}
代码逐行解读:
-
TcpClient client = new TcpClient("127.0.0.1", 8080);
创建一个TCP客户端并连接到本地8080端口。 -
NetworkStream stream = client.GetStream();
从TcpClient中获取NetworkStream实例,用于后续的数据读写。 -
stream.Close();
关闭流以释放资源。注意:关闭NetworkStream并不会自动关闭底层的Socket或TcpClient。 -
client.Close();
关闭客户端连接,释放网络资源。
4.1.2 数据读取与写入的基本方法
NetworkStream 提供了 Read() 和 Write() 方法,用于同步读写数据。以下是一个简单的数据收发示例:
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string received = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Received: " + received);
string message = "Hello Server!";
byte[] data = Encoding.UTF8.GetBytes(message);
stream.Write(data, 0, data.Length);
Console.WriteLine("Sent: " + message);
参数说明:
-
buffer:用于接收数据的字节数组。 -
bytesRead:实际读取的字节数。 -
Encoding.UTF8.GetString():将字节数组转换为字符串。 -
stream.Write(data, 0, data.Length):发送字节数组。
逻辑分析:
- 客户端先读取服务器发送的数据,再向服务器发送响应消息。
- 每次读取最大1024字节,适用于小数据量通信。
- 实际开发中,需根据数据长度动态分配缓冲区,或使用
MemoryStream进行累积读取。
4.2 使用StreamReader和StreamWriter进行文本通信
虽然 NetworkStream 可以直接操作字节流,但在处理文本数据时,使用 StreamReader 和 StreamWriter 可以简化开发流程,并自动处理字符编码。
4.2.1 文本数据的同步读写操作
以下是一个使用 StreamReader 和 StreamWriter 进行文本通信的完整示例:
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
class Program
{
static void Main()
{
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
StreamReader reader = new StreamReader(stream, Encoding.UTF8);
StreamWriter writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
string response = reader.ReadLine();
Console.WriteLine("Server says: " + response);
writer.WriteLine("Hello from client!");
}
}
参数说明:
-
StreamReader(stream, Encoding.UTF8):以UTF-8编码读取流中的文本。 -
StreamWriter(stream, Encoding.UTF8):以UTF-8编码写入文本到流中。 -
AutoFlush = true:确保每次写入立即发送,避免缓冲区延迟。
4.2.2 缓冲机制与流的性能优化
StreamReader 和 StreamWriter 内部使用缓冲机制,可以显著提升文本通信的性能。但需要注意以下几点:
| 优化项 | 说明 |
|---|---|
| 缓冲区大小 | 默认为1024字节,可通过构造函数指定更大值,如 new StreamReader(stream, Encoding.UTF8, true, 4096) |
| AutoFlush | 控制是否在每次写入后自动刷新缓冲区,默认为false |
| 异步操作 | 使用 ReadLineAsync() 和 WriteLineAsync() 提升并发性能 |
建议:
- 对于高并发的文本通信,建议使用异步流操作。
- 在数据量较大时,应手动控制缓冲区大小,避免频繁GC或内存浪费。
4.3 二进制数据通信
在需要传输结构化数据或非文本数据(如图像、音频、协议数据包)时,应使用二进制通信。C#中可以使用 BinaryReader 和 BinaryWriter 来处理二进制流。
4.3.1 使用BinaryReader和BinaryWriter
以下是一个使用 BinaryReader 和 BinaryWriter 发送和接收结构化数据的示例:
using System;
using System.IO;
using System.Net.Sockets;
class Program
{
static void Main()
{
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
BinaryWriter writer = new BinaryWriter(stream);
BinaryReader reader = new BinaryReader(stream);
// 写入结构化数据
writer.Write(12345); // 写入整型
writer.Write("Hello Binary"); // 写入字符串
// 读取响应数据
int responseCode = reader.ReadInt32();
string responseMsg = reader.ReadString();
Console.WriteLine($"Response Code: {responseCode}, Message: {responseMsg}");
reader.Close();
writer.Close();
client.Close();
}
}
代码逻辑分析:
-
BinaryWriter按顺序写入整型和字符串,底层会自动添加长度前缀。 -
BinaryReader按顺序读取数据,必须与写入顺序一致,否则会引发异常。 - 使用
BinaryWriter和BinaryReader时,数据格式必须在客户端与服务器端保持一致。
4.3.2 结构化数据的序列化与反序列化
在实际项目中,通常使用 MemoryStream 配合 BinaryFormatter 或 protobuf-net 等序列化库进行结构化数据通信。以下是使用 MemoryStream 和 BinaryFormatter 的示例:
[Serializable]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// 序列化发送
Person person = new Person { Name = "Alice", Age = 30 };
BinaryFormatter formatter = new BinaryFormatter();
using (MemoryStream ms = new MemoryStream())
{
formatter.Serialize(ms, person);
stream.Write(ms.GetBuffer(), 0, (int)ms.Length);
}
// 接收并反序列化
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
using (MemoryStream ms = new MemoryStream(buffer, 0, bytesRead))
{
Person received = (Person)formatter.Deserialize(ms);
Console.WriteLine($"Received: {received.Name}, {received.Age}");
}
注意:
-
BinaryFormatter已被标记为过时,建议使用System.Text.Json或protobuf-net等现代序列化方式。 - 发送与接收端必须使用相同的序列化器和数据结构。
4.4 通信协议的编码与解码设计
在实际网络通信中,直接使用 NetworkStream 可能会遇到 粘包 和 分包 的问题。因此,设计良好的通信协议对于数据的正确解析至关重要。
4.4.1 协议格式定义与消息头设计
一个典型的通信协议格式如下:
| 字段 | 类型 | 长度(字节) | 说明 |
|---|---|---|---|
| 消息长度 | Int32 | 4 | 表示整个消息的字节数(不包括本字段) |
| 消息类型 | Byte | 1 | 表示消息类型(如登录、聊天、心跳等) |
| 消息体 | byte[] | 可变 | 实际传输的数据 |
示例:
// 构造消息头
int bodyLength = Encoding.UTF8.GetByteCount("Hello Protocol");
byte[] body = Encoding.UTF8.GetBytes("Hello Protocol");
byte[] header = BitConverter.GetBytes(bodyLength);
byte msgType = 0x01; // 登录类型
// 合并消息
byte[] message = new byte[4 + 1 + body.Length];
Buffer.BlockCopy(header, 0, message, 0, 4);
message[4] = msgType;
Buffer.BlockCopy(body, 0, message, 5, body.Length);
// 发送
stream.Write(message, 0, message.Length);
4.4.2 数据分包与粘包问题的处理策略
在TCP通信中,由于流式传输的特性,可能导致多个消息粘在一起(粘包)或一个消息被拆分为多个包(分包)。为解决这个问题,可以采用以下策略:
1. 固定长度包头 + 消息体长度
graph TD
A[数据流] --> B{是否包含完整包头?}
B -- 是 --> C{是否有完整消息体?}
C -- 是 --> D[解析消息]
C -- 否 --> E[继续接收剩余数据]
B -- 否 --> F[等待更多数据]
2. 分包缓冲机制
使用 MemoryStream 缓存接收到的数据,并在满足包头长度后提取消息体长度,逐步拼接完整数据包。
MemoryStream buffer = new MemoryStream();
private void OnDataReceived(byte[] data)
{
buffer.Write(data, 0, data.Length);
while (buffer.Length >= 4)
{
buffer.Seek(0, SeekOrigin.Begin);
byte[] lengthBuffer = new byte[4];
buffer.Read(lengthBuffer, 0, 4);
int messageLength = BitConverter.ToInt32(lengthBuffer, 0);
if (buffer.Length >= 4 + messageLength)
{
byte[] fullMessage = new byte[4 + messageLength];
Buffer.BlockCopy(lengthBuffer, 0, fullMessage, 0, 4);
buffer.Read(fullMessage, 4, messageLength);
ProcessMessage(fullMessage);
}
else
{
break;
}
}
// 保留未处理数据
byte[] remaining = buffer.ToArray();
buffer.SetLength(0);
buffer.Write(remaining, 0, remaining.Length);
}
逻辑分析:
- 使用
MemoryStream缓存所有接收到的数据。 - 每次读取前4字节作为消息长度。
- 如果缓冲区数据足够,则提取完整消息进行处理。
- 否则继续等待下一次接收。
本章深入讲解了C#中TCP通信的核心流操作机制,包括 NetworkStream 、文本与二进制通信、以及通信协议的设计与实现。这些内容为构建高性能、稳定可靠的网络应用程序奠定了坚实的基础。下一章将围绕完整TCP服务器与客户端程序的设计展开,进一步提升实际开发能力。
5. 完整TCP服务器与客户端程序设计
5.1 TCP服务器程序的整体架构设计
构建一个稳定可靠的TCP服务器程序,不仅需要关注数据通信本身,还要从系统架构的角度出发,设计合理的启动流程、配置加载机制以及连接管理策略。
5.1.1 服务器启动与配置加载
一个良好的TCP服务器应当具备灵活的配置能力,包括监听的IP地址、端口号、最大连接数等。我们可以通过配置文件(如 appsettings.json )来实现参数的外部化配置,避免硬编码。
// appsettings.json 示例
{
"ServerSettings": {
"IpAddress": "127.0.0.1",
"Port": 8888,
"MaxConnections": 100,
"BufferSize": 1024
}
}
在C#中,我们可以通过 IConfiguration 接口加载这些配置:
// Program.cs 启动逻辑示例
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var serverSettings = builder.Configuration.GetSection("ServerSettings");
string ipAddress = serverSettings["IpAddress"];
int port = int.Parse(serverSettings["Port"]);
服务器启动时,会根据配置初始化 TcpListener ,并启动监听线程。
5.1.2 连接管理与资源释放机制
为了高效管理多个客户端连接,服务器应使用连接池或连接列表来跟踪所有活动连接,并在连接断开时释放相关资源(如Socket、NetworkStream、线程等)。
// 客户端连接信息类
public class ClientSession
{
public TcpClient TcpClient { get; set; }
public NetworkStream Stream { get; set; }
public Thread ReadThread { get; set; }
public bool IsConnected => TcpClient != null && TcpClient.Connected;
}
资源释放应通过事件或异常处理触发,确保不会出现资源泄露:
// 异常断开时释放资源
private void HandleClientDisconnect(ClientSession session)
{
session.Stream.Close();
session.TcpClient.Close();
Clients.Remove(session);
Console.WriteLine("客户端断开连接。");
}
5.2 TCP客户端通信程序实现
5.2.1 客户端连接与数据发送
客户端程序通常由用户界面或控制台触发连接,连接建立后,可以发送和接收数据。
// 客户端连接示例
TcpClient client = new TcpClient();
await client.ConnectAsync("127.0.0.1", 8888);
NetworkStream stream = client.GetStream();
// 发送数据
string message = "Hello Server!";
byte[] data = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(data, 0, data.Length);
为提升交互性,可封装一个 SendAsync 方法,支持异步发送:
public async Task SendAsync(string message)
{
byte[] data = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(data, 0, data.Length);
}
5.2.2 客户端异常处理与重连机制
客户端应具备异常处理能力,尤其是网络中断时的自动重连机制。我们可以使用循环尝试连接,并设置最大重试次数。
int retryCount = 0;
const int maxRetries = 5;
while (retryCount < maxRetries)
{
try
{
await client.ConnectAsync("127.0.0.1", 8888);
Console.WriteLine("重连成功!");
break;
}
catch (SocketException ex)
{
retryCount++;
Console.WriteLine($"连接失败,正在重试第 {retryCount} 次:{ex.Message}");
await Task.Delay(3000); // 3秒后重试
}
}
5.3 心跳机制与连接保持设计
5.3.1 心跳包的发送与接收逻辑
心跳机制用于检测连接是否有效。客户端或服务器定期发送心跳包(如”PING”),对方回应”PONG”。
// 心跳发送逻辑
private async Task SendHeartbeatAsync()
{
while (true)
{
await SendAsync("PING");
await Task.Delay(5000); // 每5秒发送一次心跳
}
}
服务器端接收到心跳后应立即回应:
if (message == "PING")
{
await SendAsync("PONG");
}
5.3.2 检测断线与自动重连方案
客户端收到”PONG”表示连接正常,若超时未收到响应,则认为断线并触发重连机制。
CancellationTokenSource cts = new CancellationTokenSource();
Task heartbeatTask = SendHeartbeatAsync(cts.Token);
Task responseTask = WaitForPongAsync(cts.Token);
if (await Task.WhenAny(heartbeatTask, responseTask) == responseTask)
{
Console.WriteLine("收到心跳响应,连接正常。");
}
else
{
Console.WriteLine("未收到心跳响应,尝试重连...");
cts.Cancel();
// 触发重连逻辑
}
5.4 完整示例:简易聊天程序的实现
5.4.1 服务器端代码结构与功能实现
服务器端采用异步监听,每个连接分配独立线程进行通信,并支持消息广播。
TcpListener listener = new TcpListener(IPAddress.Any, 8888);
listener.Start();
while (true)
{
TcpClient client = await listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client);
}
private async Task HandleClientAsync(TcpClient client)
{
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
string username = "";
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
username = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"{username} 已加入聊天室");
BroadcastMessage($"{username} 进入了聊天室");
while (true)
{
bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
BroadcastMessage($"{username}: {message}");
}
BroadcastMessage($"{username} 离开了聊天室");
}
5.4.2 客户端界面与消息处理逻辑
客户端采用控制台交互方式,支持输入用户名并发送消息:
Console.Write("请输入用户名:");
string username = Console.ReadLine();
await SendAsync(username);
Thread receiveThread = new Thread(ReceiveMessages);
receiveThread.Start();
string input;
while ((input = Console.ReadLine()) != "exit")
{
await SendAsync(input);
}
接收线程用于持续监听服务器消息:
private void ReceiveMessages()
{
while (true)
{
byte[] buffer = new byte[1024];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine(message);
}
}
5.4.3 多用户通信与消息广播机制
服务器端维护一个客户端列表,每当收到消息时,向所有连接的客户端广播:
private List<NetworkStream> clients = new List<NetworkStream>();
private void BroadcastMessage(string message)
{
byte[] data = Encoding.UTF8.GetBytes(message);
foreach (var client in clients.ToList())
{
try
{
client.Write(data, 0, data.Length);
}
catch
{
clients.Remove(client);
}
}
}
通过上述设计,我们可以实现一个完整的TCP聊天系统,具备连接管理、消息广播、心跳检测、异常处理与自动重连等功能,适用于实际的网络通信场景。
简介:网络通信是构建分布式系统的重要组成部分,C#通过丰富的网络库支持TCP通信开发。本文详细介绍C#中基于 System.Net.Sockets 命名空间构建TCP服务器的方法,涵盖 TcpListener 监听连接、 TcpClient 建立通信、 NetworkStream 数据传输、异步处理、异常处理及资源释放等关键环节。通过示例代码讲解服务器与客户端的基本交互流程,并探讨多线程、异步任务、数据读写操作等核心技术,帮助开发者掌握构建稳定高效TCP通信程序的方法。
638

被折叠的 条评论
为什么被折叠?



