改善网络程序的44个技巧
《不要慌太阳下山有月光》
安利一首好听的歌儿~
也许你身处黑暗之中
快记不清当初的梦
低下头两手空空
怅然若失般心痛
世上每个人花期不同
你也会和美好相逢
张开双臂迎向风
那时你一定更好更勇更从容
不要迷茫 不要慌张
太阳下山 还有月光
它会把人生路照亮
陪你到想去的地方
月亮睡了 还有朝阳
抬头看天一定会亮
技巧1:理解面向链接和无连接协议之间的区别
- 对于无连接协议来说,每个分组的处理都独立于所有其他分组,而对于面向连接的协议来说,协议实现维护了与后继分组有关的状态信息。
- 对TCP来说,连接完全是想象的,它是由端点所记忆的状态组成的, 并不存在“物理”连接。
面向链接
定义: 面向连接的协议在数据传输之前,需要建立一个稳定的连接,并在数据传输完成后拆除这个连接。它确保数据在传输过程中不会丢失、重复、乱序或损坏。
示例:
-
TCP(传输控制协议)
: 用于互联网中的许多应用,如网页浏览、电子邮件、文件传输等。
- 连接建立: 通过三次握手(SYN, SYN-ACK, ACK)建立连接。
- 可靠性: 确保数据包的可靠传输和顺序。
无连接协议
定义: 无连接的协议在数据传输过程中不需要建立连接,每个数据包(或称为数据报)都是独立的。它不保证数据的到达、顺序或完整性,接收方可能需要处理数据丢失或乱序的问题。
示例:
-
UDP(用户数据报协议)
: 用于需要低延迟和高吞吐量的应用,如视频流、在线游戏、DNS 查询等。
- 无连接: 发送数据包时不需要建立连接。
- 不可靠: 数据包可能会丢失、重复或乱序。
以下是一个简单的 UDP 客户端和服务器示例,展示了如何使用 UDP 进行数据传输。
UDP 服务器(C++)
1 |
|
UDP 客户端(C++)
1 |
|
技巧2:理解子网和CIDR的概念
A、B、C、D、E类网络,网络ID和主机ID
D类地址用于多播编址,E类地址留作未来使用。剩下的A、B和C类地址是用于标识单个网络和主机的主要地址类型。
地址类别是由前导1比特的个数标识的。A类有零个前导1比特,B类有一个,C类有两个,以此类推。标识地址的类型是非常重要的,因为对其余比特的解释都取决于地址的类型。
子网划分
-
概念:子网划分是通过修改 IP 地址和子网掩码来创建多个逻辑上的子网络。每个子网都是原始网络的一部分,但它们在逻辑上是分开的。子网划分的核心是调整 IP 地址中的网络部分和主机部分,以便将一个大的网络拆分成多个小的网络。
-
例子:
192.168.1.0/26 (地址范围:192.168.1.0 到 192.168.1.63)
192.168.1.64/26 (地址范围:192.168.1.64 到 192.168.1.127)
192.168.1.128/26 (地址范围:192.168.1.128 到 192.168.1.191)
192.168.1.192/26 (地址范围:192.168.1.192 到 192.168.1.255) -
好处:
-
提高网络性能:
- 将大型网络划分为多个子网可以减少广播风暴的范围。广播仅在子网内部传播,从而降低了网络的负荷和拥塞。
-
增强网络安全:
- 子网划分可以隔离网络中的不同部门或功能,使得安全策略可以在每个子网中独立应用。这可以限制潜在的安全威胁和数据泄露。
优化地址空间使用:
- 子网划分可以有效地利用 IP 地址空间,避免了地址浪费。例如,将一个大型网络划分为多个小网络可以更精确地分配 IP 地址。
-
CIDR——Classless Inter Domain Routing,无类别域间路由
CIDR(Classless Inter-Domain Routing,无类别域间路由)是一种用于 IP 地址分配和路由选择的方法,旨在解决传统分类地址(Classful Addressing)中的一些限制。CIDR 通过引入更灵活的子网掩码和更有效的路由聚合方法来提高 IP 地址的利用率和路由效率。
CIDR 的主要特点
- 无类别地址分配:
- 传统分类地址: IP 地址分为 A、B、C 类等,每类有固定的子网掩码(如 A 类是 8 位,B 类是 16 位,C 类是 24 位)。
- CIDR: IP 地址和子网掩码的长度不再受到固定分类的限制。CIDR 允许使用任意长度的子网掩码,使得地址分配更灵活和高效。
- 前缀长度表示法:
- CIDR 使用前缀长度来表示子网掩码的大小,例如
192.168.0.0/24
。 192.168.0.0/24
表示前 24 位是网络部分,剩余的 8 位是主机部分。这种表示方法比传统的 IP 地址和子网掩码形式更加直观。
- CIDR 使用前缀长度来表示子网掩码的大小,例如
- 路由聚合(Prefix Aggregation):
- CIDR 支持路由聚合,即将多个连续的 IP 地址块聚合为一个更大的路由前缀,从而减少路由表的大小和复杂性。
- 例如,将
192.168.0.0/24
和192.168.1.0/24
聚合为192.168.0.0/23
。
- 改进了路由表的可扩展性:
- 由于路由聚合的能力,CIDR 减少了需要在网络路由器中存储的路由条目数量,从而减轻了路由器的负担,提高了网络的可扩展性。
CIDR 的示例
1. IP 地址和前缀长度
- 地址:
192.168.1.0
- 前缀长度/24
- 子网掩码:
255.255.255.0
- 子网掩码:
这表示 192.168.1.0
到 192.168.1.255
的所有地址属于同一网络。
2. 路由聚合
- 原始网络
192.168.0.0/24
192.168.1.0/24
192.168.2.0/24
- 聚合后的网络
192.168.0.0/22
192.168.0.0/22
覆盖了从 192.168.0.0
到 192.168.3.255
的地址范围,可以将多个小的网络聚合为一个更大的网络,从而减少路由条目。
技巧3:理解私有地址和NAT
NAT(Network Address Translation) 和 PAT(Port Address Translation) 是两种在网络中常见的地址转换技术,用于处理 IP 地址的分配和管理。
- NAT——Network Address Translation,网络地址翻译
- PAT——Port Address Translation,端口地址转换
NAT(网络地址翻译)
NAT 是一种技术,用于将私有网络中的 IP 地址转换为公共网络中的 IP 地址,允许多个设备共享一个公共 IP 地址。这主要用于解决 IP 地址短缺问题以及提供网络安全性。
NAT 的工作原理:
- 内部网络(私有网络):
- 在本地网络中,设备使用私有 IP 地址(例如
192.168.1.0/24
)。 - 这些私有地址在互联网上是不可路由的。
- 在本地网络中,设备使用私有 IP 地址(例如
- NAT 设备(通常是路由器或防火墙):
- NAT 设备将内部网络中的私有 IP 地址转换为公共 IP 地址。
- 内部设备发出的数据包在离开本地网络时,源 IP 地址被替换为 NAT 设备的公共 IP 地址。
- 外部网络(公共网络):
- 返回的数据包通过 NAT 设备,将公共 IP 地址和端口号转换回原始的私有 IP 地址和端口号。
NAT 示例:
- 私有 IP 地址:
192.168.1.10
- 公共 IP 地址:
203.0.113.1
- 转换过程
- 内部设备
192.168.1.10
发出请求到互联网。 - NAT 设备将源 IP 地址
192.168.1.10
替换为公共 IP 地址203.0.113.1
。 - 外部服务器看到的请求来源是
203.0.113.1
。 - 响应返回时,NAT 设备将目标 IP 地址
203.0.113.1
转换回192.168.1.10
,并将数据包送回内部设备。
- 内部设备
PAT(端口地址转换)
PAT,也称为 NAT Overloading,是 NAT 的一种扩展,允许多个内部设备通过同一个公共 IP 地址访问互联网。它通过使用不同的端口号来区分不同的会话。
PAT 的工作原理:
- 内部网络(私有网络):
- 多个设备使用私有 IP 地址。
- PAT 设备(通常是路由器或防火墙):
- PAT 设备将内部设备的 IP 地址和端口号映射到一个公共 IP 地址的不同端口号上。
- 例如,两个内部设备的流量都可以通过公共 IP 地址的不同端口号进行区分。
- 外部网络(公共网络):
- 外部服务器看到的是公共 IP 地址和端口号。
- 响应数据包通过 PAT 设备,将公共 IP 地址和端口号映射回内部设备的 IP 地址和端口号。
PAT 示例:
- 私有 IP 地址和端口号
192.168.1.10:12345
192.168.1.11:54321
- 公共 IP 地址:
203.0.113.1
- PAT 映射
192.168.1.10:12345
→203.0.113.1:10001
192.168.1.11:54321
→203.0.113.1:10002
- 转换过程
- 内部设备
192.168.1.10
通过端口12345
发出请求。 - PAT 设备将请求的源地址
192.168.1.10:12345
转换为203.0.113.1:10001
。 - 内部设备
192.168.1.11
通过端口54321
发出请求。 - PAT 设备将请求的源地址
192.168.1.11:54321
转换为203.0.113.1:10002
。 - 外部服务器响应时,返回的目标地址
203.0.113.1:10001
和203.0.113.1:10002
被 PAT 设备映射回内部设备的 IP 地址和端口号。
- 内部设备
总结
- NAT 用于将私有 IP 地址转换为公共 IP 地址,以便多个设备可以共享一个公共 IP 地址进行互联网访问。
- PAT 是 NAT 的一种形式,通过不同的端口号来区分不同的内部会话,允许多个内部设备共享同一个公共 IP 地址。
补充:
私有 IP 地址范围:
10.0.0.0/8
:- 范围:
10.0.0.0
到10.255.255.255
- 这个范围支持 1677 万个地址。
- 范围:
172.16.0.0/12
:- 范围:
172.16.0.0
到172.31.255.255
- 这个范围支持 1048 万个地址。
- 范围:
192.168.0.0/16
:- 范围:
192.168.0.0
到192.168.255.255
- 这个范围支持 65,536 个地址。
- 范围:
技巧4:开发并使用应用程序“框架”
- TCP服务端:socket->bind->listen->accept
- TCP客户端:socket->connect
- UDP服务端:socket->bind
- UDP客户端:socket
技巧5:套接字接口比XTI/TLI更好用
略
技巧6:记住,TCP是一种流协议
- TCP是一种流协议,数据以字节流的形式传递给接收者的,没有固有的“报文”或“报文边界”的概念
技巧7:不要低估TCP的性能
可以查看原书中的性能对比
技巧8:避免重新编写TCP
假设你正在开发一个需要可靠数据传输的网络应用程序:
- 正确做法: 使用操作系统提供的 TCP 协议栈,通过
socket
创建 TCP 连接,使用send
和recv
函数进行数据传输。这样,你可以利用 TCP 的流量控制、拥塞控制等特性,确保数据的可靠传输。 - 错误做法: 重新实现 TCP 协议的所有功能,例如手动实现数据重传、顺序保证、流量控制等,这将是一个非常复杂且容易出错的任务。
技巧9:要认识到TCP是一个可靠的,但并不绝对可靠的协议
(1) 永久或临时的网络中断
背景:
- 场景: 一个分布式应用程序在不同地理位置的数据中心之间传输数据。连接依赖于互联网或广域网,这些网络可能会遭遇临时或永久的中断。
解释:
- 临时网络中断: 网络中断可能是暂时的,例如由于网络设备重启或维护。TCP 会尝试重新连接和重传数据,但可能会引入延迟。
- 永久网络中断: 如果网络中断是永久性的,例如由于物理链路断裂或网络设备故障,TCP 连接将无法恢复,应用程序需要检测连接中断并采取适当措施,如尝试重新建立连接或通知用户。
示例:
- 在一个跨国企业的文件同步应用中,如果两地之间的网络链路出现故障,TCP 可能会重试连接,但长时间的中断可能导致数据同步失败。
(2) 对等的应用程序崩溃
背景:
- 场景: 一个客户端-服务器应用程序,其中客户端和服务器通过 TCP 连接进行通信。服务器应用程序崩溃或退出时,连接将被中断。
解释:
- 应用程序崩溃: 如果服务器端应用程序崩溃或异常退出,TCP 连接将被断开。虽然 TCP 会尝试重连,但在应用程序崩溃的情况下,重新建立连接可能无法进行,且数据丢失或未处理的数据需要应用层进行补救。
示例:
- 在一个实时聊天应用中,如果服务器端的聊天服务崩溃,客户端会失去连接。TCP 会检测到连接丢失,但客户端需要能够处理连接丢失,重新连接或通知用户。
(3) 运行对等应用程序的主机崩溃
背景:
- 场景: 一个分布式系统,其中某些应用程序运行在主机上。这些主机可能会因为硬件故障或操作系统崩溃而宕机。
解释:
- 主机崩溃: 如果运行 TCP 应用程序的主机崩溃或关闭,TCP 连接会被断开。虽然 TCP 连接的另一端可能会检测到连接中断并尝试重连,但主机崩溃的情况通常需要应用层处理,以便在主机恢复后能够继续操作或重启服务。
示例:
- 在一个分布式数据库系统中,如果存储节点的主机崩溃,数据传输会中断。数据库系统需要实现机制来检测节点故障、重新分配任务,并在节点恢复时重新同步数据。
技巧10:记住,TCP/IP不是轮询的
- TCP没有提供将连接丢失即时通知给应用程序的方法。
- TCP 协议本身并不会主动或立即通知应用程序某个连接已经断开或失效。TCP 提供了可靠的数据传输保证,但它不会即时地将连接状态的变化(如连接丢失)反馈给应用程序。
- 保持活跃(keep-alive)
- 心跳信号(epoll, pthread)
技巧11:提防对等实体的不友好动作
技巧12:成功的LAN策略不一定能推广到WAN中
核心思想
- 网络特性差异:
- 局域网(LAN): 通常具有较低的延迟和较高的带宽,网络环境相对稳定,数据传输速度较快。
- 广域网(WAN): 包括多个网络环境,通常存在较高的延迟和较低的带宽,网络波动和丢包率可能较高。
- 策略和优化:
- LAN 策略: 在 LAN 环境中,应用程序和网络策略可能会基于高速连接和低延迟的假设进行设计和优化。
- WAN 环境: 这些策略在 WAN 环境中可能不再适用,因为 WAN 的网络条件较差,可能需要不同的策略和优化手段。
- Example 1: 实时视频会议:
- LAN: 在企业内部的局域网中,视频会议可以实现高质量的视频和音频流,因为网络延迟低且带宽充足。
- WAN: 当视频会议扩展到广域网时,需要考虑网络延迟和带宽限制,可能会导致视频质量下降或出现卡顿现象。需要使用更高效的视频压缩技术和自适应码率调整策略。
- Example 2: 文件同步应用:
- LAN: 内部文件同步可以实现快速的实时同步。
- WAN: 在跨区域同步文件时,需要处理更大的延迟和带宽波动,采用增量同步和数据压缩等策略来提高性能。
技巧13:了解协议是怎样工作的
- RFC是TCP/IP的官方规范
技巧14:不要把OSI七层参考模型太当回事
- OSI和TCP/IP
技巧15:理解TCP的写操作
- 从应用程序的角度来看,最好把写操作当作是一项将数据从用户空间拷贝到内核发送缓冲区,然后就返回的操作。
- 如果发送端应用程序崩溃了,TCP会继续尝试着将数据传递给对等实体。
技巧16:理解TCP的有序释放操作
-
有序释放是在确保没有数据丢失的情况下拆除n连接的一个过程。
-
shutdown与close的区别
shutdown
:- 用于部分关闭 socket(如停止发送或接收数据),允许对端继续操作,但不能再使用指定的方向进行通信。
- 并没有释放套接字及其资源
- 调用shutdown时,会影响到所有打开了那个套接字的进程。
- FIN发送:用how =1来调用shutdown 时,不管其他进程是否打开了这个套接字,都可以保证对等实体会收到一个EOF。
close
- 用于完全关闭 socket,释放所有资源,断开连接,对端会收到连接关闭的通知。
- 释放套接字及其资源
- 调用close或closesocket,套接字的其他持有者仍然能够像什么事情都没有发生一样使用它。
- 调用close或closesocket就无法确保这一点,因为在套接字的引用计数减少到零之前,它都不会将FIN发送给对等实体。也就是说,所有进程关闭套接字后,它才将FIN发送给对等实体。
总结:
shutdown
是通过操作系统内核直接影响套接字的状态的。close
只是释放了当前进程的套接字描述符,并不会立即影响其他进程或线程的套接字描述符。Q:可以用
shutdown
释放资源吗?A:不可以,
shutdown
专注于数据流控制,而不是资源管理。必须使用close
函数释放资源。
技巧17:考虑用inetd来装载应用程序
inetd
介绍:
inetd
(Internet Daemon)是一个服务守护进程,负责监听指定的网络端口,并根据配置文件中的规则启动相应的服务程序处理请求。- 通过配置文件 (
/etc/inetd.conf
),inetd
可以决定哪些服务应该由哪些程序处理,并在接收到连接请求时启动相应的程序。
使用 inetd
的优点:
- 资源节省:
inetd
只有在收到连接请求时才启动相应的服务程序,而不是让服务程序常驻内存。这样可以节省系统资源。 - 简化管理: 通过集中管理配置文件,减少了对多个守护进程的管理负担。只需编辑
inetd.conf
配置文件即可添加或修改服务。 - 灵活配置: 可以灵活配置哪些服务由
inetd
处理,并轻松调整服务程序的路径或参数。
举例说明
假设你希望在系统上提供 FTP 和 Telnet 服务,但不希望这些服务程序一直运行,从而节省系统资源。可以使用 inetd
来管理这些服务。
-
安装和配置
inetd
:-
确保系统上安装了
inetd
。在许多现代 Linux 系统上,inetd
被xinetd
替代,你可以使用xinetd
进行类似的配置。 -
编辑
/etc/inetd.conf
文件,添加以下配置来定义 FTP 和 Telnet 服务:1
2ftp stream tcp nowait root /usr/sbin/in.ftpd in.ftpd
telnet stream tcp nowait root /usr/sbin/in.telnetd in.telnetdftp
: 服务名。stream
: 套接字类型(流式)。tcp
: 使用的协议。nowait
: 表示服务程序不会并发处理多个连接。root
: 服务程序的用户身份。/usr/sbin/in.ftpd
: 服务程序的路径。in.ftpd
: 服务程序的名称。
-
-
启动
inetd
:- 启动
inetd
守护进程(如果未运行)。通常,inetd
会在系统启动时自动启动。
- 启动
-
测试配置:
- 连接到指定的端口(如 FTP 的 21 端口或 Telnet 的 23 端口),
inetd
会启动相应的服务程序来处理请求。
- 连接到指定的端口(如 FTP 的 21 端口或 Telnet 的 23 端口),
示例操作
假设我们在 /etc/inetd.conf
中配置了 Telnet 服务。连接到 Telnet 的 23 端口时,inetd
会启动 in.telnetd
程序来处理连接。
启动 inetd
:
1 | sudo service inetd start |
连接 Telnet:
1 | telnet localhost |
在连接到 Telnet 端口时,inetd
会自动启动 in.telnetd
程序,并处理该连
技巧18:考虑用tcpmux为服务器“分配”知名端口
tcpmux
用于在一个知名端口上复用多个服务。它允许多个不同的网络服务共享一个端口,通过接收服务请求并根据请求内容将连接转发到对应的服务程序,从而简化端口管理和配置。
tcpmux
介绍
tcpmux
(TCP Port Multiplexer)是一种协议,用于在单个端口上复用多个服务。它通过 TCP 端口复用允许多个服务共享一个知名端口(通常是 1-1023 端口范围内的端口)。tcpmux
协议使用一个预定义的端口(通常是端口 1)来接收请求,并根据请求内容决定将连接转发到哪个实际服务程序。
tcpmux
的优点
- 简化端口管理: 可以通过一个端口管理多个服务,减少对知名端口的占用。
- 减少冲突: 减少服务程序之间的端口冲突,尤其是当系统上有多个服务需要运行时。
- 提高安全性: 通过集中管理端口,可以简化防火墙配置,并对外提供统一的入口。
举例说明
假设你有多个服务需要运行,如 FTP、Telnet 和 HTTP。你希望将它们集中在一个知名端口上,以简化管理和配置。可以使用 tcpmux
来实现这一点。
1. 配置 tcpmux
步骤 1: 安装和配置 tcpmux
- 在现代系统上,
tcpmux
可能不再广泛使用,或者其实现方式可能有所不同。如果使用tcpmux
的传统实现,你可以在/etc/tcpmuxd.conf
或类似的配置文件中设置服务。
步骤 2: 编辑配置文件
1 | # 在 tcpmuxd.conf 文件中配置服务 |
- 上述配置将 FTP、Telnet 和 HTTP 服务与
tcpmux
绑定,并使用tcpmux
端口(通常是端口 1)来复用这些服务。
步骤 3: 启动 tcpmux
1 | sudo service tcpmuxd start |
步骤 4: 测试 tcpmux
- 连接到端口 1,并发送服务请求。例如,使用 telnet 连接到端口 1,发送相应的服务名称来选择所需的服务:
1 | telnet localhost 1 |
补充·:Telnet 使用 Telnet 协议,这是一种基于 TCP 的应用层协议。它通过 TCP 的端口 23 进行通信,允许用户在远程计算机上进行交互式命令行会话。
在 tcpmux
接收到请求后,它会解析请求并将连接转发到正确的服务程序,如 FTP、Telnet 或 HTTP 服务。
示例操作
假设你配置了以下服务:
- FTP 通过端口 21
- Telnet 通过端口 23
- HTTP 通过端口 80
你可以使用 tcpmux
监听端口 1,根据请求的内容将连接转发到相应的服务程序。
简化配置
通过 tcpmux
管理的网络服务配置示例:
1 | # 通过 tcpmux 管理多个服务 |
在配置文件中定义服务:
1 | # 配置服务程序 |
技巧19:考虑使用两条TCP连接
使用两条 TCP 连接的好处:
- 分离控制和数据流: 在某些应用场景中,分离控制信息和数据传输可以提高通信的效率和可靠性。例如,一个连接专用于控制命令和管理操作,而另一个连接用于数据传输,这样可以减少数据和控制信息之间的干扰。
- 提高性能: 通过将控制和数据流分开,应用程序可以更好地优化每条连接的性能。例如,控制连接可以保持较低的延迟,而数据连接可以处理大量数据传输,提高整体效率。
- 增强可靠性: 使用两条连接可以提高系统的容错能力。如果一个连接出现问题,另一个连接仍然可以继续工作,减少服务中断的风险。
举例说明
1. 文件传输与控制命令
在某些文件传输协议中,如 FTP(文件传输协议),使用两条 TCP 连接的设计是典型的例子:
- 控制连接: 用于发送命令和接收服务器的响应。通常使用端口 21。
- 数据连接: 用于实际传输文件数据。通常在建立控制连接后动态分配端口。
示例操作:
- FTP 控制连接: 客户端通过端口 21 连接到 FTP 服务器,发送命令如
LIST
、RETR
(取回文件)、STOR
(存储文件),服务器响应这些命令。 - FTP 数据连接: 当客户端请求文件列表时,服务器在另一条数据连接上传输文件列表数据。数据连接在不同的端口上建立,用于传输实际的文件数据或目录列表。
2. 聊天应用程序
在聊天应用程序中,可以使用两条 TCP 连接来分离聊天消息和控制信号:
- 聊天消息连接: 用于传输聊天消息内容,确保消息的高效传输。
- 控制信号连接: 用于传输控制信号,如用户状态更新、连接管理等。
示例操作:
- 聊天消息连接: 客户端和服务器通过一个专用的连接传输用户的聊天消息。
- 控制信号连接: 另一个连接处理用户登录、注销、状态更新等控制信息,确保这些操作不会干扰实际的聊天消息传输。
技巧20:使应用程序成为事件驱动的(1)
高效资源使用: 事件驱动模型可以减少资源的消耗,通过在事件发生时处理任务而不是不断轮询或等待,这样可以提高应用程序的整体效率。
更好的响应性: 应用程序可以即时响应事件(如网络请求、用户输入等),从而提供更快的用户体验。
处理高并发: 事件驱动编程特别适合处理大量并发事件,通过异步处理和非阻塞 I/O 操作来支持大规模并发用户。
简化设计: 通过将事件处理逻辑与业务逻辑分离,可以使程序设计更加清晰和可维护。
技巧21:使应用程序成为事件驱动的(2)
1. 异步 I/O 操作
在事件驱动编程中,异步 I/O 操作是关键,允许程序在等待 I/O 操作完成的同时继续执行其他任务。
示例:
- Linux AIO: 使用 Linux 的异步 I/O 接口(如
io_uring
或libaio
)来进行非阻塞文件 I/O 操作。
1 | c++Copy code |
2. 使用异步编程库
现代 C++ 提供了多种异步编程库,如 Boost.Asio 和 C++20 的协程(coroutines),这些库和语言特性可以更简洁地实现事件驱动编程。
示例:
- Boost.Asio: 用于实现异步网络通信。
1 | c++Copy code |
技巧22:不要用TIME-WAIT暗杀来关闭一条连接
-
通常只有一端——主动关闭的那一端会进入TIME-WAIT状态
-
使用TIME-WAIT状态主要由两个目的:
-
维护连接状态,以防主动关闭连接的那段发送的最后一条ACK丢失后造成另一端重新发送FIN信号
-
为耗尽网络中所有此连接的“走失段”提供时间
-
-
暗杀
TIME-WAIT 暗杀是指通过不适当的方法关闭 TIME-WAIT 状态中的连接,可能会导致以下问题:
- 端口资源浪费: 在高负载的系统中,大量的 TIME-WAIT 状态连接可能会消耗系统的端口资源,导致端口耗尽问题。
- 连接重用问题: 如果在 TIME-WAIT 状态期间重新使用相同的端口号,可能会导致数据包混淆,进而影响连接的稳定性和数据完整性。
- 网络流量问题: 不正确的处理可能导致网络中的包丢失或重复,从而引发更多的网络流量问题和性能下降。
技巧23:服务器应该设置SO_REUSERADDR选项
技巧24:可能的话,使用一个大规模的写操作,而不是多个小规模的写操作
- 上下文的切换耗时
- Nagle算法影响
技巧25:理解如何使connect调用超时
connect
调用的超时
在 TCP 网络编程中,connect
系统调用用于建立一个到远程主机的连接。然而,如果远程主机无法在合理的时间内响应,connect
调用可能会导致程序阻塞,直到连接建立或者超时。设置超时可以防止这种情况,并提高程序的可靠性和响应性。
设置 connect
超时的步骤
- 使用
SO_RCVTIMEO
和SO_SNDTIMEO
选项: 这些选项可以设置接收和发送的超时时间,适用于 TCP 连接,但它们不会直接影响connect
调用的超时。 - 使用
select
或poll
函数: 通过将套接字文件描述符传递给select
或poll
,可以设置超时,帮助实现connect
超时。 - 使用
fcntl
函数设置非阻塞模式: 通过将套接字设置为非阻塞模式,可以使用非阻塞的connect
调用,然后利用select
或poll
来等待连接的完成或超时。
补充:
SO_RCVTIMEO
和 SO_SNDTIMEO
选项 是用于设置套接字的接收和发送操作的超时时间的选项。这些选项对 connect
调用的超时没有直接影响,但对于套接字在连接成功后如何处理接收和发送超时非常有用。
SO_RCVTIMEO
和 SO_SNDTIMEO
选项概述
SO_RCVTIMEO
: 设置接收操作的超时时间。SO_SNDTIMEO
: 设置发送操作的超时时间。
这两个选项通常用于控制套接字的接收和发送操作的超时,而不是 connect
调用本身。它们可以帮助处理由于网络问题导致的接收或发送延迟。
示例代码
以下示例演示了如何使用 SO_RCVTIMEO
和 SO_SNDTIMEO
选项来设置套接字的接收和发送超时时间。
1 |
|
技巧26:避免数据复制
避免数据复制的策略
-
使用零拷贝技术:
- 零拷贝是一种技术,旨在减少数据在内存中被复制的次数。操作系统和网络库提供了一些机制来实现零拷贝。
- 示例: 使用
sendfile()
函数可以直接将文件的数据从内核缓冲区发送到网络连接中,而不需要将数据先复制到用户空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30e
int main() {
int src_fd = open("source_file.txt", O_RDONLY);
int dest_fd = /* 目标套接字描述符 */;
if (src_fd < 0 || dest_fd < 0) {
perror("open");
return 1;
}
off_t offset = 0;
struct stat file_stat;
fstat(src_fd, &file_stat);
ssize_t bytes_sent = sendfile(dest_fd, src_fd, &offset, file_stat.st_size);
if (bytes_sent < 0) {
perror("sendfile");
close(src_fd);
close(dest_fd);
return 1;
}
close(src_fd);
close(dest_fd);
return 0;
} -
使用内存映射 (Memory Mapping):
- 通过内存映射文件 (
mmap
) 将文件直接映射到进程的地址空间,避免了显式的读写操作。 - 示例: 使用
mmap
映射文件并在内存中直接操作数据,减少了内存复制操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main() {
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
off_t file_size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* file_data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (file_data == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 操作映射的内存
// ...
munmap(file_data, file_size);
close(fd);
return 0;
} - 通过内存映射文件 (
-
使用共享内存:
- 共享内存允许不同的进程访问同一块物理内存区域,从而避免了数据在进程之间的复制。
- 示例: 使用 POSIX 共享内存 (
shm_open
) 或 System V 共享内存 (shmget
) 来实现进程间的数据共享。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main() {
const char* shm_name = "/my_shm";
size_t size = 4096;
int shm_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
if (shm_fd < 0) {
perror("shm_open");
return 1;
}
ftruncate(shm_fd, size);
void* shm_ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
close(shm_fd);
return 1;
}
// 操作共享内存
// ...
munmap(shm_ptr, size);
close(shm_fd);
shm_unlink(shm_name);
return 0;
} -
使用高效的序列化和反序列化方法:
- 避免不必要的数据转换,使用高效的序列化方法来减少数据复制。
- 示例: 使用 Google 的 Protocol Buffers 或 FlatBuffers,这些库提供了高效的序列化机制。
1
2
3
4
5
6
7
8
9
10
11
12// 使用 Protocol Buffers 库示例
void SerializeToString(const your::proto::Message& msg, std::string* output) {
msg.SerializeToString(output);
}
void ParseFromString(const std::string& input, your::proto::Message* msg) {
msg->ParseFromString(input);
}
总结
避免数据复制 通过以下几种方式来提升性能:
- 使用零拷贝技术,如
sendfile
。 - 使用内存映射,如
mmap
。 - 使用共享内存来避免进程间的数据复制。
- 使用高效的序列化库来减少数据转换开销。
这些技术有助于减少延迟和内存消耗,提高应用程序的性能。
技巧27:使用前将结构sockaddr_in清零
避免未定义行为:
sockaddr_in
结构体在未初始化时可能包含随机值,这些随机值可能会导致未定义的行为。- 在函数如
bind
、connect
或sendto
中使用未初始化的结构体可能导致意外错误或不确定的结果。
技巧28:不要忘记字节的“性别”
技巧28: 不要忘记字节的“性别” 是网络编程中的一个重要注意事项,尤其是在处理数据的序列化和反序列化时。这个技巧强调了在网络编程中需要处理数据的字节顺序(字节序),并确保数据在不同系统之间的一致性。
技巧29:不要将IP地址或端口号硬编入应用程序中
技巧30:理解已连接的UDP套接字
Q:UDP可以用connect函数操作吗?
A:可以,目的就是为了简化编程。不影响UDP的无连接特性,UDP的无连接特性仍然存在,套接字仍然可以接收来自不同地址的数据。
使用connect
1 |
|
不使用
1 |
|
技巧31:记住,并不是所有程序都是用C编写的
技巧32:理解缓冲区长度带来的影响
- 发送缓冲区长度:
- 网络拥塞: 如果发送缓冲区过小,可能会导致频繁的阻塞,尤其是在网络拥塞或高负载时。应用程序可能会因为缓冲区满而被阻塞,直到缓冲区有空间可用。
- 吞吐量: 较大的发送缓冲区可以提高数据的吞吐量,减少由于等待缓冲区空间不足而导致的阻塞时间。尤其在高数据速率的应用中,合理设置发送缓冲区长度有助于提高性能。
- 接收缓冲区长度:
- 数据丢失: 如果接收缓冲区过小,可能会导致接收到的数据包被丢弃,尤其是在高数据速率的情况下。当接收缓冲区满时,新的数据包可能会被丢弃,导致数据丢失。
- 延迟: 较大的接收缓冲区可以减少因缓冲区满而导致的丢包概率,从而减少数据丢失的情况。它还可以降低延迟,因为应用程序可以在接收缓冲区中存储更多的数据。
[发送:小,阻塞。接收:小,丢包]
- 设置缓冲区大小
- 可以使用
setsockopt()
函数来调整套接字的缓冲区大小。例如,设置发送缓冲区大小可以使用SO_SNDBUF
选项,设置接收缓冲区大小可以使用SO_RCVBUF
选项。
- 可以使用
1 |
|
技巧33:熟悉ping使用工具
- ping没有使用TCP或UDP,因此没有相关的知名端口,使用ICMP
技巧34:学习使用tcpdump或类似的工具
tcpdump
通过使用 libpcap
库来捕获网络上的数据包。libpcap
是一个提供跨平台的网络数据包捕获功能的库,支持多种操作系统。tcpdump
实际上是一个基于 libpcap
的命令行工具。
tcpdump
的工作原理包括:
- 通过
libpcap
捕获网络数据包。 - 使用过滤表达式选择性捕获数据包。
- 解析和展示数据包的详细信息。
- 支持将数据保存到文件中以供后续分析。
技巧35:学习使用traceroute
- traceroute是诊断网络问题,学习网络路由以及探查网络拓扑的非常游泳的工具。
技巧36:学习使用ttcp
ttcp
是一个用于网络性能测试的工具,可以用来测量 TCP 和 UDP 网络连接的吞吐量和延迟。它通常用于评估网络带宽和性能,并测试网络在不同条件下的响应能力。以下是有关 ttcp
工具的一些详细信息和使用示例:
1. ttcp
的功能
- 性能测试:
ttcp
可以用来测试网络带宽、吞吐量和延迟等性能指标。 - TCP/UDP 测试:它支持 TCP 和 UDP 协议的测试,能够分别测试这两种协议在网络中的表现。
- 简单易用:
ttcp
是一个简单的工具,适合快速测量网络性能和进行基本的网络测试。
2. ttcp
的工作原理
- 客户端/服务器模式:
ttcp
运行在客户端和服务器模式下,客户端发起数据传输请求,服务器接收数据并返回结果。 - 数据传输:客户端生成一定量的数据并发送到服务器,服务器接收数据并计算吞吐量。
- 性能评估:
ttcp
根据传输的数据量和传输时间来计算网络性能指标,如吞吐量和延迟。
3. 使用示例
1. 安装 ttcp
在大多数 Linux 系统上,ttcp
可以通过软件包管理工具安装。例如,在 Debian 或 Ubuntu 上可以使用:
1 | sudo apt-get install ttcp |
在 Red Hat 或 CentOS 上可以使用:
1 | sudo yum install ttcp |
2. 启动服务器
首先,在目标机器上启动 ttcp
服务器进程:
1 | ttcp -r |
3. 启动客户端
在源机器上,使用 ttcp
客户端来测试网络性能。假设服务器的 IP 地址是 192.168.1.100
,可以使用以下命令进行 TCP 测试:
1 | ttcp -t -s 192.168.1.100 |
-t
:表示进行 TCP 测试。-s
:表示服务器的 IP 地址。
4. 解析结果
ttcp
会输出测试结果,包括传输的总数据量、传输时间、吞吐量等。例如,输出可能包括类似下面的内容:
1 | sent 10000000 bytes in 1.5 seconds = 6.67 MB/sec |
这表示客户端在 1.5 秒内发送了 10,000,000 字节的数据,吞吐量为 6.67 MB/sec。
4. 应用场景
- 网络性能评估:用于评估网络连接的实际带宽和性能。
- 故障排除:帮助诊断网络瓶颈和性能问题。
- 比较测试:测试不同网络配置或硬件对性能的影响。
技巧37:学习使用lsof
lsof
的功能(List Open Files)
- 列出打开的文件:显示当前系统上所有被进程打开的文件,包括普通文件、目录、设备文件等。
- 显示网络连接:提供当前系统上所有网络连接的信息,包括 TCP 和 UDP 连接。
- 查看文件描述符:列出与文件描述符相关的详细信息,如文件句柄和设备。
- 诊断工具:帮助查找占用文件或端口的进程,进行故障排除。
2. 常见用法
1. 列出所有打开的文件
1 | lsof |
此命令会列出当前系统上所有被打开的文件,包括网络连接、设备文件等。
2. 查找特定文件的使用情况
要查找特定文件被哪些进程打开:
1 | lsof /path/to/file |
例如,查看 /etc/passwd
文件被哪些进程打开:
1 | lsof /etc/passwd |
3. 查找某个进程打开的文件
要列出特定进程(例如 PID 为 1234)打开的所有文件:
1 | bash |
4. 查找特定用户的文件
要列出特定用户(例如用户 john
)打开的所有文件:
1 | lsof -u john |
5. 查找特定端口的使用情况
要查找特定端口(例如端口 80)被哪些进程占用:
1 | lsof -i :80 |
6. 列出所有网络连接
要列出所有网络连接(包括 TCP 和 UDP):
1 | lsof -i |
可以结合 -i
选项的不同参数来进一步过滤,例如 -i tcp
或 -i udp
。
7. 查找某个命令打开的文件
要查找正在运行的特定命令(例如 nginx
)打开的文件:
1 | lsof -c nginx |
8. 列出被某个设备或文件系统占用的文件
要列出挂载点(例如 /mnt
)下所有打开的文件:
1 | lsof +D /mnt |
技巧38:学习使用netstat
netstat
(Network Statistics)是一个用于显示网络连接、路由表、接口统计信息等网络相关数据的命令行工具。它可以帮助用户诊断网络问题、监控网络流量,并提供关于网络接口和连接状态的详细信息。
1. netstat
的功能
- 显示网络连接:列出系统上所有的网络连接,包括 TCP、UDP 连接。
- 显示路由表:提供系统的路由表信息,包括网络路由的目的地、网关等。
- 显示网络接口统计信息:提供每个网络接口的统计信息,如传输的字节数、接收的字节数等。
- 显示监听的端口:列出正在监听的网络端口,帮助识别开放的端口和运行的服务。
2. 常见用法
1. 显示所有网络连接
1 | netstat -a |
此命令会列出所有活动的网络连接(包括 TCP 和 UDP)。
2. 显示所有监听的端口
1 | netstat -l |
-l
选项显示正在监听的端口,帮助识别开放的服务端口。
3. 显示 TCP 连接
1 | netstat -at |
-t
选项过滤出 TCP 连接。加上 -a
选项可以显示所有 TCP 连接。
4. 显示 UDP 连接
1 | netstat -au |
-u
选项过滤出 UDP 连接。加上 -a
选项可以显示所有 UDP 连接。
5. 显示路由表
1 | netstat -r |
-r
选项显示系统的路由表,帮助了解数据包的转发路径。
6. 显示网络接口统计信息
1 | netstat -i |
-i
选项显示网络接口的统计信息,包括接收和发送的字节数、数据包数等。
7. 显示进程信息
1 | netstat -p |
-p
选项显示与每个连接相关的进程 ID 和名称,帮助识别哪个进程正在使用特定端口。
8. 显示每秒数据统计
1 | netstat -s |
-s
选项显示每种协议的统计信息,包括错误、丢包等。
3. 使用示例
1. 查找开放的端口
要检查系统上哪些端口处于监听状态,可以使用:
1 | netstat -tuln |
-t
:显示 TCP 连接。-u
:显示 UDP 连接。-l
:只显示监听状态的端口。-n
:以数字形式显示端口号和 IP 地址。
2. 查看进程和端口的关系
要查看每个连接的进程信息,可以使用:
1 | netstat -tulnp |
-p
:显示与连接相关的进程信息
技巧39:学习使用系统中的调用追踪工具(strace)
技巧40:构建并使用捕获ICMP报文的工具
技巧41:读Stevens的书
技巧42:阅读代码
技巧43:访问RFC编辑者的页面
技巧44:经常访问新闻组
Pro:TCP为什么不可靠
补充
补充1:TCP的最大负载和最小负载的意义
TCP(传输控制协议)的最大负载和最小负载是指TCP连接中可以传输的数据量的上限和下限。这些概念在TCP的性能调优和网络设计中具有重要意义。
最大负载(Maximum Load)
最大负载通常指的是TCP连接中可以传输的最大数据量,这主要与TCP的拥塞控制机制有关。TCP通过拥塞窗口(Congestion Window, CWND)来控制一次可以发送的数据量。拥塞窗口的大小会根据网络的拥塞程度动态调整。
意义:
- 网络利用率:最大负载决定了网络的最大吞吐量。通过调整拥塞窗口的大小,可以最大化网络的利用率,从而提高数据传输的效率。
- 拥塞控制:最大负载是TCP拥塞控制机制的核心。通过动态调整拥塞窗口,TCP可以在网络拥塞时减少数据发送量,避免网络拥塞进一步加剧。
- 性能优化:了解和调整最大负载可以帮助优化TCP连接的性能,特别是在高延迟或高带宽的网络环境中。
最小负载(Minimum Load)
最小负载通常指的是TCP连接中可以传输的最小数据量,这主要与TCP的慢启动和超时重传机制有关。TCP在建立连接时会从较小的拥塞窗口开始,逐渐增加直到达到网络的拥塞窗口上限。
意义:
- 启动速度:最小负载决定了TCP连接的启动速度。较小的最小负载可以加快TCP连接的启动,从而减少连接建立的时间。
- 超时重传:最小负载也与TCP的超时重传机制有关。在网络不稳定或丢包率较高的情况下,较小的最小负载可以减少重传的数据量,从而提高TCP的可靠性。
- 资源利用:了解和调整最小负载可以帮助更好地利用网络资源,特别是在低带宽或高延迟的网络环境中。
总结
TCP的最大负载和最小负载是TCP性能调优和网络设计中的重要参数。通过合理调整这些参数,可以提高TCP连接的性能、可靠性和网络资源的利用率。在实际应用中,需要根据具体的网络环境和应用需求来调整这些参数,以达到最佳的网络性能。
补充2:linux创建1个socket内存开销多少
文件系统inode + socket结构 是固定大小的,大约为1K。 理想情况下:每个socket占用的内存为2~3K。 以2K计算,内核空间实际可用内存约在800M,那么800M/2K = 400K. 粗略估计,Linux下,32位系统,能支持40万tcp并发连接差不多就到极限了。=
补充3:
*int confd = accept(curfd, (sockaddr )&client_addr, &client_len); 这个TCPSocket confd使用的端口号和curfd是一致的,那么客户端往这个ip+port上发送信息的时候怎么知道交给哪个描述符处理,说重点 举例说明
看这个例子你就理解啦
假设有一个服务器在 IP 地址 192.168.1.10
上监听端口 8080
,并且客户端的 IP 地址是 192.168.1.20
,客户端的端口号是 12345
。
- 客户端连接:
- 客户端使用其 IP 地址
192.168.1.20
和端口12345
连接到服务器的 IP 地址192.168.1.10
上的端口8080
。 - 服务器的监听套接字
curfd
接收到这个连接请求,并通过accept()
创建一个新的套接字描述符confd
。
- 客户端使用其 IP 地址
- 数据发送:
- 客户端向
192.168.1.10:8080
发送数据包,数据包中包含目标 IP 地址和端口号192.168.1.10:8080
。 - 操作系统内核会使用这些信息将数据包路由到
192.168.1.10
上的监听套接字curfd
。
- 客户端向
- 内核处理:
- 内核根据客户端的 IP 地址和端口号(
192.168.1.20:12345
)和服务器的 IP 地址和端口号(192.168.1.10:8080
)唯一标识连接。 - 内核知道这个连接对应于
confd
,因为这是由accept()
返回的专用描述符。
- 内核根据客户端的 IP 地址和端口号(
- 数据接收:
- 数据到达服务器后,内核将数据包传递到正确的套接字描述符
confd
,因为confd
对应于客户端与服务器之间的唯一连接。
- 数据到达服务器后,内核将数据包传递到正确的套接字描述符
补充4:都是TCPSocket?
int confd = accept(curfd, (sockaddr )&client_addr, &client_len);confd和curfd都是TCPSocket是吗?*
是的,confd
和 curfd
都是 TCP 套接字(TCPSocket
)。
补充5:sleep勿用
使用sleep()来假定其他线程的事件已经发生,是错误的。
因为线程卡死的时候,是可以拖过sleep()这个时间的。
举例说明
在多线程编程中,你可能会希望等待某个线程完成一项任务,然后再继续执行主线程或其他线程的操作。有人可能会错误地使用 sleep()
来假定此时其他线程已经完成了它的任务,例如:
1 | import threading |
在上面的例子中,主线程启动了 worker
线程,并用 time.sleep(3)
暂停了3秒,假设 worker
线程应该在这段时间内完成其任务。
问题:为什么这是错误的?
- 线程执行时间无法预测:
- 即使你设置了
worker
线程中的time.sleep(2)
来模拟任务执行时间,实际执行时间可能会因多种因素而延长,比如系统资源不足、线程调度延迟等。worker
线程可能需要超过 2 秒才能完成任务,因此主线程在 3 秒后假定worker
线程已完成的假设可能不成立。
- 即使你设置了
sleep()
不能确保线程完成任务:sleep(3)
只是让当前线程(主线程)暂停运行 3 秒,并不保证其他线程在这 3 秒内完成任何特定的任务。线程的调度由操作系统管理,worker
线程可能在主线程sleep()
的期间受到影响,甚至可能完全没有运行。
- 系统负载和线程卡死:
- 假设系统负载非常高,
worker
线程可能在 2 秒内只获得了极少的 CPU 时间,甚至可能还没开始执行。主线程的sleep()
时间结束后,worker
线程可能还没有完成任务。此时,主线程继续执行时,假设worker
线程已经完成任务是错误的。
- 假设系统负载非常高,
替代方案:使用线程同步机制
正确的方法是使用线程同步机制,如 join()
、事件(Event
)、信号量(Semaphore
)、锁(Lock
)等,来确保一个线程等待另一个线程完成任务。
以下是使用 join()
的例子,它可以确保主线程等待 worker
线程完成任务:
1 | import threading |
在这个例子中,thread.join()
会阻塞主线程的执行,直到 worker
线程完成任务。这样就不会依赖 sleep()
来猜测 worker
线程是否已经完成,这种方法更可靠。
总结
使用 sleep()
来假定其他线程的事件已经发生是错误的,因为:
- 线程执行顺序和时间不可预测:
sleep()
不能保证其他线程在某个时间内完成任务。 - 系统调度的不确定性:其他线程可能因为系统负载或者调度问题而延迟执行。
- 更好的选择是使用同步机制:如
join()
、事件、条件变量等,这些机制可以确保线程之间的正确协调和同步。
[对Linux下socket限制的理解 - YZG - 博客园](https://www.cnblogs.com/yizhinantian/archive/2011/04/03/2004340.html#:~:text=文件系统inode %2B socket结构 是固定大小的,大约为1K。,理想情况下:每个socket占用的内存为2~3K。 以2K计算,内核空间实际可用内存约在800M,那么800M%2F2K %3D 400K. 粗略估计,Linux下,32位系统,能支持40万tcp并发连接差不多就到极限了。)