2014年3月20日星期四

[]UPYUN:用Erlang开发的对象存储系统

本文自动转发自我的博客: http://www.haofengjing.org/2517.html

在国内的几家云计算创业公司当中,UPYUN(又拍云)选择了一个比较独特的定位:面向开发者提供非结构化数据云存储服务。非结构化数据存储服务一个很重要的卖点是要提供快速的静态文件访问能力,这对底层的存储系统性能和上层的CDN系统提出了较高的要求。

黄慧攀@oneoo)是UPYUN技术总监。在QCon上海2013大会上,黄慧攀介绍了UPYUN的CDN系统架构,包括Nginx的二次开发经验、防盗链服务的实现、海量小文件的性能处理等;在QCon北京2014大会上,他将对UPYUN底层的对象存储系统的研发经验进行分享。

在本次采访中,黄慧攀介绍了UPYUN对象存储系统的一些历史,团队的分工,以及做测试方面的一些思路。

InfoQ:先介绍一下你自己吧。你关于计算机的知识都是自学,从底层网络、操作系统到上层的Java、PHP都玩,Lua也玩。你对技术的选择有什么标准吗?如果有,是怎样的标准?

黄慧攀:我是出身于广东一个小城市"鹤山"人,最早是在95年接触电脑,98年开始使用互联网,那时网易还只是做邮箱服务的,我非常感谢我的初中母校,使我能这么早期接触到互联网,影响一生 :) 也因为当年电脑、互联网才刚刚起步,学校也缺乏较好的教育能力,所以很多知识需要自学。也因为这个兴趣太浓,搞得其他学科基本都挂科了,也就没考上高中和大学。到现在还是有点小后悔,起码得把英文学好。

2001年,18岁的我第一份工作是市里一个集团公司的B2B门户网站,负责程序开发工作。那时用的语言是PHP,边学边做的折腾了3年时间。

2004年,项目因为市场、资金的原因结束了。在我们的小城市互联网就业机会基本为零,只好转到一个做弱电工程的公司任技术工程师,负责网络系统方案设计、智能灯光系统等等。

2006年,压抑不住互联网的心,就出来创办 yo2.cn 优博网,国内第一个基于 WordPress 的博客服务平台,这个创业经历使我的技术能力提升很大,因为没人嘛,所以整个网站的事情都得自己做,开发、运维、客服,甚至设计等等。记得当时网站被人吐槽最多的就是用户体验,我想如果能把这块也做好,可以做UED了,哈哈。

2009年,机缘巧合来到杭州,跟朋友做了几次创业,虽然也是失败告终,但在其中的过程使自己成长了很多,因为创业嘛,所以很多事情都必须自己做的,这就奠定了我的技术层面比较广的基础。

2011年,收到又拍网的邀请,开始又拍云的开发工作至今。

经历这么多年和多次创业,积累到比较丰富的技术经验。知识比较全面,在看待技术选型方面的把握还是比较准的。比如:Java的优点是适合大型项目、团队协作开发,缺点也很明显,开发周期长、人员成本相对PHP高一些;而PHP的优点则是适合中小型项目、开发周期短、人员成本低,当然弊端也很明显,不支持多线程、系统资源占用高。每个语言都有自己的优缺点,要根据项目实际情况来选择。后来一个偶然的机会接触到Lua语言,发现它跟PHP很像,但又没了PHP几个大缺点,非常棒。所以现在我主要使用 C 和 Lua 这两个开发语言。

InfoQ:你自己做过博客平台,也在企业网站、网游等网站做过,现在在UPYUN,可以说是从面向消费者的.com公司转移到了一个更加基础一些的服务。你觉得在UPYUN做的事情跟以前有什么不一样吗?

黄慧攀:我觉得做UPYUN这件事,是之前几个项目的升华吧。因为这些面向消费者的项目让我知道在开发过程中产生的痛点,从而挖掘出开发者的需求。我很高兴能为开发者服务,帮助大家更快的把项目做好。

InfoQ:能不能简单介绍一下UPYUN这套对象存储系统的研发历程?比如是什么时候开始做的,最初的设计者是什么背景,借鉴过哪些思路,研发的过程中有没有什么好玩的故事等等。

