socket 编程基础
环境:Windows 11 x64,GCC 13.1.0(
D:/Qt/Tools/mingw1310_64/bin/gcc.exe,-O2 -lws2_32),探针是本节新写的net4/sk.c(六个模式:byte/gai/reuse/blk/pack/sel)与net4/rcb.c,全部流量打在 127.0.0.1;抓包用 Npcap(C:\Windows\System32\Npcap\wpcap.dll)。口径沿用前两节:回环 MSS=65495、本机 TIME_WAIT≈119–120 秒、TCP 动态端口 1024 起共 13977 个、每场实验都要核对”抓到的字节数 vs 应用侧字节数”,比例型结论只在两侧都完整时才下;耗时给 3 次重复的中位数(区间)。本机是 Winsock 2.2 + 中文版 Windows,错误码全是WSA*那一套(10035/10040/10048/10060/10061/10013/10022/10054),Linux 的E*只有编号含义相通、触发条件不相通,本节不预测 Linux 行为。
一、骨架:先记住”句柄不是 fd”,再记两件事
服务端与客户端的最小可跑形状(sk.c 里的实际写法,去掉了错误检查):
WSADATA w; WSAStartup(MAKEWORD(2,2),&w); /* 每个线程所在进程一次,必须先于任何 socket() */
SOCKET srv=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in m; memset(&m,0,sizeof m);
m.sin_family=AF_INET; m.sin_port=htons(port); m.sin_addr.s_addr=inet_addr("127.0.0.1");
bind(srv,(struct sockaddr*)&m,sizeof m); listen(srv,4);
SOCKET acc=accept(srv,0,0); /* 主动 accept,别用 WSAAccept 的完成例程 */
char rb[1024]; int r=recv(acc,rb,sizeof rb,0);
send(acc,"ok",2,0);
closesocket(acc); closesocket(srv); /* 不是 close()!CRT 的 close 会拿错句柄 */
WSACleanup();
WSAStartup 回来的结构体里有两处会让人误读:
WSAStartup(2.2) r=0 wVersion=0x0202 wHighVersion=0x0202 szSysStatus="Running" szDescription="WinSock 2.0" iMaxUdpDg=0 iMaxSockets=0
iMaxUdpDg=0、iMaxSockets=0 不是”不能发 UDP、不能开 socket”,它们是 Winsock 1.x 遗留字段,2.x 里规定就是 0,任何拿它们当上限的代码都是错的。真要 UDP 单包上限看第 6 节实测的 65507 字节。
SOCKET 在 x64 上是 64 位句柄,sel 模式把它的数值打了出来:
前 4 个 SOCKET 句柄的数值: 236 240 244 248
最后 1 个:444
它是内核句柄(4 递增只是巧合),不是小整数文件描述符,所以 select(maxfd+1,…) 这种写法在 Windows 上毫无意义(nfds 被忽略,实测传 0 完全合法,见第七节)。移植代码时把 #ifdef _WIN32 的分支留给这三件事:closesocket / WSAGetLastError / ioctlsocket(FIONBIO),别指望 close(fd) 和 errno。
二、绑定与端口复用:Windows 的 SO_REUSEADDR 不是 Linux 那个意思
sk.exe reuse 19890 的完整读数(A 先 bind(127.0.0.1:19890) 并 listen,全程不退出,其他 socket 都是后来者):
A bind(127.0.0.1:19890) r=0 err=0
A 自身再 bind 一次同端口 -> r=-1 err=10022 (WSAEINVAL)
B 再 bind 同一 127.0.0.1:19890 -> r=-1 err=10048 (WSAEADDRINUSE=10048)
B 设 SO_REUSEADDR 后 bind 完全相同的 127.0.0.1:19890 -> r=-1 err=10013 (WSAEACCES=10013)
C0 【对照】不设任何选项,bind(0.0.0.0:19890) 而 A 正 LISTEN 在 127.0.0.1:19890 -> r=0 err=0
C1 SO_REUSEADDR + bind(0.0.0.0:19890) -> r=0 err=0
D 干净端口 19897:setsockopt(SO_EXCLUSIVEADDRUSE) r=0 err=0,bind(0.0.0.0:19897) r=0 err=0,已 listen
F 独占者在场时,带 SO_REUSEADDR 的 F bind(0.0.0.0:19897) -> r=-1 err=10013
G 独占者在场时,不带选项的 G bind(0.0.0.0:19897) -> r=-1 err=10048
H 独占者已关闭:两个都带 SO_REUSEADDR 各自 bind(0.0.0.0:19897) -> r=0 / r=0(只有 H2 在 listen)
H 一条新连接进来:select ret=1 H2(在 listen)就绪=1 H3(只 bind 未 listen)就绪=0
E 客户端 socket bind 到 LISTEN 中的端口 -> r=-1 err=10048
F 让系统随机选端口 3 次: 7831 7832 7833
五条本机结论,其中前两条直接推翻”Windows 的 SO_REUSEADDR 就是允许抢地址”这句流传很广的话:
- 同一个 socket 重复 bind 永远是 10022(WSAEINVAL),和选项无关,这是”你想用 SO_REUSEADDR 改端口”的常见误用。
- 别人已经在该地址:端口上 LISTEN 时,你带 SO_REUSEADDR 去 bind 完全相同的地址得到的是 10013(WSAEACCES),不是成功。所以”服务重启 Address already in use → 加个 SO_REUSEADDR 就好”在 Windows 上并不成立;
TIME_WAIT场景要靠第 4 节那条正确路径(或换端口)。 - 换本地地址就不算冲突:C0 什么选项都不设,
bind(0.0.0.0:19890)在 A 已经 LISTEN 于127.0.0.1:19890时照样成功(r=0)。C1 加了 SO_REUSEADDR 结果一样 —— 也就是说这里 SO_REUSEADDR 根本没起作用,别把它当原因。这也是多网卡服务”通配 + 具体地址各绑一个”能共存的本机依据。 - 要真正的独占就得用
SO_EXCLUSIVEADDRUSE:独占者(D)在场时,带 SO_REUSEADDR 的后来者拿 10013、不带选项的拿 10048;而独占者一关闭,两个都带 SO_REUSEADDR 的 socket 就能同时 bind 在0.0.0.0:19897(H 行两个 r=0)。此时投递给谁看谁在 listen:只bind不listen的那个select就绪=0,永远收不到连接。 - 随机端口是从 1024 往上顺着给的:三次拿到 7831/7832/7833(连号),与第 4 节
netsh读回的”起始 1024 共 13977 个”一致。客户端侧想bind到已经被服务端占住的端口同样 10048(E 行)。
顺带把缓冲区口径钉死(rcb.exe,UDP 与 TCP 各扫一遍,bind 前后都读了):
UDP 请求 SO_RCVBUF=512/1024/2048/4096/8192/16384/32768/65536/131072/262144/4194304 -> 读回同名值(11 档全部原样)
TCP 请求 SO_RCVBUF=512 … 4194304 -> 读回同名值(同上)
不设置时:UDP 读回 65536,TCP 读回 65536;SO_SNDBUF 默认读回 65536
Windows 不把你要求的值翻倍也不减半(Linux 会 ×2,这是移植时的经典坑),并且 setsockopt(SO_RCVBUF) 之后 getsockopt 一定读回原值——第 6 节那条”改参数先读回来核对”在这里同样重要。
三、字节序:htonl 在 x86 上就是字节反转,但坑不在函数里
主机值 0x1234 的内存字节 : 34 12
htons(0x1234)=0x3412 : 12 34
主机值 0x0100007F 的内存 : 7f 00 00 01
htonl(0x0100007F)=0x7f000001 的内存 : 01 00 00 7f
inet_pton("127.0.0.1") r=1 字节=7f 00 00 01
inet_addr("127.0.0.1") =0x0100007f (和网络字节序一致,不能当主机序用)
误用:把主机序整数 0x7f000001 直接塞进 sin_addr.s_addr -> inet_ntoa 读出 1.0.0.127
ntohs(80)=端口字段应为 0x0050:实际 sin_port=0x5000
inet_pton("1.2.3.4") 0x04030201 -> ntohl = 1.2.3.4 若把它当主机序读则是 67305985
三件事按顺序记住:① 本机是小端,inet_pton("127.0.0.1") 写进内存的字节是 7f 00 00 01,与 inet_addr 的返回值 0x0100007f 是同一串字节、两种读法;把人类习惯的 0x7f000001 当成”网络序”填进 sin_addr.s_addr,inet_ntoa 就告诉你它连的是 1.0.0.127 —— 于是出现”代码看着没错、包发到别处去了”。② 端口字段同理:htons(80) 在内存里的 16 位值是 0x5000,所以直接 printf("%d", sin_port) 得到 20480 是正常的,读端口要 ntohs。③ inet_pton 的返回值是 1 表示成功、0 表示”不是合法地址”、-1 表示 AF 不支持,写成 if(inet_pton(...)) 恰好反了(成功才是 1,这个坑只有在拼错地址时才炸)。
还有一条不在输出里但必须提醒:inet_ntoa 返回静态缓冲区,同一条 printf 里调用它两次就会共用一块内存 —— conn.c 里就留了注释记录这个坑(把 inet_ntoa 和其它取 WSAGetLastError() 的表达式塞进同一个参数表,last error 会被同表内其它调用覆盖)。正确写法是紧跟调用把两个值分别取到局部变量再打印。新代码直接用 WSAAddressToString 或 inet_ntop(后者要传调用方缓冲区,见第四节)。
四、getaddrinfo:把”解析成功”和”能连上”分成两件事
sk.exe gai,七种输入,全部只走本机解析器(不主动打外网):
getaddrinfo(host=localhost svc=80 flags=0x0 fam=AF_INET) = 0 用时 21.97 ms
ai_family=2 socktype=1 proto=0 addrlen=16 地址=127.0.0.1:80 canon=- flags=0x0
同上但 ai_family=AF_UNSPEC -> r=0 链= v6 v4
getaddrinfo(host=localhost svc=(NULL) flags=0x1 AI_PASSIVE) = 0 用时 0.18 ms 地址=127.0.0.1
getaddrinfo(host=(NULL) svc=19777 flags=0x1 AI_PASSIVE) = 0 用时 0.00 ms 地址=0.0.0.0:19777
getaddrinfo(host=127.0.0.1 svc=www flags=0x0) = 0 用时 7.14 ms 地址=127.0.0.1:80 flags=0x4
getaddrinfo(host=no-such-host.invalid svc=80) = 0 用时 1.68 ms 地址=198.18.2.172:80
getaddrinfo(host=localhost svc=0) = 0 用时 0.34 ms
getaddrinfo(host=localhost svc=notanumber) = 10109 用时 1.61 ms -> 错误串:(空)
GetHostName=TERMINAL
四条实用结论:
-
服务端要用
AI_PASSIVE+host=NULL:返回的是0.0.0.0:19777,正是第二节讲的通配绑定;addrlen=16就是sizeof(struct sockaddr_in),可以直接喂给bind,不要自己填sin_len/sa_family。 -
AF_UNSPEC的链是v6 v4—— IPv6 在前。客户端骨架必须”按链顺序逐个试socket/connect,失败就下一个”,不能只取res->ai_next==NULL的那一个;上面svc=80那行第一次返回127.0.0.1:80(因为 hints 里钉死了AF_INET),去掉钉死后链里就有两个候选。 -
服务名会查 services 数据库:
svc="www"直接给出端口 80,并且返回的ai_flags=0x4(AI_NUMERICSERV);传"notanumber"得 10109,而gai_strerrorA(10109)在本机返回空串 —— 排错时必须把数字码一起打出来,否则日志里只有一段空白。 -
解析成功 ≠ 域名存在。
no-such-host.invalid这种按 RFC 6761 保证不可解析的名字,本机getaddrinfo返回 0 且给出198.18.2.172。原因在本机的接口与路由表里:netsh interface show interface:7 个接口,已连接的有 VMnet1 / VMnet8 / 以太网 / cherry route print:两条默认路由,网关分别是 198.18.0.2(接口 198.18.0.1)和 192.168.103.254(接口 192.168.103.24)198.18.0.0/16是那块代理网卡的 fake-IP 段(第 6 节广播实验里源地址也是198.18.0.1)。所以在有代理/劫持解析的机器上,“getaddrinfo 失败”不是唯一故障信号,要把连接结果(SO_ERROR)一起看;第一次解析 21.97 ms、后续 0.0–1.7 ms 也说明首次调用含加载解析链的固定开销,别拿它当 DNS 延迟。
五、阻塞与超时:三种”等”,各自的错误码不一样
sk.exe blk 19900(三次重复的时间写在一起):
SO_RCVTIMEO=700ms 的空连接 recv -> ret=-1 err=10060 实际等了 703.0ms (WSAETIMEDOUT=10060 WSAEWOULDBLOCK=10035)
关掉超时后读回 tv_sec=0
非阻塞 connect(127.0.0.1:19901 无人监听) ret=-1 err=10035 (WSAEWOULDBLOCK)
非阻塞 connect 后轮询 select:2016.8ms 后就绪,写集=0 异常集=1 SO_ERROR=10061 (WSAECONNREFUSED)
SO_SNDTIMEO=200ms 的阻塞 connect(死端口) ret=-1 err=10061 用时 2029.9ms
第 8 次 send(65536) ret=-1 err=10060 这次调用阻塞了 2007.0ms
对端 accept 但不 recv,SO_SNDTIMEO=2000ms:SO_SNDBUF 请求 131072 读回 131072;写出 458752 字节/7 次,累计阻塞 0.2ms err=10060
读超时:SO_RCVTIMEO 生效,返回 ret=-1 且 err=10060 WSAETIMEDOUT(不是 10035 WSAEWOULDBLOCK),三次实测 703.0 / 705.7 / 710.4 ms → 中位 705.7(区间 703.0–710.4),比设定的 700 ms 多 3–10 ms。注意 tv_sec=0 且 tv_usec=0 是”关超时”(读回也是 0),别把 0 当”立即返回”。
连接超时:两条同时成立且反直觉的实测 —— (a) SO_SNDTIMEO=200ms 管不住 connect():设了 200 ms,阻塞 connect 到死端口还是花了 2029.9 / 2022.1 ms(两次) 才返回 10061;(b) 非阻塞 connect 的失败在 Windows 上要从”异常集”读:select 的写集=0、异常集=1,然后 getsockopt(SO_ERROR)=10061。要可移植就同时把句柄放进 write 集和 except 集(Linux 上失败是通过 write 集 + SO_ERROR 报告的,两边正好互补)。
那个约 2 秒不是随便来的,抓包给出了形状(dump.exe loopback "tcp port 19961",5 秒窗口,18 条全抓到):
[ 0] TCP 8387->19961 .S.... seq=1155741428 win=65535 [MSS=65495] [wscale=8] [sackok]
[ 19] TCP 19961->8387 ..R.A. seq=0 ack=1155741429 win=0 ← RST 在 19µs 后就到了
[ 505201] TCP 8387->19961 .S.... seq=1155741428 ... chk=8c56 ← 同一个 seq/chk 的 SYN 重发
[ 1011595] TCP 8387->19961 .S.... ...
[ 1515203] TCP 8387->19961 .S.... ...
captured 18 packets ← 9 次 SYN + 9 次 RST
RST 19µs 就到了,但 connect 要 2.0 s 才把 10061 交给应用;这期间 SYN 按 505.2 / 506.4 / 503.6 ms(中位 505.2) 的节拍重发(与第 5 节 netsh 读回的”最小 RTO 300ms”同量级,但本机重传节拍是 ~505ms),序号与校验和完全相同(seq=1155741428 chk=8c56)—— 这就是”重传”的字节级形状。应用层拿不到 19µs 那个快速否定,只能等内核按自己的节拍放弃,所以”连接超时”必须由应用自己用非阻塞 + select 实现,别指望任何 SO_*TIMEO。
写阻塞:对端 accept 之后一个字节都不读,本机把数据全吞进缓冲区:
不设 SO_SNDTIMEO(blk2 模式,前 4 次 send 瞬间返回):
#1 累计 65536 字节 t=0.0ms
#2 累计 131072 字节 t=0.1ms
#3 累计 196608 字节 t=0.1ms
#4 累计 262144 字节 t=0.1ms
(第 5 次 send 之后再无任何输出)exit=124 ← 被 timeout 20/22 强杀,即阻塞 ≥ 20 秒未返回
设 SO_SNDTIMEO=2000ms(blk 模式,SO_SNDBUF 先设成 131072):
前 7 次全部返回,共 458752 字节;第 8 次阻塞 2007.0ms 后 ret=-1 err=10060
两组的差别只在”被吞掉的字节数”:裸 send 组吞了 262144 字节(SO_SNDBUF 是本机默认 65536)后卡死,先设 SO_SNDBUF=131072 的那组吞了 458752 字节才开始阻塞、第 8 次调用在 2007.0 ms 后返回 10060。这两个数是”本端发送缓冲 + 在途 + 对端接收缓冲”的合计,本机没有把它逐层拆开(未实测),所以不要按”SO_SNDBUF 多了 65536 所以多写 65536”去算。要点只有一句:阻塞 socket 上的 send 可以永远不返回,进程就那样挂着;SO_SNDTIMEO 是它唯一的解药(而它管不了 connect)。反压、超时、丢弃策略都得自己写(第 10 节)。
六、粘包与拆包:TCP 是字节流,UDP 是整包 —— 四组实测对照
sk.exe pack 19930,每个子测试都用一条新连接(第一版让四个子测试共用一条连接,上一段留在接收缓冲区里的字节被下一段当成帧头读走,recv 随后一直等不到那么多字节、程序直接卡死 —— 这个 bug 本身就是”没有消息边界”的代价):
A 三次 send(10)("ABCDEFGHIJ"/"KLMNOPQRST"/"UVWXYZ0123"),服务端一次 recv(1024):
#1 ret=30 [ABCDEFGHIJKLMNOPQRSTUVWXYZ0123] 等了 152.1ms
第 2 次 recv 前 select 说不可读 ret=0(30 字节已被第一条 recv 一次取光,这里绝不能裸 recv)
B 一次 send(60),接收端每次只 recv(4):
recv#1 ret=4 [abcd] recv#2 ret=4 [efgh] recv#3 ret=4 [ijkl] 共 15 次 recv 收到 60 字节
C 逐字节 send 20 次,接收端 recv(16):
ret=16 [ABCDEFGHIJKLMNOP] 还剩 4 字节在缓冲区里 [QRST]
D 长度前缀帧(4 字节头 + 7 字节正文),发送端每写 1 字节接收端就立刻试读:
已发第 1 字节:recv(还差 4) -> ret=1 累计帧头 1/4
已发第 2 字节:recv(还差 3) -> ret=1 累计帧头 2/4
已发第 3 字节:recv(还差 2) -> ret=1 累计帧头 3/4
已发第 4 字节:recv(还差 1) -> ret=1 累计帧头 4/4
帧头 4 字节解析出长度=7 正文收 7 字节 [MSG7777]
帧已消费干净:select 不可读 —— 这里绝不能裸调 MSG_PEEK,那会一直阻塞
UDP 对照:sendto(50) 两次,接收端先 recvfrom(8):
recvfrom(8) ret=-1 err=10040 (WSAEMSGSIZE=10040)
再 recvfrom(8) ret=-1 err=10040
select 看到 u2 可读=0 换 64 字节缓冲的机会没有了:两个数据报都已作废
同一场连 Npcap 一起抓(dump.exe loopback "tcp port 19945"),核对完再下结论:
captured 98 packets 载荷分布:63 段 payload=0、31 段 payload=1、3 段 payload=10、1 段 payload=60
抓到载荷字节合计 = 121 应用侧发出 = 30 + 60 + 20 + 11 = 121 ← 两侧完全相等,本场抓包完整
带 PSH 的段 35 条 = 3 + 1 + 20 + 11 ← 与四次子测试一一对上
于是”粘包”这个词被实测拆成两件事:
- A 场景里线上并没有粘:3 次
send(10)在抓包里就是 3 段 payload=10(Nagle 在回环上不合并,与第 5 节”8 次单字节写 = 8 段”同一个结果)。是接收端一条recv(1024)把 30 字节一次读走才”粘”的 —— 粘在读法上,不在网络上。 - B/C 场景里线上也没有拆:1 次
send(60)就是 1 段 payload=60,接收端recv(4)十五次才读完,是内核缓冲区按调用者要求的量交付。C 更直接:20 段 payload=1 上线,接收端一次recv(16)拿走 16 字节、剩 4 字节[QRST]留在缓冲区。 - 所以 TCP 只提供”有序字节流”,不保留消息边界:
recv返回 1、4、16、30 都合法。唯一可靠的办法是自己定帧(长度前缀 / 定长 / 分隔符),并且写累加循环:D 场景里recv(还差 4)真的只给 1 字节,四次才凑满一个 4 字节头 —— 任何”读一次头就够了”的代码在这台机器上都会解出错误的长度然后卡死。帧格式的设计口径见第 12 节。 - UDP 是反面教材的对照组:小缓冲
recvfrom(8)撞上一个 50 字节数据报,返回-1 / 10040 WSAEMSGSIZE,而且两个数据报一起作废(随后select说不可读)。TCP 会把你要求的那 8 字节给你、剩下的留着;UDP 直接把整包丢掉(MSG_PARTIAL/ 允许截断的行为本机未测)。这既是”UDP 不会粘包”的真相,也是”UDP 上随便给个小缓冲区更危险”的代价。
七、select 的容量与两个常数
sk.exe sel 50(默认编译)与 skbig.exe sel 200(gcc -DFD_SETSIZE=1024 重编,必须在包含 winsock2.h 之前定义,命令行 -D 正是最省事的办法):
编译期 FD_SETSIZE=64 sizeof(fd_set)=520 fd_count 类型大小=4 sizeof(fd_set.fd_array[0])=8
编译期 FD_SETSIZE=1024 sizeof(fd_set)=8200 …同上
FD_SET 50 个后 fd_count=50 FD_SET 200 个后 fd_count=200
select(0,...) 空集等待 ret=0 ← nfds 传 0 在 Windows 是合法的,它被完全忽略
往最后一个 socket 灌 1 个数据报后 select ret=1,FD_ISSET(最后一个)=1
WSAPoll(50 个,100ms) ret=1 ;WSAPoll(200 个,100ms) ret=1
sizeof(fd_set) = 4 + FD_SETSIZE×8(520 与 8200 精确吻合),因为 x64 上SOCKET是 8 字节。FD_SETSIZE 是编译期常量,默认 64;要同时管 200 个 socket 就必须重编,代价是每个fd_set8 KB、每次调用还要FD_ZERO+200 次FD_SET。- 超过
FD_SETSIZE再FD_SET是数组越界,属于未定义行为。本机探针刻意把FD_SET的数量夹在min(n, FD_SETSIZE)以内,所以没有测出”越界会怎样”(负面结果也照实说:不确定的事不在自己机器上试)。 select每次调用都要重新FD_ZERO+FD_SET全量集合(它会改写传入的集合),这是O(n)每轮的根源;WSAPoll用数组、WSAEventSelect/IOCP 用完成,才是第 10 节高并发的路子(本机只做了一次WSAPoll计数验证,性能未测)。
八、本节未在本机实测,原因写清楚
listen()第二参数(backlog)的真实效果、半连接队列溢出与 SYN 重传的关系:需要压满队列或制造 SYN 洪水,属于不允许的压测;本节所有listen(srv,4)只用来保证能 accept。第 4 节有状态机与TIME_WAIT的实测。- 跨机 / 跨网段的
bind、SO_REUSEADDR、连接超时:本机没有第二台机器;route print里那两条默认路由都指向本地虚拟网关。第五节那个 ~2.0 s 是死端口 RST 场景,不能当”SYN 被防火墙静默丢弃后的 connect 超时”用。 - IOCP、
WSAAsyncSelect、WSAIoctl的完成例程路径:本节只用了WSAIoctl(SIO_UDP_CONNRESET)(在第 6 节)与只读的SIO_TCP_INFO(在第 5 节),事件模型与完成模型一概未测,归第 10 节。 - IPv6 端到端:
gai模式证明AF_UNSPEC的链是v6 v4、localhost能给出 v6 候选,但本节所有连接都打在127.0.0.1上,v6 路径一次都没真正 connect 过,双栈回退逻辑属于”写了但没跑”。 - Linux 侧对应行为(
SO_RCVTIMEO返回EAGAIN还是ETIMEDOUT、SO_RCVBUF的 ×2、connect用 write 集报告失败):本机没有 Linux,本节只报 Windows 读数,不做跨平台推断。
九、小结
- 骨架三件事:
WSAStartup先行;WSADATA里iMaxUdpDg=0 iMaxSockets=0是遗留字段不是上限;closesocket/WSAGetLastError而非close/errno,SOCKET是 64 位内核句柄(实测值 236/240/244/248…)。 - 绑定:重复 bind 自己=10022;撞别人 LISTEN=10048;带
SO_REUSEADDR去抢同一地址=10013,不是成功;bind(0.0.0.0)与已存在的127.0.0.1可无选项共存(SO_REUSEADDR 在此没起作用);要独占用SO_EXCLUSIVEADDRUSE;SO_RCVBUF512…4194304 全部原样读回,默认 65536。 - 字节序:
inet_pton("127.0.0.1")的内存是7f 00 00 01,inet_addr返回0x0100007f;把0x7f000001当网络序填会得到 1.0.0.127;htons(80)的字段值是0x5000;inet_pton成功返回 1;inet_ntoa是静态缓冲区,一次printf里别用两次。 - getaddrinfo:服务端
AI_PASSIVE+NULL→0.0.0.0:19777;AF_UNSPEC链 v6 在前;svc="www"→80 且ai_flags=0x4;"notanumber"→10109 且gai_strerrorA是空串;不存在的域名被本机代理解析成198.18.2.172—— 解析成功不等于能连上。 - 超时:
SO_RCVTIMEO→ -1/10060(705.7ms 中位,设定 700ms);SO_SNDTIMEO对connect无效(2029.9ms vs 设定 200ms);非阻塞 connect 失败在异常集里、要getsockopt(SO_ERROR);RST 19µs 就回来而connect2.0s 才失败,期间 SYN 每 ~505ms 重发;裸send在对端不读时阻塞 ≥20 秒(前 4 次共 262144 字节瞬间返回)。 - 粘包:抓包 98 条、载荷 121 字节 = 应用侧 121 字节(本场完整);3 次
send(10)在线上仍是 3 段,一次recv(1024)拿回 30 —— 粘在读法;一次send(60)用 15 次recv(4)读完 —— 拆在读法;逐字节推帧头时recv真的只给 1 字节,累加循环必须;UDP 上小缓冲recvfrom(8)→ -1/10040 且整包作废。 - select:
FD_SETSIZE是编译期常量(64 →sizeof(fd_set)=520;-DFD_SETSIZE=1024→ 8200,元素 8 字节),nfds传 0 合法且被忽略,超界FD_SET是越界(本机未测后果);WSAPoll同场景可用。