Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9.5 网络编程基础

网络编程是现代程序员必备的技能,几乎所有的应用都需要网络通信功能。理解网络编程的原理,能够帮助我们写出高性能、高可靠的网络应用。

Socket编程原理

Socket(套接字)是网络编程的基础抽象,是应用层和传输层之间的接口,我们通过Socket来发送和接收网络数据。

什么是Socket

Socket是操作系统提供的网络编程接口,封装了底层的网络协议细节,让应用程序不需要关心TCP/IP的实现细节,只需要调用Socket接口就可以实现网络通信。

  • Socket可以看成是两个网络通信端点的抽象,每个Socket对应一个IP:端口
  • 常见的Socket类型:
    1. 流套接字(SOCK_STREAM):对应TCP协议,提供可靠的、面向连接的字节流服务
    2. 数据报套接字(SOCK_DGRAM):对应UDP协议,提供不可靠的、无连接的数据报服务
    3. 原始套接字(SOCK_RAW):可以直接访问IP层,用来构造自定义的IP数据包,用于ping、traceroute等工具

TCP Socket编程流程

服务器端流程

  1. 创建Socket:调用socket()函数,创建一个TCP套接字
  2. 绑定地址:调用bind()函数,把Socket绑定到指定的IP和端口
  3. 监听连接:调用listen()函数,让套接字进入监听状态,等待客户端连接
  4. 接受连接:调用accept()函数,阻塞等待客户端连接,有客户端连接时返回一个新的Socket,专门用来和这个客户端通信
  5. 收发数据:通过read()/recv()write()/send()函数和客户端通信
  6. 关闭连接:通信完成后,调用close()关闭Socket
服务器:
socket() → bind() → listen() → accept() → recv()/send() → close()

客户端流程

  1. 创建Socket:调用socket()函数创建TCP套接字
  2. 连接服务器:调用connect()函数,连接到服务器的IP和端口
  3. 收发数据:连接成功后,通过read()/recv()write()/send()和服务器通信
  4. 关闭连接:通信完成后调用close()关闭连接
客户端:
socket() → connect() → send()/recv() → close()

UDP Socket编程流程

UDP是无连接的,不需要建立连接:

服务器端

  1. 创建UDP Socket:socket(AF_INET, SOCK_DGRAM, 0)
  2. 绑定地址:bind()
  3. 接收数据:recvfrom(),可以得到发送方的地址
  4. 发送数据:sendto(),指定接收方地址

客户端

  1. 创建UDP Socket
  2. 发送数据:sendto()指定服务器地址
  3. 接收数据:recvfrom()

UDP不需要建立连接,每次发送数据都需要指定目标地址,适合短消息、实时性要求高的场景。

常见网络IO模型

网络IO模型决定了程序处理多个连接的能力,是高性能网络编程的核心。

1. 阻塞IO(BIO)

  • 原理:所有IO操作都是阻塞的,一个线程只能处理一个连接
  • 工作流程:accept()阻塞等待连接,连接建立后,recv()阻塞等待数据,处理完一个连接才能处理下一个
  • 优点:编程简单,容易实现
  • 缺点:并发能力低,每个连接需要一个线程,线程资源有限,而且线程切换开销大,高并发场景下性能很差
  • 适用场景:连接数不多的简单场景

2. 非阻塞IO(NIO)

  • 原理:把Socket设置为非阻塞模式,IO调用会立即返回,不会阻塞线程
  • 工作流程:线程轮询所有Socket,看哪个Socket有数据可读可写,有事件了就处理
  • 优点:一个线程可以处理多个连接,不需要为每个连接开线程
  • 缺点:轮询会消耗大量CPU,大部分轮询都是空转,效率低
  • 适用场景:很少单独使用,通常和IO多路复用结合

3. IO多路复用(IO Multiplexing)

  • 原理:用一个线程监听多个Socket的状态,当某个Socket有事件时,才通知应用程序处理
  • 核心是多路复用器:select/poll/epoll(Linux)、kqueue(BSD/macOS)、IOCP(Windows)
  • 工作流程
    1. 把要监听的Socket注册到多路复用器上
    2. 调用多路复用器的等待函数,阻塞等待事件发生
    3. 有事件发生时,返回所有就绪的Socket列表
    4. 遍历就绪的Socket列表,逐个处理
  • 优点:一个线程可以处理成千上万的连接,线程开销小,并发能力强,是现在高性能网络服务器的主流模型
  • 缺点:编程复杂度比BIO高,需要处理事件驱动的逻辑
  • **常见实现:
    • select:跨平台,最多支持1024个连接,性能随连接数增加而下降
    • poll:和select类似,但没有最大连接数限制,性能还是随连接数增加而下降
    • epoll:Linux下的高性能多路复用器,事件通知机制,性能不会随连接数增加而下降,是现在Linux服务器的首选
  • 适用场景:高并发网络服务,比如Web服务器、网关、即时通信服务等,Nginx、Redis、Node.js等都用的是IO多路复用模型

4. 异步IO(AIO)

  • 原理:用户发起IO请求后立即返回,不需要等待,内核完成IO操作后通知用户程序
  • 和IO多路复用的区别:IO多路复用是通知你Socket就绪了,需要你自己去读写数据;异步IO是内核已经帮你把数据读写完成了,直接通知你结果
  • 优点:性能最高,完全不阻塞线程,可以充分利用CPU
  • 缺点:编程复杂,实现难度高,跨平台支持不好,Linux下的原生AIO实现不够完善,现在实际使用还不多
  • 适用场景:对性能要求极高的场景,现在高并发场景下更多还是用IO多路复用 + 多线程的模式

