Linux TCP 网络编程完全指南:从三次握手到高并发服务器

引言

网络编程是Linux系统编程的重要组成部分。在前面的课程中,我们学习了多线程、多进程和进程间通信。今天,我们将进入网络编程的世界,学习如何使用TCP协议实现客户端和服务器端的通信。

网络编程的核心是套接字,它提供了一种跨主机通信的机制。与之前学习的管道、共享内存等IPC机制不同,网络编程允许不同计算机上的进程进行通信。

本文将涵盖:

  • TCP协议的核心概念(三次握手、四次挥手)

  • TCP服务器和客户端的完整实现

  • 高并发服务器的实现(多进程和多线程版本)

  • 僵尸进程的处理


第一部分:TCP协议基础

一、TCP协议的特点

特性 说明
面向连接 通信前必须建立连接(三次握手)
可靠传输 确认重传机制,保证数据不丢失
面向字节流 数据没有边界,像流水一样传输
全双工通信 双方可同时发送和接收数据

二、TCP头部结构

TCP头部共20字节固定部分,加上最多40字节的选项字段。

关键标志位:

标志位 含义
SYN 同步序列号,用于建立连接
ACK 确认号有效
FIN 发送方已无数据,请求关闭连接
RST 重置连接
PSH 立即推送数据
URG 紧急指针有效

三、TCP三次握手

三次握手是TCP建立连接的过程,目的是让双方确认彼此的发送和接收能力正常。

复制代码
客户端                                          服务器
   │                                              │
   │─────────── SYN=1, seq=I ───────────────────→ │ 第一次握手
   │                                              │
   │←──── SYN=1, ACK=1, seq=J, ack=I+1 ────────── │ 第二次握手
   │                                              │
   │─────────── ACK=1, seq=I+1, ack=J+1 ────────→ │ 第三次握手
   │                                              │

三次握手详解:

步骤 方向 报文内容 说明
1 客户端 → 服务器 SYN=1, seq=I 客户端请求建立连接
2 服务器 → 客户端 SYN=1, ACK=1, seq=J, ack=I+1 服务器确认并回应
3 客户端 → 服务器 ACK=1, seq=I+1, ack=J+1 客户端确认,连接建立

四、TCP四次挥手

四次挥手是TCP关闭连接的过程。

复制代码
客户端                                          服务器
   │                                              │
   │─────────── FIN=1, seq=N ──────────────────→  │ 第一次挥手
   │                                              │
   │←─────────── ACK=1, ack=N+1 ────────────────  │ 第二次挥手
   │                                              │
   │←─────────── FIN=1, seq=M ──────────────────  │ 第三次挥手
   │                                              │
   │─────────── ACK=1, ack=M+1 ────────────────→  │ 第四次挥手
   │                                              │

四次挥手详解:

步骤 方向 报文内容 说明
1 主动方 → 被动方 FIN=1, seq=N 主动方请求关闭连接
2 被动方 → 主动方 ACK=1, ack=N+1 被动方确认收到
3 被动方 → 主动方 FIN=1, seq=M 被动方也请求关闭
4 主动方 → 被动方 ACK=1, ack=M+1 主动方确认,连接关闭

为什么是四次而不是三次?

因为TCP是全双工的,双方都需要独立关闭自己的发送通道。被动方收到FIN后,可能还有数据要发送,所以先回复ACK,等数据发送完后再发送FIN。


第二部分:TCP编程核心接口

一、套接字创建------socket()

cpp 复制代码
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
参数 常用值 说明
domain AF_INET IPv4地址族
AF_INET6 IPv6地址族
type SOCK_STREAM TCP流式服务
SOCK_DGRAM UDP数据报服务
protocol 0 默认协议

返回值: 成功返回套接字描述符(类似文件描述符),失败返回-1。

二、绑定地址------bind()

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

用于将套接字与指定的IP地址和端口号绑定。

cpp 复制代码
struct sockaddr_in {
    sa_family_t    sin_family; // 地址族,AF_INET
    in_port_t      sin_port;   // 端口号(网络字节序)
    struct in_addr sin_addr;   // IP地址
};

struct in_addr {
    uint32_t       s_addr;     // IP地址(网络字节序)
};

三、监听连接------listen()

cpp 复制代码
int listen(int sockfd, int backlog);

backlog参数指定已完成握手队列的最大长度。

注意:

  • Linux系统中,backlog表示已完成握手队列的长度

  • 某些Unix系统中,backlog表示未完成+已完成握手队列的总长度

  • 现代内核中队列长度已大幅增加

四、接受连接------accept()

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

从已完成握手队列中取出一个连接,返回一个新的套接字描述符,用于与客户端通信。

阻塞特性: 如果队列为空,accept()会阻塞等待。

五、连接服务器------connect()

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端使用此函数向服务器发起连接。

六、收发数据------recv()/send()

