当前位置: 首页 > news >正文

C# TCP 服务器和客户端

C# TCP 服务器开发代码解析笔记

本笔记围绕 Windows Forms 环境下的 TCP 服务器代码展开,从核心组件、关键功能实现、技术细节到潜在优化点,系统梳理 TCP 服务器开发的核心逻辑与实践要点,帮助理解网络编程中套接字使用、异步任务控制及客户端管理的核心流程。

一、核心成员变量解析

代码中定义了 3 个关键成员变量,是服务器运行的基础载体,其作用与关联如下:

变量名类型核心作用注意事项
socketServerSocket服务器 “主套接字”,负责初始化服务器、绑定 IP 端口、监听客户端连接请求,是整个服务器的网络入口未启动时为null,需判断非空后再执行Bind/Listen等操作
ctsCancellationTokenSource异步任务 “取消令牌源”,用于控制后台接收连接、接收消息的任务启停,避免线程泄漏每个独立任务需对应独立令牌源,代码中存在复用问题(后续优化点)
clientsDictionary<EndPoint, Socket>存储已连接的客户端集合,键 = 客户端端点(IP + 端口)值 = 客户端专属套接字,实现客户端身份标识与通讯对象绑定线程安全问题:多线程(UI 线程 + 后台任务)操作字典需加锁

二、核心功能模块实现

1. 服务器启动(StartServer()方法)

功能定位

完成服务器套接字的创建、IP 端口绑定及监听启动,是服务器进入 “可连接” 状态的核心步骤。

实现流程(3 步)
// 步骤1:创建TCP类型的服务器套接字
socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 参数说明:
// - AddressFamily.InterNetwork:使用IPv4地址族(区别于IPv6的InterNetworkV6)
// - SocketType.Stream:字节流套接字(TCP协议专属,保证数据有序、可靠传输)
// - ProtocolType.Tcp:明确使用TCP协议
​
// 步骤2:绑定IP与端口(从界面控件获取配置)
IPAddress iPAddress = IPAddress.Parse(txtIP.Text); // 解析界面输入的IP(如127.0.0.1)
EndPoint endPoint = new IPEndPoint(iPAddress, int.Parse(txtPort.Text)); // 构建“IP+端口”端点
socketServer.Bind(endPoint); // 将套接字与端点绑定(一台机器上端口不能重复绑定)
​
// 步骤3:启动监听(允许排队的最大连接数)
socketServer.Listen(100); // 参数100=等待连接的客户端队列长度(超过则新连接被拒绝)
button1.Text = "关闭服务器"; // 更新界面按钮状态,提示服务器已启动
关键细节
  • 代码中注释了 “获取本机 IP” 的逻辑(通过Dns.GetHostName()+ 筛选 IPv4),实际开发中可用于自动填充txtIP,减少手动输入错误。

  • 异常风险:IPAddress.Parse()(无效 IP 格式)、int.Parse(txtPort.Text)(非数字输入)、Bind()(端口已被占用)均可能抛异常,需在调用处(如button1_Click)捕获。

2. 服务器关闭(CloseServer()方法)

功能定位

优雅关闭服务器,释放网络资源,通知所有客户端断开,避免资源泄漏。

实现流程
if (socketServer != null) // 先判断服务器套接字是否存在
{// 步骤1:通知所有已连接客户端“服务器即将关闭”foreach (var client in clients){Socket socket = client.Value;socket.Send(Encoding.UTF8.GetBytes("服务器即将关闭!")); // 发送关闭通知socket.Disconnect(false); // 断开客户端连接(false=不允许后续重用该套接字)}// 步骤2:关闭服务器主套接字,释放端口socketServer.Close();// 步骤3:重置状态,便于下次启动socketServer = null;button1.Text = "启动服务器";
}
潜在问题
  • 未处理Send()异常:若客户端已断开但未从clients中移除,socket.Send()会抛异常,需加try-catch

  • 未清空clients字典:关闭服务器后字典仍保留旧客户端数据,下次启动可能出现逻辑错误,需添加clients.Clear()

3. 接收客户端连接(Accept()方法)

功能定位

在后台异步循环接收客户端连接请求,将新客户端加入管理字典,并为每个客户端启动独立的 “消息接收任务”。

