C# TCP 服务端与客户端代码分析与补充
一、代码结构分析
1. 服务端代码特点
使用
Socket
类实现 TCP 服务器功能支持多客户端连接(通过嵌套 Task 实现)
自动获取本机 IPv4 地址
采用异步方式接收客户端连接和数据,避免阻塞 UI 线程
基本的异常处理机制
2. 客户端代码特点
使用异步连接方式(
ConnectAsync
)避免 UI 卡顿提供连接状态显示和基本输入验证
简单的消息发送功能
二、代码问题与改进点
1. 服务端存在的问题
没有正确释放资源,可能导致内存泄漏
接收缓冲区大小依赖
client.Available
,可能导致数据不完整异常处理不完善,客户端断开连接可能导致服务端崩溃
没有客户端管理机制,无法区分不同客户端
缺少向客户端发送消息的功能
2. 客户端存在的问题
没有接收服务端消息的功能
关闭连接的功能不完善
缺少断线重连机制
没有消息历史记录功能
三、改进后的代码实现
服务端改进代码
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace 服务端 {public partial class ServerFrm : Form{// 服务器Socket实例private Socket server = null;// 用于取消任务的令牌源private CancellationTokenSource ctsAcceptClients = null;// 存储所有连接的客户端private Dictionary<Socket, string> connectedClients = new Dictionary<Socket, string>();// 用于线程安全访问客户端字典private object clientLock = new object(); public ServerFrm(){InitializeComponent();// 初始化UIthis.FormClosing += ServerFrm_FormClosing;} private void ServerFrm_Load(object sender, EventArgs e){try{// 1. 实例化服务器Socketserver = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 绑定IP和端口int port = 12345;string ipStr = GetLocalIPAddress();if (string.IsNullOrEmpty(ipStr)){ShowMessage("无法获取有效的IPv4地址");return;} IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ipStr), port);server.Bind(endPoint); // 3. 启动服务器监听server.Listen(100);ShowMessage($"服务器已启动,监听地址: {ipStr}:{port}"); // 4. 开始接受客户端连接StartAcceptingClients();}catch (Exception ex){ShowMessage($"服务器启动失败: {ex.Message}");}} // 获取本地IPv4地址private string GetLocalIPAddress(){IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());foreach (IPAddress ip in host.AddressList){if (ip.AddressFamily == AddressFamily.InterNetwork){return ip.ToString();}}return null;} // 开始接受客户端连接private void StartAcceptingClients(){ctsAcceptClients = new CancellationTokenSource();Task.Run(() =>{try{while (!ctsAcceptClients.IsCancellationRequested){// 等待客户端连接Socket client = server.Accept();if (client.Connected){// 获取客户端信息string clientInfo = client.RemoteEndPoint.ToString();ShowMessage($"新客户端连接: {clientInfo}");// 保存客户端连接lock (clientLock){connectedClients[client] = clientInfo;UpdateClientList();}// 开始接收该客户端的消息StartReceivingMessages(client);}}}catch (Exception ex){if (!ctsAcceptClients.IsCancellationRequested){ShowMessage($"接受客户端连接出错: {ex.Message}");}}}, ctsAcceptClients.Token);} // 接收客户端消息private void StartReceivingMessages(Socket client){CancellationTokenSource cts = new CancellationTokenSource();Task.Run(() =>{byte[] buffer = new byte[1024]; // 使用固定大小缓冲区string clientInfo = connectedClients[client];try{while (!cts.IsCancellationRequested){// 等待接收数据int bytesRead = client.Receive(buffer);if (bytesRead == 0){// 客户端断开连接ShowMessage($"客户端断开连接: {clientInfo}");break;}// 转换为字符串string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);ShowMessage($"收到来自 {clientInfo} 的消息: {message}");}}catch (Exception ex){ShowMessage($"与客户端 {clientInfo} 通信出错: {ex.Message}");}finally{// 清理资源lock (clientLock){connectedClients.Remove(client);UpdateClientList();}try{if (client.Connected)client.Shutdown(SocketShutdown.Both);}catch { }client.Close();ShowMessage($"客户端连接已关闭: {clientInfo}");}}, cts.Token);} // 向客户端发送消息private void SendMessageToClient(Socket client, string message){if (client == null || !client.Connected)return;try{byte[] data = Encoding.UTF8.GetBytes(message);client.Send(data);ShowMessage($"已向 {connectedClients[client]} 发送消息: {message}");}catch (Exception ex){ShowMessage($"发送消息失败: {ex.Message}");}} // 关闭所有客户端连接private void CloseAllClients(){lock (clientLock){foreach (var client in connectedClients.Keys.ToList()){try{if (client.Connected)client.Shutdown(SocketShutdown.Both);client.Close();}catch { }}connectedClients.Clear();UpdateClientList();}} // 更新客户端列表(UI操作)private void UpdateClientList(){Invoke(new Action(() =>{// 假设你有一个ListBox控件名为lbClientslbClients.Items.Clear();foreach (var clientInfo in connectedClients.Values){lbClients.Items.Add(clientInfo);}}));} // 在富文本框中显示消息(线程安全)private void ShowMessage(string message){Invoke(new Action(() =>{richTextBox1.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\n");richTextBox1.ScrollToCaret(); // 滚动到最后一行}));} // 发送按钮点击事件private void btnSend_Click(object sender, EventArgs e){if (lbClients.SelectedItem == null){MessageBox.Show("请选择一个客户端");return;}string clientInfo = lbClients.SelectedItem.ToString();string message = textBoxSend.Text.Trim();if (string.IsNullOrEmpty(message)){MessageBox.Show("请输入要发送的消息");return;}// 查找对应的客户端Socketvar client = connectedClients.FirstOrDefault(c => c.Value == clientInfo).Key;if (client != null){SendMessageToClient(client, message);textBoxSend.Clear();}} // 关闭服务器private void ServerFrm_FormClosing(object sender, FormClosingEventArgs e){// 取消接受客户端的任务if (ctsAcceptClients != null)ctsAcceptClients.Cancel();// 关闭所有客户端连接CloseAllClients();// 关闭服务器if (server != null){server.Close();}}} }
改进后的TCP服务端代码
客户端改进代码
using System; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace 客户端 {public partial class ClientFrm : Form{// 客户端Socketprivate Socket client = null;// 接收消息的 cancellation tokenprivate CancellationTokenSource receiveCts = null;// 连接状态private bool isConnected = false; public ClientFrm(){InitializeComponent();// 初始化UIthis.FormClosing += ClientFrm_FormClosing;btnDisconnect.Enabled = false;} // 连接/断开按钮点击事件private async void btnConnect_Click(object sender, EventArgs e){if (!isConnected){await ConnectToServer();}else{DisconnectFromServer();}} // 连接到服务器private async Task ConnectToServer(){try{string ip = txtIpAddress.Text.Trim();int port;if (!int.TryParse(txtPort.Text.Trim(), out port) || port < 1 || port > 65535){ShowStatus("请输入有效的端口号(1-65535)");return;} // 禁用连接按钮,防止重复点击btnConnect.Enabled = false;ShowStatus("正在连接到服务器..."); // 1. 实例化客户端Socketclient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 连接到服务器IPEndPoint remoteEp = new IPEndPoint(IPAddress.Parse(ip), port);await client.ConnectAsync(remoteEp); // 连接成功isConnected = true;ShowStatus($"已连接到服务器: {ip}:{port}");btnConnect.Text = "断开连接";btnDisconnect.Enabled = true; // 开始接收服务器消息StartReceivingMessages();}catch (Exception ex){ShowStatus($"连接失败: {ex.Message}");client = null;}finally{btnConnect.Enabled = true;}} // 断开与服务器的连接private void DisconnectFromServer(){if (client != null && client.Connected){try{// 取消接收任务if (receiveCts != null)receiveCts.Cancel();// 关闭连接client.Shutdown(SocketShutdown.Both);client.Close();}catch (Exception ex){ShowStatus($"断开连接出错: {ex.Message}");}} // 更新状态isConnected = false;client = null;ShowStatus("已断开与服务器的连接");btnConnect.Text = "连接";btnDisconnect.Enabled = false;} // 开始接收服务器消息private void StartReceivingMessages(){receiveCts = new CancellationTokenSource();byte[] buffer = new byte[1024]; Task.Run(() =>{try{while (!receiveCts.IsCancellationRequested){// 等待接收数据int bytesRead = client.Receive(buffer);if (bytesRead == 0){ShowStatus("服务器已关闭连接");break;}// 转换为字符串string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);ShowMessage($"收到服务器消息: {message}");}}catch (Exception ex){if (!receiveCts.IsCancellationRequested){ShowStatus($"接收消息出错: {ex.Message}");}}finally{// 如果不是主动取消,自动重连if (!receiveCts.IsCancellationRequested){Invoke(new Action(() =>{DisconnectFromServer();// 可以在这里添加自动重连逻辑}));}}}, receiveCts.Token);} // 发送消息到服务器private void btnSend_Click(object sender, EventArgs e){SendMessageToServer();} // 发送消息到服务器private void SendMessageToServer(){if (!isConnected || client == null || !client.Connected){ShowStatus("请先建立连接再发送消息");return;} string message = txtMessage.Text.Trim();if (string.IsNullOrEmpty(message)){ShowStatus("请输入要发送的消息");return;} try{byte[] data = Encoding.UTF8.GetBytes(message);client.Send(data);ShowMessage($"我: {message}");txtMessage.Clear();}catch (Exception ex){ShowStatus($"发送消息失败: {ex.Message}");DisconnectFromServer();}} // 在消息区域显示消息private void ShowMessage(string message){Invoke(new Action(() =>{richTextBox1.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\n");richTextBox1.ScrollToCaret();}));} // 显示状态信息private void ShowStatus(string status){Invoke(new Action(() =>{labelStatus.Text = status;// 同时在消息区域也显示状态信息richTextBox1.AppendText($"[{DateTime.Now:HH:mm:ss}] {status}\n");richTextBox1.ScrollToCaret();}));} // 窗体关闭时确保资源释放private void ClientFrm_FormClosing(object sender, FormClosingEventArgs e){DisconnectFromServer();} // 断开连接按钮点击事件private void btnDisconnect_Click(object sender, EventArgs e){DisconnectFromServer();} // 按Enter键发送消息private void txtMessage_KeyPress(object sender, KeyPressEventArgs e){if (e.KeyChar == (char)Keys.Enter){e.Handled = true; // 防止换行SendMessageToServer();}}} }
四、服务端与客户端功能对比
功能 | 原始代码 | 改进后代码 |
---|---|---|
多客户端支持 | 基础支持,无管理 | 完善支持,可显示 / 选择客户端 |
资源释放 | 不完善 | 完善的资源释放机制 |
消息接收 | 依赖 Available 属性,可能不完整 | 使用固定缓冲区,确保完整接收 |
双向通信 | 服务端无法向客户端发送 | 支持双向通信 |
异常处理 | 基础处理 | 完善的异常捕获和处理 |
UI 体验 | 简单 | 更友好,状态显示更清晰 |
连接管理 | 基础 | 支持手动断开、自动重连 |
消息记录 | 简单显示 | 带时间戳的完整记录 |
五、关键技术点总结
Socket 通信基础
使用
Socket
类创建 TCP 连接服务端需要
Bind
、Listen
、Accept
三个步骤客户端需要
Connect
步骤建立连接
异步编程
使用
Task.Run
和async/await
避免 UI 线程阻塞CancellationTokenSource
用于安全取消任务
多客户端管理
使用字典存储客户端连接
采用线程锁确保多线程安全访问
数据处理
使用固定大小缓冲区接收数据
采用 UTF8 编码进行字符串转换
异常处理
捕获网络通信中可能出现的各种异常
客户端断开连接时的妥善处理
通过这些改进,TCP 通信程序变得更加健壮、易用,能够更好地满足实际应用场景的需求。