cpp 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数 方向 说明
recv() 接收数据 从套接字读取数据
send() 发送数据 向套接字写入数据

flags参数通常设为0,表示无特殊选项。

七、字节序转换函数

网络中统一使用大端字节序(网络字节序),需要进行转换:

cpp 复制代码
#include <arpa/inet.h>

// 主机字节序 → 网络字节序
uint16_t htons(uint16_t hostshort);   // 短整型(端口)
uint32_t htonl(uint32_t hostlong);    // 长整型(IP地址)

// 网络字节序 → 主机字节序
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

// IP地址转换
in_addr_t inet_addr(const char *cp);              // 字符串 → 整数
char *inet_ntoa(struct in_addr in);                // 整数 → 字符串

第三部分:TCP服务器实现

一、基础版本(单线程)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error");
        exit(1);
    }
    
    // 3. 监听
    if (listen(listen_fd, 5) == -1) {
        perror("listen error");
        exit(1);
    }
    
    printf("服务器启动成功,端口:%d\n", PORT);
    
    while (1) {
        // 4. 接受连接
        client_len = sizeof(client_addr);
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept error");
            continue;
        }
        
        printf("客户端连接:%s:%d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        
        // 5. 接收数据
        memset(buffer, 0, BUFFER_SIZE);
        int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
        if (n > 0) {
            printf("收到数据:%s\n", buffer);
            send(client_fd, "OK", 2, 0);
        }
        
        // 6. 关闭连接
        close(client_fd);
    }
    
    close(listen_fd);
    return 0;
}

二、多进程版本(解决并发问题)

单线程版本的recv()会阻塞,导致无法同时处理多个客户端。使用多进程可以解决这个问题:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

void handle_client(int client_fd, struct sockaddr_in client_addr) {
    char buffer[BUFFER_SIZE];
    
    printf("客户端连接:%s:%d\n",
           inet_ntoa(client_addr.sin_addr),
           ntohs(client_addr.sin_port));
    
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
        
        if (n == 0) {
            printf("客户端已断开:%s:%d\n",
                   inet_ntoa(client_addr.sin_addr),
                   ntohs(client_addr.sin_port));
            break;
        }
        
        if (n == -1) {
            perror("recv error");
            break;
        }
        
        printf("[%s:%d] %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        
        send(client_fd, "OK", 2, 0);
    }
    
    close(client_fd);
    exit(0);
}

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    
    // 忽略SIGCHLD信号,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);
    
    // 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error");
        exit(1);
    }
    
    // 监听
    if (listen(listen_fd, 5) == -1) {
        perror("listen error");
        exit(1);
    }
    
    printf("多进程服务器启动成功,端口:%d\n", PORT);
    
    while (1) {
        client_len = sizeof(client_addr);
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept error");
            continue;
        }
        
        // 创建子进程处理客户端
        pid_t pid = fork();
        
        if (pid == 0) {
            // 子进程
            close(listen_fd);  // 子进程不需要监听套接字
            handle_client(client_fd, client_addr);
        } else if (pid > 0) {
            // 父进程
            close(client_fd);  // 父进程不需要客户端套接字
        } else {
            perror("fork error");
            close(client_fd);
        }
    }
    
    close(listen_fd);
    return 0;
}

三、多线程版本

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

typedef struct {
    int client_fd;
    struct sockaddr_in client_addr;
} ClientInfo;

void* handle_client(void* arg) {
    ClientInfo* info = (ClientInfo*)arg;
    char buffer[BUFFER_SIZE];
    
    printf("客户端连接:%s:%d\n",
           inet_ntoa(info->client_addr.sin_addr),
           ntohs(info->client_addr.sin_port));
    
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recv(info->client_fd, buffer, BUFFER_SIZE - 1, 0);
        
        if (n == 0) {
            printf("客户端已断开:%s:%d\n",
                   inet_ntoa(info->client_addr.sin_addr),
                   ntohs(info->client_addr.sin_port));
            break;
        }
        
        if (n == -1) {
            perror("recv error");
            break;
        }
        
        printf("[%s:%d] %s\n",
               inet_ntoa(info->client_addr.sin_addr),
               ntohs(info->client_addr.sin_port),
               buffer);
        
        send(info->client_fd, "OK", 2, 0);
    }
    
    close(info->client_fd);
    free(info);
    return NULL;
}

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    pthread_t tid;
    
    // 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error");
        exit(1);
    }
    
    // 监听
    if (listen(listen_fd, 5) == -1) {
        perror("listen error");
        exit(1);
    }
    
    printf("多线程服务器启动成功,端口:%d\n", PORT);
    
    while (1) {
        client_len = sizeof(client_addr);
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept error");
            continue;
        }
        
        ClientInfo* info = (ClientInfo*)malloc(sizeof(ClientInfo));
        info->client_fd = client_fd;
        info->client_addr = client_addr;
        
        pthread_create(&tid, NULL, handle_client, info);
        pthread_detach(tid);  // 分离线程,自动回收资源
    }
    
    close(listen_fd);
    return 0;
}