核心逻辑(异步任务嵌套)
cts = new CancellationTokenSource(); // 创建任务取消令牌源
Task.Run(() => // 启动后台任务(避免阻塞UI线程)
{// 外层循环:持续接收新客户端连接while (!cts.IsCancellationRequested){// 步骤1:阻塞等待客户端连接,获取客户端专属套接字Socket socketClient = socketServer.Accept(); // 阻塞方法,有新连接才返回
​// 步骤2:将新客户端加入管理字典(线程安全风险点)if (!clients.ContainsKey(socketClient.RemoteEndPoint)){clients.Add(socketClient.RemoteEndPoint, socketClient);// 更新UI:将客户端列表绑定到ComboBox(需通过Invoke切换到UI线程)Invoke(new Action(() =>{comboBox1.DataSource = null; // 先清空旧数据源(避免绑定冲突)comboBox1.DataSource = new BindingSource(clients, null); // 绑定字典comboBox1.DisplayMember = "Key"; // 界面显示“客户端端点(IP+端口)”comboBox1.ValueMember = "Value"; // 选中项的值为“客户端套接字”}));}
​// 步骤3:为当前客户端启动独立的“消息接收任务”CancellationTokenSource clientCts = new CancellationTokenSource(); // 每个客户端用独立令牌源(修复代码复用问题)Socket currentClient = socketClient; // 捕获变量,避免闭包陷阱Task.Run(() =>{// 内层循环:持续接收当前客户端的消息while (!clientCts.IsCancellationRequested && currentClient.Connected){// 读取客户端发送的字节数据byte[] buffer = new byte[currentClient.Available]; // buffer长度=当前待读取字节数int len = currentClient.Receive(buffer); // 实际读取的字节数
​if (len > 0) // 读取到有效数据{string message = Encoding.UTF8.GetString(buffer); // 字节转字符串(UTF8编码)EndPoint clientEndPoint = currentClient.RemoteEndPoint; // 客户端身份标识
​// 更新UI:显示接收的消息Invoke(new Action(() =>{// 特殊处理:客户端主动断开的通知if (message == "客户端即将断开连接!"){clients.Remove(clientEndPoint); // 从字典移除客户端// 重新绑定ComboBox数据源comboBox1.DataSource = null;if (clients.Count > 0){comboBox1.DataSource = new BindingSource(clients, null);comboBox1.DisplayMember = "Key";comboBox1.ValueMember = "Value";}}// 追加消息到富文本框(格式:【客户端端点】消息内容)richTextBox1.Text += $"【{clientEndPoint}】{message}\r\n";}));}}}, clientCts.Token);}
}, cts.Token);
关键技术点
  1. UI 线程安全:Windows Forms 控件仅允许创建它的线程(UI 线程)修改,因此更新ComboBoxrichTextBox1时必须通过Invoke(new Action(() => { ... }))切换到 UI 线程。

  2. 闭包陷阱:内层Task.Run中若直接使用socketClient,会因闭包导致所有任务共享同一个变量,需用currentClient = socketClient捕获当前客户端套接字。

  3. Accept()阻塞特性socketServer.Accept()是阻塞方法,若无新连接会一直等待,因此必须放在后台任务中,避免卡死 UI。

  4. socketClient.Available:获取当前套接字接收缓冲区中待读取的字节数,以此定义buffer长度,避免内存浪费(但需注意:若数据分批次到达,可能导致读取不完整,后续优化点)。

4. 发送消息(button2_Click()方法)

功能定位

支持 “群发” 和 “单发” 两种模式,将界面输入的文本发送给指定客户端。

