目录
- 摘要
- 注释
- 3.1 回顾
- 3.2 新知识
- 3.3 注意
- 3.4 反思
- 正文
- 4.1 生命周期与返回值约定
- 4.2 服务端路径
- 4.3 收发与阻塞
- 4.4 开发中遇到的问题
- 总结
- 常见问题
- 标签
摘要
Socket.hpp 是 header-only 的单句柄 TCP 封装,唯一成员为int _sockfd,默认构造置 -1,析构close后复位。对外提供Create / Bind / Listen / Accept / Connect / Recv / Send / Close,另有SetNonBlock、SetNonBlockRecv / SetNonBlockSend与CreateServer / CreateClient两个组合入口。价值在于把系统调用返回值判定、sockaddr_in填充与字节序转换收在一处。注释段自述:本实现保证 HTTP 请求的服务端而非客户端。
代码仓库:https://gitee.com/btshadow/Ubuntu_learning/blob/master/October/10-17/muduo/src/server.hpp
注释
3.1 回顾
htons/ntohs:TCP/IP 规定用大端序,即网络字节序;本机算出要发到网络用htons/htonl,从网络收到转本机用ntohs/ntohl。Bind与Connect里的htons(port)属于服务端自身绑定、要让客户端看到该端口的那一侧。
accept 二、三参数:第二参数是输出参数,存放对端地址;第三参数是值-结果参数,传入时为 addr 缓冲区大小,返回时为实际写入长度(见 bug3)。
ET 模式:epoll 的边缘触发,只有状态变化(从无到有/从有到多)时通知一次,对比 LT。本文件未含 epoll,仅记录。
3.2 新知识
noexcept(true)等价于noexcept,是"不抛异常"的显式声明;内部真抛了异常则直接std::terminate()。承诺的是不抛 C++ 异常,不是调用一定成功。价值在于移动构造/移动赋值标 noexcept 后,std::vector等容器扩容走移动而非拷贝。只在确实不抛处用(移动构造、swap、析构、getter),可能分配内存或抛业务异常的不标。本文件中带 noexcept 的只有注释抄录的listen原型。
3.3 注意
sockaddr_in在 bind 与 connect 前有必要清空(Bind、Connect各有memset),accept 时没必要,因为 accept 内是覆盖式写入(Accept未 memset)。EAGAIN与EINTR作为读/写异常:EAGAIN 关联非阻塞(MSG_DONTWAIT),EINTR 关联信号,与非阻塞关系小(Recv、Send的 errno 判定)。
客户端无需 bind:客户端间各自独立且难以手动管理,内核已代做,故CreateClient只有 Create + Connect。
3.4 反思
测试时太大意了,主要是不想测试、想快点结束,结果代码本身没有 bug,却因测试方式以为有 bug。
正文
4.1 生命周期与返回值约定
Socket()置_sockfd = -1,Socket(int)接管外部 fd,Fd()取值;析构close后置 -1。Close()只close不复位_sockfd,故析构时会对同一 fd 再close一次。每个系统调用失败路径都走ERR_LOG,成功的组合入口用INF_LOG。
4.2 服务端路径
CreateServer(port, ip = "0.0.0.0")依次Create→Bind→Listen,成功后INF_LOG并调SetReuseAddrPort。Create用socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);Bind先memset再填sin_family、htons(port),地址由inet_pton写入sin_addr,旧的inet_addr写法留在注释中;Listen缺省MAX_LISTEN = 1024。SetReuseAddrPort对SO_REUSEADDR与SO_REUSEPORT各setsockopt一次,位置在 bind/listen 之后,失败只记录。
4.3 收发与阻塞
Recv/Send直接转发系统调用,n <= 0时若 errno 为EAGAIN或EINTR返回 0,否则ERR_LOG后返回 -1;Send另有n == 0的独立日志分支。SetNonBlockRecv/SetNonBlockSend以MSG_DONTWAIT调用。SetNonBlock用F_GETFL取标志,再F_SETFL | O_NONBLOCK。客户端方向CreateClient只有 Create 与 Connect,不含 Bind,Connect同样 memset、htons、inet_pton。
4.4 开发中遇到的问题
bug1:INADDR_ANY是 0 不是字符串,极易误会(注释记 [30min])。原写法const std::string& ip = INADDR_ANY已注释掉,现默认值为"0.0.0.0",仍交给inet_pton转换。
bug2:测试时注意,监听套接字不能用于通信。标注位于Send()内。
bug3:accept 的第三参数同时作为输入输出参数,调用前须给出缓冲区大小:
// Accept()// 原:socklen_t len;socklen_t len=sizeof(client_addr);总结
封装边界是套接字生命周期加系统调用的返回判定;三条注意分别落在Bind/Connect的 memset、Recv/Send的 errno 分支、CreateClient不 Bind,bug3 正是"值-结果参数"未传初值。bug1 属类型不匹配,bug2 属套接字角色混用。
常见问题
- 为何
Bind、Connect要 memset 而Accept不用?前两者是值传入,Accept内覆盖式写入,client_addr无需清空。 - 为何
INADDR_ANY不能当std::string默认参数?它是 0 而非字符串(bug1),现用"0.0.0.0"经inet_pton转换。 Recv/Send中EAGAIN与EINTR为何都返回 0?二者都不视为致命错误,交由上层下轮再试。- bug3 中 len 不初始化会怎样?
accept把它当输入缓冲区大小,未给值即未定义,故须socklen_t len = sizeof(client_addr)。
标签
C++、TCP Socket 封装、inet_pton、EAGAIN/EINTR、header-only
【统计:全文约 1180 字 / 原代码 256 行 / 比例约 4.6;注释字段小节 4 个 / 成文 bug 3 处】