从这一课起,我们进入第六阶段:网络编程。前面全是"单机"视角——进程、内存、磁盘,都是自己跟自己玩。但从现在起,视野要扩展到网络:一个程序,怎么跟另一台机器上的另一个程序对话?"连接"到底意味着什么?这一课,我们从最基础的 socket 讲起,亲手写一个能跑通的 TCP echo 服务器。

从"打电话"说起

你想跟远方的朋友说话,需要什么?一部电话机

你拿起电话、拨号、等对方接、然后开始说话——你根本不关心信号是怎么经过基站、光纤传过去的。你只做两件事:拨号 + 说话

socket(套接字)就是那部电话机:程序用它来"拨号 + 收发数据",至于数据在下面怎么走(IP、以太网、光纤),一概不用管。


22.1 网络分层与 socket 的位置

一句话理解:OSI/TCP-IP 分层里,socket 是应用层和传输层之间的"插座"——程序不用管下面 IP、以太网怎么传,只要"插进 socket 读写字节"即可。

网络通信被分成好几层(OSI 七层 / TCP-IP 四层),每一层只负责自己的事:

  • 最底下是物理层(网线、光纤)、链路层(以太网)、网络层(IP,负责"找到对方");
  • 中间是传输层(TCP/UDP,负责"可靠地传");
  • 最上面是应用层(你的程序)。

socket 就坐在应用层和传输层之间,像墙上的一个插座

你的程序只要"插进 socket,然后 read/write 字节"就行了。下面 IP 怎么寻址、以太网怎么成帧、数据包怎么在路由器间跳转——通通不用管

22.2 TCP vs UDP

传输层有两大"传法",先分清:

TCPUDP
连接面向连接(三次握手)无连接
可靠性可靠、有序、不丢不重不可靠、可能丢/乱
速度慢(要确认重传)
场景网页、文件、聊天视频、游戏、DNS
一句话理解:TCP 是"打电话"(要拨通、确认对方在听),UDP 是"发短信"(发出去就完事,不保证收到)。
  • TCP:先建立连接(三次握手),之后像一条"可靠管道",数据有序、不丢、不重。代价是要确认、要重传,慢一点。适合网页、文件传输、聊天——一个字节都不能错的场景;
  • UDP无连接,直接把数据包扔出去,不保证到达、不保证顺序。好处是。适合视频、游戏、DNS——丢了就丢了,快更重要的场景。
这一课我们聚焦 TCP,因为"连接"这个概念最值得深挖。

22.3 socket 是什么

一句话理解:socket = 一个五元组 (协议, 本地IP, 本地端口, 对端IP, 对端端口) 的句柄,程序通过对它 read/write 来收发数据。

一个 socket,本质是一个句柄(就是一个数字,像文件描述符那样),但它背后绑定着五个信息:

(协议, 本地IP, 本地端口, 对端IP, 对端端口)

这五个信息唯一确定了一条连接。程序拿到这个 socket,对它 read 就是"收数据",write 就是"发数据"——跟读写文件一模一样(还记得"一切皆文件"吗?socket 也是文件的一种)。

例子:服务器监听 0.0.0.0:8080,客户端连过去后,双方各拿到一个 socket,互发字节就像读写文件。"收发数据" = "读写 socket"

22.4 服务器端的固定套路

服务器端有个万年不变的固定流程,五步:

一句话理解:服务器走固定五步——socket(造插座)→ bind(占个端口)→ listen(开始接客)→ accept(接受连接)→ read/write(收发数据)。
  1. socket:造一个"插座";
  2. bind:把这个插座绑定到一个端口上(比如 8080),相当于"公布自己的门牌号";
  3. listen:开始监听,等待别人连过来;
  4. accept:接受一个进来的连接,返回一个新的 socket
  5. read / write:用这个新 socket 收发数据。
关键点:accept 返回的是一个新的 socket,专门服务这一个客户端;原来的监听 socket 继续监听。所以一个服务器能同时接多个客户端——来一个,分配一个新 socket 伺候着,老的继续等下一个。

22.5 三次握手在代码里的体现

"连接"是怎么建立的?这里有个经典概念——三次握手

一句话理解:客户端 connect 发起,内核完成 SYN→SYN-ACK→ACK 三次握手;服务器端 accept 拿到的是握手已完成的连接。所以"连接"不是 accept 那一瞬间才建立,而是内核已经握手好了等你去取。