实现逻辑
// 输入验证:避免发送空消息
if (string.IsNullOrWhiteSpace(textBox1.Text))
{MessageBox.Show("输入消息,再发送!");return;
}
​
// 模式1:群发(勾选checkBox1)
if (checkBox1.Checked)
{foreach (var client in clients){Socket socket = client.Value;socket.Send(Encoding.UTF8.GetBytes(textBox1.Text)); // 字符串转UTF8字节数组发送}
}
// 模式2:单发(未勾选checkBox1,从ComboBox选择客户端)
else
{Socket client = (Socket)comboBox1.SelectedValue; // 获取选中的客户端套接字// 验证客户端状态:非空且已连接if (client != null && client.Connected){client.Send(Encoding.UTF8.GetBytes(textBox1.Text));}
}
潜在问题
  • 未处理Send()异常:若客户端断开但字典未更新,Send()会抛SocketException,需加try-catch

  • 无 “发送成功 / 失败” 反馈:用户无法知晓消息是否实际发送,可在发送后更新richTextBox1提示发送状态。

5. 窗体关闭处理(Form1_FormClosing事件)

功能定位

确保窗体关闭时,服务器优雅关闭,避免资源泄漏。

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{CloseServer(); // 调用关闭逻辑,释放套接字、通知客户端
}

三、关键技术细节与问题

1. 线程安全问题(高频考点)

代码中存在线程安全风险,主要集中在clients字典的操作:

  • 写操作:后台任务(Accept())向字典添加客户端、接收消息时移除客户端。

  • 读操作:UI 线程(button2_Click)遍历字典群发消息、ComboBox绑定数据源。

  • 解决方案:使用lock

    关键字加锁,确保同一时间只有一个线程操作字典:

    private readonly object clientLock = new object(); // 定义锁对象
    ​
    // 添加客户端时加锁
    lock (clientLock)
    {if (!clients.ContainsKey(socketClient.RemoteEndPoint)){clients.Add(socketClient.RemoteEndPoint, socketClient);}
    }
    ​
    // 移除客户端时加锁
    lock (clientLock)
    {clients.Remove(clientEndPoint);
    }
    ​
    // 遍历字典群发时加锁
    lock (clientLock)
    {foreach (var client in clients){// 发送逻辑}
    }

2. 任务取消令牌源复用问题

原代码中,外层Accept()任务和内层 “消息接收任务” 共用同一个cts,导致:

  • 取消外层任务时,所有内层 “消息接收任务” 也会被取消,不符合预期。

  • 解决方案:为每个 “消息接收任务” 创建独立的CancellationTokenSource(如上文代码优化中所示),确保取消粒度精准。

3. 数据读取不完整问题

原代码中buffer长度由currentClient.Available决定,若客户端发送的消息较大,数据会分批次到达,Available仅表示当前待读取字节数,会导致读取不完整(如消息 “Hello World” 分两次到达,第一次读取 “Hello”,第二次读取 “World”)。

解决方案:

  • 定义固定长度的buffer(如byte[] buffer = new byte[1024]),循环读取直到获取完整数据。

  • 约定 “消息边界”(如末尾加\n),读取到边界符视为消息结束。

四、总结

本 TCP 服务器代码实现了 “启动 - 监听 - 接客 - 收发消息 - 关闭” 的核心流程,基于 Windows Forms 提供了可视化交互界面,关键知识点包括:

  1. Socket类的核心用法:Bind(绑定)、Listen(监听)、Accept(接客)、Send(发消息)、Receive(收消息)。

  2. 异步任务与 UI 线程安全:Task.Run避免 UI 阻塞,Invoke确保控件操作线程安全。

  3. 客户端管理:通过Dictionary<EndPoint, Socket>实现客户端身份与通讯对象的绑定。

C# TCP 客户端开发代码解析笔记

本笔记基于 Windows Forms 环境下的 TCP 客户端代码,从核心组件定义、关键功能实现逻辑、技术细节到优化方向,系统梳理 TCP 客户端与服务器通讯的完整流程,帮助理解客户端侧套接字使用、异步消息接收及连接管理的核心要点。

一、核心成员变量解析

客户端代码仅定义 2 个关键成员变量,承担连接管理与异步任务控制的核心作用,结构简洁但需关注状态一致性:

变量名类型核心作用注意事项
socketClientSocket客户端 “专属套接字”,负责与服务器建立连接、发送消息、接收消息,是客户端与服务器通讯的唯一通道未连接时为null,所有网络操作(Connect/Send/Receive)需先判断非空 + 已连接
ctsCancellationTokenSource异步消息接收任务的 “取消令牌源”,用于控制后台接收服务器消息的任务启停,避免线程泄漏仅关联 “消息接收任务”,需在断开连接时主动取消,防止任务空跑

二、核心功能模块实现

1. 连接服务器(ConnectServer()方法)

功能定位

完成客户端套接字初始化、与服务器建立 TCP 连接,并发送 “连接成功” 通知,是客户端进入通讯状态的第一步。

实现流程(3 步)
// 步骤1:创建TCP类型的客户端套接字
socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 参数说明:
// - AddressFamily.InterNetwork:使用IPv4地址族(需与服务器一致)
// - SocketType.Stream:字节流套接字(TCP协议专属,保证数据可靠传输)
// - ProtocolType.Tcp:明确使用TCP协议,与服务器通讯协议匹配
​
// 步骤2:构建服务器端点(IP+端口)并建立连接
IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text));
// 解析界面输入的服务器IP(如192.168.1.100)和端口(如8888),构建端点对象
socketClient.Connect(iPEndPoint); // 主动向服务器发起连接请求(阻塞方法,直到连接成功或失败)
​
// 步骤3:发送连接成功通知+更新界面状态
socketClient.Send(Encoding.UTF8.GetBytes($"建立连接成功!")); // 向服务器发送连接确认消息
button1.Text = "断开服务器"; // 按钮文本切换,提示当前已连接
关键细节
  • Connect()阻塞特性socketClient.Connect()是阻塞方法,调用后会等待服务器响应(成功 / 失败),若服务器未启动或网络不通,会抛SocketException,需在调用处(如button1_Click)用try-catch捕获异常(如 “无法连接到远程服务器”)。

  • 输入合法性风险IPAddress.Parse(txtIP.Text)(无效 IP 格式,如 “256.256.256.256”)、int.Parse(txtPort.Text)(非数字或端口范围超界,0-65535)会抛异常,实际开发中需先做格式校验(如用IPAddress.TryParseint.TryParse)。

