网络编程的一些胡思乱想 以下胡乱列出我对网络编程的一些想法前后无关联。网络编程是什么网络编程是什么是熟练使用Sockets API吗说实话在实际项目里我只用过两次Sockets API其他时候都是使用封装好的网络库。第一次是2005年在学校做一个羽毛球赛场计分系统我用C# 编写运行在PC机上的软件负责比分的显示再用C# 写了运行在PDA上的计分界面记分员拿着PDA记录比分这两部分程序通过 TCP协议相互通信。这其实是个简单的分布式系统体育馆有不止一片场地每个场地都有一名拿PDA的记分员每个场地都有两台显示比分的PC机显示器是42吋平板电视放在场地的对角这样两边看台的观众都能看到比分。这两台PC机功能不完全一样一台只负责显示当前比分另一台还要负责与PDA通信并更新数据库里的比分信息。此外还有一台PC机负责周期性地从数据库读出全部7片场地的比分显示在体育馆墙上的大屏幕上。这台PC上还运行着一个程序负责生成比分数据的静态页面通过FTP上传发布到某门户网站的体育频道。系统中还有一个录入赛程参赛队运动员出场顺序等数据库的程序运行在数据库服务器上。算下来整个系统有十来个程序运行在二十多台设备PC和PDA上还要考虑可靠性。将来有机会把这个小系统仔细讲一讲挺有意思的。这是我第一次写实际项目中的网络程序当时写下来的感觉是像写命令行与用户交互的程序程序在命令行输出一句提示语等待客户输入一句话然后处理客户输入再输出下一句提示语如此循环。只不过这里的“客户”不是人而是另一个程序。在建立好TCP连接之后双方的程序都是read/write循环为求简单我用的是blocking读写直到有一方断开连接。第二次是2010年编写muduo网络库我再次拿起了Sockets API写了一个基于Reactor模式的C 网络库。写这个库的目的之一就是想让日常的网络编程从Sockets API的琐碎细节中解脱出来让程序员专注于业务逻辑把时间用在刀刃上。Muduo 网络库的示例代码包含了几十个网络程序这些示例程序都没有直接使用Sockets API。在此之外无论是实习还是工作虽然我写的程序都会通过TCP协议与其他程序打交道但我没有直接使用过Sockets API。对于TCP网络编程我认为核心是处理“三个半事件”见《Muduo 网络编程示例之零前言》中的“TCP 网络编程本质论”。程序员的主要工作是在事件处理函数中实现业务逻辑而不是和Sockets API较劲。这里还是没有说清楚“网络编程”是什么请继续阅读后文“网络编程的各种任务角色”。学习网络编程有用吗以上说的是比较底层的网络编程程序代码直接面对从TCP或UDP收到的数据以及构造数据包发出去。在实际工作中另一种常见 的情况是通过各种 client library 来与服务端打交道或者在现成的框架中填空来实现server或者采用更上层的通信方式。比如用libmemcached与memcached打交道使用libpq来与PostgreSQL 打交道编写Servlet来响应http请求使用某种RPC与其他进程通信等等。这些情况都会发生网络通信但不一定算作“网络编程”。如果你的工作是前面列举的这些学习TCP/IP网络编程还有用吗我认为还是有必要学一学至少在troubleshooting 的时候有用。无论如何这些library或framework都会调用底层的Sockets API来实现网络功能。当你的程序遇到一个线上问题如果你熟悉Sockets API那么从strace不难发现程序卡在哪里尽管可能你没有直接调用这些Sockets API。另外熟悉TCP/IP协议、会用tcpdump也大大有助于分析解决线上网络服务问题。在什么平台上学习网络编程对于服务端网络编程我建议在Linux上学习。如果在10年前这个问题的答案或许是FreeBSD因为FreeBSD根正苗红在2000年那一次互联网浪潮中扮演了重要角色是很多公司首选的免费服务器操作系统。2000年那会儿Linux还远未成熟连epoll都还没有实现。FreeBSD在2001年发布4.1版加入了kqueue从此C10k不是问题。10年后的今天事情起了变化Linux成为了市场份额最大的服务器操作系统(http://en.wikipedia.org/wiki/Usage_share_of_operating_systems)。在Linux这种大众系统上学网络编程遇到什么问题会比较容易解决。因为用的人多你遇到的问题别人多半也遇到过同样因为用的人多如果真的有什么内核bug很快就会得到修复至少有work around的办法。如果用别的系统可能一个问题发到论坛上半个月都不会有人理。从内核源码的风格看FreeBSD更干净整洁注释到位但是无奈它的市场份额远不如Linux学习Linux是更好的技术投资。可移植性重要吗写网络程序要不要考虑移植性这取决于项目需要如果贵公司做的程序要卖给其他公司而对方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX等等操作系统这时候考虑移植性。如果编写公司内部的服务器上用的网络程序那么大可只关注一个平台比如Linux。因为编写和维护可移植的网络程序的代价相当高平台间的差异可能远比想象中大即便是POSIX系统之间也有不小的差异比如Linux没有SO_NOSIGPIPE选项错误的返回码也大不一样。我就不打算把muduo往Windows或其他操作系统移植。如果需要编写可移植的网络程序我宁愿用libevent或者Java Netty这样现成的库把脏活累活留给别人。网络编程的各种任务角色计算机网络是个 big topic涉及很多人物和角色既有开发人员也有运维人员。比方说公司内部两台机器之间 ping 不通通常由网络运维人员解决看看是布线有问题还是路由器设置不对两台机器能ping通但是程序连不上经检查是本机防火墙设置有问题通常由系统管理员解决两台机器能连上但是丢包很严重发现是网卡或者交换机的网口故障由硬件维修人员解决两台机器的程序能连上但是偶尔发过去的请求得不到响应通常是程序bug应该由开发人员解决。本文主要关心开发人员这一角色。下面简单列出一些我能想到的跟网络打交道的编程任务其中前三项是面向网络本身后面几项是在计算机网络之上构建信息系统。1. 开发网络设备编写防火墙、交换机、路由器的固件 firmware2. 开发或移植网卡的驱动3. 移植或维护TCP/IP协议栈特别是在嵌入式系统上4. 开发或维护标准的网络协议程序HTTP、FTP、DNS、SMTP、POP3、NFS5. 开发标准网络协议的“附加品”比如HAProxy、squid、varnish等web load balancer6. 开发标准或非标准网络服务的客户端库比如ZooKeeper客户端库memcached客户端库7. 开发与公司业务直接相关的网络服务程序比如即时聊天软件的后台服务器网游服务器金融交易系统互联网企业用的分布式海量存储微博发帖的内部广播通知等等8. 客户端程序中涉及网络的部分比如邮件客户端中与 POP3、SMTP通信的部分以及网游的客户端程序中与服务器通信的部分本文所指的“网络编程”专指第7项即在TCP/IP协议之上开发业务软件。面向业务的网络编程的特点跟开发通用的网络程序不同开发面向公司业务的专用网络程序有其特点· 业务逻辑比较复杂而且时常变化如果写一个HTTP服务器在大致实现HTTP /1.1标准之后程序的主体功能一般不会有太大的变化程序员会把时间放在性能调优和bug修复上。而开发针对公司业务的专用程序时功能说明书spec很可能不如HTTP/1.1标准那么细致明确。更重要的是程序是快速演化的。以即时聊天工具的后台服务器为例可能第一版只支持在线聊天几个月之后发布第二版支持离线消息又过了几个月第三版支持隐身聊天随后第四版支持上传头像如此等等。这要求程序员能快速响应新的业务需求公司才能保持竞争力。· 不一定需要遵循公认的通信协议标准比方说网游服务器就没什么协议标准反正客户端和服务端都是本公司开发如果发现目前的协议设计有问题两边一起改了就是了。· 程序结构没有定论对于高并发大吞吐的标准网络服务一般采用单线程事件驱动的方式开发比如HAProxy、lighttpd等都是这个模式。但是对于专用的业务系统其业务逻辑比较复杂占用较多的CPU资源这种单线程事件驱动方式不见得能发挥现在多核处理器的优势。这留给程序员比较大的自由发挥空间做好了横扫千军做烂了一败涂地。· 性能评判的标准不同如果开发httpd这样的通用服务必然会和开源的Nginx、lighttpd等高性能服务器比较程序员要投入相当的精力去优化程序才能在市场上占有一席之地。而面向业务的专用网络程序不一定有开源的实现以供对比性能程序员通常更加注重功能的稳定性与开发的便捷性。性能只要一代比一代强即可。· 网络编程起到支撑作用但不处于主导地位程序员的主要工作是实现业务逻辑而不只是实现网络通信协议。这要求程序员深入理解业务。程序的性能瓶颈不一定在网络上瓶颈有可能是CPU、Disk IO、数据库等等这时优化网络方面的代码并不能提高整体性能。只有对所在的领域有深入的了解明白各种因素的权衡(trade-off)才能做出一些有针对性的优化。几个术语互联网上的很多口水战是由对同一术语的不同理解引起的比我写的《多线程服务器的适用场合》就曾经人被说是“挂羊头卖狗肉”因为这篇文章中举的 master例子“根本就算不上是个网络服务器。因为它的瓶颈根本就跟网络无关。”· 网络服务器“网络服务器”这个术语确实含义模糊到底指硬件还是软件到底是服务于网络本身的机器交换机、路由器、防火墙、NAT还是利用网络为其他人或程序提供服务的机器打印服务器、文件服务器、邮件服务器。每个人根据自己熟悉的领域可能会有不同的解读。比方说或许有人认为只有支持高并发高吞吐的才算是网络服务器。为了避免无谓的争执我只用“网络服务程序”或者“网络应用程序”这种含义明确的术语。“开发网络服务程序”通常不会造成误解。· 客户端服务端在TCP网络编程里边客户端和服务端很容易区分主动发起连接的是客户端被动接受连接的是服务端。当然这个“客户端”本身也可能是个后台服务程序HTTP Proxy对HTTP Server来说就是个客户端。· 客户端编程服务端编程但是“服务端编程”和“客户端编程”就不那么好区分。比如 Web crawler它会主动发起大量连接扮演的是HTTP客户端的角色但似乎应该归入“服务端编程”。又比如写一个 HTTP proxy它既会扮演服务端——被动接受 web browser 发起的连接也会扮演客户端——主动向 HTTP server 发起连接它究竟算服务端还是客户端我猜大多数人会把它归入服务端编程。那么究竟如何定义“服务端编程”服务端编程需要处理大量并发连接也许是也许不是。比如云风在一篇介绍网游服务器的博客http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html中就谈到网游中用到的“连接服务器”需要处理大量连接而“逻辑服务器”只有一个外部连接。那么开发这种网游“逻辑服务器”算服务端编程还是客户端编程呢我认为“服务端网络编程”指的是编写没有用户界面的长期运行的网络程序程序默默地运行在一台服务器上通过网络与其他程序打交道而不必和人打交道。与之对应的是客户端网络程序要么是短时间运行比如wget要么是有用户界面无论是字符界面还是图形界面。本文主要谈服务端网络编程。7x24重要吗内存碎片可怕吗一谈到服务端网络编程有人立刻会提出7x24运行的要求。对于某些网络设备而言这是合理的需求比如交换机、路由器。对于开发商业系统我认为要求程序7x24运行通常是系统设计上考虑不周。具体见《分布式系统的工程化开发方法》第20页起。重要的不是7x24而是在程序不必做到7x24的情况下也能达到足够高的可用性。一个考虑周到的系统应该允许每个进程都能随时重启这样才能在廉价的服务器硬件上做到高可用性。既然不要求7x24那么也不必害怕内存碎片理由如下· 64-bit系统的地址空间足够大不会出现没有足够的连续空间这种情况。· 现在的内存分配器malloc及其第三方实现今非昔比除了memcached这种纯以内存为卖点的程序需要自己设计分配器之外其他网络程序大可使用系统自带的malloc或者某个第三方实现。· Linux Kernel也大量用到了动态内存分配。既然操作系统内核都不怕动态分配内存造成碎片应用程序为什么要害怕· 内存碎片如何度量有没有什么工具能为当前进程的内存碎片状况评个分如果不能比较两种方案的内存碎片程度谈何优化有人为了避免内存碎片不使用STL容器也不敢new/delete这算是premature optimization还是因噎废食呢协议设计是网络编程的核心对于专用的业务系统协议设计是核心任务决定了系统的开发难度与可靠性但是这个领域还没有形成大家公认的设计流程。系统中哪个程序发起连接哪个程序接受连接如果写标准的网络服务那么这不是问题按RFC来就行了。自己设计业务系统有没有章法可循以网游为例到底是连接服务器主动连接逻辑服务器还是逻辑服务器主动连接“连接服务器”似乎没有定论两种做法都行。一般可以按照“依赖-被依赖”的关系来设计发起连接的方向。比新建连接难的是关闭连接。在传统的网络服务中特别是短连接服务不少是服务端主动关闭连接比如daytime、HTTP/1.0。也有少部分是客户端主动关闭连接通常是些长连接服务比如 echo、chargen等。我们自己的业务系统该如何设计连接关闭协议呢服务端主动关闭连接的缺点之一是会多占用服务器资源。服务端主动关闭连接之后会进入TIME_WAIT状态在一段时间之内hold住一些内核资源。如果并发访问量很高这会影响服务端的处理能力。这似乎暗示我们应该把协议设计为客户端主动关闭让TIME_WAIT状态分散到多台客户机器上化整为零。这又有另外的问题客户端赖着不走怎么办会不会造成拒绝服务攻击或许有一个二者结合的方案客户端在收到响应之后就应该主动关闭这样把 TIME_WAIT 留在客户端。服务端有一个定时器如果客户端若干秒钟之内没有主动断开就踢掉它。这样善意的客户端会把TIME_WAIT留给自己buggy的客户端会把 TIME_WAIT留给服务端。或者干脆使用长连接协议这样避免频繁创建销毁连接。比连接的建立与断开更重要的是设计消息协议。消息格式很好办XML、JSON、Protobuf都是很好的选择难的是消息内容。一个消息应该包含哪些内容多个程序相互通信如何避免race condition见《分布式系统的工程化开发方法》p.16的例子系统的全局状态该如何跃迁可惜这方面可供参考的例子不多也没有太多通用的指导原则我知道的只有30年前提出的end-to-end principle和happens-before relationship。只能从实践中慢慢积累了。网络编程的三个层次侯捷先生在《漫談程序員與編程》中讲到 STL 运用的三个档次“會用STL是一種檔次。對STL原理有所了解又是一個檔次。追蹤過STL源碼又是一個檔次。第三種檔次的人用起 STL 來虎虎生風之勢絕非第一檔次的人能夠望其項背。”我认为网络编程也可以分为三个层次1. 读过教程和文档2. 熟悉本系统TCP/IP协议栈的脾气3. 自己写过一个简单的TCP/IP stack第一个层次是基本要求读过《Unix网络编程》这样的编程教材读过《TCP/IP详解》基本理解TCP/IP协议读过本系统的manpage。这个层次可以编写一些基本的网络程序完成常见的任务。但网络编程不是照猫画虎这么简单若是按照manpage的功能描述就能编写产品级的网络程序那人生就太幸福了。第二个层次熟悉本系统的TCP/IP协议栈参数设置与优化是开发高性能网络程序的必备条件。摸透协议栈的脾气还能解决工作中遇到的比较复杂的网络问题。拿Linux的TCP/IP协议栈来说· 有可能出现自连接见《学之者生用之者死——ACE历史与简评》举的三个硬伤程序应该有所准备。· Linux的内核会有bug比如某种TCP拥塞控制算法曾经出现TCP window clamping窗口箝位bug导致吞吐量暴跌可以选用其他拥塞控制算法来绕开(work around)这个问题。这些阴暗角落在manpage里没有描述要通过其他渠道了解。编写可靠的网络程序的关键是熟悉各种场景下的error code文件描述符用完了如何本地ephemeral port暂时用完不能发起新连接怎么办服务端新建并发连接太快backlog用完了客户端connect会返回什么错误有的在manpage里有描述有的要通过实践或阅读源码获得。第三个层次通过自己写一个简单的TCP/IP协议栈能大大加深对TCP/IP的理解更能明白TCP为什么要这么设计有哪些因素制约每一步操作的代价是什么写起网络程序来更是成竹在胸。其实实现TCP/IP只需要操作系统提供三个接口函数一个函数两个回调函数。分别是send_packet()、on_receive_packet()、on_timer()。多年前有一篇文章《使用libnet与libpcap构造TCP/IP协议软件》介绍了在用户态实现TCP/IP的方法。lwIP也是很好的借鉴对象。如果有时间我打算自己写一个Mini/Tiny/Toy/Trivial/Yet-Another TCP/IP。我准备换一个思路用TUN/TAP设备在用户态实现一个能与本机点对点通信的TCP/IP协议栈这样那三个接口函数就表现为我最熟悉的文件读写。在用户态实现的好处是便于调试协议栈做成静态库与应用程序链接到一起库的接口不必是标准的Sockets API。做完这一版还可以继续发挥用FTDI的USB-SPI接口芯片连接ENC28J60适配器做一个真正独立于操作系统的TCP/IP stack。如果只实现最基本的IP、ICMP Echo、TCP的话代码应能控制在3000行以内也可以实现UDP如果应用程序需要用到DNS的话。最主要的三个例子我认为TCP网络编程有三个例子最值得学习研究分别是echo、chat、proxy都是长连接协议。Echo的作用熟悉服务端被动接受新连接、收发数据、被动处理连接断开。每个连接是独立服务的连接之间没有关联。在消息内容方面Echo有一些变种比如做成一问一答的方式收到的请求和发送响应的内容不一样这时候要考虑打包与拆包格式的设计进一步还可以写简单的HTTP服务。Chat的作用连接之间的数据有交流从a收到的数据要发给b。这样对连接管理提出的更高的要求如何用一个程序同时处理多个连接fork() per connection似乎是不行的。如何防止串话b有可能随时断开连接而新建立的连接c可能恰好复用了b的文件描述符那么a会不会错误地把消息发给cProxy的作用连接的管理更加复杂既要被动接受连接也要主动发起连接既要主动关闭连接也要被动关闭连接。还要考虑两边速度不匹配见《Muduo 网络编程示例之十socks4a 代理服务器》。这三个例子功能简单突出了TCP网络编程中的重点问题挨着做一遍基本就能达到层次一的要求。TCP的可靠性有多高TCP是“面向连接的、可靠的、字节流传输协议”这里的“可靠”究竟是什么意思《Effective TCP/IP Programming》第9条说Realize That TCP Is a Reliable Protocol, Not an Infallible Protocol那么TCP在哪种情况下会出错这里说的“出错”指的是收到的数据与发送的数据不一致而不是数据不可达。我在《一种自动反射消息类型的 Google Protobuf 网络传输方案》中设计了带check sum的消息格式很多人表示不理解认为是多余的。IP header里边有check sumTCP header也有check sum链路层以太网还有CRC32校验那么为什么还需要在应用层做校验什么情况下TCP传送的数据会出错IP header和TCP header的check sum是一种非常弱的16-bit check sum算法把数据当成反码表示的16-bit integers再加到一起。这种checksum算法能检出一些简单的错误而对某些错误无能为力由于是简单的加法遇到“和”不变的情况就无法检查出错误比如交换两个16-bit整数加法满足交换律结果不变。以太网的CRC32只能保证同一个网段上的通信不会出错两台机器的网线插到同一个交换机上这时候以太网的CRC是有用的。但是如果两台机器之间经过了多级路由器呢上图中Client向Server发了一个TCP segment这个segment先被封装成一个IP packet再被封装成ethernet frame发送到路由器图中消息a。Router收到ethernet frame (b)转发到另一个网段(c)最后Server收到d通知应用程序。Ethernet CRC能保证a和b相同c和d相同TCP header check sum的强度不足以保证收发payload的内容一样。另外如果把Router换成NAT那么NAT自己会构造c替换掉源地址这时候a和d的payload不能用tcp header checksum校验。路由器可能出现硬件故障比方说它的内存故障或偶然错误导致收发IP报文出现多bit的反转或双字节交换这个反转如果发生在payload区那么无法用链路层、网络层、传输层的check sum查出来只能通过应用层的check sum来检测。这个现象在开发的时候不会遇到因为开发用的几台机器很可能都连到同一个交换机ethernet CRC能防止错误。开发和测试的时候数据量不大错误很难发生。之后大规模部署到生产环境网络环境复杂这时候出个错就让人措手不及。有一篇论文《When the CRC and TCP checksum disagree》分析了这个问题。另外《The Limitations of the Ethernet CRC and TCP/IP checksums for error detection》( http://noahdavids.org/self_published/CRC_and_checksum.html )也值得一读。这个情况真的会发生吗会的Amazon S3 在2008年7月就遇到过单bit反转导致了一次严重线上事故所以他们吸取教训加了 check sum。见http://status.aws.amazon.com/s3-20080720.html另外一个例证下载大文件的时候一般都会附上MD5这除了有安全方面的考虑防止篡改也说明应用层应该自己设法校验数据的正确性。这是end-to-end principle的一个例证。三本必看的书谈到Unix编程和网络编程W. Richard Stevens 是个绕不开的人物他生前写了6本书APUE、两卷UNP、三卷TCP/IP。有四本与网络编程直接相关。UNP第二卷其实跟网络编程关系不大是APUE在多线程和进程间通信(IPC)方面的补充。很多人把TCP/IP一二三卷作为整体推荐其实这三本书用处不同应该区别对待。这里谈到的几本书都没有超出孟岩在《TCP/IP 网络编程之四书五经》中的推荐说明网络编程这一领域已经相对成熟稳定。· 《TCP/IP Illustrated,Vol. 1: The Protocols》中文名《TCP/IP 详解》以下简称 TCPv1。TCPv1 是一本奇书。这本书迄今至少被三百多篇学术论文引用过http://portal.acm.org/citation.cfm?id161724。一本学术专著被论文引用算不上出奇难得的是一本写给程序员看的技术书能被学术论文引用几百次我不知道还有哪本技术书能做到这一点。TCPv1 堪称 TCP/IP领域的圣经。作者 W. Richard Stevens 不是 TCP/IP 协议的发明人他从使用者程序员的角度以 tcpdump 为工具对 TCP 协议抽丝剥茧娓娓道来第17~24章让人叹服。恐怕 TCP 协议的设计者也难以讲解得如此出色至少不会像他这么耐心细致地画几百幅收发 package 的时序图。TCP作为一个可靠的传输层协议其核心有三点1. Positive acknowledgement with retransmission2. Flow control using sliding window包括Nagle 算法等3. Congestion control包括slow start、congestion avoidance、fast retransmit等第一点已经足以满足“可靠性”要求为什么第二点是为了提高吞吐量充分利用链路层带宽第三点是防止过载造成丢包。换言之第二点是避免发得太慢第三点是避免发得太快二者相互制约。从反馈控制的角度看TCP像是一个自适应的节流阀根据管道的拥堵情况自动调整阀门的流量。TCP的 flow control 有一个问题每个TCP connection是彼此独立的保存有自己的状态变量一个程序如果同时开启多个连接或者操作系统中运行多个网络程序这些连接似乎不知道他人的存在缺少对网卡带宽的统筹安排。或许现代的操作系统已经解决了这个问题TCPv1 唯一的不足是它出版太早了1993 年至今网络技术发展了几代。链路层方面当年主流的 10Mbit 网卡和集线器早已经被淘汰100Mbit 以太网也没什么企业在用了交换机(switch)也已经全面取代了集线器(hub)服务器机房以 1Gbit 网络为主有些场合甚至用上了 10Gbit 以太网。另外无线网的普及也让TCP flow control面临新挑战原来设计TCP的时候人们认为丢包通常是拥塞造成的这时应该放慢发送速度减轻拥塞而在无线网中丢包可能是信号太弱造成的这时反而应该快速重试以保证性能。网络层方面变化不大IPv6 雷声大雨点小。传输层方面由于链路层带宽大增TCP window scale option 被普遍使用另外 TCP timestamps option 和 TCP selective ack option 也很常用。由于这些因素在现在的 Linux 机器上运行 tcpdump 观察 TCP 协议程序输出会与原书有些不同。