C++网络编程
网络编程设个术语的范围比较广,这里指用 Sockets API 开发基于 TCP/IP 的网络应用程序。
一、网络编程
网络编程设个术语的范围比较广,这里指用 Sockets API 开发基于 TCP/IP 的网络应用程序。
1.1、网络编程是什么?
TCP 网络编程,核心是处理三个半事件。程序员的主要工作是在事件处理函数中实现业务逻辑。
1.2、学习网络编程有用吗?
程序代码直接面对从 TCP 或 UDP 收到的数据以及构造数据包发出去。实际工作中,另一种常见的情况是通过各种 client library 来与服务端打交道。比如用 libmemcached 与 memcached 打交道,使用 libpa 来与 PostgreSQL 打交道,编写 Servlet 来响应 HTTP 请求,使用某种 RPC 与其他进程通信等等。这些过程都会发生网络通信,但不一定算作网络编程。
熟悉 TCP/IP 协议、会用 tcpdump 也非常有助于分析解决线上网络服务问题。至少在 troubleshooting 的时候有用。
1.3、网络编程的各种任务角色
- 开发网络设备,编写防火墙、交换机、路由器的固件(firmwave)。
- 开发或移植网卡的驱动。
- 移植或维护 TCP/IP 协议栈(特别是在嵌入式系统上)。
- 开发或维护标准的网络协议程序,HTTP、FTP、DNS、SMTP、POP3、NFS。
- 开发标准网络协议的附加品,比如 HAProxy、squid、varnish 等 Web load balancer。
- 开发标准或非标准网络服务的客户端库,比如 ZooKeeper 客户端库、memcached 客户端库。
- 开发与公司业务直接相关的网络服务程序,比如即时聊天软件的后台服务器、网游服务器、金融交易系统、互联网企业用的分布式海量存储、微博发帖的内部广播通知等等。
- 客户端程序中涉及网络的部分,比如邮件客户端中的 POP3、SMTP 通信的部分,以及网游的客户端程序中与服务器通信的部分。
1.4、面向业务的网络编程的特点
业务逻辑比较复杂,而且时常变化 以即时聊天工具的后台服务器为例,可能第一版只支持在线聊天;几个月之后发布第二版,支持离线消息;又过了几个月,第三版支持隐身聊天;随后,第四版支持上传头像;等等。
不一定需要遵循公认的通信协议标准 网游服务器就没什么协议标准,可以绕开一些性能难点。比方说,对于多线程的服务程序,如果用短连接 TCP 协议,为了优化性能通常要精心设计 accept 新连接的机制,避免惊群并减少上下文切换。但是如果改用长连接,用最简单的单线程 accept 就行了。
程序结构没有定论 对于高并发大吞吐的标准网络服务,一般采用单线程事件驱动的方式开发,比如 HAProxy、lighttpd 等都是这个模式。目前 one loop per thread 是通用性较高的一种程序结构,能发挥多核的优势。
性能评判的标准不同 如果开发 httpd 这样的通用服务,必然会和开源的 Nginx、lighttpd 等高性能服务器比较,程序员要投入相当的精力去优化程序。而面向业务的专用网络程序不一定是 IO bound,也不一定有开源的实现以供对比行性能,优化方向也可能不同。程序员通常更加注重功能的稳定性与开发的便捷性。
网络编程起到支撑作用,但不处于主导地位 程序的性能瓶颈不一定在网络上,瓶颈有可能是 CPU、Disk IO、数据库等,这时优化网络方面的代码并不能提高整体性能。
1.5、几个术语
网络服务器 到底是服务于网络本身的机器(交换机、路由器、防火墙、NAT),还是利用网络为其他人或程序提供服务的机器(打印服务器、文件服务器、邮件服务器)?
客户端?服务端? 在 TCP 网络编程中,客户端和服务端很容易区分,主动发起连接的是客户端,被动接受连接的是服务端。这个客户端本身也可能是个后台服务程序,HTTP proxy 对 HTTP server 来说就是个客户端。
客户端编程?服务端编程? Web crawler,它会主动发起大量连接,扮演的是 HTTP 客户端的角色,但似乎应该归入服务端编程。又比如写一个 HTTP proxy,它既会扮演客户端–被动接受 Web browser 发起的链接,也会扮演客户端–主动向 HTTP server 发起连接。
服务端网络编程指的是编写没有用户界面的长期运行的网络程序,程序默默地运行在一台服务器上、通过网络与其他程序打交道,而不必和人打交道。与之对应的是客户端网络程序,要么是短时间运行,比如 wget;要么是有用户界面(无论是字符界面还是图形界面)。
1.6、7 × 24 重要吗?内存碎片可怕吗?
分布式系统的可靠性重要的不是 7 × 24,而是在程序不必做到 7 × 24 的情况下也能达到足够高的可用性。既然不要求 7 × 24,那么也不必害怕内存碎片:
- 64-bit 系统的地址空间足够大,不会出现没有足够的连续空间这种情况。
- 现在的内存分配器(malloc 及其第三方实现)今非昔比,除了 memcached 这种纯以内存为卖点的程序需要自己设计分配器之外,其他网络程序大可使用系统自带的 malloc 或者某个第三方实现。重新发明 memmory pool 似乎已经不流行了。
- Linux Kernal 也大量用到了动态分配内存。
1.7、协议设计是网络编程的核心
以网游为例,到底是连接服务器主动连接逻辑服务器,还是逻辑服务器主动连接“连接服务器”,似乎没有定论。一般可以按照“依赖 -> 被依赖”的关系来设计发起连接的方向。
比起新建连接难的是关闭连接。在传统的网络服务中(特别是短连接服务),不少是服务端主动关闭连接,比如 daytime、HTTP 1.0。也有少部分是客户端主动关闭连接,通常是些长连接服务,比如 echo、chargen 等。
服务端主动关闭连接的缺点之一是会多占用服务器资源。服务端主动关闭连接之后会进入 TIME_WAIT 状态,在一段时间之内持有(hold)一些内核资源。如果并发访问量很高,就会影响服务端的处理能力。
比连接的建立与断开更重要的是设计消息协议。消息格式好办,XML、JSON、Protobuf 都是很好的选择。难的是消息内容,一个消息应该包含哪些内容?多个程序相互通信如何避免 race condtion?外部事件发生时,网络消息应该发 snapshot 还是 delta?新增功能时,各个组件如何平滑升级?
1.8、网络编程的三个层次
- 读过教程和文档,做过练习;
- 熟悉本系统的 TCP/IP 协议栈的脾气;
- 自己写过一个简单的 TCP/IP stack。
第一个层次是基本要求,读过《UNIX 网络编程》、《TCP/IP 详解》并基本理解 TCP/IP 协议,读过本系统的 manpage。
第二个层次,熟悉本系统的 TCP/IP 协议栈参数设置与优化是开发高性能网络程序的必备条件。拿 Linux 的 TCP/IP 协议栈来说:
- 有可能出现 TCP 自连接(self-connection),程序应该有所准备。
- Linux 的内核会有 bug,比如某种 TCP 拥塞控制算法正经出现 TCP window clamping(窗口箝位)bug,导致吞吐量暴跌,可以选用其他拥塞控制方法来绕开(work around)这个问题。
编写可靠的网络程序的关键是熟悉各种场景下的 error code(文件描述符用完了如何?本地 ephemeral port 暂时用完,不能发起新连接怎么办?服务端新建并发连接太快,backlog 用完了,客户端 connect 会返回什么错误?),有的在 manpage 里有描述,有的要通过实践或阅读源码获得。
第三个层次,通过自己写一个简单的 TCP/IP 协议栈,能打打加深对 TCP/IP 的理解,更能明白 TCP 为什么要这么设计,有哪些因素制约,每一步操作的代价是什么。实现 TCP/IP 只需要操作系统提供三个接口函数:一个函数,两个回调函数。分别是:send_packet()、on_receive_packet()、on_timer()。
1.9、TCP 的可靠性有多高
IP header 和 TCP header 的 checksum 是一种非常弱的 16-bit check sum 算法,其把数据当成反码表示的 16-bit integers,再加到一起。由于是简单的加法,遇到和(sum)不变的情况情况就无法检查出错误(比如交换两个 16-bit 整数,加法满足交换律,checksum 不变)。以太网的 CRC32 只能保证同一个网段上的通信不会出错(两台机器的网线插到同一个交换机上,这时候以太网的 CRC 是有用的)。但是,如果两台机器之间经过了多级路由器呢?
二、三本必看的书
谈到 Unix 编程和网络编程,W.Richard Stevens 写了6本书,即 APUE、两卷《UNIX 网络编程》、三卷《TCP/IP 详解》。
第一本:《TCP/IP IIustrated, Vol.1: The Protocols》
作者以 tcpdump 为工具,对 TCP 协议抽丝剥茧、娓娓道来。TCP 作为一个可靠的传输协议,其核心有三点:
- Positive acknowledgement with retransmission;
- Flow control using sliding window(包括 Nagle 算法等);
- Congestion control(包括 slow start、congestion avoidance、fast retransmit 等)。
第一点足以满足可靠性要求;第二点是为了提高吞吐量,充分利用链路层带宽;第三点是防止过载造成丢包。换言之,第二点是避免发得太慢,第三点是避免发得太快,二者相互制约。从反馈控制的角度看,TCP 像是一个自适应的节流阀,根据管道的拥堵情况自动调整阀门的流量。
第二本:《Unix Network Programming, Vol.1: Networking API》
UNP 是 Sockets API 的权威指南。
第三本:《Effective TCP/IP Programming》
三、关于 TCP 并发连接的几个思考题与试验
思考题一
有一台机器,它有一个 IP,上面运行了一个 TCP 服务程序,程序只侦听一个端口,问:从理论上讲(只考虑 TCP/IP 这一层面,不考虑 IPv6)这个服务程序可以支持多少并发 TCP 连接?
这个问题等价于:有一个 TCP 服务程序的地址是 1.2.3.4:8765,问它从理论上能接受多少个并发连接?
思考题二
一台被测试机器 A,功能同上,同一交换机上还接有一台机器 B,如果允许 B 的程序直接收发以太网 frame,问:让 A 承担10万个并发 TCP 连接需要用多少 B 的资源?100万个呢?
一个 TCP 连接要占用多少系统资源呢?
在现在的 Linux 操作系统上,如果用 socket() 或 accept() 来创建 TCP 连接,那么每个连接至少要占用一个文件描述符(file descriptor)。为什么说至少?因为文件描述符可以复制,比如 dup();也可以被继承,比如 fork();这样可能出现系统中同一个 TCP 连接有多个文件描述符与之对应。
TCP/IP 协议中的三路握手中 TCP 连接是虚拟的连接,不是电路连接。维持 TCP 连接理论上不占用网络资源(会占用两头程序的系统资源)。只要连接的双方认为 TCP 连接存在,并且可以互相发送 IP packet,那么 TCP 连接就一直存在。
对于问题1,向一个 TCP 服务程序发起一个连接,客户端(faketcp 客户端)只需要三件事情(三路握手):
1a. 向 TCP 服务程序发一个 IP packet,包含 SYN 的 TCP segment;
1b. 等待对方返回一个包含 SYN 和 ACK 的 TCP segment;
1c. 向对方发送一个包含 ACK 的 segment。
对于问题2,为了让一个 TCP 客户端程序认为连接已建立,faketcp 服务端也只需要做三件事情:
2a. 等待客户端发来的 SYN TCP segment;
2b. 发送一个包含 SYN 和 ACK 的 TCP segment;
2c. 忽视对方发来的包含 ACK 的 segment。
faketcp 服务端在做完头两件事情(收一个 SYN、发一个 SYN+ACK)之后,TCP 客户端程序会认为连接已建立。而做这三件事情并不占用 facktcp 服务端的资源。换句话说,facktcp 服务端可以一直重复做这三件事,接受不计其数的 TCP 连接,而 facktcp 服务端自己毫发无损。
第一题的答案:
在只考虑 IPv4 的情况下,并发数的理论上限了248。实际的限制是操作系统的全局文件描述符的数量,以及内存大小。一个 TCP 连接有两个 end point 已经固定,每个 end point 是 {ip, port},题目说其中一个 end point 已经固定,那么留下一个 end point 的自由度,即248。客户端 IP 的上限是232,每个客户端 IP 发起连接的上限是216,乘到一起得到理论上限。
- 本文标题:C++网络编程
- 本文作者:beyondhxl
- 本文链接:https://www.beyondhxl.com/post/57227f14.html
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!