第一百零六章:望海
问道期涉及内核源码:
一
林小源站在一片无垠的海岸边。
风从远处吹来,带着咸湿的气息。眼前是一片浩瀚的海——网络之海。海面上波光粼粼,无数船影穿梭往来,有的扬帆疾行,有的缓缓靠岸。岸边是一排长长的木制码头,每个泊位前都立着一块编号牌。
一个身穿粗布短褂的老者坐在码头边的木桩上,手里搓着一根麻绳,眯眼望着海面。
"前辈,这些船是……"
老者头也不回:"船就是船。每一条船,都是一个 socket。进程要往海里送东西、从海里捞东西,都得先在我这儿领一条船。"
他拍了拍身旁的木桩,上面刻着 的字样。
"我是码头管事的,socket.c 就是我守的地盘。你要出海,得走这几个步骤——" 他伸出粗糙的手指,一根一根掰着,"先造船,;再给船挂上牌子,bind();然后竖起耳朵等客人来,listen();客人到了,把船交出去,accept();你要是去找别人,就 connect()。之后装货卸货,send()、recv()。用完了,close(),船回坞。"
林小源望着码头上来来往往的船只,有的船身巨大、吃水很深,有的轻巧如叶。"前辈,那些船……都不一样?"
"当然不一样。" 老者终于转过头来,露出一张被海风刻满皱纹的脸。"那边那些大肚子的,,走的是 TCP 航线,一趟一趟确认,丢不了货。那些轻飘飘的,,走 UDP 航线,撒出去就不管了,快是快,信鸽嘛,飞丢了也没处找。还有那边几条透明的——,那是直接钻到水底去的潜艇,没点修为你别碰。"
他顿了顿,指了指远处另一片平静的内湖:"那边是 ,不走海,走湖,本地方圆内的事。再远还有 的航道,那是 IPv6 的新海域,比这边宽广得多。"
林小源深吸一口气。海风灌进肺里,他第一次感到"网络"这两个字有了重量——它不是一行代码,而是一片真实的、活生生的海。
二
老者从木桩旁拿起一只贝壳,递给林小源。
"你听。"
林小源把贝壳贴到耳边。里面不是海浪的声音,而是无数细微的声响交织在一起——有数据流的嗡鸣,有协议头碰撞的叮当,有队列排队时的低语。
"这就是 socket 的声音。" 老者说,"你看到的是船,看不到的是船底下连着的那一整套机关。每条船都有自己的发送队列、接收队列,有自己的状态——、、……你以为 就是造了一条船?不,它造的是一个完整的港口工位。"
林小源把贝壳翻过来,看到壳内壁刻着密密麻麻的符号:struct sock、struct sk_buff_head、。
"这些……"
"每条船的龙骨。" 老者说,"你以后会一条一条看清楚的。现在你只需要记住一件事——socket 不是船,socket 是码头上的一个泊位。船可以换,泊位才是恒定的。进程拿着的文件描述符,指向的就是这个泊位。"
他琢磨了一下,觉得这个比喻挺到位——socket 不是船,是泊位。船可以换,泊位不动。
三
黄昏时分,海面上亮起了渔火。
林小源坐在码头边,看着远处的一对船只缓缓靠近。一条船挂着"服务器"的旗号,早早地泊在了 bind 过的泊位上,竖着 listen 的桅灯,安静地等待。另一条船挂着"客户端"的旗号,从远处驶来,径直 connect 过去。
两条船靠在一起,货物开始装卸。
老者不知何时又坐到了他身旁。"看到了?这就是客户端-服务器模型。一个等,一个找。服务器得先占好泊位、挂好牌子、竖起灯,然后耐心等着。客户端呢,知道目的地的地址,直接开过去。"
"如果服务器没准备好呢?"
"那客户端就吃闭门羹。connect() 返回 -1,。" 老者吐了口烟,"所以顺序很重要。先有等的人,才有来的人。反过来不行。"
林小源望着那两条船之间来回穿梭的小舢板——那是数据在 send() 和 recv() 之间流转。每一舢板都装得满满当当,到了对岸就被卸下来,空船返回继续装。
"前辈,如果两边同时发呢?"
"TCP 是全双工的,两条航道互不干扰。你发你的,我发我的,各走各的队列。" 老者敲了敲烟斗,"别把 socket 想得太简单,也别想得太复杂。它就是一个端点——通信的端点。仅此而已。"
/*
* socket 是网络通信的抽象:
*
* 进程 A 进程 B
* ↓ ↓
* socket() socket()
* ↓ ↓
* bind() bind()
* ↓ ↓
* listen() ←→ connect()
* ↓ ↓
* accept() send()
* ↓ ↓
* recv() close()
*
* socket 的系统调用:
* socket() — 创建 socket
* bind() — 绑定地址
* listen() — 监听连接
* accept() — 接受连接
* connect() — 发起连接
* send()/recv() — 发送/接收数据
* close() — 关闭 socket
*/
printf("=== socket — 网络通信的端点 ===\n\n");
printf("socket 的生命周期:\n\n");
printf("1. 创建:\n");
printf(" int fd = socket(AF_INET, SOCK_STREAM, 0);\n\n");
printf("2. 绑定地址:\n");
printf(" struct sockaddr_in addr = {\n");
printf(" .sin_family = AF_INET,\n");
printf(" .sin_port = htons(8080),\n");
printf(" .sin_addr.s_addr = INADDR_ANY,\n");
printf(" };\n");
printf(" bind(fd, &addr, sizeof(addr));\n\n");
printf("3. 监听 (服务器):\n");
printf(" listen(fd, 128);\n\n");
printf("4. 接受连接 (服务器):\n");
printf(" int client = accept(fd, &client_addr, &len);\n\n");
printf("5. 连接 (客户端):\n");
printf(" connect(fd, &server_addr, sizeof(server_addr));\n\n");
printf("6. 通信:\n");
printf(" send(fd, buf, len, 0);\n");
printf(" recv(fd, buf, len, 0);\n\n");
printf("7. 关闭:\n");
printf(" close(fd);\n\n");
printf("--- socket 的类型 ---\n");
printf("SOCK_STREAM: 可靠、有序、双向(TCP)\n");
printf("SOCK_DGRAM: 无连接、不可靠(UDP)\n");
printf("SOCK_RAW: 原始 socket\n\n");
printf("--- 地址族 ---\n");
printf("AF_INET: IPv4\n");
printf("AF_INET6: IPv6\n");
printf("AF_UNIX: Unix domain socket\n");#include <stdio.h>
/*
* socket 是网络通信的抽象:
*
* 进程 A 进程 B
* ↓ ↓
* socket() socket()
* ↓ ↓
* bind() bind()
* ↓ ↓
* listen() ←→ connect()
* ↓ ↓
* accept() send()
* ↓ ↓
* recv() close()
*
* socket 的系统调用:
* socket() — 创建 socket
* bind() — 绑定地址
* listen() — 监听连接
* accept() — 接受连接
* connect() — 发起连接
* send()/recv() — 发送/接收数据
* close() — 关闭 socket
*/
int main() {
printf("=== socket — 网络通信的端点 ===\n\n");
printf("socket 的生命周期:\n\n");
printf("1. 创建:\n");
printf(" int fd = socket(AF_INET, SOCK_STREAM, 0);\n\n");
printf("2. 绑定地址:\n");
printf(" struct sockaddr_in addr = {\n");
printf(" .sin_family = AF_INET,\n");
printf(" .sin_port = htons(8080),\n");
printf(" .sin_addr.s_addr = INADDR_ANY,\n");
printf(" };\n");
printf(" bind(fd, &addr, sizeof(addr));\n\n");
printf("3. 监听 (服务器):\n");
printf(" listen(fd, 128);\n\n");
printf("4. 接受连接 (服务器):\n");
printf(" int client = accept(fd, &client_addr, &len);\n\n");
printf("5. 连接 (客户端):\n");
printf(" connect(fd, &server_addr, sizeof(server_addr));\n\n");
printf("6. 通信:\n");
printf(" send(fd, buf, len, 0);\n");
printf(" recv(fd, buf, len, 0);\n\n");
printf("7. 关闭:\n");
printf(" close(fd);\n\n");
printf("--- socket 的类型 ---\n");
printf("SOCK_STREAM: 可靠、有序、双向(TCP)\n");
printf("SOCK_DGRAM: 无连接、不可靠(UDP)\n");
printf("SOCK_RAW: 原始 socket\n\n");
printf("--- 地址族 ---\n");
printf("AF_INET: IPv4\n");
printf("AF_INET6: IPv6\n");
printf("AF_UNIX: Unix domain socket\n");
return 0;
}道藏笔记
内核启示
socket 就是网络通信的端点。进程想收发数据,得先弄一个 socket 出来。
生命周期就那几步:socket() 建出来,bind() 绑地址,listen() 等人来,accept() 接上头,之后 send/recv 收发数据,close() 收工。客户端那边简单点,connect() 直接连过去就行。
类型方面,SOCK_STREAM 是 TCP,靠谱但慢;SOCK_DGRAM 是 UDP,快但不保证送达;SOCK_RAW 是直接操底层,一般用不着。
socket 之试
望海时,客户端要主动向远端 socket 发起连接,本章对应的系统调用是什么?