第二章:网络基础与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算法后,延迟直接降了一半。
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,轮询太慢又会错过数据。
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%。
2.4 本章知识体系
下面这张图展示了本章的核心知识结构,我建议你对照着梳理自己的知识盲区:
这张图把本章的三个核心模块串起来了。TCP/IP协议栈是基础,Socket API是工具,IO模型是策略。三者缺一不可。
- TCP可靠但延迟高,UDP不可靠但延迟低——按场景选择
- Socket编程要注意字节序、Nagle算法等细节
- 交易网关必须用非阻塞IO + epoll,才能支撑高并发低延迟
- 边缘触发配合非阻塞IO,性能最优
好了,这一章的内容就到这里。网络基础是交易网关的"地基",地基不牢,上层建筑再漂亮也没用。下一章我们会深入Linux内核,看看如何进一步压榨网络性能。
公众号:蓝海资料掘金营,微信deep3321