三次握手(简化):

  1. 客户端发 SYN:"我想连你";
  2. 服务器回 SYN-ACK:"收到,可以连";
  3. 客户端再发 ACK:"好的,开始传数据"。

这三步完成后,连接就建立了。在代码里的对应:

  • 客户端调 connect,内核帮它完成这三次握手;
  • 服务器调 accept,拿到的是已经握手完成的连接。
所以"连接"不是在 accept 那一瞬间才建立的——内核早就握手好了,accept 只是"把已经建好的连接取出来"

可以用 ss -tan 看到两种状态:

  • LISTEN:服务器在监听,等着被连;
  • ESTAB:连接已建立。

动手实验:C echo 服务器 + Python 客户端

C 服务器

// lesson22_server.c —— 最简单的 TCP echo 服务器
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(void) {
    int srv = socket(AF_INET, SOCK_STREAM, 0);       // 1. 造插座
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);        // 监听所有网卡
    addr.sin_port = htons(8080);

    bind(srv, (struct sockaddr*)&addr, sizeof(addr)); // 2. 占端口
    listen(srv, 5);                                   // 3. 开始监听

    printf("服务器监听 8080,等待连接……\n");
    int cli = accept(srv, NULL, NULL);                // 4. 接受连接
    printf("有客户端连进来了!\n");

    char buf[256];
    while (1) {
        int n = read(cli, buf, sizeof(buf) - 1);      // 5. 收
        if (n <= 0) break;                             // 对方断开
        buf[n] = '\0';
        printf("收到:%s", buf);
        write(cli, buf, n);                            // 原样回(echo)
    }
    close(cli); close(srv);
    return 0;
}

Python 客户端

# lesson22_client.py —— Python 客户端(对照)
import socket

c = socket.socket()
c.connect(("127.0.0.1", 8080))   # 三次握手在这里完成
c.sendall(b"hello os\n")           # 发
print("收到回显:", c.recv(1024).decode(), end="")
c.close()

运行:

gcc lesson22_server.c -o server && ./server   # 终端 1
python3 lesson22_client.py                    # 终端 2

观察:服务器打印"收到:hello os",客户端收到同样的字——这就是 echo(回声)。你发出什么,对方原样回给你。

这就是网络编程的"hello world"。别小看它——TCP echo 是所有网络服务的最小骨架,理解它,后面 Nginx、Redis 的原理就都通了一半。

深入点:两个进阶话题

① bind 的"端口占用"

端口是稀缺资源。服务器重启时,常报一个经典错误:

Address already in use
本质:上一个连接还处于 TIME_WAIT 状态(主动关闭方要等一段时间确认对方收到最后的 ACK),端口还没释放。用 SO_REUSEADDR 选项可缓解——允许复用处于 TIME_WAIT 的端口。

② 字节序(htonl / htons)

代码里的 htons(8080) 是什么意思?

网络传输统一用大端(高字节在前),而你的主机可能是小端(低字节在前)。所以端口、IP 这些数字,发送前要用 htons(host to network short)/ htonl(long)转换,否则端口号会"反"——你以为在监听 8080,实际别人看到的是另一个数。

小结与思考题

这一课,我们迈出了网络编程第一步:

  • socket:应用层和传输层之间的"插座",五元组唯一标识一条连接,读写它就像读写文件;
  • TCP vs UDP:可靠但慢(打电话)vs 快但不可靠(发短信);
  • 服务器五步:socket → bind → listen → accept → read/write;
  • 三次握手:连接是内核先握手好的,accept 只是"取出来"。

留三个问题:

  1. TCP 和 UDP 的核心区别?
  2. 服务器端 socket/bind/listen/accept 各做什么?
  3. 为什么 accept 返回的是"新 socket",而不是复用监听 socket?
这一课的服务器有个明显毛病:一次只能服务一个客户端。第一个客户端连进来不说话,服务器就永远卡在 read,其他人都进不来。怎么破?——这正是下一课的主题:网络 I/O 模型。我们要让一个进程同时伺候成千上万个客户端,看看 select、epoll 这些"多路复用"武器是怎么做到的。

标签: 计算机基础, 操作系统, Linux

添加新评论