黄慧攀:UPYUN的对象存储系统其实早在08年就开始设计的,当初用的是 MogileFS,为又拍网服务。因为早期的MogileFS的设计本身有一定限制,tracker角色的元信息使用单个MySQL实例存储,无法满足我们日益增长的存储量,所以在2010年转为使用Erlang语言开发。设计目标是提供PB级别的存储服务,经历1年多的业务测试才正式对外开放存储服务。

选择Erlang语言进行开发,主要是语言本身就支持分布式,这可以节省很多开发工作。且Erlang语言在电信行业的应用非常广泛,稳定性有保障。

在分布式算法的选型上是参考Dynamo方案。而在具体的数据存储结构方面则是自主研发的一致性哈希算法,以实现多机柜、多服务器和多磁盘之间的数据备份工作。做到每文件的对应备份点在不同机柜、不同服务器上,避免某台服务器甚至某个机柜的服务器宕机而影响到文件的读写操作。

至于测试周期长达1年多,是因我们本身又拍网(照片社区)的数据量就非常庞大,从老的MogileFS集群迁移到新的云存储服务器占大部分时间,另外是因分布式存储服务的容灾测试过程比起应用测试要漫长得多,主要的测试点会有:某磁盘故障、某服务器故障、某机柜故障等好几种灾难测试,且每个故障都会产生一定量的数据迁移,文件会在集群内部自动寻找合适的备份点再建备份,所以说测试周期需要很长时间。也只有做到充分的测试,我们才放心的在集群上存储大量数据。否则等遇到无法排除的问题,要考虑新建集群的话,迁移成本和周期都会非常巨大。比如10PB的数据要从A集群迁移到N集群,网络传输就要100pb,基于10gb网络也得耗时半年;且要保障迁移期间内不再发生新故障,这是很难做到的。所以我们选择前期测试做得非常充分,来保障日后服务的可持续性。

InfoQ:又拍云专注于做图片的存储,你们提供了一些很有特色的服务(如缩略图、防盗链),同时非常专注于服务质量。相对于文件备份类的应用场景,海量小图片存储是非常吃资源的,你们在存储系统的设计上做了哪些工作以确保在资源占用高的情况下仍然能保持图片访问的服务质量?

黄慧攀:是的,UPYUN主要面向小于100MB的小文件提供服务,目前我们的存储集群已存有超过2PB的数据。面向海量小文件所面临的主要问题是:随机读取非常高、磁盘性能低;大家都知道缓存系统可以解决这类问题,而CDN其实就是个巨大的缓存系统,所以我们自建了CDN并对外提供服务。不仅能解决海量小文件所产生的磁盘性能问题,还能加速文件在互联网上的传输,一举两得。

InfoQ:UPYUN系统的测试是如何做的?

黄慧攀:我们团队还比较小,目前未专门设立测试部门,所有测试工作均由项目开发者来完成,毕竟开发人员更清楚会有哪些潜在问题,并制定自动化测试的样例。下面是我们一个项目的开发、测试与发布流程:

  1. 项目策划、文档和方案撰写
  2. 开发(过程中会有两名以上开发人员交叉 review)
  3. 本地测试(主要测试该项目的功能是否正常和程序稳定性、资源占用率等等)
  4. 模拟平台测试(主要测试该项目的功能上线是否对原平台上其他子系统产生不良影响,这里会有我们自己编写的一批批量测试脚本,以验证平台每项功能逻辑是否正确)
  5. 灰度测试(业务环境中抽取1%的服务器更新或指定某个别客户可使用该功能来进行测试)
  6. 全网发布

从整个流程来看,我们的测试周期是比较长的,测试工作占整个项目周期50%以上,甚至个别影响范围大的项目,测试周期会长达半年以上。

InfoQ:你们的团队是怎样分工的?研发跟产品运营、系统运维的同学又是如何沟通的?

黄慧攀:大家从我们的产品介绍上会知道我们主要提供3块服务,

  1. 云存储
  2. 云分发
  3. 云处理

所以我们的开发团队主要是根据这3个方向进行分组。现在我们团队分应用开发组和核心研发组,而在核心研发组中又分存储、分发、处理3个小组,分得比较细。因此我们的小组成员之间会有交叉分工,以便大家对整体系统能有充分的了解。

