ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# Socket通信从零实现:TCP服务端与客户端完整教程

C# Socket通信从零实现:TCP服务端与客户端完整教程 写Socket通信这件事起因特别简单我手头有个数据采集的小工具采集端在局域网里的一台工控机上处理端在我自己的电脑上中间隔着几台交换机。试过用文件共享传数据延迟高不说文件锁还常年打架也考虑过数据库中转但为这点数据专门架库多少有点杀鸡用牛刀。最后兜兜转转换回了最朴素的方案——直接用C#写Socket通信服务端挂在一台常开的机器上客户端把数据打过去干净利落。这篇博文要聊的就是一个超简单上手的Socket通信C#项目完整覆盖服务端与客户端的核心实现。不扯那些又长又绕的理论从零开始把代码写出来、跑起来每一步都讲清楚为什么这么写。适合刚接触C#网络编程的人也适合那些写过一些业务代码、但没正经碰过Socket的新手朋友。看完之后你能独立搭出属于自己的服务端和客户端并且知道遇到常见报错该往哪个方向排查。1. 整体设计与思路拆解为什么用Socket怎么选通信方式1.1 为什么选Socket而不是HTTP或数据库很多人在做的第一反应是既然要通信直接用HTTP接口不就行了为什么还要碰Socket这个思路没错但要看场景。HTTP是“请求-响应”模型客户端发一个请求服务端必须回一个响应这个流程天生是单向的、由客户端主动发起的。但很多实际场景里服务端需要主动向客户端推送数据或者两端需要保持长连接、频繁互发消息。你用HTTP做要么靠客户端轮询要么靠一些变通手段既浪费资源又不够实时。Socket就不一样了。它在两端之间建立一条持久的双向通道数据随时可以往任何一个方向流动。两台机器之间一旦建立连接彼此就像同桌传纸条一样想到什么就传什么不需要每次先举手、等回应。实时性、灵活性和省资源的程度都甩HTTP几条街。我那个采集场景还有个痛点数据量不大但频率很高每秒几十条的那种。HTTP每发一次都要建连、封装、断连开销全花在重复劳动上。Socket建好连接后一直复用省下来的资源相当可观。所以说当你需要频繁交互、低延迟、双向通信或者要自己控制连接生命周期时Socket就是比HTTP更合脚的鞋。1.2 TCP与UDP的选择为什么先学TCPSocket底层有两种常用的传输协议TCP和UDP。很多教程上来就讲TCP但很少有人解释为什么。我在这里直接把决策逻辑给你摊开。TCP是面向连接的、可靠的字节流协议。它保证数据一定送达、顺序不会乱、内容不会丢就像寄挂号信每一封都有回执丢了一封会自动重发。UDP则是无连接的报文协议发出去就不管了快是真快但不保证能到也不保证顺序像往楼下扔纸飞机扔出去就不能控制。绝大多数业务场景可靠比快重要。文件传输、命令下发、聊天消息、数据采集这些都不能丢数据。你总不希望客户端发一条“开机”命令服务端因为丢了包而没反应。而且TCP的连接机制天然帮我们解决了“确认对方在线”这个问题UDP你要是想确认对方是否还活着反而得自己设计心跳逻辑。C#的Socket类对TCP和UDP都做了很好的抽象但先学TCP、把连接的建立和维护搞懂再切UDP会容易很多。服务端和客户端的核心交互逻辑第一次做Socket的朋友从TCP入手踩坑最少、成就感来得最快。1.3 同步还是异步初学者别慌着上异步Socket的收发操作有同步和异步两种模式。初学者纠结最多的就是人家说异步好我是不是应该直接写异步我的建议是第一版老老实实写同步代码。同步模式线程会阻塞在接收操作上看起来“傻”但逻辑是一条线走到底的出了问题容易排查。异步模式当然有性能优势尤其是面对大量并发连接时但它引入了回调、状态机这些概念新手很容易被绕晕。我当年第一次写异步版本回调里再套回调出了问题日志根本看不出来是哪一环在等哪一环。后来老老实实先用同步把流程跑通再逐步优化成异步才发现那些抽象的难点只要顺着“谁在等谁”这个思路去理自然就明白了。先同步、后异步这个顺序不是妥协而是一种稳扎稳打的策略。这篇文章的第一版就用同步阻塞模式等核心流程清晰了我再用Async/Await演示升级方案。2. 环境准备与核心概念把地基打牢2.1 开发环境与项目创建C#开发Socket选择哪个.NET版本并不需要纠结。我使用的是.NET 8你可以使用.NET 6及以上版本差异不影响这里写的任何代码。如果机器上装了Visual Studio那直接新建一个控制台应用就行如果你跟我一样偏好轻量用VS Code加SDK也不差命令行里dotnet new console一条命令就能把项目拉起来。核心的类都在System.Net.Sockets命名空间里所以只要在代码文件头部加上using System.Net.Sockets;和using System.Net;该有的API基本都齐了。这里不需要引入任何第三方NuGet包纯粹的原生东西这也是Socket最大的好处之一——没有版本冲突没有包依赖写了就能跑。记得把项目框架版本确认一下。新建控制台项目时csproj文件里TargetFramework如果低于net6.0建议顺手升一下。太老的框架不是不能跑但一些语法糖基础库支持不完整没必要给自己添堵。2.2 Socket核心对象与方法状态机Socket类是一个封装很厚的对象它把底层Tcp/Ip的复杂性全都收起来了对外暴露的就是几个方法。理解这几个方法整个通信流程就懂了一大半。服务端这边是先动手方先Bind()把自己绑定到一个端口上相当于在门牌号上写下自己的地址然后Listen()开始监听表示“我把门打开了等客户上门”接着Accept()就位等着有人敲门。Accept是阻塞的没人来就一直停在那里。客户端这边是上门的一方先Connect()到服务端的IP和端口这一步会发起三次握手像敲门并互相确认身份连上之后双方就可以用Send()和Receive()互相传数据了。Send负责把数据从你的缓冲区推给对方Receive则等着对方的数据送进来。这里有一个特别容易误解的地方Socket的Send和Receive是对同一个连接的两个方向服务端可以Send客户端也可以Send没有固定的谁发谁收。这种双向性正是Socket通信的魅力所在。最后不需要连接了Shutdown()优雅地关闭数据流Close()释放底层资源。2.3 TCP的连接过程与断开过程很多人的理解停留在“Socket就是建立连接然后收发数据”但对连接本身没有直观概念。这里用一个日常场景来比喻。TCP建立连接要经过三次握手。客户端发起Connect时相当于对服务端喊“你好我想连你”服务端收到后回应“好的我知道了我也顺便确认你还在”客户端再回一句“收到那我们开始吧”。三次往返之后双方都确定了对方在线、都同意通信连接才算正式建立。断开时则是四次挥手。任一方说“我这边没话要说了先不发了”另一方收到后可能还有话要交代于是先回一句“收到了”多等一会儿如果自己也没话说再回一个“结束”两轮交互之后连接才完全断开。这种设计是为了保证双方都有机会把最后的话说完不会因为贸然关闭而丢数据。理解这个握手和挥手的过程对你排查问题很有用。比如客户端显示“连接被强制关闭”通常就是对方没走完挥手流程直接拔了网线或进程被杀了这种异常报错在网络编程里非常常见。3. 服务端实现全流程写一个可以跑起来的TCP监听者3.1 服务端核心流程梳理服务端代码表面看着长但核心流程就是五步走创建Socket、绑定端口、启动监听、接受连接、收发数据。第一步创建Socket时有三个参数要理解。AddressFamily.InterNetwork表示使用IPv4地址SocketType.Stream表示流式套接字对应TCPProtocolType.Tcp指定协议为TCP。这三者是配套使用的不要乱改。比如你选了Stream却配UDP运行起来行为会很奇怪。绑定端口的时候我通常会绑定IPAddress.Any意思是监听本机所有网卡上的该端口。这样不管是回环地址、局域网IP还是外网IP只要指向这台机器这个端口都能连进来。如果只绑定127.0.0.1那就只有本机能访问外部机器连不上这在测试环境很容易傻眼。监听的第二个参数是backlog表示排队等待Accept的连接数。我写的10实际生产环境可以调到更高这个数值不能太小否则连接一多后来的连接会被直接拒绝。3.2 服务端完整代码与逐段说明下面这份代码是同步阻塞TCP服务端的完整实现我逐段拆开讲。using System; using System.Net; using System.Net.Sockets; using System.Text; class TcpServer { private static readonly int Port 8888; static void Main() { // 1. 创建Socket对象 Socket listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 绑定IP和端口 IPEndPoint endPoint new IPEndPoint(IPAddress.Any, Port); listenSocket.Bind(endPoint); Console.WriteLine($服务端已启动监听端口: {Port}); // 3. 开始监听 listenSocket.Listen(10); Console.WriteLine(正在等待客户端连接...); while (true) { // 4. 接受客户端连接 Socket clientSocket listenSocket.Accept(); Console.WriteLine($客户端接入: {clientSocket.RemoteEndPoint}); // 5. 接收客户端发送的数据 byte[] buffer new byte[1024]; int length clientSocket.Receive(buffer); string message Encoding.UTF8.GetString(buffer, 0, length); Console.WriteLine($收到消息: {message}); // 6. 回复客户端 string response 服务端已收到: message; byte[] responseBytes Encoding.UTF8.GetBytes(response); clientSocket.Send(responseBytes); // 7. 关闭连接 clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); } } }第4步Accept()会阻塞直到有客户端连接进来它会返回一个新的Socket对象。这里有个关键认知这个新Socket才是跟客户端“专线通话”的通道原来那个listenSocket只负责接客、不负责聊天。消息处理完之后你只要关闭这个clientSocket就行listenSocket要继续守在那里等下一个连接。第5步接收数据我用了一个1024字节的缓冲区。这里面的坑就是数据如果超过1024字节一次Receive拿不完剩下的数据你得再调一次Receive才能取出。实际项目中要么把缓冲区开大要么约定一个应用层协议来处理丢包拆包。平时做小工具1024通常够用但如果传图片视频不能这么写。第6步回复客户端意思是你也得让对方知道你收到了。这步是双向通信里最典型的“回声”模式的体现客户端发什么服务端原样带上标记返回去。第7步关闭时我先调Shutdown再调Close。Shutdown会优雅地通知对端“我不会再发数据了”让对端有机会把接收缓冲区里的数据读完也避免直接Close导致数据丢失或对端异常。这个是良好的关闭习惯值得长期坚持。3.3 用Async/Await升级并发处理上面的同步版本有个明显短板Accept和Receive都会阻塞当前线程。如果你起的是控制台应用主线程被阻塞之后没法干别的事如果有多个客户端同时来连一次只能处理一个其它都要排队。升级方式是用Async/Await配合Task.Run做多客户端处理。下面是改良版的思路using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; class TcpServerAsync { private static readonly int Port 8888; static async Task Main() { Socket listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); listenSocket.Bind(new IPEndPoint(IPAddress.Any, Port)); listenSocket.Listen(10); Console.WriteLine($服务端已启动监听端口: {Port}); while (true) { Socket clientSocket await listenSocket.AcceptAsync(); Console.WriteLine($客户端接入: {clientSocket.RemoteEndPoint}); // 新客户端放在独立任务里处理主循环继续等下一个连接 _ Task.Run(() ProcessClient(clientSocket)); } } private static void ProcessClient(Socket clientSocket) { try { byte[] buffer new byte[1024]; int length clientSocket.Receive(buffer); string message Encoding.UTF8.GetString(buffer, 0, length); Console.WriteLine($收到消息: {message}); string response 服务端已收到: message; byte[] responseBytes Encoding.UTF8.GetBytes(response); clientSocket.Send(responseBytes); } catch (Exception ex) { Console.WriteLine($处理客户端数据时出错: {ex.Message}); } finally { clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); } } }这个版本的核心变化是主循环用AcceptAsync异步等待接进来的客户端丢进后台线程去处理自己立即回过头继续等下一个连接。这样无论多少个客户端同时连进来服务端都能从容应对。“异步方法 后台任务”的组合就是无脑解决并发接入的通用钥匙。3.4 服务端的边界情况与注意事项服务端不是写完就万事大吉的有几个边界情况我每次都要特别小心。第一个是端口占用的问题。Windows上如果你程序崩溃退出或者上一轮还在调试没关干净再启动就会报“Address already in use”。解决办法是先看看谁占了端口netstat -ano | findstr 8888找到PID之后去任务管理器结束对应进程。还有一个狠但实用的办法在Bind之前设置listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)允许端口复用。但注意如果别人正在正常服务别这么干会把人家的连接顶掉。第二个是接收缓冲区大小与粘包问题。同一个TCP连接里如果客户端连续发两条消息服务端一次Receive可能读到两条黏在一起的数据这叫粘包也可能读到的是一条消息拆成两半这叫半包。基础教学代码可以忽略但实际使用中我建议自定义一个信封格式前4个字节表示内容长度后面跟着内容。这样读数据时先读长度再按长度读内容就能精确切分消息。第三个是异常处理。网络通信的异常是常态不是例外客户端可能随时断线服务端读写就会抛异常。一定要用try-catch把收发逻辑包起来尤其多客户端时代不能让一个客户端掉线把整个进程带崩。我见过很多新手写的服务端一个客户端拔了网线整个服务端就崩了就是因为没做隔离。4. 客户端实现全流程主动上门的通信发起者4.1 客户端核心流程梳理客户端逻辑比服务端简单得多核心就三步创建Socket、连接服务端、收发数据。虽然简单却有几个关键点值得展开。客户端的Socket创建参数和服务端完全一样因为对接的必须是同一套协议栈。连接时要填对服务端的IP和端口这两个参数错了就不可能有后续。IP最常写的是127.0.0.1这是本机回环地址用于同一台机器上服务端和客户端联调如果客户端和服务端在不同机器就填服务端机器的局域网IP。连接时需要注意Connect是阻塞的。如果服务端没起来、IP不对、防火墙拦了这一步可能会卡很久甚至抛异常。所以客户端代码一般会设置连接超时而不是傻等。最简单的做法是在Connect之前设置clientSocket.ReceiveTimeout和SendTimeout不过Connect本身的超时可以用LingerState等配置控制或者用异步Connect加超时等待。小工具其实不用太复杂保持简单就好。4.2 客户端完整代码与逐段说明using System; using System.Net; using System.Net.Sockets; using System.Text; class TcpClient { static void Main(string[] args) { // 1. 创建Socket对象 Socket clientSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 连接服务端 IPAddress ip IPAddress.Parse(127.0.0.1); int port 8888; clientSocket.Connect(new IPEndPoint(ip, port)); Console.WriteLine(已连接到服务端); // 3. 发送消息 string message Hello, Server!; byte[] sendBytes Encoding.UTF8.GetBytes(message); clientSocket.Send(sendBytes); // 4. 接收服务端回复 byte[] buffer new byte[1024]; int length clientSocket.Receive(buffer); string response Encoding.UTF8.GetString(buffer, 0, length); Console.WriteLine(服务端回复: response); // 5. 关闭连接 clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); } }第2步是最容易出问题的地方。Connect之前请确认三件事服务端确实在跑、监听端口是8888、防火墙放行了TCP入站规则。我遇到最多的情况是服务端开着却忘记防火墙放行客户端连上去直接超时。第3步发送消息时为什么每次都先Encoding.UTF8.GetBytes()因为网络传输只有字节没有字符。C#的字符串是Unicode字符序列不能直接丢进Socket必须编码成字节序列。同理接收端拿到的也是字节数组要解码回字符串才能显示。编码方式两端必须一致如果一边用UTF8另一边用GBK中文就会变成乱码。这也是跨语言、跨平台通信时最常见的坑。第4步接收等待时要理解Receive是“一直等到至少读到一点数据才返回”。它不保证把对方一次Send的数据全读出来这又回到了粘包半包的问题。不过对于基础教学测试服务端发的消息小一次能读完问题不大。第5步关闭连接和服务端一样先Shutdown再Close。别小看这个动作很多网络故障就是客户端不优雅关闭导致的比如服务端那边会出现“远程主机强迫关闭了一个现有的连接”的报错。4.3 超时控制与断线重连的思路真实项目里客户端长期挂在一个连接上对方服务端可能重启、网络可能闪断。所以断线重连几乎是必须做的。我的做法是写一个循环尝试执行发送逻辑捕获到异常后先关闭旧Socket然后延时几秒重新执行Connect。加上重试上限比如连续5次失败就不再死循环。while (retryCount 5) { try { Socket client new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); client.Connect(serverIp, port); // 收发业务逻辑... break; } catch (SocketException ex) { retryCount; Console.WriteLine($连接失败正在重试({retryCount}/5): {ex.Message}); Thread.Sleep(2000); } }这个模式在工控、巡检等场景里非常实用。千万别小看这个重连逻辑它能让整个系统在面对偶发网络抖动时依然保持稳定。4.4 服务端与客户端的联调验证代码写完之后怎么验证我的习惯是先在本机跑联调。开两个终端第一个运行服务端第二个运行客户端。服务端会打印“客户端接入”和收到的消息客户端会打印服务端的回复。看到这两条日志一一对上整个链路就通了。如果要模拟跨机器通信把客户端的IP换成服务端那台机器的局域网IP即可。联调时最推荐的验证方式是把服务端那台机器上的防火墙临时关掉确认连通性没问题之后再精确添加允许端口规则。这样能区分到底是业务代码问题还是网络策略问题。5. 常见问题与排查技巧实录踩过的坑都给你列出来5.1 典型报错速查表下面这张表是我实际调试中反复遇到、最有代表性的几个报错每一个的背后原因都值得单独说一嘴。报错信息最常见原因解决方向Address already in use端口被占用或上次进程未退出换端口或结束占用进程A connection attempt failed because connected party did not properly respond目标IP不可达服务端没监听确认服务端已启动检查IP和端口An existing connection was forcibly closed by the remote host对端直接关闭连接或崩溃检查对端程序异常退出增加异常处理SocketException: 10054对方重置连接服务端重启或崩溃客户端做好断线重连Access denied 或 没有权限端口号低于1024或防火墙拦截Windows下用大于1024的端口或配置防火墙10054这个错误码很有代表性我早期第一次遇到完全懵了。后来理解TCP挥手过程才明白这就是对方没来得及说“再见”就强行断开了——服务端进程被kill或者网络闪断。知道了原因处理方式也就清晰了客户端把这些异常视为连接失效走重连逻辑即可。5.2 粘包与半包问题的现场还原粘包这个事光看定义很难有直观感受我来说说实际遇到的情况。有次我写的一个小项目客户端每次给服务端发一条坐标数据。测单条时一切正常一旦循环快速发送服务端收到的消息就出现了“1,2,3,4,5,6,7,8”这样两三条数据黏在一起的情况。这就是TCP的字节流特性导致的——TCP不关心你怎么切分消息它只会尽力保证字节按序送到。你Send两次它可能合并成一次传输你Send一次它也可能拆成好几段。解决思路是设计简单的应用层协议。我在这里给出一个最直白的长度前缀方案发送时先算出消息的字节长度转成4字节整数跟消息内容拼在一起发接收时先读4字节得到长度再按这个长度读消息内容。这样无论粘包还是半包都能精确还原出每一条完整消息。// 发送端 byte[] body Encoding.UTF8.GetBytes(message); byte[] header BitConverter.GetBytes(body.Length); byte[] packet new byte[4 body.Length]; Buffer.BlockCopy(header, 0, packet, 0, 4); Buffer.BlockCopy(body, 0, packet, 4, body.Length); clientSocket.Send(packet); // 接收端读取完整消息的辅助方法 byte[] buffer new byte[4]; int idx 0; while (idx 4) { int len clientSocket.Receive(buffer, idx, 4 - idx, SocketFlags.None); idx len; } int bodyLength BitConverter.ToInt32(buffer, 0);这段伪代码的思路是把“按长度读取”封装成一个循环保证一定拿到4字节的头再按长度读body。虽然看起来多写了几行但它把混乱的网络字节流变成了可预测的消息序列。你做上位机、做采集工具只要涉及多条消息传输这个模式早晚要遇到提前掌握省心很多。5.3 高效率调试的几个习惯调试网络程序纯靠打日志是行不通的我慢慢养成了一套自己的排查套路。第一多窗口并行。服务端、客户端分开开窗口窗口标题上写明角色和端口。看起来是小事但当你服务端同时连着好几个客户端时没有清晰的分区和命名极其容易搞混。第二在关键路径上打印对端地址和消息体。比如Console.WriteLine($收到来自 {clientSocket.RemoteEndPoint} 的消息: {message})。光打印“收到消息”没有用你得知道是谁发的、从哪个端口发的。RemoteEndPoint在这里就是唯一线索很多定位问题就靠它。第三会用抓包工具或Wireshark做检查。虽然写的是C#客户端但抓包看的是最底层的通信状态——TCP握手有没有完成、数据有没有重传、RST包是谁发出来的。这个工具一开始用会觉得复杂但一旦上手效率提升立竿见影。先用小小的过滤规则比如tcp.port 8888就能把干扰信息过滤得差不多。第四别忽略防火墙。Windows防火墙默认会拦外来入站连接。如果你跑在同一台机器上通常没问题但如果客户端在另一台机器上连不上先检查防火墙监听规则再查代码否则你会白白在代码里找半天bug。5.4 从控制台到实际项目的演进路径这篇文章里的Demo代码是学习用的真正用到项目里还需要几个方向的演进我把自己的路线整理出来给你指个方向。首先是日志替代Console。生产环境不可能一直盯着控制台窗口你需要把Socket连接事件、收发消息、异常堆栈写进日志文件最好再带时间戳。一个连接什么时候建立、什么时候断开、发送了什么这些信息在排查问题时就是生命线。其次是消息格式的规范化。上面聊天室里可以传字符串但真实系统通常需要传结构化对象。我的习惯是定义JSON消息体发之前序列化成字节收到后再反序列化。C#这边用System.Text.Json就够了不需要额外引包。然后是心跳机制。TCP本身有KeepAlive选项但默认时间太长且不灵活很多场景下服务端和客户端之间会自己约定一个心跳包比如每5秒发一个“我是客户端我还活着”的消息服务端一段时间内没收到心跳就判定连接死了主动清理资源。这在无人值守的工控环境里几乎是标配。6. 从通信到系统的进阶思考下一步往哪里走6.1 安全与异常处理的价值Socket通信的代码可以在局域网里跑得很欢但一旦涉及到公网或跨网络安全问题就得提上日程。TCP本身是明文传输的数据包在网络上是裸奔的任何能抓包的人都能看到你传的内容。对于敏感业务建议在应用层做加密或者直接用TLS/SSL包装现有Socket。C#里用SslStream包一层就能实现加密通道代码结构不需要大改。另外网络程序最大的特性就是“永远不知道对方下一秒还在不在”。你的代码必须把所有网络操作都看作可能失败的try-catch、超时控制、数据校验、重试机制这些不是可选项而是必选项。我在前文不断强调异常处理因为在这方面丢过的分远比任何业务逻辑都来得痛。6.2 从单服务端到多客户端架构如果一个服务端要跟几十上百个客户端通信你在第3节看到的简单循环方式就不够用了。方向大致有两个一是保持异步Socket模式配合线程池或Task来处理每个客户端连接二是用现成的框架简化底层细节。要不要上框架我的看法是学习阶段一定要亲手写过裸Socket否则你不知道框架帮你解决了什么但项目阶段该上框架就上框架把精力放到业务上。这里不做具体的框架推荐但你可以把本文的知识当作理解底层的基础框架用起来会通透得多。6.3 实践建议动起手来才是王道最后分享一点点个人体会。Socket通信这个主题网上教程一抓一大把但看一百个教程也不如自己动手跑一遍来得实在。我把代码贴在这里真心建议你把它们原样敲进项目里先跑通再改着玩。你可以试着改改端口、改改监听地址、故意把服务端关掉看看客户端报什么错、每隔几百毫秒连续发多条消息看看会不会粘包。这些看似无聊的试验恰恰是积累直觉最快的方式。等你真实地踩过几次坑再回头看这篇博客里的提醒你会发现自己已经真正理解这些东西的含义了。毕竟网络编程的答案不在文字里在一行行自己跑起来的代码里。
返回列表