2. 断开服务器连接(DisConnectServer()方法)

功能定位

优雅断开与服务器的 TCP 连接,释放套接字资源,向服务器发送 “断开通知”,确保服务器及时清理客户端记录。

实现流程
if (socketClient != null) // 先判断套接字是否存在,避免空引用异常
{// 步骤1:向服务器发送“即将断开”通知(让服务器主动移除当前客户端)socketClient.Send(Encoding.UTF8.GetBytes("客户端即将断开连接!"));
​// 步骤2:断开连接+关闭套接字socketClient.Disconnect(false); // 断开与服务器的连接(false=不允许后续重用该套接字)socketClient.Close(); // 关闭套接字,释放占用的网络资源
​// 步骤3:重置状态,便于下次连接socketClient = null;button1.Text = "连接服务器"; // 按钮文本恢复初始状态
}
潜在问题
  • 未处理Send()异常:若客户端与服务器的连接已断开(但socketClient未置空),调用Send()会抛SocketException,需加try-catch包裹发送逻辑。

  • 未取消消息接收任务:若后台 “消息接收任务” 仍在运行,断开连接后需调用cts.Cancel()终止任务,否则任务会因socketClient.Connectedfalse退出,但建议主动取消以释放资源。

3. 接收服务器消息(Accept()方法)

功能定位

在后台异步循环接收服务器发送的消息,解析后显示到界面,避免阻塞 UI 线程(核心异步逻辑)。

实现流程(异步任务 + 循环接收)
cts = new CancellationTokenSource(); // 初始化任务取消令牌源
Task.Run(() => // 启动后台任务(无返回值),任务执行在非UI线程
{// 循环条件:任务未取消 且 客户端与服务器保持连接// 【注意】原代码逻辑错误:用“||”导致任务取消后仍可能继续运行,需改为“&&”while (!cts.IsCancellationRequested && socketClient.Connected){// 步骤1:创建缓冲区(1MB大小,足够接收大部分场景的消息)byte[] buffer = new byte[1024 * 1024]; // 1024*1024=1048576字节=1MB// 步骤2:接收服务器发送的字节数据(阻塞方法,直到有数据或连接断开)int len = socketClient.Receive(buffer); // 返回实际读取的字节数if (len > 0) // 读取到有效数据(避免空数据处理){// 步骤3:截取有效数据(避免缓冲区多余的空字节)byte[] data = new byte[len]; // 新建与实际数据长度一致的数组Array.Copy(buffer, 0, data, 0, len); // 从缓冲区复制有效数据到新数组// 步骤4:更新UI显示消息(需切换到UI线程)Invoke(new Action(() =>{// 格式:【服务器端点(IP+端口)】消息内容,追加到富文本框richTextBox1.Text += $"【{socketClient.RemoteEndPoint}】{Encoding.UTF8.GetString(data)}\r\n";}));}}
}, cts.Token); // 传入取消令牌,关联任务与令牌源
关键技术点
  1. UI 线程安全:Windows Forms 控件(如richTextBox1)仅允许创建它的线程(UI 线程)修改,因此必须通过Invoke(new Action(() => { ... }))将 UI 更新逻辑 “委托” 到 UI 线程执行,否则会抛 “跨线程操作无效” 异常。

  2. 缓冲区设计:使用1024*1024字节(1MB)的固定缓冲区,避免因消息过大导致读取不完整(相比 “动态获取Available字节数”,固定缓冲区更稳定,适合大部分场景)。

  3. 有效数据截取Receive(buffer)会将数据写入缓冲区,但缓冲区长度可能大于实际数据长度,因此需用Array.Copy截取前len个字节(len为实际读取长度),避免解析时包含空字符。

  4. 原代码逻辑错误修复:循环条件!cts.IsCancellationRequested || !socketClient.Connected错误,“||” 表示 “任务未取消 或 未连接” 时都循环,会导致任务取消后仍继续运行;需改为 “&&”,表示 “任务未取消 且 已连接” 时才循环,符合预期逻辑。