我们的产品服务与一般互联网服务不太一样,我们是以产品为主导而非运营主导,且我们的产品经理也是开发出身的,所以在与开发团队的协作沟通上不会存在什么问题。另外的运维部门则是更加紧密,因我们正在开始整个平台的自动化运维系统开发,我们的开发人员已走到一线,跟运维人员一起探讨运维自动化系统的功能性问题,开发人员能亲身了解运维工作和痛点,并以此来驱动运维自动化系统的开发工作。

InfoQ:这次QCon北京,你希望面向哪些人群进行分享?他们能从你的分享中获得什么?

黄慧攀:很感谢QCon能让我们来继续跟大家做些云计算方面的分享。在上一次的QCon上海大会我跟大家分享了又拍云的CDN技术,按我们公司的服务层次划分,这次的分享主题是我们在云存储系统的研发和构建过程中遇到的一些问题和经验。希望大家能通过我们这次的分享,对云存储能有更深入的了解,比如分布式算法、存储结构和日常维护等等。


[]解决eclipse导入erlang项目中文乱码

本文自动转发自我的博客: http://www.haofengjing.org/2516.html

eclipse可以很方便地管理erlang项目,eclipse对erlang的默认编码ISO-8859-1,所以就会经常发生中文乱码问题。文章将说明如何解决eclipse导入erlang项目中文乱码问题。

eclipse先导入erlang项目,再通过修改eclipse配置来解决乱码。如下:

Window Preferences > General Appearance > Content Types

在右边的栏目中,选Text,再选Erlang source file

ISO-8859-1改成UTF-8,然后点击右边的Update按钮即可。


[]基于 Erlang/OTP 搭建TCP服务器

本文自动转发自我的博客: http://www.haofengjing.org/2515.html

这两天在研究 erlang 如何构建 TCP 服务器,看到一篇文章,基于Erlang OTP构建一个TCP服务器,里面讲述了两种混合型Socket的实现方法,着实让人欢欣鼓舞。对比老外写的Building a Non-blocking TCP server using OTP principles,作者写的那个有点简单。本文将结合这两篇文章,继续讨论Erlang/OTP 构建TCP服务器的具体实现,以示例演示如何如何使用标准Erlang/OTP行为创建一个简单的无阻塞的TCP服务器。

TCP Socket模式

主动模式{active, true},非阻塞方式接收消息,但在系统无法应对超大流量请求时,客户端发送的数据过快,而且超过服务器可以处理的速度,那么,系统就可能会造成消息缓冲区被塞满,出现持续繁忙的流量的极端情况,系统因请求过多而溢出,造成Erlang虚拟机内存不足而崩溃。 被动模式{active, false},阻塞方式接收消息,底层的TCP缓冲区可用于抑制请求,并拒绝客户端的消息,在接收数据的地方都会调用gen_tcp:recv,造成阻塞(单进程模式下就只能消极等待某一个具体的客户端Socket ,很危险)。需要注意的是,操作系统可能还会做一些缓存允许客户端机器继续发送少量数据,然后才将其阻塞,但这个时候Erlang还没有调用recv函数。 混合型模式(半阻塞,{active, once}),主动套接字仅针对一条消息,在控制进程发送完一个消息数据后,必须显式地调用inet:setopts(Socket, [{active, once}]) 重新激活以便接受下一个消息(在此之前,系统处于阻塞状态)。可见,混合型模式综合了主动模式和被动模式的两者优势,可实现流量控制,防止服务器被过多消息淹没。 所以如果想构建TCP服务器,比较合理的是建立在TCP Socket 混合型模式(半阻塞)基础上。

TCP服务器设计

这个TCP服务器的设计包含了主应用程序 tcp_server_app 和监督者 tcp_server_sup 进程,监督者进程拥有 tcp_server_listener 和 tcp_client_sup 两个子进程。tcp_server_listener 负责处理客户端的连接请求,并通知 tcp_client_sup 启动一个 tcp_server_handler 实例进程来处理一条客户端的请求,然后由该实例进程负责处理服务器与客户端的交互数据。

应用程序和监督行为

为了构建一个 Erlang/OTP 应用程序,我们需要构建一些模块来实现应用程序和监督行为。当应用程序启动时,tcp_server_app:start/2 会调用 tcp_server_sup:start_link/1 来创建主监督进程。该监督进程通过回调 tcp_server_sup:init/1 来实例化子工作进程 tcp_server_listener 和子监督进程 tcp_client_sup。该子监督进程回调 tcp_server_sup:init/1 来实例化负责处理客户端连接的工作进程 tcp_server_handler。

