一、核心概念:IP、端口、网络字节序

1. IP 地址(复习+编程视角)

  • 本质in_addr 结构体,一个 32 位无符号整数。
  • 常用表示:点分十进制字符串 "192.168.1.10"
  • 编程转换函数
#include <arpa/inet.h>

// 点分十进制字符串 → 网络字节序的32位整数
int inet_pton(int af, const char *src, void *dst);

// 网络字节序32位整数 → 点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

过时的 inet_addr()inet_ntoa() 已不推荐(不支持 IPv6,线程不安全)。

2. 端口号(Port)

  • 本质:16 位无符号整数,范围 0~65535
  • 0~1023:知名端口(HTTP=80,HTTPS=443,SSH=22),需要 root 权限绑定。
  • 1024~49151:注册端口。
  • 49152~65535:动态/私有端口(客户端临时使用)。

作用:让传输层知道把数据交给哪个应用进程。

3. 网络字节序(大端 vs 小端)

  • 问题:不同 CPU 存储多字节数据的方式不同。

    • 大端(Big Endian):高位字节存低地址(网络传输用这个)。
    • 小端(Little Endian):低位字节存低地址(x86、ARM 默认)。
  • 转换函数<arpa/inet.h>):

uint16_t htons(uint16_t hostshort);   // 主机序 → 网络序(16位,用于端口)
uint32_t htonl(uint32_t hostlong);    // 主机序 → 网络序(32位,用于IPv4)
uint16_t ntohs(uint16_t netshort);    // 网络序 → 主机序
uint32_t ntohl(uint32_t netlong);

口诀:host to network short / long。


二、Socket API 核心函数速览

函数 作用 关键参数
socket() 创建套接字 协议族(AF_INET)、类型(SOCK_STREAM/SOCK_DGRAM)
bind() 绑定本地地址和端口 struct sockaddr_in
listen() 将套接字设为被动监听(仅 TCP) 最大等待连接数
accept() 从已完成连接队列取一个连接(仅 TCP) 返回新的 client_fd
connect() 主动发起连接(客户端) 服务器地址
send() / recv() TCP 收发数据 已连接套接字
sendto() / recvfrom() UDP 收发数据 需指定对方地址
close() 关闭套接字 文件描述符

通用地址结构

struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET
    in_port_t      sin_port;     // 端口(网络字节序)
    struct in_addr sin_addr;     // IP地址(网络字节序)
};

三、UDP 客户端/服务器实现

UDP 是无连接、不可靠、面向数据报的。代码最简单。

服务器端核心步骤

// 1. 创建 socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

// 2. 绑定地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;   // 监听所有网卡
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

// 3. 循环接收
char buf[1024];
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
while (1) {
    ssize_t n = recvfrom(sockfd, buf, sizeof(buf), 0,
                         (struct sockaddr*)&client_addr, &len);
    // 处理数据,可通过 sendto 回复
    sendto(sockfd, buf, n, 0, (struct sockaddr*)&client_addr, len);
}

客户端核心步骤

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

sendto(sockfd, "hello", 5, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));

注意:UDP 客户端可以不显式 bind,系统会自动分配临时端口。


四、TCP 单连接版本(迭代服务器)

TCP 是面向连接、可靠、字节流协议。

服务器(一次只能处理一个客户端)

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind 同上 ...
listen(listenfd, 5);   // 最大等待队列长度

while (1) {
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &len);

    // 处理这个客户端,直到它断开
    char buf[1024];
    while (1) {
        ssize_t n = recv(connfd, buf, sizeof(buf), 0);
        if (n <= 0) break;   // 对方关闭或出错
        send(connfd, buf, n, 0);   // 回声
    }
    close(connfd);
}

客户端

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// ... connect 到服务器 ...
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 发送请求,接收响应
send(sockfd, "hello", 5, 0);
char buf[1024];
recv(sockfd, buf, sizeof(buf), 0);
close(sockfd);

缺点:一个客户端占用期间,其他客户端只能排队等 accept


五、TCP 多进程版本(经典并发模型)

核心思路:accept 返回后,fork() 一个子进程专门处理这个客户端,父进程继续 accept

int listenfd = socket(...);
bind(...);
listen(...);

while (1) {
    int connfd = accept(listenfd, ...);
    
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        close(listenfd);        // 子进程不需要监听套接字
        // 处理客户端...
        close(connfd);
        exit(0);
    } else {
        // 父进程
        close(connfd);          // 父进程不需要已连接套接字
        // 可选:回收子进程避免僵尸进程(信号处理)
    }
}

必须注意的两点

  1. 僵尸进程:父进程需调用 waitpid 或忽略 SIGCHLD 信号(signal(SIGCHLD, SIG_IGN))。
  2. 文件描述符泄漏:子进程复制了父进程的 listenfdconnfd,需要按需 close

六、TCP 多线程版本(轻量级并发)

核心思路:每来一个连接,创建一个线程去处理。

void *client_handler(void *arg) {
    int connfd = *(int*)arg;
    free(arg);   // 如果动态分配了内存
    // 处理客户端...
    close(connfd);
    return NULL;
}

int main() {
    int listenfd = socket(...);
    // ... bind, listen ...

    while (1) {
        int *connfd_ptr = malloc(sizeof(int));
        *connfd_ptr = accept(listenfd, ...);

        pthread_t tid;
        pthread_create(&tid, NULL, client_handler, connfd_ptr);
        pthread_detach(tid);   // 分离线程,自动回收资源
    }
}

对比多进程

  • 线程:共享地址空间,创建销毁开销小,但需注意线程安全。
  • 进程:隔离性好,一个子进程崩溃不影响主进程,但开销较大。

七、TCP 连接建立、数据传输、断开全流程

用代码视角重温三次握手和四次挥手。

1. 建立连接(三次握手)

步骤 客户端调用 服务器调用 TCP 状态变化
connect() 阻塞 listen 后等待) 客户端发 SYN → SYN_SENT
仍阻塞 accept() 阻塞 服务器收到 SYN → SYN_RCVD;回复 SYN+ACK
connect() 返回 accept() 返回新 fd 客户端回复 ACK → ESTABLISHED

listen已完成连接队列满了,新的 SYN 会被忽略或回复 RST。

2. 数据传输

  • send():将数据拷贝到内核发送缓冲区,默认阻塞直到有空闲空间。
  • recv():从内核接收缓冲区取数据,默认阻塞直到有数据。

TCP 是字节流,没有消息边界,需应用层自己处理粘包/拆包(如长度前缀、分隔符)。

3. 断开连接(四次挥手)

步骤 主动关闭方(如客户端) 被动关闭方(服务器) TCP 状态
close() → 发 FIN recv() 返回 0 FIN_WAIT_1
收到 FIN,回复 ACK CLOSE_WAIT(客户端进 FIN_WAIT_2)
处理完剩余数据,close() 发 FIN LAST_ACK
收到 FIN,回复 ACK 收到 ACK TIME_WAIT(客户端) / CLOSED(服务器)

关键细节

  • recv() 返回 0 表示对方已关闭连接(正常断开)。
  • 主动关闭方会进入 TIME_WAIT 状态(约 2MSL,60 秒),确保最后的 ACK 能到达对方。

八、补充:常见陷阱与调试技巧

  1. bind 失败:Address already in use
    • 原因:上次程序退出后,端口仍在 TIME_WAIT。
    • 解决:设置 SO_REUSEADDR 选项。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  1. send 返回值小于预期

    • TCP 可能只发送了部分数据,需循环发送剩余部分。
  2. recv 返回值 0

    • 不是错误,是对端正常关闭连接。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