同一台电脑两个WinForm程序TCP通信(程序A监听版)
本文提供两套完整可直接运行的WinForm程序代码,实现同一台电脑上的TCP通信:**程序A(服务端)**负责启动TCP监听、等待连接并接收消息,**程序B(客户端)**负责连接程序A并发送消息。代码详细带注释,界面简易,复制即可部署使用,适合新手入门学习。
一、前期准备(VS2022环境)
首先在Visual Studio 2022中创建两个「Windows 窗体应用 (.NET Framework)」项目,命名如下,避免混淆:
-
项目1:
TCPServer(程序A,TCP监听端/服务端) -
项目2:
TCPClient(程序B,TCP连接端/客户端)
提示:两个项目的.NET Framework版本保持一致(建议选择4.7.2及以上),避免兼容性问题。
二、程序A(TCP监听服务端)完整实现
2.1 界面设计(简易版)
在TCPServer项目的Form1窗体上,拖拽3个控件,布局如下(无需复杂设计,能满足基础功能即可):
-
TextBox控件:命名为
txtMsg,设置Multiline = true(允许多行显示)、Dock = Top(停靠在顶部),用于显示监听状态、接收的消息。 -
Button控件1:命名为
btnStart,文本设置为「启动监听」,用于启动TCP监听服务。 -
Button控件2:命名为
btnStop,文本设置为「停止监听」,用于停止监听并释放资源。
界面效果参考:顶部是消息显示框,下方两个按钮并排,简洁直观。
2.2 完整代码(直接复制覆盖)
双击Form1窗体,打开代码编辑器,删除默认代码,复制以下完整代码(带详细注释,关键步骤标注清晰):
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace TCPServer
{
public partial class Form1 : Form
{
// TCP监听套接字(核心对象,负责监听客户端连接)
private TcpListener tcpListener;
// 客户端连接套接字(与客户端建立连接后,用于接收消息)
private TcpClient clientSocket;
// 监听线程(将监听操作放入后台线程,防止界面卡死)
private Thread listenThread;
// 服务端IP和端口(同一台电脑固定用本地回环地址127.0.0.1,端口可自定义,需与客户端一致)
private readonly string ip = "127.0.0.1";
private readonly int port = 8888;
public Form1()
{
InitializeComponent();
// 绑定窗体关闭事件,关闭时释放所有资源,避免内存泄漏
this.FormClosing += Form1_FormClosing;
}
// 「启动监听」按钮点击事件
private void btnStart_Click(object sender, EventArgs e)
{
// 启动后台监听线程,避免阻塞主线程(界面卡死)
listenThread = new Thread(StartListen);
listenThread.IsBackground = true; // 设为后台线程,关闭程序时自动退出
listenThread.Start();
// 向消息框添加状态提示
AppendMsg("服务端已启动监听:" + ip + ":" + port);
}
// 核心方法:启动TCP监听,等待客户端连接
private void StartListen()
{
try
{
// 创建TCP监听对象,绑定IP和端口
tcpListener = new TcpListener(IPAddress.Parse(ip), port);
// 启动监听服务
tcpListener.Start();
AppendMsg("等待客户端连接...");
// 死循环监听(阻塞方法,必须放在线程中),持续等待客户端连接
while (true)
{
// 接受客户端的连接请求,建立连接
clientSocket = tcpListener.AcceptTcpClient();
AppendMsg("客户端已连接!");
// 开启新线程接收该客户端的消息(多线程支持,可扩展多客户端连接)
Thread receiveThread = new Thread(ReceiveMsg);
receiveThread.IsBackground = true;
receiveThread.Start(clientSocket);
}
}
catch (Exception ex)
{
// 捕获异常,避免程序崩溃,提示异常信息
AppendMsg("监听异常:" + ex.Message);
}
}
// 接收客户端发送的消息
private void ReceiveMsg(object obj)
{
// 将传入的参数转为TcpClient对象(与当前连接的客户端绑定)
TcpClient client = (TcpClient)obj;
// 获取网络数据流,用于读取客户端发送的数据
NetworkStream stream = client.GetStream();
try
{
byte[] buffer = new byte[1024]; // 数据缓冲区,用于存储接收的字节数据(1024字节足够日常使用)
int len; // 实际读取到的字节长度
// 循环读取客户端消息(stream.Read()是阻塞方法,客户端断开连接时会抛出异常)
while ((len = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// 将字节数据转为UTF-8编码的字符串(与客户端编码一致,避免乱码)
string msg = Encoding.UTF8.GetString(buffer, 0, len);
// 显示客户端发送的消息
AppendMsg("客户端说:" + msg);
}
}
catch
{
// 客户端断开连接时,提示状态
AppendMsg("客户端断开连接");
}
}
// 「停止监听」按钮点击事件
private void btnStop_Click(object sender, EventArgs e)
{
// 停止监听,释放监听资源
tcpListener?.Stop();
// 关闭客户端连接,释放连接资源
clientSocket?.Close();
AppendMsg("服务端已停止监听");
}
// 窗体关闭事件:释放所有资源
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
// 安全释放资源,避免程序异常
tcpListener?.Stop();
clientSocket?.Close();
}
// 跨线程安全更新界面消息(子线程不能直接操作窗体控件,需通过Invoke实现)
private void AppendMsg(string msg)
{
// 判断是否需要跨线程调用
if (txtMsg.InvokeRequired)
{
// 跨线程调用,更新消息框内容
txtMsg.Invoke(new Action(() =>
{
txtMsg.AppendText(DateTime.Now.ToString("HH:mm:ss ") + msg + Environment.NewLine);
}));
}
else
{
// 主线程直接更新
txtMsg.AppendText(DateTime.Now.ToString("HH:mm:ss ") + msg + Environment.NewLine);
}
}
}
}
三、程序B(TCP客户端)完整实现
3.1 界面设计(简易版)
在TCPClient项目的Form1窗体上,拖拽4个控件,布局如下:
-
TextBox控件1:命名为
txtMsg,设置Multiline = true、Dock = Top,用于显示连接状态、发送/接收的消息。 -
TextBox控件2:命名为
txtSend,用于输入要发送给服务端的消息(单行即可)。 -
Button控件1:命名为
btnConnect,文本设置为「连接服务端」,用于连接程序A(服务端)。 -
Button控件2:命名为
btnSend,文本设置为「发送消息」,用于向服务端发送消息。
界面效果参考:顶部消息显示框,中间是输入框,下方两个按钮并排。
3.2 完整代码(直接复制覆盖)
双击Form1窗体,打开代码编辑器,删除默认代码,复制以下完整代码(与服务端对应,编码一致,注释详细):
csharp
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace TCPClient
{
public partial class Form1 : Form
{
// TCP客户端套接字(核心对象,用于连接服务端并发送消息)
private TcpClient clientSocket;
// 服务端IP和端口(必须与服务端完全一致,否则无法连接)
private readonly string serverIp = "127.0.0.1";
private readonly int serverPort = 8888;
public Form1()
{
InitializeComponent();
// 绑定窗体关闭事件,释放资源
this.FormClosing += Form1_FormClosing;
}
// 「连接服务端」按钮点击事件
private void btnConnect_Click(object sender, EventArgs e)
{
try
{
// 创建TCP客户端对象
clientSocket = new TcpClient();
// 连接服务端(IP和端口必须与服务端一致)
clientSocket.Connect(serverIp, serverPort);
AppendMsg("成功连接到服务端!");
// 开启线程接收服务端消息(本案例服务端暂只接收消息,可扩展双向通信)
Thread receiveThread = new Thread(ReceiveMsg);
receiveThread.IsBackground = true;
receiveThread.Start();
}
catch (Exception ex)
{
// 连接失败时提示异常信息(常见原因:服务端未启动、端口不一致)
AppendMsg("连接失败:" + ex.Message);
}
}
// 接收服务端发送的消息(可扩展双向通信,目前预留)
private void ReceiveMsg()
{
try
{
// 获取与服务端通信的网络数据流
NetworkStream stream = clientSocket.GetStream();
byte[] buffer = new byte[1024]; // 数据缓冲区
int len; // 实际读取的字节长度
// 循环读取服务端消息
while ((len = stream.Read(buffer, 0, buffer.Length)) > 0)
{
string msg = Encoding.UTF8.GetString(buffer, 0, len);
AppendMsg("服务端说:" + msg);
}
}
catch
{
// 与服务端断开连接时提示
AppendMsg("与服务端断开连接");
}
}
// 「发送消息」按钮点击事件
private void btnSend_Click(object sender, EventArgs e)
{
// 先判断是否已连接服务端,未连接则提示
if (clientSocket == null || !clientSocket.Connected)
{
MessageBox.Show("请先连接服务端!");
return;
}
try
{
// 获取输入框中的消息,去除前后空格
string sendMsg = txtSend.Text.Trim();
if (string.IsNullOrEmpty(sendMsg)) return; // 空消息不发送
// 将字符串转为UTF-8编码的字节流(与服务端编码一致)
byte[] buffer = Encoding.UTF8.GetBytes(sendMsg);
// 获取网络数据流,发送消息到服务端
NetworkStream stream = clientSocket.GetStream();
stream.Write(buffer, 0, buffer.Length);
// 显示自己发送的消息
AppendMsg("你说:" + sendMsg);
txtSend.Clear(); // 发送后清空输入框
}
catch (Exception ex)
{
// 发送失败时提示异常信息
AppendMsg("发送失败:" + ex.Message);
}
}
// 窗体关闭事件:释放客户端资源
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
clientSocket?.Close();
}
// 跨线程安全更新界面消息(与服务端逻辑一致)
private void AppendMsg(string msg)
{
if (txtMsg.InvokeRequired)
{
txtMsg.Invoke(new Action(() =>
{
txtMsg.AppendText(DateTime.Now.ToString("HH:mm:ss ") + msg + Environment.NewLine);
}));
}
else
{
txtMsg.AppendText(DateTime.Now.ToString("HH:mm:ss ") + msg + Environment.NewLine);
}
}
}
}
四、运行步骤(超级简单,新手也能搞定)
-
先启动「程序A(TCPServer)」:点击窗体上的【启动监听】按钮,消息框会显示「服务端已启动监听:127.0.0.1:8888」和「等待客户端连接...」,说明监听成功。
-
再启动「程序B(TCPClient)」:点击窗体上的【连接服务端】按钮,消息框显示「成功连接到服务端!」,此时程序A的消息框会显示「客户端已连接!」,说明双方连接成功。
-
发送消息:在程序B的「txtSend」输入框中输入文字(比如"Hello,服务端!"),点击【发送消息】,程序B会显示自己发送的消息,程序A会实时收到并显示「客户端说:Hello,服务端!」。
-
停止测试:程序A点击【停止监听】,会停止接收消息;关闭两个程序时,会自动释放所有资源,避免异常。
五、关键知识点说明(新手必看)
5.1 同一台电脑通信的核心
使用本地回环地址 127.0.0.1(无论电脑是否联网,都能实现本地通信),服务端和客户端的端口必须完全一致(本文用8888,可自定义,建议选择1024以上端口,避免与系统端口冲突)。
5.2 为什么要用线程?
TCP的AcceptTcpClient()(监听连接)和 stream.Read()(读取消息)都是「阻塞方法」,如果直接放在主线程(界面线程)中,会导致界面卡死,无法操作按钮、输入内容,因此必须放在后台线程中执行。
5.3 跨线程更新界面
C# WinForm中,子线程(后台线程)不能直接操作窗体控件(比如更新TextBox的内容),否则会报错,因此需要通过 Invoke方法实现跨线程安全更新,本文中 AppendMsg方法就是专门做这件事的。
5.4 资源释放
在窗体关闭、停止监听时,必须调用 Stop()、Close() 方法释放 TcpListener、TcpClient 和 NetworkStream 的资源,避免内存泄漏和程序异常。
六、扩展功能(可选,按需扩展)
本文实现的是「客户端发送、服务端接收」的单向通信,可轻松扩展以下功能:
-
双向通信:在服务端添加一个输入框和发送按钮,复制客户端的发送逻辑,即可实现服务端向客户端发送消息。
-
多客户端连接:修改服务端的监听逻辑,用集合存储多个客户端的
TcpClient对象,实现群聊效果。 -
发送文件:通过
NetworkStream传输文件字节流,添加文件选择、进度显示功能。 -
心跳检测:定期发送心跳包,检测客户端/服务端是否在线,避免连接异常断开。
-
消息加密:对发送的字节流进行加密(比如AES加密),提高通信安全性。
七、常见问题排查
-
问题1:客户端连接失败,提示「无法连接到目标主机」?
解决:先启动服务端,再启动客户端;检查服务端和客户端的IP、端口是否一致。
-
问题2:发送消息后,服务端接收不到?
解决:检查编码是否一致(本文均用UTF-8);检查客户端是否已成功连接服务端。
-
问题3:界面卡死?
解决:确认监听、接收消息的操作是否放在了后台线程中,是否设置了
IsBackground = true。 -
问题4:关闭程序后,再次启动提示「地址已在使用」?
解决:端口被占用,可更换一个端口(比如9999),确保服务端和客户端端口一致;或等待1-2分钟,系统自动释放端口。
总结
本文提供的两套代码,完全独立、可直接运行,实现了同一台电脑上两个WinForm程序的TCP基础通信,包含了线程安全、异常处理、资源释放等核心细节,适合新手学习TCP通信的基本原理,也可直接用于简单的项目场景。
如果需要扩展功能(比如双向通信、发送文件),可根据文中提示自行修改,或留言咨询。
(注:文档部分内容可能由 AI 生成)