一个跨 Windows 和 Linux 的 C++ Socket 网络编程学习仓库。项目从原始 TCP/UDP API 开始,逐步演示阻塞与非阻塞连接、UDP 广播、Linux epoll、多线程服务端、跨平台 xsocket 动态库、简化 HTTP 服务和 UDP Syslog 监听。
每个目录都是相对独立的实验。Windows 侧使用 Visual Studio 和 Winsock,Linux 侧使用 Makefile、POSIX Socket、pthread 和 epoll。
- TCP 服务端的
socket -> bind -> listen -> accept -> recv/send流程。 - TCP 客户端的
socket -> connect -> send/recv流程。 - UDP 为什么不需要建立连接,以及
sendto/recvfrom如何携带对端地址。 - 主机字节序与网络字节序为什么需要
htons/ntohs。 - 阻塞 Socket、非阻塞 Socket 和
select()超时连接。 - 一个连接一个线程的模型有什么优点和扩展限制。
- epoll 的注册、等待、水平触发和边缘触发。
- 如何把平台差异封装到
XTcp和XUdp。 - HTTP 请求行、响应头、静态文件发送的基本流程。
- Syslog UDP 监听和正则提取登录失败信息。
socket_study/
├── bin/ Windows 可执行文件和 xsocket.dll
├── doc/ 项目文档
├── lib/ Windows xsocket 导入库
├── src/
│ ├── testsocket/ 独立的 XTcp 与多线程 TCP 服务端
│ ├── tcpserver/ 依赖 xsocket 的 TCP 服务端
│ ├── tcpclient/ 依赖 xsocket 的 TCP 客户端
│ ├── udpserver/ UDP 服务端
│ ├── udpclient/ UDP 单播客户端
│ ├── budpclient/ UDP 广播客户端
│ ├── epoll/ Linux epoll 服务端
│ ├── xsocket/ XTcp、XUdp 跨平台动态库
│ ├── xhttp/ 简化 HTTP 文件服务
│ └── xsyslog/ Syslog UDP 监听
└── README.md
当前 Visual Studio 工程主要使用:
- C++20
- MSVC 平台工具集
v145 x64- Winsock 系统库
Ws2_32.lib xsocket.lib和运行时xsocket.dll
独立示例可以直接打开对应解决方案:
src/testsocket/testsocket.slnx
src/udpserver/udpserver.slnx
src/udpclient/udpclient.slnx
src/xsyslog/xsyslog.slnx
依赖 xsocket 的工程建议按以下顺序构建:
- 打开
src/xsocket/xsocket.slnx。 - 构建
Debug|x64或需要的配置。 - 确认
xsocket.lib位于lib/,xsocket.dll位于运行目录或bin/。 - 再构建
tcpserver、tcpclient、xhttp等上层工程。
其他解决方案:
src/tcpclient/tcpclient.slnx
src/epoll/epoll.sln
src/xhttp/tcpserver.sln
epoll 的核心接口只存在于 Linux。目录中的 Visual Studio 工程文件不代表 sys/epoll.h 能在 Windows 原生编译。
多个目录提供 makefile。常见依赖:
g++- pthread
- 标准 Linux Socket 头文件
- epoll
先构建共享库:
cd src/xsocket
make再构建依赖它的程序,例如:
cd ../tcpserver
make部分现有 Makefile 会把:
libxsocket.so 复制到 /usr/lib
可执行文件复制到 /usr/bin
这些步骤会修改系统目录,通常需要管理员权限。运行前先阅读对应 Makefile。如果只想在仓库内测试,可以移除安装用的 cp 命令,并临时指定:
export LD_LIBRARY_PATH=/path/to/socket_study/src/xsocket:$LD_LIBRARY_PATH| 示例 | 默认行为 | 关键接口 | 注意点 |
|---|---|---|---|
testsocket |
8080 端口多线程 TCP 服务端 | socket/bind/listen/accept |
每个客户端创建一个分离线程 |
tcpserver |
8080 端口回显式服务端 | XTcp::Bind/Accept/Recv/Send |
收到 quit 后回复并关闭当前连接 |
tcpclient |
连接参数 IP 的 8080 端口 | SetBlock/Connect/Send/Recv |
源码默认 IP 是 192.168.60.128 |
udpserver |
8080 接收一个 UDP 数据报并回复 nihao |
recvfrom/sendto |
UDP 不需要 listen() |
udpclient |
向 127.0.0.1:8080 发送 hello |
sendto/recvfrom |
先启动 UDP 服务端 |
budpclient |
向广播地址发送 hello |
SO_BROADCAST |
网络和防火墙必须允许广播 |
epoll |
epoll ET 模式 HTTP 单字符响应 | epoll_create/ctl/wait |
仅 Linux,非阻塞 Socket |
xsocket |
导出 XTcp、XUdp |
Winsock/POSIX 平台封装 | 其他工程依赖其库文件 |
xhttp |
8080 端口提供 www/ 文件 |
请求解析、文件响应、多线程 | 仅为教学实现,安全边界很弱 |
xsyslog |
UDP 514 端口接收 Syslog | XUdp、std::regex |
Linux 低端口可能需要额外权限 |
socket(AF_INET, SOCK_STREAM)
|
v
bind(本地地址和端口)
|
v
listen(连接队列长度)
|
v
accept() -> 新的客户端 Socket
|
v
recv()/send()
|
v
close()/closesocket()
监听 Socket 只负责接受连接。每次 accept() 返回的客户端 Socket 才负责与一个具体对端通信。
socket(AF_INET, SOCK_STREAM)
|
v
connect(服务端 IP 和端口)
|
v
send()/recv()
|
v
close()/closesocket()
TCP 是字节流协议。一次 send() 不保证对端用一次 recv() 收到完全相同的边界。代码必须根据协议长度、分隔符或固定结构处理粘包和半包。
服务端: socket -> bind -> recvfrom -> sendto
客户端: socket -> sendto -> recvfrom
UDP 保留数据报边界,但不保证到达、不保证顺序,也不会自动重传。单个数据报过大还可能发生 IP 分片。
IPv4 地址结构:
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");常见转换:
| 函数 | 含义 |
|---|---|
htons |
16 位主机字节序转网络字节序 |
ntohs |
16 位网络字节序转主机字节序 |
htonl |
32 位主机字节序转网络字节序 |
ntohl |
32 位网络字节序转主机字节序 |
inet_addr |
点分十进制 IPv4 字符串转二进制地址 |
inet_ntoa |
IPv4 地址转字符串,旧接口 |
inet_pton/inet_ntop |
更通用的现代地址转换接口 |
端口必须使用 htons(),否则在不同字节序机器上会得到错误端口值。
testsocket 内置一份简化 XTcp,用于先理解封装本身。主线程持续 Accept(),每个连接创建一个 TcpThread:
主线程 accept
|
v
new TcpThread
|
v
std::thread(&TcpThread::Main, object)
|
v
detach
客户端线程循环接收:
- 普通消息:回复
ok。 - 包含
quit:回复quit success!,退出循环。 recv()返回 0 或负值:认为连接关闭或出错。
线程函数最后执行 delete this。这要求对象只能由 new 创建,并且之后没有其他代码再访问它。这种自删除模式容易出错,通用代码更适合用 shared_ptr 或由连接管理器统一回收。
一个连接一个线程的模型便于学习,但大量连接会带来线程栈、上下文切换和调度成本。
当前客户端:
- 从命令行读取 IP;没有参数时使用
192.168.60.128。 - 创建 TCP Socket。
- 设置非阻塞。
- 调用带 1000ms 超时的
XTcp::Connect()。 - 发送字符串
client。 - 阻塞接收服务端回复。
运行示例:
# 终端 1
.\tcpserver.exe 8080
# 终端 2
.\tcpclient.exe 127.0.0.1XTcp::Connect() 在 connect() 未立即成功时使用 select() 等待可写。完整实现还应在 select() 返回后调用 getsockopt(SO_ERROR),确认连接真的成功;“可写”也可能表示连接失败已经完成。当前封装省略了这一步。
timeval.tv_usec 必须小于 1,000,000。当前代码直接使用 timeoutms * 1000,如果超时参数大于等于 1000ms,严格实现应拆分成秒和微秒。
先启动:
.\udpserver.exe 8080再运行:
.\udpclient.exe 8080客户端向 127.0.0.1:8080 发送 hello。服务端通过 recvfrom() 同时得到消息和客户端地址,然后回复 nihao。
udpserver 中调用了 listen(sock, 10),但 listen() 只对面向连接的流式 Socket 有意义,UDP 不需要这一步。
budpclient 先开启:
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, ...);然后把数据发送到 INADDR_BROADCAST。广播能否到达取决于网卡、子网、路由器、防火墙和系统策略。很多网络会限制全局广播,更常见的是使用具体子网广播地址。
epoll 示例使用:
epoll_create(...)
epoll_ctl(..., EPOLL_CTL_ADD, ...)
epoll_wait(...)监听 Socket 和客户端 Socket 都注册为:
EPOLLIN | EPOLLETEPOLLET 是边缘触发。事件只在状态发生变化时通知,因此配套 Socket 必须非阻塞,并在一次回调中持续 accept() 或 recv(),直到返回 EAGAIN/EWOULDBLOCK。否则缓冲区里剩余的数据可能不会再次产生通知。
当前示例对监听 Socket 使用循环 Accept(),对客户端采取“一次读取、一次回复、立即关闭”的简化 HTTP 处理:
HTTP/1.1 200 OK
Content-Length: 1
X
它不是通用 HTTP 服务端,只用于观察 epoll 注册、等待和事件分发。
| 方法 | 作用 |
|---|---|
CreateSocket() |
创建 IPv4 TCP Socket |
Bind(port) |
绑定本地端口并开始监听 |
Accept() |
接受连接并返回新的 XTcp |
Connect(ip, port, timeout) |
非阻塞发起连接并等待 |
SetBlock(bool) |
Windows 使用 ioctlsocket,Linux 使用 fcntl |
Recv(buf, size) |
接收字节 |
Send(buf, size) |
循环发送,尽量处理短写 |
Close() |
使用平台对应 API 关闭 Socket |
平台差异:
Windows Linux
WSAStartup 无需对应初始化
closesocket close
ioctlsocket fcntl
WSAGetLastError errno
Ws2_32.lib 系统 Socket 接口
当前 XTcp 没有自定义析构函数自动关闭句柄,也允许默认复制。复制只是复制同一个 Socket 数值,不会复制内核连接;多个对象关闭同一句柄可能产生问题。使用时需要明确谁拥有 Socket。
XUdp 封装创建、绑定、接收和关闭。第一次 Recv() 时动态创建地址结构,Close() 负责释放。当前析构函数为空,因此调用者仍应显式执行 Close()。
XHttpServer::Start():
- 创建并绑定 TCP 监听 Socket。
- 启动分离线程执行
XHttpServer::Main()。 - 主循环持续接受客户端。
- 每个连接创建
XHttpClient和独立线程。
XHttpClient::Main():
Recv()读取请求字节。XHttpResponse::SetRequest()解析请求第一行。- 只接受
GET。 /映射到/index.html。- 从
www目录打开文件。 - 生成响应头。
- 循环读取文件并发送。
运行时工作目录应包含:
www/
└── index.html
启动:
.\tcpserver.exe 8080浏览器或 curl 访问:
curl http://127.0.0.1:8080/请求 .php 时,代码会拼接 php-cgi 命令,通过 system() 生成临时 HTML 文件。这要求 php-cgi 可执行文件可被找到。
这一实现直接把 URL 路径和查询参数拼入文件路径及系统命令,没有可靠转义,存在目录穿越和命令注入风险。只能在隔离的本地测试目录中学习,不能暴露到不可信网络。
其他教学限制:
- 一次
Recv()不保证收到完整 HTTP 请求。 - 没有完整解析请求头、Content-Length、keep-alive 和分块传输。
- MIME 类型基本固定。
- 文件句柄生命周期和错误响应不完整。
Stop()只设置标记,但Accept()可能仍在阻塞。- 分离线程依赖服务端对象持续存活。
xsyslog 在 UDP 514 端口接收日志,并用正则匹配:
Failed password for 用户 from IP port 端口
匹配成功后打印警告。Linux 上绑定小于 1024 的端口通常需要 root 或 CAP_NET_BIND_SERVICE。学习时也可以把源码端口改成高端口,例如 5514。
测试数据可以由另一台机器或本机 UDP 工具发送。UDP Syslog 本身不保证消息到达,也没有内建认证。
常见原因:
- 端口已经被其他进程占用。
- 程序刚退出,旧连接仍影响绑定。
- 绑定低端口但权限不足。
- 地址或端口字节序错误。
- 防火墙或系统安全策略限制。
阻塞 Socket 会等待数据。检查服务端是否已经启动、IP/端口是否一致、服务端是否真的发送回复,以及协议是否在等待更多输入。
TCP 没有消息边界。应循环接收并按协议解析,不能认为一次 recv() 就是一条完整消息。
确保 DLL 位于:
- 可执行文件所在目录;
- 当前工作目录;
- 或系统
PATH中可搜索的位置。
构建成功只表示链接时找到了 .lib,运行时仍需要 .dll。
确认共享库路径并设置 LD_LIBRARY_PATH,或者使用正确的 rpath/系统安装路径。
检查服务端是否监听对应端口、网络是否允许广播、发送地址是否适合当前子网,以及 Windows 防火墙规则。
写入 sockaddr_in.sin_port 时使用 htons();从收到的地址读取时使用 ntohs()。
testsocketudpserverudpclientbudpclient
xsockettcpservertcpclient
epollxhttpxsyslog
- 给
tcpserver增加命令行 IP 和端口参数。 - 修改客户端,让它循环处理
send()和recv()的短读短写。 - 为 TCP 消息增加 4 字节长度头,解决消息边界问题。
- 为
XTcp禁止复制并实现移动构造和移动赋值。 - 为
XTcp增加 RAII 析构关闭,同时明确所有权转移。 - 在
Connect()的select()后检查SO_ERROR。 - 把 epoll 客户端读取改成循环读到
EAGAIN。 - 为 HTTP 服务增加 400、404、405 响应和安全路径规范化。
- 把
xsyslog端口改成 5514,并从另一个终端发送测试日志。
这些程序用于学习 Socket API 和网络执行流程,不是生产级网络库。真实服务还需要处理协议分帧、非阻塞错误码、连接超时、重连、半关闭、背压、并发上限、资源所有权、异常安全、TLS、身份认证、输入校验、目录穿越和命令注入防护。