问题背景
最近在学习Linux网络编程,跟着视频教程实现一个简单的TCP服务器。代码写完后运行,发现日志中显示的socket文件描述符总是0,而不是预期的4:
bash
get ad new link, socket address: [:13976] sockfd: 0
而视频教程中显示的是sockfd: 4。这个差异引起了我的注意,开始了一段有趣的debug之旅。
初始代码
我的TcpSocket类实现如下(简化版):
cpp
class TcpSocket : public Socket {
public:
TcpSocket() : _sockfd(0) {} // 默认构造函数
TcpSocket(int sockfd) : _sockfd(sockfd) {} // 带参构造函数
std::shared_ptr<Socket> Accept(InetAddr& clientaddr) override {
InetAddr addr;
socklen_t len = sizeof(addr);
int sockfd = accept(_sockfd, CONV(&addr), &len);
if(sockfd < 0) {
LOG(LogLevel::WARNING) << "accept error ";
return nullptr;
}
clientaddr = addr;
return std::make_shared<TcpSocket>(); // 问题在这!
}
int Sockfd() { return _sockfd; }
private:
int _sockfd;
};
问题定位
1. 文件描述符的基础知识
在Unix/Linux系统中,文件描述符的分配规律是:
-
0:标准输入(stdin)
-
1:标准输出(stdout)
-
2:标准错误(stderr)
-
3:第一个打开的socket/文件
-
4:第二个打开的socket/文件
-
以此类推...
2. 代码执行流程
cpp
// 服务器启动
TcpServer server(8080); // 创建监听socket,获得fd=3
// 客户端连接
while(true) {
auto conn = listensocket->Accept(clientaddr); // accept返回新fd=4
LOG << "sockfd: " << conn->Sockfd(); // 应该显示4,但显示0
}
3. 问题根源
仔细检查Accept方法:
cpp
std::shared_ptr<Socket> Accept(InetAddr& clientaddr) {
int new_sockfd = accept(_sockfd, ...); // new_sockfd = 4
// 错误:使用默认构造函数,_sockfd被初始化为0
return std::make_shared<TcpSocket>(); // ❌ 没有传入new_sockfd
}
accept系统调用确实返回了正确的文件描述符(4),但是在创建TcpSocket对象时,我使用了默认构造函数,导致新对象的_sockfd被初始化为0。
解决方案
修复很简单:将accept返回的文件描述符传给TcpSocket的构造函数。
cpp
std::shared_ptr<Socket> Accept(InetAddr& clientaddr) override {
InetAddr addr;
socklen_t len = sizeof(addr);
int new_sockfd = accept(_sockfd, CONV(&addr), &len); // 获得新fd=4
if(new_sockfd < 0) {
LOG(LogLevel::WARNING) << "accept error ";
return nullptr;
}
clientaddr = addr;
// 正确:传入new_sockfd
return std::make_shared<TcpSocket>(new_sockfd); // ✅ 现在Sockfd()返回4
}
验证结果
修改后运行,终于得到了正确的结果:
cpp
监听socket fd: 3
get ad new link, socket address: [:13976] sockfd: 4 ✅
深入理解:监听socket vs 连接socket
这个bug也让我更深入理解了两种socket的区别:
| 特性 | 监听socket | 连接socket |
|---|---|---|
| 文件描述符 | 固定(如fd=3) | 每次accept新分配 |
| 主要功能 | accept新连接 | recv/send数据 |
| 数量 | 通常1个 | 多个(每个客户端一个) |
| 生命周期 | 整个程序运行期 | 单次连接期间 |
经验教训
-
不要被表面现象迷惑 :日志显示0并不意味着
accept失败,可能是对象构造的问题。 -
构造函数的选择至关重要:在多态场景下,确保使用正确的构造函数传递关键数据。
-
添加调试信息:在关键位置打印中间值:
cppint new_sockfd = accept(...); std::cout << "debug: accept returned " << new_sockfd << std::endl;
最后修改的代码:
cpp
return std::make_shared<TcpSocket>(new_sockfd); // 传入accept返回的fd
思考题
如果多个客户端同时连接,文件描述符会如何分配?