TCP服务器应用程序 (tcp_server_app.erl)

[plain] view plaincopy
  1. -module(tcp_server_app).  
  2. -behaviour(application).  
  3. -export([start/2, stop/1]).  
  4. -define(PORT,  2222).  
  5.   
  6. start(_Type, _Args) ->  
  7.   io:format("tcp app start~n"),  
  8.   case tcp_server_sup:start_link(?PORT) of  
  9.     {ok, Pid} ->  
  10.       {ok, Pid};  
  11.     Other ->  
  12.       {error, Other}  
  13.   end.  
  14.   
  15. stop(_S) ->  
  16.   ok.  

TCP服务器监督者进程(tcp_server_sup.erl)

[plain] view plaincopy
  1. -module(tcp_server_sup).  
  2. -behaviour(supervisor).  
  3. -export([start_link/1, start_child/1]).  
  4. -export([init/1]).  
  5.   
  6. start_link(Port) ->  
  7.   io:format("tcp sup start link~n"),  
  8.   supervisor:start_link({local, ?MODULE}, ?MODULE, [Port]).  
  9.   
  10. start_child(LSock) ->  
  11.   io:format("tcp sup start child~n"),  
  12.   supervisor:start_child(tcp_client_sup, [LSock]).  
  13.   
  14. init([tcp_client_sup]) ->  
  15.   io:format("tcp sup init client~n"),  
  16.   {ok,  
  17.    { {simple_one_for_one, 0, 1},  
  18.     [  
  19.      { tcp_server_handler,   
  20.       {tcp_server_handler, start_link, []},  
  21.       temporary,   
  22.       brutal_kill,   
  23.       worker,   
  24.       [tcp_server_handler]  
  25.      }  
  26.     ]  
  27.    }  
  28.   };  
  29.   
  30. init([Port]) ->  
  31.   io:format("tcp sup init~n"),  
  32.   {ok,  
  33.     { {one_for_one, 5, 60},  
  34.      [  
  35.       % client supervisor  
  36.      { tcp_client_sup,   
  37.       {supervisor, start_link, [{local, tcp_client_sup}, ?MODULE, [tcp_client_sup]]},  
  38.       permanent,   
  39.       2000,   
  40.       supervisor,   
  41.       [tcp_server_listener]  
  42.      },  
  43.      % tcp listener  
  44.      { tcp_server_listener,   
  45.       {tcp_server_listener, start_link, [Port]},  
  46.       permanent,   
  47.       2000,   
  48.       worker,   
  49.       [tcp_server_listener]  
  50.      }  
  51.     ]  
  52.    }  
  53.   }.  
