C# TCP服务器通信程序设计与实现

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:网络通信是构建分布式系统的重要组成部分,C#通过丰富的网络库支持TCP通信开发。本文详细介绍C#中基于 System.Net.Sockets 命名空间构建TCP服务器的方法,涵盖 TcpListener 监听连接、 TcpClient 建立通信、 NetworkStream 数据传输、异步处理、异常处理及资源释放等关键环节。通过示例代码讲解服务器与客户端的基本交互流程,并探讨多线程、异步任务、数据读写操作等核心技术,帮助开发者掌握构建稳定高效TCP通信程序的方法。
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聊天系统,具备连接管理、消息广播、心跳检测、异常处理与自动重连等功能,适用于实际的网络通信场景。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:网络通信是构建分布式系统的重要组成部分,C#通过丰富的网络库支持TCP通信开发。本文详细介绍C#中基于 System.Net.Sockets 命名空间构建TCP服务器的方法,涵盖 TcpListener 监听连接、 TcpClient 建立通信、 NetworkStream 数据传输、异步处理、异常处理及资源释放等关键环节。通过示例代码讲解服务器与客户端的基本交互流程,并探讨多线程、异步任务、数据读写操作等核心技术,帮助开发者掌握构建稳定高效TCP通信程序的方法。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值