-
谈一谈网络编程学习经验
作者
:
陈硕
来源
:
博客园
发布时间
: 2011-07-05 16:07
阅读
: 1842
次
推荐
:
0
原文链接
[
收
藏
]
本文谈一谈我在学习网络编程方面的一些个人经验。
“
网络编程
”
这个术语
的范围很广,
本文
指用
Sockets
API
开发基于
TCP/IP
的网络应
用程序,
具体定义见
“
网络编程的各种
任务角
色
”
一节。
受限于本人的经历和经验,这篇文章的适应范围是:
·
x86-64
Linux
服务端网络编程,直接或间接使用
Sockets API
·
公司内网。不一定是局域网,但总
体位于公司防火墙之内,环境可控
本文可能不适合:
·
PC
客户端网络编程,程序运行在
客户的
PC
上,环境多变且不可控
·
Windows
网络编程
·
面向公网的服务程序
·
高性能网络服务器
本文分两个部分:
1.
网络编程的一些胡思乱想,谈谈我对这一领域的认识
2.
几本必看的书,基本上还是
W. Richard
Stevents
那几本
另外,
本文没有特别说明时均暗指
TCP
协议,
“
连接
”
p>
是
“
TCP
连接<
/p>
”
,
“
服务端<
/p>
”
是
“TCP
服
务
端
”
。
网络编程的一些胡思乱想
以下胡乱列出我对网络编程的一些想法,前后无关联。
网络编程是什么?
网络编程是什么?是熟练使用
Sockets
API
吗?说实话,在实际项目里我只用过两次
Sockets
API
,其他时候都是使用封装好的网络库。
第一次是
2005
< br>年在学校做一个羽毛球赛场计分系统:我用
C#
编写运
行在
PC
机上的软
件,负责比分的显示
;再用
C#
写了运行在
PDA
上的计分界面,记分员拿着
PDA
记录比
p>
分;这两部分程序通过
TCP
协议相互通信。这其实是个简单的分布式系统,体育馆有不止
一片场地,每
个场地都有一名拿
PDA
的记分员,每个场地都有两台显示比分
的
PC
机(显
示器是
< br>42
吋平板电视,放在场地的对角,这样两边看台的观众都能看到比分)。这两台
PC
机功能不完全一样,一台只负责显示当前比分,另一台还要
负责与
PDA
通信,并更新
数据库里的
比分信息。
此外,
还有一台
PC
机负责周期性地从数据库读出全部
7
片场地的
比
分,显示在体育馆墙上的大屏幕上。这台
PC
上还运行着一个程序,负责生成比分数据的静
态页面,通过
FTP
上传发布到某门户网站的体育频道。系统中还有一个录入赛程(参赛队,
p>
运动员,出场顺序等)数据库的程序,运行在数据库服务器上。算下来整个系统有十来个程<
/p>
序,运行在二十多台设备(
PC
和
PDA
)上,还要考虑可靠性。将来有机会把这个小系统仔
细讲一讲,挺有意思的。
这是我第一次写实际项目中的网络程序,当时写下来的感觉是像写命令行与用户交互的程
序:程序在命令行输出一句提示语,
等待客户输入一句话,然后处理客户输入,再输
出下一
句提示语,如此循环。只不过这里的
“
< br>客户
”
不是人,而是另一个程序。在建立好
TCP
连接
之后,双方的程序都是
< br>read/write
循环(为求简单,我用的是
blo
cking
读写),直到有
一方断开连接。
第二次是
2010
年编写
muduo
网络库,
我再次拿起了
Sockets API
,
写了一个基于
Reactor
模式的
C++
网络库。写这个库的目的之一就是想让日常的网络编程从
Sockets API
的琐
碎细节中解脱出来,让程序员
专注于业务逻辑,把时间用在刀刃上。
Muduo
网络库的示
例
代码包含了几十个网络程序,这些示例程序都没有直接使用
S
ockets API
。
在此之外,无论是实习还是工作,虽然我写的程序都会通过
TCP
协议与其他程序打交道,
但我没有直接使用过
Sockets API
。
对于
TCP
网络编程,
我认为核心是处理
“
三个半事件
”
,
见《
Muduo
网络编程示例之零:前言》中的
“TCP
网络编程本质论
”
。程序员的主要工作
是在事件处理函数中实现业务逻辑,而不是和
Sockets
API
较劲。
这里还是没有说清楚
“
网络编程
”
是什么,请继续阅读后文
“
网络编程的
各种任务角色
”
。
学习网络编程有用吗?
以上说的是比较底层的网络编程,
程序代码直接面对从
TCP
或
UDP
收到的数据以及构
造数
据包发出去。在实际工作中,另一种常见
的情况是通过各种
client
library
来与服务端打
交道,或者在现成的框架中填空
来实现
server
,或者采用更上层的通信方式。比如用
p>
libmemcached
与
memcac
hed
打交道,使用
libpq
来与<
/p>
PostgreSQL
打交道,编写
S
ervlet
来响应
http
请求,使
用某种
RPC
与其他进程通信,等等。这些情况都会发生网络<
/p>
通信,但不一定算作
“
网络编程
”
。如果你的工作是前面列举的这些,学习
TC
P/IP
网络编程
还有用吗?
我认为还是有必要学一学,
至少在<
/p>
troubleshooting
的时候有用。
无论如何,
这些
library
或
framework
都会调用底层的
Sockets API
来实现网络功能。当你的程序遇到一个线上
< br>问题,如果你熟悉
Sockets API
,那么从
p>
strace
不难发现程序卡在哪里,尽管可能你没有
直接调用这些
Sockets API
。
另外,
熟悉
TCP/IP
协议、
会用
tcpdump
也大大有助
于分析
解决线上网络服务问题。
在什么平台上学习网络编程?
对于服
务端网络编程,我建议在
Linux
上学习。
< br>
如果在
10
年前,这个问题的答案或许是
FreeBSD
,因为<
/p>
FreeBSD
根正苗红,在
2000<
/p>
年那一次互联网浪潮中扮演了重要角色,
是很多公司首选的免费服
务器操作系统。
2000
年
那会儿
p>
Linux
还远未成熟,连
epoll
p>
都还没有实现。(
FreeBSD
在
2001
年发布
4.1
版,
加入了
kqueue
,从此
p>
C10k
不是问题。)
10
年后的今天,事情起了变化,<
/p>
Linux
成为了市场份额最大的服务器操作系统
(/wiki/Usage_share_of_operating_systems)
。
在
Linux
这
种大众系统上学网络编程,
遇到什么问题会比较容易解决。
因为用的人多,
你遇到的问题别
人多半也遇到过;同
样因为用的人多,如果真的有什么内核
bug
,很快就会得到修
复,至
少有
work around
的
办法。如果用别的系统,可能一个问题发到论坛上半个月都不会有人
理。从内核源码的风
格看,
FreeBSD
更干净整洁,注释到位,但是无奈它的市
场份额远不
如
Linux
,学习
Linux
是更好的技术投资。
可移植性重要吗?
写网络程序要不要考虑移植性?这取决于项目需要,如果贵公司做的程序要卖给其他公司,
而对方可能使用
Windows
、
< br>Linux
、
FreeBSD
、
Solaris
、
AIX
、
HP-UX
等等操作系统,
这时候考虑移植性。
如果编写公司内部的服务器上用的网络程序,
那么大可只关注一个平台,
比如
Linux
。
因为编写和维护可移植的网络程序的代价相当高,
平
台间的差异可能远比想象
中大,
即便是
POSIX
系统之间也有不小的差异
(比如
Linux
没有
SO_NOSIGPIPE
选项)
,
错误的返回码也大不一样。
< br>
我就不打算把
muduo<
/p>
往
Windows
或其他操作系统移植。
如果需要编写可移植的网络程序,
我宁愿用
libevent
或者
Java
Netty
这样现成的库,把脏活累活留给别人。
网络编程的各种任务角色
计算机网络是个
big topic
,
涉及很多人物和角色,
既有开发人员
,
也有运维人员。
比方说:
公司内部两
台机器之间
ping
不通,通常由
网络运维人员解决,看看是布线有问题还是路
由器设置不对;两台机器能
ping
通,但是程序连不上,经检查是本机防火墙设置有问题,
通常由系统管理员解决;
两台机器能连上,
但是丢包
很严重,
发现是网卡或者交换机的网口
故障,由硬件维修人员解
决;两台机器的程序能连上,但是偶尔发过去的请求得不到响应,
通常是程序
bug
,应该由开发人员解决。
本文主要关心开发人员这一角色。下面简单列出一些我能想到
的跟网络打交道的编程任务,
其中前三项是面向网络本身,后面几项是在计算机网络之上
构建信息系统。
1.
开发网络设备,编写防火墙、交换机、路由器的固件
firmware
2.
开发或移植网卡的驱动
3.
移植或维护
TCP/IP
协议栈(特别是在嵌入式系统上)
4.
开发或维护标准的网络协议程序,
HTTP
、
FTP
、
DNS
、
SMTP
、
POP3
、
NFS
5.
开发标准网络协议的
“
附加品
”
,
比如
HAProxy
、
squid
、
varnish
等
web
load balancer
6.
开发标准或非标准网络服务的客户端库,比如
ZooKeeper
客户端库,
memcached
客
户端库
7.
开发与公司业务直接相关的网络服务程序,比如即时聊天软件的后台服务器,网游服务
器,金融交易系统,互联网企业用的分布式海量存储,微博发帖的内部广播通知,等等
8.
客户端程序中涉及网络的部分,比如邮件客户端中与
POP3
、
SMTP
通
信的部分,以及
网游的客户端程序中与服务器通信的部分
本文所指的
“
网络编程
”
专指第
7
项,即在
TCP/IP
协议之上开发业务软件。
面向业务的网络编程的特点
跟开发通
用的网络程序不同,开发面向公司业务的专用网络程序有其特点:
·
业务逻辑比较复杂,而且时常变化
如果写一个
HTTP
服务器,在大致实
现
HTTP /1.1
标准之后,程序的主体功能一般不会有<
/p>
太大的变化,程序员会把时间放在性能调优和
bug
修复上。而开发针对公司业务的专用程
序时,功能说明书(
< br>spec
)很可能不如
HTTP/1.1
标准那么细致明确。更重要的是,程序
是快速演化的。
以即时聊天工具的后台服务器为例,
可能第一版只支持在线聊天;
几个月之
后发布第二版,支持离线消息;又过了几个月,第三版支持隐身聊天;随后,
第四版支持上
传头像;如此等等。这要求程序员能快速响应新的业务需求,公司才能保持
竞争力。
·
不一定需要遵循公认的通信协议标准
比方说网游服务器就没什么协议标准,
反正客户端和服务端都是
本公司开发,
如果发现目前
的协议设计有问题,两边一起改了就
是了。
·
程序结构没有定论
对于高并发大吞吐的标准网络服务,
一般采用单线程事件驱动的方式开发,
p>
比如
HAProxy
、
lighttpd
等都是这个模式。但是对于专用的业务系统,其业务逻辑比较复杂
,占用较多的
CPU
资源,这种单线程事件驱动方式不见得能发
挥现在多核处理器的优势。这留给程序员
比较大的自由发挥空间,做好了横扫千军,做烂
了一败涂地。
·
性能评判的标准不同
如果开发
httpd
这样的通用服务,
必然会和开源的
Nginx
、
lighttpd
等高性能服务器比较,
程序员要投入相当的
精力去优化程序,
才能在市场上占有一席之地。
而面向业务的专
用网络
程序不一定有开源的实现以供对比性能,程序员通常更加注重功能的稳定性与开发
的便捷
性。性能只要一代比一代强即可。
·
网络编程起到支撑作用,但不处于主导地位
程序员的主要工作是实现业务逻辑,
而不只是实现网络通信协议。
这要求程序员深入理解业
务。程序
的性能瓶颈不一定在网络上,瓶颈有可能是
CPU
、
Disk IO
、数据库等等,这时优
化网络方面
的代码并不能提高整体性能。
只有对所在的领域有深入的了解,
明白各种因素的
权衡
(trade-
off)
,才能做出一些有针对性的优化。
几个术语
互联网上的很多口水战是由对同一术语的不同理解引起的,
比我写的
《多线程服务器的适用
场合》就曾经人被说是
“
挂羊头卖狗肉
”
,因为这篇文章中举的
master
例子
“<
/p>
根本就算不上
是个网络服务器。因为它的瓶颈根本就跟网络无关。
”
·
网络服务器
“
网络服务器
”
这个术语确实含义模
糊,
到底指硬件还是软件?到底是服务于网络本身的机器
(交换
机、路由器、防火墙、
NAT
),还是利用网络为其他人或程序
提供服务的机器(打印
服务器、文件服务器、邮件服务器)。每个人根据自己熟悉的领域
,可能会有不同的解读。
比方说或许有人认为只有支持高并发高吞吐的才算是网络服务器
。
为了避免无谓的争执,
我只用
“
网络服务程序
”
或者
“
网络应用程序
”
这种含义明确的术语。
“
开
发网络服务程序
”
通常不会造成误解。
·
客户端?服务端?
在
TCP
网络编程里边,客户端和服务端很容易区分,
主动发起连接的是客户端,被动接受
连接的是服务端。
当然,<
/p>
这个
“
客户端
”
本身也可能是个后台服务程序,
HTTP
Proxy
对
HTTP
Server
来说就是个客户端。
·
客户端编程?服务端编程?
但是
“
服务端编程
”
和
“
客户端编程
”
就不那么好区分。比如
Web
crawler
,它会主动发起大
量连接,
扮演的是
HTTP
客户端的角色,
但似乎应该归入
“
服务端编程
”
。
又比如写一个
HTTP
proxy
,
它既会扮演服务端
——
被动接受
web browser
发起的连接,
也会扮演客户端
——
主动向
HTTP server
发起连接,它究竟算服务端还是客户
端?我猜大多数人会把它归入
服务端编程。
那么究竟如何定义
“
服务端编程
”
?
服务端编程需要处理大量并发连接?也许是,
也许不是。
比如云风在一篇介绍网游服务器的
博客
p>
/2006/04/iocp_kqueue_
中就谈到,网
游中用到的
“
连接服务器
”
需要处理大量连接,
而
“
逻辑服务器
”
只有一个外部连接。
那么开发
这种网游
“
逻辑
服务器
”
算服务端编程还是客户端编程呢?
我认为,
“
< br>服务端网络编程
”
指的是编写没有用户界面的长期运行的
网络程序,
程序默默地运
行在一台服务器上,
< br>通过网络与其他程序打交道,
而不必和人打交道。
与之对
应的是客户端
网络程序,要么是短时间运行,比如
wget
p>
;要么是有用户界面(无论是字符界面还是图形
界面)。本文主要谈
服务端网络编程。
7x24
重要吗?内存碎片可怕吗?
一谈到服务端网络编程,
有人立刻会提出
7x24
运行的要求。
对于某些网络设备而言,
这是
合理的需求,比如交换机、路由器。对于开发商业系统,
我认为要求程序
7x24
运行通常是
< br>系统设计上考虑不周。具体见《分布式系统的工程化开发方法》第
20
页起。重要的不是
7x24
,而是在程序不必做到
7x24
的情况下也能达到足够高的可用性。一个考虑周到的系
统应该允许每个进程都能随时重启,这样才能在廉价的服务器硬件上做到高可用性。
p>
既然不要求
7
x24
,那么也不必害怕内存碎片,理由如下:
·
64-bit
< br>系统的地址空间足够大,不会出现没有足够的连续空间这种情况。
·
现在的
内存分配器
(
malloc
及其第三方
实现)
今非昔比,除了
memcached
这种纯以内
存为卖点的程序需要自己设计分配器之外,
其他
网络程序大可使用系统自带的
malloc
或者
某个第三方实现。
·
Linux Kernel
也大量
用到了动态内存分配。既然操作系统内核都不怕动态分配内存造成
碎片,应用程序为什么
要害怕?
·
内存碎片如何度量?有没有什么工
具能为当前进程的内存碎片状况评个分?如果不能比
较两种方案的内存碎片程度,谈何优
化?
有人为了避免内存碎片,不使
用
STL
容器,也不敢
new/del
ete
,这算是
premature
optimization
还是因噎废食呢?
协议设计是网络编程的核心
对于专用的业务系统,
协议设计是核心任务,
决定了系统的开发难度与可靠性,
但是这个领
域还没有形成大家
公认的设计流程。
系统中哪个程序
发起连接,哪个程序接受连接?如果写标准的网络服务,那么这不是问题,
按
RFC
来就行了。自己设计业务系统,有没有章法可循?以网游为例,到底
是连接服务器
主动连接逻辑服务器,
还是逻辑服务器主动连接<
/p>
“
连接服务器
”
?似乎没有定论,
两种做法都
行。一般可以按照
“
依赖
->
被依赖
”
的关系来设计发起连接的方向。
比新建连接难的是关闭连接。在传统的网络服务中
(特别是短连接服务),不少是服务端主
动关闭连接,比如
daytime
、
HTTP/1.0
< br>。也有少部分是客户端主动关闭连接,通常是些长
连接服务,比如
echo
、
chargen
等。我们自己的业务系统该如何设计连接关闭协议呢?
服务端主动关闭连接的缺点之一是会多占用服务器资源。服务
端主动关闭连接之后会进入
TIME_WAIT
状态,在一段时
间之内
hold
住一些内核资源。如果并发访问量很高,这会影
响服务端的处理能力。
这似乎暗示我们应该把协议设计为客户端
主动关闭,
让
TIME_WAIT
状态
分散到多台客户机器上,化整为零。
这又有另外的问题:
客户端赖着不走怎么办?会不会造成拒绝服务攻击?或许有一个二
者结
合的方案:客户端在收到响应之后就应该主动关闭,这样把
TIME_WAIT
留在客户端。服
务端有一个定时器,
如果客户端若干秒钟之内没有主动断开,
就
踢掉它。
这样善意的客户端
会把
TIM
E_WAIT
留给自己,
buggy
的
客户端会把
TIME_WAIT
留给
服务端。或者干脆
使用长连接协议,这样避免频繁创建销毁连接。
比连接的建立与断开更重要的是设计消息协议。消息格式
很好办,
XML
、
JSON
、
Protobuf
都是很好的选择;
难的是消息内容。
一个消息应该包含哪些内容?多个程序相互通信如何避<
/p>
免
race condition
(见《
分布式系统的工程化开发方法》
p.16
的例子)?系统的全局
状态
该如何跃迁?可惜这方面可供参考的例子不多,
也没有太多
通用的指导原则,
我知道的只有
30
年
前提出的
end-to-end
principle
和
happens-before rel
ationship
。只能从实践中
慢慢积累了。
网络编程的三个层次
侯捷先生在《漫談程序員與編程》中讲到
STL
运用的三个档次:
“
會用
STL
,是一種檔次。
對
STL
原理有所了解,又是一個檔次。追蹤過
STL
源碼,又是一個檔次。第三種檔次的人
用起
STL
來,虎虎生風之勢絕非第一檔次的人能夠望其項背。
”
我认为网络编程也可以分为三个层次:
1.
读过教程和文档
2.
熟悉本系统
TCP/IP
协议栈的脾气
3.
自己写过一个简单的
TCP/IP stack
第一个层次是基本要求,读过《
Un
ix
网络编程》这样的编程教材,读过《
TCP/IP
详解》
基本理解
TCP/IP
< br>协议,
读过本系统的
manpage
。
这个层次可以编写一些基本的网络程序,
完成常见的任务
。但网络编程不是照猫画虎这么简单,若是按照
manpage
的功能描述就
能编写产品级的网络程序,那人生就太幸福了。
第二个层次,
熟悉本系统的
TCP/IP
协议栈参数设置与优化是开发高性能网络程序的必备条
p>
件。摸透协议栈的脾气还能解决工作中遇到的比较复杂的网络问题。拿
Linux
的
TCP/IP
协议栈来
说:
·
有可能出现自连接(见《学之者生,用之者死
——
ACE
历史与简评》举的三个硬伤),
程序应该有所
准备。
·
Linux
的内核会有
bug
,比如某
种
TCP
拥塞控制算法曾经出现
TCP
window clamping
(窗口箝位)
bug
,导致吞吐量暴跌,可以选用其他拥塞控制算法来绕开
(work
around)
这个问题。
这些阴暗角落在
manpage
里没有描述,
要通过其他渠道了解。
编写可靠的网络程序的关键是熟悉各种场景下的
error c
ode
(文件描述符用完了如何?本
地
ephemeral port
暂时用完,不能发起新连接怎么办?服务端新建并发连接
太快,
backlog
用完了,客户端
connect
会返回什么错误?),有的在
manpage<
/p>
里有描述,有
的要通过实践或阅读源码获得。
-
-
-
-
-
-
-
-
-
上一篇:如何写电视新闻解说词
下一篇:写作指导:如何写电视剧剧本