Skip to content

第一百零六章:望海

问道期

涉及内核源码:

林小源站在一片无垠的海岸边。

风从远处吹来,带着咸湿的气息。眼前是一片浩瀚的海——网络之海。海面上波光粼粼,无数船影穿梭往来,有的扬帆疾行,有的缓缓靠岸。岸边是一排长长的木制码头,每个泊位前都立着一块编号牌。

一个身穿粗布短褂的老者坐在码头边的木桩上,手里搓着一根麻绳,眯眼望着海面。

"前辈,这些船是……"

老者头也不回:"船就是船。每一条船,都是一个 socket。进程要往海里送东西、从海里捞东西,都得先在我这儿领一条船。"

他拍了拍身旁的木桩,上面刻着 的字样。

"我是码头管事的,socket.c 就是我守的地盘。你要出海,得走这几个步骤——" 他伸出粗糙的手指,一根一根掰着,"先造船,;再给船挂上牌子,bind();然后竖起耳朵等客人来,listen();客人到了,把船交出去,accept();你要是去找别人,就 connect()。之后装货卸货,send()recv()。用完了,close(),船回坞。"

林小源望着码头上来来往往的船只,有的船身巨大、吃水很深,有的轻巧如叶。"前辈,那些船……都不一样?"

"当然不一样。" 老者终于转过头来,露出一张被海风刻满皱纹的脸。"那边那些大肚子的,,走的是 TCP 航线,一趟一趟确认,丢不了货。那些轻飘飘的,,走 UDP 航线,撒出去就不管了,快是快,信鸽嘛,飞丢了也没处找。还有那边几条透明的——,那是直接钻到水底去的潜艇,没点修为你别碰。"

他顿了顿,指了指远处另一片平静的内湖:"那边是 ,不走海,走湖,本地方圆内的事。再远还有 的航道,那是 IPv6 的新海域,比这边宽广得多。"

林小源深吸一口气。海风灌进肺里,他第一次感到"网络"这两个字有了重量——它不是一行代码,而是一片真实的、活生生的海。

老者从木桩旁拿起一只贝壳,递给林小源。

"你听。"

林小源把贝壳贴到耳边。里面不是海浪的声音,而是无数细微的声响交织在一起——有数据流的嗡鸣,有协议头碰撞的叮当,有队列排队时的低语。

"这就是 socket 的声音。" 老者说,"你看到的是船,看不到的是船底下连着的那一整套机关。每条船都有自己的发送队列、接收队列,有自己的状态——……你以为 就是造了一条船?不,它造的是一个完整的港口工位。"

林小源把贝壳翻过来,看到壳内壁刻着密密麻麻的符号:struct sockstruct sk_buff_head

"这些……"

"每条船的龙骨。" 老者说,"你以后会一条一条看清楚的。现在你只需要记住一件事——socket 不是船,socket 是码头上的一个泊位。船可以换,泊位才是恒定的。进程拿着的文件描述符,指向的就是这个泊位。"

他琢磨了一下,觉得这个比喻挺到位——socket 不是船,是泊位。船可以换,泊位不动。

黄昏时分,海面上亮起了渔火。

林小源坐在码头边,看着远处的一对船只缓缓靠近。一条船挂着"服务器"的旗号,早早地泊在了 bind 过的泊位上,竖着 listen 的桅灯,安静地等待。另一条船挂着"客户端"的旗号,从远处驶来,径直 connect 过去。

两条船靠在一起,货物开始装卸。

老者不知何时又坐到了他身旁。"看到了?这就是客户端-服务器模型。一个等,一个找。服务器得先占好泊位、挂好牌子、竖起灯,然后耐心等着。客户端呢,知道目的地的地址,直接开过去。"

"如果服务器没准备好呢?"

"那客户端就吃闭门羹。connect() 返回 -1。" 老者吐了口烟,"所以顺序很重要。先有等的人,才有来的人。反过来不行。"

林小源望着那两条船之间来回穿梭的小舢板——那是数据在 send()recv() 之间流转。每一舢板都装得满满当当,到了对岸就被卸下来,空船返回继续装。

"前辈,如果两边同时发呢?"

"TCP 是全双工的,两条航道互不干扰。你发你的,我发我的,各走各的队列。" 老者敲了敲烟斗,"别把 socket 想得太简单,也别想得太复杂。它就是一个端点——通信的端点。仅此而已。"

c
/*
 * 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");

道藏笔记

内核启示

socket 就是网络通信的端点。进程想收发数据,得先弄一个 socket 出来。

生命周期就那几步:socket() 建出来,bind() 绑地址,listen() 等人来,accept() 接上头,之后 send/recv 收发数据,close() 收工。客户端那边简单点,connect() 直接连过去就行。

类型方面,SOCK_STREAM 是 TCP,靠谱但慢;SOCK_DGRAM 是 UDP,快但不保证送达;SOCK_RAW 是直接操底层,一般用不着。


破关试炼

socket 之试

望海时,客户端要主动向远端 socket 发起连接,本章对应的系统调用是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道