TCP服务器 Socket 监听进程(tcp_server_listener.erl)
[plain] view plaincopy
  1. -module(tcp_server_listener).  
  2. -behaviour(gen_server).  
  3. -export([start_link/1]).  
  4. -export([init/1, handle_call/3, handle_cast/2, handle_info/2,  
  5.          terminate/2, code_change/3]).  
  6. -record(state, {lsock}).  
  7.   
  8. start_link(Port) ->  
  9.   io:format("tcp server listener start ~n"),  
  10.   gen_server:start_link({local, ?MODULE}, ?MODULE, [Port], []).  
  11.   
  12. init([Port]) ->  
  13.   process_flag(trap_exit, true),  
  14.   Opts = [binary, {packet, 0}, {reuseaddr, true},  
  15.            {keepalive, true}, {backlog, 30}, {active, false}],  
  16.   State =  
  17.   case gen_tcp:listen(Port, Opts) of  
  18.     {ok, LSock} ->  
  19.       start_server_listener(LSock),  
  20.       #state{lsock = LSock};  
  21.     _Other ->  
  22.       throw({error, {could_not_listen_on_port, Port}}),  
  23.       #state{}  
  24.   end,  
  25.     {ok, State}.  
  26.   
  27. handle_call(_Request, _From, State) ->  
  28.   io:format("tcp server listener call ~p~n", [_Request]),  
  29.   {reply, ok, State}.  
  30.   
  31. handle_cast({tcp_accept, Pid}, State) ->  
  32.   io:format("tcp server listener cast ~p~n", [tcp_accept]),  
  33.   start_server_listener(State, Pid),  
  34.     {noreply, State};  
  35.   
  36. handle_cast(_Msg, State) ->  
  37.   io:format("tcp server listener cast ~p~n", [_Msg]),  
  38.   {noreply, State}.  
  39.   
  40. handle_info({'EXIT', Pid, _}, State) ->  
  41.   io:format("tcp server listener info exit ~p~n", [Pid]),  
  42.   start_server_listener(State, Pid),  
  43.   {noreply, State};  
  44.   
  45. handle_info(_Info, State) ->  
  46.   io:format("tcp server listener info ~p~n", [_Info]),  
  47.   {noreply, State}.  
  48.   
  49. terminate(_Reason, _State) ->  
  50.   io:format("tcp server listener terminate ~p~n", [_Reason]),  
  51.   ok.  
  52.   
  53. code_change(_OldVsn, State, _Extra) ->  
  54.   {ok, State}.  
  55.   
  56. start_server_listener(State, Pid) ->  
  57.   unlink(Pid),  
  58.   start_server_listener(State#state.lsock).  
  59.   
  60. start_server_listener(Lsock) ->  
  61.   case tcp_server_sup:start_child(Lsock) of  
  62.     {ok, Pid} ->  
  63.       link(Pid);  
  64.     _Other ->  
  65.       do_log  
  66.   end.  
TCP服务器处理客户端请求进程(tcp_server_handler.erl)
[plain] view plaincopy
  1. -module(tcp_server_handler).  
  2. -behaviour(gen_server).  
  3. -export([start_link/1]).  
  4. -export([init/1, handle_call/3, handle_cast/2, handle_info/2,  
  5.          terminate/2, code_change/3]).  
  6. -record(state, {lsock, socket, addr}).  
  7. -define(Timeout, 120*1000).  
  8.   
  9. start_link(LSock) ->  
  10.   io:format("tcp handler start link~n"),  
  11.   gen_server:start_link(?MODULE, [LSock], []).  
  12.   
  13. init([LSock]) ->  
  14.   io:format("tcp handler init ~n"),  
  15.   inet:setopts(LSock, [{active, once}]),  
  16.   gen_server:cast(self(), tcp_accept),  
  17.   {ok, #state{lsock = LSock}}.  
  18.   
  19. handle_call(Msg, _From, State) ->  
  20.   io:format("tcp handler call ~p~n", [Msg]),  
  21.   {reply, {ok, Msg}, State}.  
  22.   
  23. handle_cast(tcp_accept, #state{lsock = LSock} = State) ->  
  24.   {ok, CSock} = gen_tcp:accept(LSock),  
  25.   io:format("tcp handler info accept client ~p~n", [CSock]),  
  26.   {ok, {IP, _Port}} = inet:peername(CSock),  
  27.    start_server_listener(self()),  
  28.   {noreply, State#state{socket=CSock, addr=IP}, ?Timeout};  
  29.   
  30. handle_cast(stop, State) ->  
  31.   {stop, normal, State}.  
  32.   
  33. handle_info({tcp, Socket, Data}, State) ->  
  34.   inet:setopts(Socket, [{active, once}]),  
  35.   io:format("tcp handler info ~p got message ~p~n", [self(), Data]),  
  36.   ok = gen_tcp:send(Socket, <<Data/binary>>),  
  37.   {noreply, State, ?Timeout};  
  38.   
  39. handle_info({tcp_closed, _Socket}, #state{addr=Addr} = State) ->  
  40.   io:format("tcp handler info ~p client ~p disconnected~n", [self(), Addr]),  
  41.   {stop, normal, State};  
  42.   
  43. handle_info(timeout, State) ->  
  44.   io:format("tcp handler info ~p client connection timeout~n", [self()]),  
  45.   {stop, normal, State};  
  46.   
  47. handle_info(_Info, State) ->  
  48.   io:format("tcp handler info ingore ~p~n", [_Info]),  
  49.   {noreply, State}.  
  50.    
  51. terminate(_Reason, #state{socket=Socket}) ->  
  52.   io:format("tcp handler terminate ~p~n", [_Reason]),  
  53.   (catch gen_tcp:close(Socket)),  
  54.   ok.  
  55.   
  56. code_change(_OldVsn, State, _Extra) ->  
  57.   {ok, State}.  
  58.   
  59. start_server_listener(Pid) ->  
  60.   gen_server:cast(tcp_server_listener, {tcp_accept, Pid}).  
TCP服务器资源文件(tcp_server.app) 
[plain] view plaincopy
  1. {application,tcp_server,  
  2.   [{description,"TCP Server"},  
  3.    {vsn,"1.0.0"},  
  4.    {modules,[tcp_server,tcp_server_app,tcp_server_handler,  
  5.          tcp_server_listener,tcp_server_sup]},  
  6.    {registered,[]},  
  7.    {mod,{tcp_server_app,[]}},  
  8.    {env,[]},  
  9.    {applications,[kernel,stdlib]}]}.  

编译程序

为应用程序创建如下的目录结构:

[plain] view plaincopy
  1. ./tcp_server  
  2. ./tcp_server/ebin/  
  3. ./tcp_server/ebin/tcp_server.app  
  4. ./tcp_server/src/tcp_server_app.erl  
  5. ./tcp_server/src/tcp_server_sup.erl  
  6. ./tcp_server/src/tcp_server_listener.erl  
  7. ./tcp_server/src/tcp_server_handler.erl  

Linux:

[plain] view plaincopy
  1. $ cd tcp_server/src  
  2. $ for f in tcp*.erl ; do erlc -o ../ebin $f  
Windows:
[plain] view plaincopy
  1. cd tcp_server/src  
  2. for %%i in (tcp*.erl) do erlc -o ../ebin %%i  

运行程序

1、启动TCP服务器

[plain] view plaincopy
  1. erl -pa ebin  
  2. ...  
  3. 1> application:start(tcp_server).  
  4. tcp app start  
  5. tcp sup start link  
  6. tcp sup init  
  7. tcp sup init client  
  8. tcp server listener start  
  9. tcp sup start child  
  10. tcp handler start link  
  11. tcp handler init  
  12. ok  
  13. 2> appmon:start().  
  14. {ok,<0.41.0>}  

2、创建一个客户端来请求TCP服务器:
[plain] view plaincopy
  1. 3> f(S), {ok,S} = gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).  
  2. {ok,#Port<0.1859>}  
  3. tcp handler info accept client #Port<0.1860>  
  4. tcp server listener cast tcp_accept  
  5. tcp sup start child  
  6. tcp handler start link  
  7. tcp handler init  

3、使用该请求向服务端发送消息:

[plain] view plaincopy
  1. 4> gen_tcp:send(S,<<"hello">>).  
  2. ok  
  3. tcp handler info <0.53.0> got message <<"hello">>  
4、接收到服务端发来的信息:

[plain] view plaincopy
  1. 5> f(M), receive M -> M end.  
  2. {tcp,#Port<0.1861>,"hello"}  
5、现在让我们尝试向服务端发送多个连接请求:

[plain] view plaincopy
  1. 6> gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).  
  2. ...  
  3. 7> gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).  
  4. ...  
  5. 8> gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).  
  6. ...  


6、服务端程序写有超时功能,如果2分钟内没操作,连接将自动退出

[plain] view plaincopy
  1. 9> tcp handler info <0.39.0> client connection timeout  
  2. 9> tcp handler terminate normal  
  3. 9> tcp handler info <0.52.0> client connection timeout  
  4. 9> tcp handler terminate normal  
  5. 9> tcp handler info <0.54.0> client connection timeout  
  6. 9> tcp handler terminate normal  
  7. 9> tcp handler info <0.56.0> client connection timeout  
  8. 9> tcp handler terminate normal  

7、下面我们简单演示一下服务器的监督行为:

[plain] view plaincopy
  1. 9> exit(pid(0,58,0),kill).  
  2. tcp server listener info exit <0.58.0>  
  3. true  
  4. tcp sup start child  
  5. tcp handler start link  
  6. tcp handler init  

结束语

本例演示了如何创建一个简单的无阻塞的TCP服务器以及如何使用标准 Erlang/OTP 行为。作为一个练习,鼓励读者尝试通用的无阻塞TCP服务器功能抽象成一个独立式的行为。