第二章:网络基础与Socket编程

各位同学,今天我们来聊聊交易网关开发中最基础、也最关键的一环——网络通信。说白了,你的交易指令能不能在微秒级内送达交易所,全靠这一层。

2.1 TCP/IP协议栈回顾

很多人觉得TCP/IP是老生常谈,但我做量化这十年,发现真正吃透它的人不多。尤其是做高频交易,你连协议栈的脾气都摸不透,怎么跟别人抢单?

2.1.1 四层模型与数据封装

TCP/IP协议栈分为四层:应用层、传输层、网络层、网络接口层。每一层都会给数据加上自己的头部信息,这个过程叫封装。

我举个例子,你发一条"买入100手"的指令:

  • 应用层:FIX协议或自定义协议,包含订单内容
  • 传输层:加上TCP头部,包含源端口和目标端口
  • 网络层:加上IP头部,包含源IP和目标IP
  • 网络接口层:加上以太网帧头,包含MAC地址

嗯,这里要注意,每一层头部都会增加延迟。我在优化交易网关时,曾经把应用层协议从文本改成二进制,直接省了20微秒——这在抢单时就是天壤之别。

2.1.2 TCP的可靠性与代价

TCP为什么可靠?因为它有三次握手、确认重传、滑动窗口这些机制。但代价是什么?延迟。

我记得有一次排查交易延迟问题,发现TCP的Nagle算法把多个小包合并了,导致订单迟迟没发出去。关闭Nagle算法后,延迟直接降了一半。

避坑指南:我曾经在实盘环境中因为没关闭Nagle算法,导致订单延迟从50微秒飙升到200微秒。后来我养成了习惯,所有交易Socket创建后第一件事就是设置TCP_NODELAY。

2.1.3 UDP在交易中的特殊地位

说到UDP,很多人觉得它不可靠,不适合交易。其实不然。在行情数据推送场景下,UDP反而是主流。

为什么?因为行情数据是"状态快照",丢了上一帧,下一帧马上覆盖。你想想看,如果每个行情包都要确认重传,那延迟得多高?

我个人习惯:订单走TCP,行情走UDP。这样既保证了订单的可靠性,又保证了行情的低延迟。

2.2 Socket API详解

Socket是应用层与传输层之间的接口。说白了,它就是操作系统给我们的一把"钥匙",让我们能操作网络通信。

2.2.1 核心API函数

我们来看看最常用的几个函数:

函数 作用 注意事项
socket() 创建套接字 指定协议族、类型、协议
bind() 绑定地址和端口 端口冲突时会返回错误
listen() 监听连接 设置backlog队列长度
accept() 接受连接 返回新的Socket描述符
connect() 发起连接 阻塞直到连接建立
send()/recv() 发送/接收数据 注意缓冲区大小
close() 关闭连接 优雅关闭用shutdown()

我建议你把这张表打印出来贴在工位上。当年我刚开始写交易网关时,就因为没搞清楚accept()返回的是新Socket,导致所有数据都发到了监听Socket上——嗯,那是一个难忘的加班夜。

2.2.2 地址结构与字节序

网络字节序是大端模式,而x86机器是小端模式。这个转换如果不做,你发出去的端口号可能就是错的。

// 创建Socket的典型代码
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);  // 主机字节序转网络字节序
addr.sin_addr.s_addr = inet_addr("192.168.1.1");
bind(sock, (struct sockaddr*)&addr, sizeof(addr));

你看,htons()这个函数就是做字节序转换的。我见过有人忘了这一步,结果端口号对不上,排查了半天。

2.3 阻塞与非阻塞IO模型

这是交易网关性能的分水岭。选对了,延迟可控;选错了,系统崩溃。

2.3.1 阻塞IO模型

阻塞IO,就是调用recv()时,如果没有数据,线程就挂在那等。简单吧?但问题来了:

  • 一个线程只能处理一个连接
  • 如果某个连接一直没数据,线程就白白浪费了
  • 连接数一多,线程数爆炸

我早期做的一个小项目,只对接一个交易所,用阻塞IO完全够用。但后来要同时对接三个交易所,线程数直接飙到30个,系统负载高得吓人。

2.3.2 非阻塞IO模型

非阻塞IO,就是调用recv()时,不管有没有数据,立即返回。没数据就返回-1,设置errno为EAGAIN。

这样做的好处是:一个线程可以轮询多个连接。但坏处是:轮询太频繁会浪费CPU,轮询太慢又会错过数据。

核心要点:非阻塞IO必须配合IO多路复用(select/poll/epoll)使用,否则就是"忙等",CPU利用率会爆表。

2.3.3 IO多路复用:epoll的王者地位

在Linux下,epoll是性能最好的IO多路复用机制。它解决了select和poll的两个痛点:

  • select有1024个文件描述符的限制
  • poll每次都要全量遍历,效率低

epoll采用事件驱动机制,只返回有事件发生的连接。复杂度从O(n)降到了O(1)。

// epoll使用示例
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 边缘触发
ev.data.fd = sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);

struct epoll_event events[1024];
while (1) {
    int n = epoll_wait(epfd, events, 1024, -1);
    for (int i = 0; i < n; i++) {
        // 处理有事件发生的连接
        handle_event(events[i].data.fd);
    }
}

这里我特别强调一下边缘触发(ET)和水平触发(LT)的区别。边缘触发只在状态变化时通知一次,你必须把数据一次性读完。水平触发只要还有数据就会一直通知。

我个人习惯用边缘触发,配合非阻塞IO,性能最好。但要注意,必须循环读取直到返回EAGAIN,否则会漏数据。

2.3.4 交易网关的IO模型选择

在交易网关中,我推荐使用"主从Reactor"模型:

  • 一个主线程负责accept新连接
  • 多个工作线程负责处理已建立的连接
  • 每个工作线程有自己的epoll实例

这样做的好处是:连接数可以扩展到上万,而且每个连接的延迟都很稳定。我曾经用这个模型支撑过同时对接8个交易所、5000个连接的生产环境,CPU占用率不到30%。

实战技巧:在交易网关中,建议把订单连接和行情连接分开到不同的线程池。订单连接需要低延迟,可以分配更多CPU资源;行情连接数据量大,可以分配更大的缓冲区。

2.4 本章知识体系

下面这张图展示了本章的核心知识结构,我建议你对照着梳理自己的知识盲区:

交易网关网络基础 TCP/IP协议栈 四层模型 TCP可靠性 UDP行情应用 Socket API详解 核心函数 地址与字节序 IO模型 阻塞IO 非阻塞IO epoll多路复用 核心目标:微秒级低延迟通信

这张图把本章的三个核心模块串起来了。TCP/IP协议栈是基础,Socket API是工具,IO模型是策略。三者缺一不可。

本章核心要点
  • TCP可靠但延迟高,UDP不可靠但延迟低——按场景选择
  • Socket编程要注意字节序、Nagle算法等细节
  • 交易网关必须用非阻塞IO + epoll,才能支撑高并发低延迟
  • 边缘触发配合非阻塞IO,性能最优

好了,这一章的内容就到这里。网络基础是交易网关的"地基",地基不牢,上层建筑再漂亮也没用。下一章我们会深入Linux内核,看看如何进一步压榨网络性能。


公众号:蓝海资料掘金营,微信deep3321