C# Socket多线程数据交互:学生信息查询功能实现问询
实现C# Socket多线程服务器与客户端(限制2个连接)的完整方案
刚好做过类似的Socket多线程项目,给你整理一个能直接落地的实现方案,从服务器到客户端再到XML数据处理都拆解清楚,完全贴合你的需求:
一、先理清楚核心流程
怕走偏,先把需求再明确一遍:
- 服务器最多同时接受2个客户端连接,多的会排队等待名额释放(你也可以按需改成直接拒绝)
- 客户端全程自动交互:发学生ID → 收专业 → 立刻回发专业 → 收该专业的平均GPA
- 服务器要处理两次查询:ID查专业,专业查平均GPA,数据都存在XML里
二、先搞定XML数据结构
先定义好XML的格式,后续查询才方便,创建一个StudentData.xml文件,内容如下:
<?xml version="1.0" encoding="utf-8" ?> <Students> <Student> <ID>S001</ID> <Major>PHYSICS</Major> <GPA>3.75</GPA> </Student> <Student> <ID>S002</ID> <Major>PHYSICS</Major> <GPA>3.9</GPA> </Student> <Student> <ID>S003</ID> <Major>COMPUTER SCIENCE</Major> <GPA>3.8</GPA> </Student> </Students>
三、服务器端实现
服务器要做这几件事:监听端口、限制最多2个连接、为每个客户端创建独立会话处理交互逻辑、查询XML数据。我用SemaphoreSlim控制并发连接数,用async/await处理会话(比手动创建线程更简洁高效):
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; namespace SocketServer { class Program { // 限制最多2个客户端同时连接 private static readonly SemaphoreSlim _connectionSemaphore = new SemaphoreSlim(2, 2); private static readonly XDocument _studentDoc = XDocument.Load("StudentData.xml"); private const int Port = 8888; static async Task Main(string[] args) { var listener = new TcpListener(IPAddress.Any, Port); listener.Start(); Console.WriteLine($"服务器已启动,监听端口 {Port},最多接受2个客户端连接"); try { while (true) { // 等待可用的连接名额 await _connectionSemaphore.WaitAsync(); Console.WriteLine("等待客户端连接..."); // 接受客户端连接 var client = await listener.AcceptTcpClientAsync(); Console.WriteLine($"新客户端连接:{client.Client.RemoteEndPoint}"); // 用Task处理客户端会话,不阻塞主线程 _ = HandleClientSession(client); } } catch (Exception ex) { Console.WriteLine($"服务器异常:{ex.Message}"); } finally { listener.Stop(); } } private static async Task HandleClientSession(TcpClient client) { try { using (client) using (var stream = client.GetStream()) { byte[] buffer = new byte[1024]; int bytesRead; // 第一步:接收学生ID,返回对应专业 Console.WriteLine("等待客户端发送学生ID..."); bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); string studentId = Encoding.UTF8.GetString(buffer, 0, bytesRead).Trim(); Console.WriteLine($"收到学生ID:{studentId}"); // 查询专业,处理不存在的ID情况 string major = GetMajorById(studentId) ?? "UNKNOWN"; Console.WriteLine($"查询到专业:{major},发送给客户端"); await stream.WriteAsync(Encoding.UTF8.GetBytes(major)); // 第二步:接收客户端回发的专业,返回平均GPA Console.WriteLine("等待客户端回发专业..."); bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); string receivedMajor = Encoding.UTF8.GetString(buffer, 0, bytesRead).Trim(); Console.WriteLine($"收到专业:{receivedMajor}"); // 查询平均GPA,处理不存在的专业情况 double avgGpa = GetAverageGpaByMajor(receivedMajor); string gpaResponse = avgGpa.ToString("F2"); Console.WriteLine($"查询到平均GPA:{gpaResponse},发送给客户端"); await stream.WriteAsync(Encoding.UTF8.GetBytes(gpaResponse)); Console.WriteLine($"客户端会话结束:{client.Client.RemoteEndPoint}"); } } catch (Exception ex) { Console.WriteLine($"客户端会话异常:{ex.Message}"); } finally { // 释放连接名额,让下一个客户端可以连接 _connectionSemaphore.Release(); Console.WriteLine("连接名额已释放"); } } // 根据学生ID查询专业 private static string GetMajorById(string studentId) { var student = _studentDoc.Descendants("Student") .FirstOrDefault(s => s.Element("ID")?.Value == studentId); return student?.Element("Major")?.Value; } // 根据专业查询平均GPA private static double GetAverageGpaByMajor(string major) { var students = _studentDoc.Descendants("Student") .Where(s => s.Element("Major")?.Value.Equals(major, StringComparison.OrdinalIgnoreCase) == true) .Select(s => double.TryParse(s.Element("GPA")?.Value, out var gpa) ? gpa : 0) .ToList(); return students.Count == 0 ? 0 : students.Average(); } } }
四、客户端实现
客户端全程自动交互,无需用户输入(这里把测试用的学生ID写死了,你也可以改成从命令行传入或者读取配置):
using System; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace SocketClient { class Program { private const string ServerIp = "127.0.0.1"; private const int Port = 8888; // 测试用的学生ID,可按需修改 private const string TestStudentId = "S001"; static async Task Main(string[] args) { using (var client = new TcpClient()) { try { // 连接服务器 await client.ConnectAsync(ServerIp, Port); Console.WriteLine("已连接到服务器"); using (var stream = client.GetStream()) { // 第一步:发送学生ID Console.WriteLine($"发送学生ID:{TestStudentId}"); await stream.WriteAsync(Encoding.UTF8.GetBytes(TestStudentId)); // 接收专业 byte[] buffer = new byte[1024]; int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); string major = Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine($"收到专业:{major}"); // 第二步:回发专业 Console.WriteLine($"回发专业:{major}"); await stream.WriteAsync(Encoding.UTF8.GetBytes(major)); // 接收平均GPA bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); string avgGpa = Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine($"收到该专业的平均GPA:{avgGpa}"); } Console.WriteLine("客户端会话结束"); } catch (Exception ex) { Console.WriteLine($"客户端异常:{ex.Message}"); } } } } }
五、关键要点说明
- 连接限制:用
SemaphoreSlim(2,2)严格控制同时在线的客户端数量,第三个客户端会自动排队,直到有连接释放;如果要直接拒绝第三个连接,可以在WaitAsync前判断当前计数,或者设置超时时间。 - 异步处理:全程用
async/await和Task处理会话,避免主线程阻塞,性能比手动创建线程更优。 - XML查询:用LINQ to XML实现快速查询,同时处理了空值、格式错误等异常情况,避免程序崩溃。
- 资源释放:用
using语句自动释放TcpClient和NetworkStream资源,确保连接关闭后资源被及时回收。
测试步骤
- 把
StudentData.xml放到服务器项目的输出目录下(或者修改代码中的文件路径)。 - 启动服务器,再启动最多2个客户端,观察控制台输出是否符合预期流程。
- 如果启动第三个客户端,它会等待直到前两个客户端中的一个断开,才会建立连接。
内容的提问来源于stack exchange,提问作者John Wick
相关产品推荐
相关产品推荐