4. 发送消息到服务器(button2_Click()方法)

功能定位

将界面输入的文本消息转换为字节数组,通过套接字发送给服务器,是客户端主动通讯的核心操作。

实现流程
// 校验客户端状态:仅当套接字非空时才执行发送(未校验“已连接”,需优化)
if (socketClient != null)
{// 步骤1:文本转字节数组(UTF8编码,需与服务器解码方式一致)byte[] messageBytes = Encoding.UTF8.GetBytes(textBox1.Text);// 步骤2:发送字节数组到服务器socketClient.Send(messageBytes);// 【优化点】发送后可清空输入框+更新UI显示“自己发送的消息”,提升用户体验// Invoke(new Action(() => { textBox1.Clear(); richTextBox1.Text += $"【我】{textBox1.Text}\r\n"; }));
}
关键问题
  • 状态校验不完整:仅判断socketClient != null,未判断socketClient.Connected(若套接字存在但连接已断开,Send()会抛异常),需补充校验:

    if (socketClient != null && socketClient.Connected)
    {// 发送逻辑
    }
    else
    {MessageBox.Show("未连接到服务器,无法发送消息!");
    }
  • 无发送反馈:用户点击 “发送” 后,无法知晓消息是否成功发送(如网络中断导致发送失败),需加try-catch捕获SocketException,并提示用户发送结果。

5. 窗体关闭处理(Form1_FormClosing事件)

功能定位

确保窗体关闭时,客户端优雅断开与服务器的连接,释放资源,避免 “僵尸连接”(服务器以为客户端仍在线)。

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{DisConnectServer(); // 调用断开连接逻辑,发送断开通知+关闭套接字cts?.Cancel(); // 【补充优化】主动取消消息接收任务,释放线程资源
}
关键补充

原代码未在窗体关闭时取消cts任务,需补充cts?.Cancel()?.表示若cts非空则执行Cancel()),避免消息接收任务在窗体关闭后仍后台运行,造成线程泄漏。

三、核心技术细节与优化方向