第四部分:TCP客户端实现

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 本机测试
    
    // 3. 连接服务器
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect error");
        close(sock_fd);
        exit(1);
    }
    
    printf("连接服务器成功\n");
    
    // 4. 循环收发数据
    while (1) {
        printf("请输入消息(输入end退出):");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strlen(buffer) - 1] = '\0';
        
        if (strcmp(buffer, "end") == 0) {
            break;
        }
        
        send(sock_fd, buffer, strlen(buffer), 0);
        
        memset(buffer, 0, BUFFER_SIZE);
        recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
        printf("服务器响应:%s\n", buffer);
    }
    
    // 5. 关闭连接
    close(sock_fd);
    printf("客户端退出\n");
    
    return 0;
}

第五部分:常见问题与解决方案

一、僵尸进程问题

在使用多进程版本时,子进程退出后,如果父进程没有调用wait(),会产生僵尸进程。

解决方案1:忽略SIGCHLD信号

cpp 复制代码
signal(SIGCHLD, SIG_IGN);

解决方案2:在信号处理函数中调用wait()

cpp 复制代码
void sigchld_handler(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

signal(SIGCHLD, sigchld_handler);

二、端口占用问题

服务器程序退出后,端口不会立即释放,需要等待一段时间(约2分钟)。

解决方案: 设置套接字选项SO_REUSEADDR

cpp 复制代码
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

三、send()返回成功是否保证数据送达?

send()返回成功只表示数据已成功写入发送缓冲区,并不保证对端已经收到。TCP的可靠性保证数据最终会送达,但如果网络断开,数据可能会丢失。

四、recv()返回值含义

返回值 含义
>0 成功接收的数据字节数
=0 对端已关闭连接
=-1 发生错误

总结

一、TCP编程核心接口

函数 服务端 客户端 说明
socket() 创建套接字
bind() 绑定地址
listen() 监听连接
accept() 接受连接
connect() 连接服务器
recv() 接收数据
send() 发送数据
close() 关闭连接

二、TCP三次握手的面试答案

  1. 第一次握手:客户端发送SYN报文(SYN=1, seq=x),进入SYN_SENT状态

  2. 第二次握手:服务器回复SYN+ACK报文(SYN=1, ACK=1, seq=y, ack=x+1),进入SYN_RCVD状态

  3. 第三次握手:客户端回复ACK报文(ACK=1, seq=x+1, ack=y+1),双方进入ESTABLISHED状态

三、高并发服务器实现方案

方案 优点 缺点
多进程 隔离性好,子进程崩溃不影响主进程 资源开销大
多线程 资源开销小,共享内存方便 需要同步,一个线程崩溃可能影响整个进程
IO多路复用 单线程处理多个连接 编程复杂度高

本文涵盖了TCP网络编程的核心内容:

  1. TCP理论基础:三次握手、四次挥手、头部结构

  2. 编程接口:socket、bind、listen、accept、connect、recv、send

  3. 服务器实现:基础版本、多进程版本、多线程版本

  4. 客户端实现:连接服务器、循环收发数据

  5. 常见问题:僵尸进程、端口占用、高并发

课后作业:

  • 编写并运行多进程和多线程版本的并发服务器

  • 使用telnet或自己编写的客户端测试服务器功能

  • 观察僵尸进程的产生与解决方案的效果

相关推荐
咖喱o2 小时前
QinQ/VLAN Stacking
linux·运维·服务器·网络
sduwcgg3 小时前
IQ-Learn 在 RTX 3090 服务器上的环境配置与踩坑记录
运维·服务器
AI周红伟3 小时前
周红伟:运营商一季度净利集体下滑 Token运营提速
大数据·网络·人工智能
QFIUNE4 小时前
CD-HIT 详解:序列去冗余、安装使用与聚类结果解析
linux·服务器·机器学习·数据挖掘·conda·聚类
marsh02064 小时前
43 openclaw熔断与降级:保障系统在异常情况下的可用性
java·运维·网络·ai·编程·技术
汽车仪器仪表相关领域5 小时前
Kvaser Memorator Professional 5xHS CB:五通道CAN FD裸板记录仪,赋能多总线系统集成测试的旗舰级核心装备
大数据·网络·人工智能·单元测试·汽车·集成测试
勇闯逆流河5 小时前
【Linux】linux进程控制(进程池的详解与实现)
linux·运维·服务器
初学者,亦行者5 小时前
计算机网络必考:一文吃透 TCP/IP 体系结构(附高清思维导图)
网络·tcp/ip
段一凡-华北理工大学5 小时前
【高炉炼铁领域炉温监测、预警、调控智能体设计与应用】~系列文章10:实时预警机制:跑在问题前面!
网络·人工智能·python·知识图谱·高炉炼铁·工业智能体