描述
开 本: 16开纸 张: 胶版纸包 装: 平装-胶订是否套装: 否国际标准书号ISBN: 9787121192821
√ 豆瓣8.8高分,众多Programming专家和网络编程高手盛赞力荐,用Effective写法写出Primer体系与架构。
√ 给出大量 C 多线程网络编程的标准做法,让出自你手的网络程序从此可以对接商用,不再需要日夜修补。
√ 成体系示范现代C 代码,并通过好的写法传授C 设计理念,从而实现用极少的代码写出可靠的代码质量。
√ Linux网络新特性|多线程实践|理解语言三管齐下,既强调基础和对业务理解的重要性,又直接给出避坑指南。
《Linux多线程服务端编程》主要讲述采用现代 C 在 x86-64 Linux 上编写多线程 TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即 one loop per thread。这是在Linux 下以 native 语言编写用户态高性能网络程序*成熟的模式,掌握之后可顺利地开发各类常见的服务端网络应用程序。《Linux多线程服务端编程》以muduo网络库为例,讲解这种编程模型的使用方法及注意事项。《Linux多线程服务端编程》的宗旨是贵精不贵多。掌握两种基本的同步原语就可以满足各种多线程同步的功能需求,还能写出更易用的同步设施。掌握一种进程间通信方式和一种多线程网络编程模型就足以应对日常开发任务,编写运行于公司内网环境的分布式服务系统。
第 1 部分 C 多线程系统编程
第 1 章 线程安全的对象生命期管理
1.1 当析构函数遇到多线程
1.1.1 线程安全的定义
1.1.2 MutexLock 与 MutexLockGuard
1.1.3 一个线程安全的 Counter 示例
1.2 对象的创建很简单
1.3 销毁太难
1.3.1 mutex 不是办法
1.3.2 作为数据成员的 mutex 不能保护析构
1.4 线程安全的 Observer 有多难
1.5 原始指针有何不妥
1.6 神器 shared_ptr/weak_ptr
1.7 插曲:系统地避免各种指针错误
1.8 应用到 Observer 上
1.9 再论 shared_ptr 的线程安全
1.10 shared_ptr 技术与陷阱
1.11 对象池
1.11.1 enable_shared_from_this
1.11.2 弱回调
1.12 替代方案
1.13 心得与小结
1.14 Observer 之谬
第 2 章 线程同步精要
2.1 互斥器(mutex)
2.1.1 只使用非递归的 mutex
2.1.2 死锁
2.2 条件变量(condition variable)
2.3 不要用读写锁和信号量
2.4 封装 MutexLock、MutexLockGuard、Condition
2.5 线程安全的 Singleton 实现
2.6 sleep(3) 不是同步原语
2.7 归纳与总结
2.8 借 shared_ptr 实现 copy-on-write
第 3 章 多线程服务器的适用场合与常用编程模型
3.1 进程与线程
3.2 单线程服务器的常用编程模型
3.3 多线程服务器的常用编程模型
3.3.1 one loop per thread
3.3.2 线程池
3.3.3 推荐模式
3.4 进程间通信只用 TCP
3.5 多线程服务器的适用场合
3.5.1 必须用单线程的场合
3.5.2 单线程程序的优缺点
3.5.3 适用多线程程序的场景
3.6 “多线程服务器的适用场合”例释与答疑
第 4 章 C 多线程系统编程精要
4.1 基本线程原语的选用
4.2 C/C 系统库的线程安全性
4.3 Linux 上的线程标识
4.4 线程的创建与销毁的守则
4.4.1 pthread_cancel 与 C
4.4.2 exit(3) 在 C 中不是线程安全的
4.5 善用 __thread 关键字
4.6 多线程与 IO
4.7 用 RAII 包装文件描述符
4.8 RAII 与 fork()
4.9 多线程与 fork()
4.10 多线程与 signal
4.11 Linux 新增系统调用的启示
第 5 章 高效的多线程日志
5.1 功能需求
5.2 性能需求
5.3 多线程异步日志
5.4 其他方案
第 2 部分 muduo 网络库
第 6 章 muduo 网络库简介
6.1 由来
6.2 安装
6.3 目录结构
6.3.1 代码结构
6.3.2 例子
6.3.3 线程模型
6.4 使用教程
6.4.1 TCP 网络编程本质论
6.4.2 echo 服务的实现
6.4.3 七步实现 finger 服务
6.5 性能评测
6.5.1 muduo 与 Boost.Asio、libevent2 的吞吐量对比
6.5.2 击鼓传花:对比 muduo 与 libevent2 的事件处理效率
6.5.3 muduo 与 Nginx 的吞吐量对比
6.5.4 muduo 与 ZeroMQ 的延迟对比
6.6 详解 muduo 多线程模型
6.6.1 数独求解服务器
6.6.2 常见的并发网络服务程序设计方案
第 7 章 muduo 编程示例
7.1 五个简单 TCP 示例
7.2 文件传输
7.3 Boost.Asio 的聊天服务器
7.3.1 TCP 分包
7.3.2 消息格式
7.3.3 编解码器 LengthHeaderCodec
7.3.4 服务端的实现
7.3.5 客户端的实现
7.4 muduo Buffer 类的设计与使用
7.4.1 muduo 的 IO 模型
7.4.2 为什么 non-blocking 网络编程中应用层 buffer 是必需的
7.4.3 Buffer 的功能需求
7.4.4 Buffer 的数据结构
7.4.5 Buffer 的操作
7.4.6 其他设计方案
7.4.7 性能是不是问题
7.5 一种自动反射消息类型的 Google Protobuf 网络传输方案
7.5.1 网络编程中使用 Protobuf 的两个先决条件
7.5.2 根据 type name 反射自动创建 Message 对象
7.5.3 Protobuf 传输格式
7.6 在 muduo 中实现 Protobuf 编解码器与消息分发器
7.6.1 什么是编解码器(codec)
7.6.2 实现 ProtobufCodec
7.6.3 消息分发器(dispatcher)有什么用
7.6.4 ProtobufCodec 与 ProtobufDispatcher 的综合运用
7.6.5 ProtobufDispatcher 的两种实现
7.6.6 ProtobufCodec 和 ProtobufDispatcher 有何意义
7.7 限制服务器的并发连接数
7.7.1 为什么要限制并发连接数
7.7.2 在 muduo 中限制并发连接数
7.8 定时器
7.8.1 程序中的时间
7.8.2 Linux 时间函数
7.8.3 muduo 的定时器接口
7.8.4 Boost.Asio Timer 示例
7.8.5 Java Netty 示例
7.9 测量两台机器的网络延迟和时间差
7.10 用 timing wheel 踢掉空闲连接
7.10.1 timing wheel 原理
7.10.2 代码实现与改进
7.11 简单的消息广播服务
7.12 “串并转换”连接服务器及其自动化测试
7.13 socks4a 代理服务器
7.13.1 TCP 中继器
7.13.2 socks4a 代理服务器
7.13.3 N : 1 与 1 : N 连接转发
7.14 短址服务
7.15 与其他库集成
7.15.1 UDNS
7.15.2 c-ares DNS
7.15.3 curl
7.15.4 更多
第 8 章 muduo 网络库设计与实现
8.0 什么都不做的 EventLoop
8.1 Reactor 的关键结构
8.1.1 Channel class
8.1.2 Poller class
8.1.3 EventLoop 的改动
8.2 TimerQueue 定时器
8.2.1 TimerQueue class
8.2.2 EventLoop 的改动
8.3 EventLoop::runInLoop() 函数
8.3.1 提高 TimerQueue 的线程安全性
8.3.2 EventLoopThread class
8.4 实现 TCP 网络库
8.5 TcpServer 接受新连接
8.5.1 TcpServer class
8.5.2 TcpConnection class
8.6 TcpConnection 断开连接
8.7 Buffer 读取数据
8.7.1 TcpConnection 使用 Buffer 作为输入缓冲
8.7.2 Buffer::readFd()
8.8 TcpConnection 发送数据
8.9 完善 TcpConnection
8.9.1 SIGPIPE
8.9.2 TCP No Delay 和 TCP keepalive
8.9.3 WriteCompleteCallback 和 HighWaterMarkCallback
8.10 多线程 TcpServer
8.11 Connector
8.12 TcpClient
8.13 epoll
8.14 测试程序一览
第 3 部分 工程实践经验谈
第 9 章 分布式系统工程实践
9.1 我们在技术浪潮中的位置
9.1.1 分布式系统的本质困难
9.1.2 分布式系统是个险恶的问题
9.2 分布式系统的可靠性浅说
9.2.1 分布式系统的软件不要求 7 × 24 可靠
9.2.2 “能随时重启进程”作为程序设计目标
9.3 分布式系统中心跳协议的设计
9.4 分布式系统中的进程标识
9.4.1 错误做法
9.4.2 正确做法
9.4.3 TCP 协议的启示
9.5 构建易于维护的分布式程序
9.6 为系统演化做准备
9.6.1 可扩展的消息格式
9.6.2 反面教材:ICE 的消息打包格式
9.7 分布式程序的自动化回归测试
9.7.1 单元测试的能与不能
9.7.2 分布式系统测试的要点
9.7.3 分布式系统的抽象观点
9.7.4 一种自动化的回归测试方案
9.7.5 其他用处
9.8 分布式系统部署、监控与进程管理的几重境界
9.8.1 境界 1:全手工操作
9.8.2 境界 2:使用零散的自动化脚本和第三方组件
9.8.3 境界 3:自制机群管理系统,集中化配置
9.8.4 境界 4:机群管理与 naming service 结合
第 10 章 C 编译链接模型精要
10.1 C 语言的编译模型及其成因
10.1.1 为什么 C 语言需要预处理
10.1.2 C 语言的编译模型
10.2 C 的编译模型
10.2.1 单遍编译
10.2.2 前向声明
10.3 C 链接(linking)
10.3.1 函数重载
10.3.2 inline 函数
10.3.3 模板
10.3.4 虚函数
10.4 工程项目中头文件的使用规则
10.4.1 头文件的害处
10.4.2 头文件的使用规则
10
本书主要讲述采用现代C 在x86-64 Linux 上编写多线程TCP 网络服务程序的主流常规技术,这也是我对过去5 年编写生产环境下的多线程服务端程序的经验总结。本书重点讲解多线程网络服务器的一种IO 模型,即one loop per thread。这是一种适应性较强的模型,也是Linux 下以native 语言编写用户态高性能网络程序成熟的模式,掌握之后可顺利地开发各类常见的服务端网络应用程序。本书以muduo网络库为例,讲解这种编程模型的使用方法及注意事项。
muduo 是一个基于非阻塞IO 和事件驱动的现代C 网络库,原生支持oneloop per thread 这种IO 模型。muduo 适合开发Linux 下的面向业务的多线程服务端网络应用程序,其中“面向业务的网络编程”的定义见附录A。“现代C ”指的不是C 11 新标准,而是2005 年TR1 发布之后的C 语言和库。与传统C 相比,现代C 的变化主要有两方面:资源管理(见第1 章)与事件回调(见第449 页)。
本书不是多线程编程教程,也不是网络编程教程,更不是C 教程。读者应该已经大致读过《UNIX 环境高级编程》、《UNIX 网络编程》、《C Primer》或与之内容相近的书籍。本书不谈C 11,因为目前(2012 年)主流的Linux 服务端发行版的g 版本都还停留在4.4,C 11 进入实用尚需一段时日。
本书适用的硬件环境是主流x86-64 服务器,多路多核CPU、几十GB 内存、千兆以太网互联。除了第5 章讲诊断日志之外,本书不涉及文件IO。
本书分为四大部分,第1 部分“C 多线程系统编程”考察多线程下的对象生命期管理、线程同步方法、多线程与C 的结合、高效的多线程日志等。第2 部分“muduo 网络库”介绍使用现成的非阻塞网络库编写网络应用程序的方法,以及muduo 的设计与实现。第3 部分“工程实践经验谈”介绍分布式系统的工程化开发方法和C 在工程实践中的功能特性取舍。第4 部分“附录”分享网络编程和C 语言的学习经验。
本书的宗旨是贵精不贵多。掌握两种基本的同步原语就可以满足各种多线程同步的功能需求,还能写出更易用的同步设施。掌握一种进程间通信方式和一种多线程网络编程模型就足以应对日常开发任务,编写运行于公司内网环境的分布式服务系统。(本书不涉及分布式存储系统,也不涉及UDP。)
陈硕
中国·香港
附录A
谈一谈网络编程学习经验
本文谈一谈我在学习网络编程方面的一些个人经验。“网络编程”这个术语的范围很广,本文指用Sockets API开发基于TCP/IP 的网络应用程序,具体定义见§A.1.5 “网络编程的各种任务角色”。
受限于本人的经历和经验,本附录的适应范围是:
o x86-64 Linux 服务端网络编程,直接或间接使用Sockets API。
o 公司内网。不一定是局域网,但总体位于公司防火墙之内,环境可控。
本文可能不适合:
o PC 客户端网络编程,程序运行在客户的PC 上,环境多变且不可控。
o Windows 网络编程。
o 面向公网的服务程序。
o 高性能网络服务器。
本文分两个部分:
1. 网络编程的一些“胡思乱想”,以自问自答的形式谈谈我对这一领域的认识。
2. 几本必看的书,基本上还是W. Richard Stevents 的那几本。
另外,本文没有特别说明时均暗指TCP 协议,“连接”是“TCP 连接”,“服务
端”是“TCP 服务端”。
网络编程的一些“胡思乱想”
以下大致列出我对网络编程的一些想法,前后无关联。
网络编程是什么
网络编程是什么?是熟练使用Sockets API 吗?说实话,在实际项目里我只用过
两次Sockets API,其他时候都是使用封装好的网络库。
次是2005 年在学校做一个羽毛球赛场计分系统:我用C# 编写运行在PC上的软件,负责比分的显示;再用C#写了运行在PDA 上的计分界面,记分员拿着PDA 记录比分;这两部分程序通过TCP协议相互通信。这其实是个简单的分布式系统,体育馆有几片场地,每个场地都有一名拿PDA 的记分员,每个场地都有两台显示比分的PC(显示器是42 寸平板电视,放在场地的对角,这样两边看台的观众都能看到比分)。这两台PC的功能不完全一样,一台只负责显示当前比分,另一台还要负责与PDA 通信,并更新数据库里的比分信息。此外,还有一台PC负责周期性地从数据库读出全部7 片场地的比分,显示在体育馆墙上的大屏幕上。这台PC上还运行着一个程序,负责生成比分数据的静态页面,通过FTP上传发布到某门户网站的体育频道。系统中还有一个录入赛程(参赛队、运动员、出场顺序等)数据库的程序,运行在数据库服务器上。算下来整个系统有十来个程序,运行在二十多台设备(PC和PDA)上,还要考虑可靠性,避免single point of failure。
这是我次写实际项目中的网络程序,当时写下来的感觉是像写命令行与用户交互的程序:程序在命令行输出一句提示语,等待客户输入一句话,然后处理客户输入,再输出下一句提示语,如此循环。只不过这里的“客户”不是人,而是另一个程序。在建立好TCP连接之后,双方的程序都是read/write 循环(为求简单,我用的是blocking 读写),直到有一方断开连接。
第二次是2010 年编写muduo 网络库,我再次拿起了Sockets API,写了一个基于Reactor 模式的C 网络库。写这个库的目的之一就是想让日常的网络编程从Sockets API的琐碎细节中解脱出来,让程序员专注于业务逻辑,把时间用在刀刃上。muduo网络库的示例代码包含了几十个网络程序,这些示例程序都没有直接使用Sockets API。
在此之外,无论是实习还是工作,虽然我写的程序都会通过TCP 协议与其他程序打交道,但我没有直接使用过SocketsAPI。对于TCP 网络编程,我认为核心是处理“三个半事件”,见§6.4.1 “TCP网络编程本质论”。程序员的主要工作是在事件处理函数中实现业务逻辑,而不是和Sockets API“较劲”。
这里还是没有说清楚“网络编程”是什么,请继续阅读后文§A.1.5“网络编程的各种任务角色”。
学习网络编程有用吗
以上说的是比较底层的网络编程,程序代码直接面对从TCP 或UDP收到的数据以及构造数据包发出去。在实际工作中,另一种常见的情况是通过各种clientlibrary来与服务端打交道,或者在现成的框架中填空来实现server,或者采用更上层的通信方式。比如用libmemcached与memcached 打交道,使用libpq 来与PostgreSQL 打交道,编写Servlet 来响应HTTP请求,使用某种RPC与其他进程通信,等等。这些情况都会发生网络通信,但不一定算作“网络编程”。如果你的工作是前面列举的这些,学习TCP/IP网络编程还有用吗?
我认为还是有必要学一学,至少在troubleshooting 的时候有用。无论如何,这些library 或framework都会调用底层的Sockets API 来实现网络功能。当你的程序遇到一个线上问题时,如果你熟悉SocketsAPI,那么从strace 不难发现程序卡在哪里,尽管可能你没有直接调用这些Sockets API。另外,熟悉TCP/IP协议、会用tcpdump 也非常有助于分析解决线上网络服务问题。
在什么平台上学习网络编程
对于服务端网络编程,我建议在Linux 上学习。
如果在10 年前,这个问题的答案或许是FreeBSD,因为FreeBSD“根正苗红”,在2000年那一次互联网浪潮中扮演了重要角色,是很多公司的免费服务器操作系统。2000 年那会儿Linux 还远未成熟,连epoll都还没有实现。(FreeBSD 在2001年发布4.1 版,加入了kqueue,从此C10k 不是问题。)
10 年后的今天,事情起了一些变化,Linux 成为市场份额的服务器操作系统。在Linux这种大众系统上学网络编程,遇到什么问题会比较容易解决。因为用的人多,你遇到的问题别人多半也遇到过;同样因为用的人多,如果真的有什么内核bug,很快就会得到修复,至少有workaround 的办法。如果用别的系统,可能一个问题发到论坛上半个月都不会有人理。从内核源码的风格看,FreeBSD更干净整洁,注释到位,但是无奈它的市场份额远不如Linux,学习Linux 是更好的技术投资。
可移植性重要吗
写网络程序要不要考虑移植性?要不要跨平台?这取决于项目需要,如果贵公司做的程序要卖给其他公司,而对方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX等等操作系统,这时候当然要考虑移植性。如果编写公司内部的服务器上用的网络程序,那么大可只关注一个平台,比如Linux。因为编写和维护可移植的网络程序的代价相当高,平台间的差异可能远比想象中大,即便是POSIX系统之间也有不小的差异(比如Linux 没有SO_NOSIGPIPE 选项,Linux 的pipe(2) 是单向的,而FreeBSD是双向的),错误的返回码也大不一样。
我就不打算把muduo 往Windows或其他操作系统移植。如果需要编写可移植的网络程序,我宁愿用libevent、libuv、Java Netty这样现成的库,把“脏活、累活”留给别人。
网络编程的各种任务角色
计算机网络是个big topic,涉及很多人物和角色,既有开发人员,也有运维人员。比方说:公司内部两台机器之间ping不通,通常由网络运维人员解决,看看是布线有问题还是路由器设置不对;两台机器能ping通,但是程序连不上,经检查是本机防火墙设置有问题,通常由系统管理员解决;两台机器能连上,但是丢包很严重,发现是网卡或者交换机的网口故障,由硬件维修人员解决;两台机器的程序能连上,但是偶尔发过去的请求得不到响应,通常是程序bug,应该由开发人员解决。
面向业务的网络编程的特点
与通用的网络服务器不同,面向公司业务的专用网络程序有其自身的特点。
业务逻辑比较复杂,而且时常变化 如果写一个HTTP 服务器,在大致实现HTTP 1.1标准之后,程序的主体功能一般不会有太大的变化,程序员会把时间放在性能调优和bug修复上。而开发针对公司业务的专用程序时,功能说明书(spec)很可能不如HTTP 1.1标准那么细致明确。更重要的是,程序是快速演化的。以即时聊天工具的后台服务器为例,可能版只支持在线聊天;几个月之后发布第二版,支持离线消息;又过了几个月,第三版支持隐身聊天;随后,第四版支持上传头像;如此等等。这要求程序员能快速响应新的业务需求,公司才能保持竞争力。由于业务时常变化(假设每月一次版本升级),也会降低服务程序连续运行时间的要求。相反,我们要设计一套流程,通过轮流重启服务器来完成平滑升级(§9.2.2)。
不一定需要遵循公认的通信协议标准比方说网游服务器就没什么协议标准,反正客户端和服务端都是本公司开发的,如果发现目前的协议设计有问题,两边一起改就行了。由于可以自己设计协议,因此我们可以绕开一些性能难点,简化程序结构。比方说,对于多线程的服务程序,如果用短连接TCP协议,为了优化性能通常要精心设计accept新连接的机制2,避免惊群并减少上下文切换。但是如果改用长连接,用简单的单线程accept 就行了。
程序结构没有定论 对于高并发大吞吐的标准网络服务,一般采用单线程事件驱动的方式开发,比如HAProxy、lighttpd等都是这个模式。但是对于专用的业务系统,其业务逻辑比较复杂,占用较多的CPU资源,这种单线程事件驱动方式不见得能发挥现在多核处理器的优势。这留给程序员比较大的自由发挥空间,做好了“横扫千军”,做烂了一败涂地。我认为目前oneloop per thread 是通用性较高的一种程序结构,能发挥多核的优势,见§3.3 和§6.6。
性能评判的标准不同 如果开发httpd 这样的通用服务,必然会和开源的Nginx、lighttpd等高性能服务器比较,程序员要投入相当的精力去优化程序,才能在市场上占有一席之地。而面向业务的专用网络程序不一定是IObound,也不一定有开源的实现以供对比性能,优化方向也可能不同。程序员通常更加注重功能的稳定性与开发的便捷性。性能只要一代比一代强即可。
网络编程起到支撑作用,但不处于主导地位程序员的主要工作是实现业务逻辑,而不只是实现网络通信协议。这要求程序员深入理解业务。程序的性能瓶颈不一定在网络上,瓶颈有可能是CPU、DiskIO、数据库等,这时优化网络方面的代码并不能提高整体性能。只有对所在的领域有深入的了解,明白各种因素的权衡(trade-off),才能做出一些有针对性的优化。现在的机器上,简单的并发长连接echo服务程序不用特别优化就做到十多万qps,但是如果每个业务请求需要1ms密集计算,在8 核机器上充其量能达到8 000 qps,优化IO 不如去优化业务计算(如果投入产出合算的话)。
几个术语
互联网上的很多“口水战”是由对同一术语的不同理解引起的,比如我写的《多线程服务器的适用场合》3,就曾经被人说是“挂羊头卖狗肉”,因为这篇文章中举的master例子“根本就算不上是个网络服务器。因为它的瓶颈根本就跟网络无关。”
网络服务器“网络服务器”这个术语确实含义模糊,到底指硬件还是软件?到底是服务于网络本身的机器(交换机、路由器、防火墙、NAT),还是利用网络为其他人或程序提供服务的机器(打印服务器、文件服务器、邮件服务器)?每个人根据自己熟悉的领域,可能会有不同的解读。比方说,或许有人认为只有支持高并发、高吞吐量的才算是网络服务器。
为了避免无谓的争执,我只用“网络服务程序”或者“网络应用程序”这种含义明确的术语。“开发网络服务程序”通常不会造成误解。
客户端?服务端? 在TCP网络编程中,客户端和服务端很容易区分,主动发起连接的是客户端,被动接受连接的是服务端。当然,这个“客户端”本身也可能是个后台服务程序,HTTPproxy 对HTTP server 来说就是个客户端。
客户端编程?服务端编程? 但是“服务端编程”和“客户端编程”就不那么好区分了。比如Webcrawler,它会主动发起大量连接,扮演的是HTTP 客户端的角色,但似乎应该归入“服务端编程”。又比如写一个HTTPproxy,它既会扮演服务端–被动接受Web browser 发起的连接,也会扮演客户端–主动向HTTP server发起连接,它究竟算服务端还是客户端?我猜大多数人会把它归入服务端编程。
那么究竟如何定义“服务端编程”?
服务端编程需要处理大量并发连接?也许是,也许不是。比如云风在一篇介绍网游服务器的博客4中就谈到,网游中用到的“连接服务器”需要处理大量连接,而“逻辑服务器”只有一个外部连接。那么开发这种网游“逻辑服务器”算服务端编程还是客户端编程呢?又比如机房的服务进程监控软件,并发数跟机器数成正比,至多也就是两三千的并发连接。(再大规模就超出本书的范围了。)
我认为,“服务端网络编程”指的是编写没有用户界面的长期运行的网络程序,程序默默地运行在一台服务器上,通过网络与其他程序打交道,而不必和人打交道。与之对应的是客户端网络程序,要么是短时间运行,比如wget;要么是有用户界面(无论是字符界面还是图形界面)。本文主要谈服务端网络编程。
评论
还没有评论。