1. 异步任务与取消机制

  • 问题Accept()方法中创建的cts未在断开连接时主动取消,导致即使客户端断开,任务仍可能因循环条件判断延迟而继续运行。

  • 优化:在DisConnectServer()中补充取消逻辑:

    private void DisConnectServer()
    {if (socketClient != null){try{socketClient.Send(Encoding.UTF8.GetBytes("客户端即将断开连接!"));}catch (Exception ex){MessageBox.Show($"发送断开通知失败:{ex.Message}");}socketClient.Disconnect(false);socketClient.Close();socketClient = null;button1.Text = "连接服务器";cts?.Cancel(); // 取消消息接收任务}
    }

2. 异常处理完善

客户端所有网络操作(Connect/Send/Receive)均可能抛SocketException(如网络中断、服务器关闭),原代码仅在button1_Click加了异常捕获,需补充其他场景的异常处理:

  • ConnectServer()异常:捕获 “无效 IP”“端口超界”“无法连接服务器” 等错误:

    private void button1_Click(object sender, EventArgs e)
    {try{if (button1.Text == "连接服务器"){// 先校验IP和端口格式if (!IPAddress.TryParse(txtIP.Text, out IPAddress ip)){MessageBox.Show("请输入有效的IP地址!");return;}if (!int.TryParse(txtPort.Text, out int port) || port < 0 || port > 65535){MessageBox.Show("请输入有效的端口号(0-65535)!");return;}ConnectServer();Accept();}else{DisConnectServer();}}catch (SocketException ex){MessageBox.Show($"网络错误:{ex.Message}");}catch (Exception ex){MessageBox.Show($"未知错误:{ex.Message}");}
    }

3. 用户体验优化

  • 发送消息后清空输入框:在button2_Click发送成功后,调用textBox1.Clear(),避免重复发送。

  • 显示自己发送的消息

    :发送消息时,同步在richTextBox1追加 “【我】消息内容”,让用户清晰看到通讯记录:

    private void button2_Click(object sender, EventArgs e)
    {if (string.IsNullOrWhiteSpace(textBox1.Text)){MessageBox.Show("请输入消息内容!");return;}if (socketClient != null && socketClient.Connected){try{string message = textBox1.Text;socketClient.Send(Encoding.UTF8.GetBytes(message));// 显示自己发送的消息Invoke(new Action(() =>{richTextBox1.Text += $"【我】{message}\r\n";textBox1.Clear();}));}catch (SocketException ex){MessageBox.Show($"发送失败:{ex.Message}");}}else{MessageBox.Show("未连接到服务器,无法发送消息!");}
    }

四、总结

本 TCP 客户端代码实现了 “连接 - 收发消息 - 断开” 的核心功能,基于 Windows Forms 提供了可视化交互界面,关键知识点包括:

  1. Socket类的客户端用法:Connect(主动连接)、Send(发送)、Receive(接收)。

  2. 异步任务控制:用Task.Run+CancellationTokenSource实现后台消息接收,避免 UI 阻塞。

  3. UI 线程安全:用Invoke委托更新界面控件,解决跨线程操作问题。

实际开发中,需重点完善异常处理、状态校验和用户体验优化,确保客户端通讯稳定、交互友好。

http://www.dtcms.com/a/568686.html

相关文章:

  • 【R语言】构建GO、KEGG相关不同物种的R包
  • 缓存三部曲:从线程到分布式
  • LS67211_VC1:48KHz低延时AI降噪USB直播麦克风音频处理器
  • 【C++】分治-快速排序算法习题
  • MySQL第四次作业(索引、视图)
  • Partial Prompt Templates in LangChain
  • 泉州网站平台建设公司网站建设素材图
  • 计算机技术员网站建设怎么网站底部 设计
  • 第50届ICPC亚洲区域赛·成都站,非凸科技持续护航顶尖赛事
  • 企业微信自建应用开发详细教程,如何获取授权链接?如何使用js-sdk?
  • html css js网页制作成品——高定晚礼服HTML+CSS网页设计(5页)附源码
  • 蓝牙钥匙 第43次 特殊用户群体场景下的汽车数字钥匙系统:包容性设计与技术创新
  • 万网如何建设购物网站wordpress分类目录 菜单 页面
  • 智能网联汽车 HD map架构解析
  • HTML常用单标签速查手册
  • 告别算法死记硬背,Hello-Algo 让抽象知识变直观,搭配cpolar穿透工具更自由
  • Go从入门到精通(27) - 并行任务处理器
  • Claude Code 使用 MiniMax M2 模型
  • Auto CAD二次开发——复制和旋转图形对象
  • 全屏响应式网站模板网站seo综合公司
  • php做简单网站教程视频教程企业门户网站模板 下载
  • Rust开发实战之WebSocket通信实现(tokio-tungstenite)
  • 编译缓存利器 ccahce、sccahce
  • Rust开发实战之使用 Reqwest 实现 HTTP 客户端请求
  • 各大公司开源网站广州出台21条措施扶持餐饮住宿
  • gmt_create为啥叫gmt
  • 从 NGINX 到 Kubernetes Ingress:现代微服务流量管理实战
  • 【C++】继承(2):继承与友元,静态成员,多继承黑/白盒复用
  • css实战:常用伪元素选择器介绍
  • 4.4 路由算法与路由协议【2013统考真题】