5. 信号驱动IO

  • 原理:Socket注册信号处理函数,IO就绪时内核发送SIGIO信号通知程序处理,很少使用。

高并发模型对比

模型线程数并发能力编程复杂度性能适用场景
BIO连接数低(几百)连接少的简单场景
NIO1中等(几千)很少单独使用
IO多路复用1+高(几万到几十万)中高高并发场景,主流方案
AIO1+极高很高高性能场景,还未普及

RPC远程过程调用

RPC(Remote Procedure Call)远程过程调用,让调用远程服务就像调用本地函数一样简单,屏蔽了网络通信的细节,是现在分布式系统的核心技术。

RPC的原理

RPC封装了网络通信的细节,让用户不需要关心网络传输、序列化、错误处理等细节,就像调用本地函数一样调用远程服务:

客户端调用本地Stub函数 → Stub将参数序列化 → 发送网络请求到服务器
                                                        ↓
服务器接收请求 → 反序列化参数 → 调用实际服务函数 → 结果序列化返回给客户端
                                                        ↓
客户端Stub收到结果 → 反序列化返回给调用者

RPC的核心组件

  1. 客户端Stub:客户端的代理,封装远程调用的细节,把函数调用转换为网络请求
  2. 服务端Stub:服务端的代理,接收网络请求,反序列化参数,调用对应的服务函数
  3. 序列化/反序列化:把内存中的对象转换为字节流在网络上传输,常见的序列化协议:JSON、Protocol Buffer、Thrift、Hessian等
  4. 网络传输层:负责数据的传输,通常使用TCP协议,也有用HTTP、UDP的
  5. 服务注册与发现:分布式场景下,客户端需要知道服务端的地址,通过服务注册中心发现服务地址

常见的RPC框架

  • gRPC:Google开源的RPC框架,基于HTTP/2,使用Protocol Buffer序列化,性能高,跨语言
  • Thrift:Apache开源的跨语言RPC框架,性能优异
  • Dubbo:阿里巴巴开源的Java RPC框架,国内使用广泛
  • Spring Cloud:基于HTTP的微服务框架,使用RESTful API

网络编程常见坑点和最佳实践

常见坑点

  1. TCP粘包/拆包问题:TCP是面向字节流的,没有报文边界,发送端发送的多个数据包可能会被粘在一起,或者一个包被拆成多个,接收方需要自己处理报文边界

    • 解决方法:
      1. 固定长度报文,不够的补位
      2. 特殊分隔符分隔,比如换行符分隔
      3. 报文头加长度字段,先读取长度,再读取对应长度的内容
  2. 忽略错误处理:网络是不可靠的,所有的网络调用都可能失败,必须处理各种错误情况:连接中断、超时、重置等,不要假设网络调用一定会成功

  3. 阻塞模式下的超时问题:阻塞IO一定要设置超时时间,否则连接断了或者网络有问题时,线程会一直阻塞,无法释放资源

  4. 大端小端字节序问题:网络字节序是大端序,不同主机的字节序可能不同,多字节整数传输时一定要转成网络字节序,接收方再转为主机字节序

  5. 连接泄漏:网络连接使用完一定要关闭,特别是异常路径下也要关闭,否则会导致连接泄漏,文件描述符耗尽

  6. 流量控制和拥塞控制:不要发送太快,要考虑接收方的处理能力和网络拥塞情况,避免被系统丢包

最佳实践

  1. 优先使用成熟的网络库:不要自己从零实现Socket服务,优先使用成熟的高性能网络库:

    • C++:libevent、libuv、Boost.Asio
    • Java:Netty
    • Go:标准库net包
    • Python:asyncio、Twisted 这些库已经处理了所有底层的复杂问题,性能和稳定性都经过了验证。
  2. 合理设置超时时间:所有的网络操作都要设置超时时间,包括连接超时、读取超时、写入超时,避免无限等待

  3. 心跳机制:长连接场景下要加心跳机制,定期检测连接是否存活,及时清理死连接

  4. 限流和熔断:高并发场景下要加限流机制,避免请求量过大打垮服务;下游服务故障时要熔断,避免雪崩效应

  5. 二进制协议优先:性能要求高的场景优先使用二进制序列化协议(Protobuf、Thrift),比JSON性能高很多,体积小

  6. IO密集型服务用IO多路复用模型:高并发网络服务优先使用IO多路复用 + 线程池的模型,充分利用CPU资源

  7. 使用长连接减少握手开销:频繁通信的场景尽量使用长连接,避免TCP三次握手和四次挥手的开销,比如连接池、HTTP keep-alive

  8. 合理设置缓冲区大小:Socket的发送和接收缓冲区大小要根据业务场景合理设置,太小会导致频繁IO,太大会浪费内存

思考问题

  1. 简述TCP服务端Socket编程的主要流程,每个系统调用的作用是什么?
  2. IO多路复用模型相比多线程阻塞模型有什么优势?为什么高并发场景下都用IO多路复用?
  3. 什么是TCP粘包问题?为什么会出现?怎么解决?
  4. RPC相比直接使用HTTP通信有什么优势?适合什么场景?