应用——Linux Socket编程

Linux Socket编程

TCP服务器编程模型

基本流程

复制代码
// 1. 创建监听socket
int listfd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 绑定地址和端口
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (SA)&ser, sizeof(ser));

// 3. 开始监听
listen(listfd, 3);

// 4. 接受连接
int conn = accept(listfd, (SA)&cli, &len);

// 5. 通信
recv() / send()

// 6. 关闭
close();

关键结构体

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

// 字节序转换函数
htons()  // 主机字节序转网络字节序(short)
htonl()  // 主机字节序转网络字节序(long)
ntohs()  // 网络字节序转主机字节序(short)
ntohl()  // 网络字节序转主机字节序(long)

select模型详解

核心代码分析

复制代码
tcp_select_ser.c
cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>

typedef struct sockaddr*(SA);

int main(int argc, char** argv)
{
    // 1. 创建监听socket
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 2. 绑定地址
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    bind(listfd, (SA)&ser, sizeof(ser));
    
    // 3. 监听
    listen(listfd, 3);
    
    // 4. select初始化
    fd_set rd_set, tmp_set;
    FD_ZERO(&rd_set);
    FD_ZERO(&tmp_set);
    FD_SET(listfd, &tmp_set);
    int maxfd = listfd;
    
    while (1)
    {
        rd_set = tmp_set;
        
        // 5. 等待事件
        select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        
        // 6. 处理事件
        for(int i = listfd; i < maxfd + 1; i++)
        {
            if(FD_ISSET(i, &rd_set) && i == listfd)  // 监听socket有新连接
            {
                int conn = accept(listfd, (SA)&cli, &len);
                FD_SET(conn, &tmp_set);
                if(conn > maxfd) maxfd = conn;
            }
            else if(FD_ISSET(i, &rd_set))  // 已连接socket有数据
            {
                // 处理数据收发
                char buf[512] = {0};
                int ret = recv(i, buf, sizeof(buf), 0);
                if(ret <= 0)  // 客户端断开
                {
                    FD_CLR(i, &tmp_set);
                    close(i);
                }
                else
                {
                    // 处理数据并回复
                    send(i, buf, strlen(buf), 0);
                }
            }
        }
    }
    close(listfd);
    return 0;
}

select模型特点

特点 说明
优点 1. 跨平台性好 2. 实现相对简单 3. 支持多个文件描述符
缺点 1. 每次调用需要复制fd_set 2. 需要遍历所有fd 3. 默认最大支持1024个连接 4. 内核每次都要遍历所有fd
适用场景 连接数较少(<1000)的场景

select相关函数

复制代码
// 清除集合中所有fd
void FD_ZERO(fd_set *set);

// 将fd加入集合
void FD_SET(int fd, fd_set *set);

// 将fd从集合移除
void FD_CLR(int fd, fd_set *set);

// 检查fd是否在集合中
int  FD_ISSET(int fd, fd_set *set);

// 等待事件发生
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

select 模型工作流程

epoll模型详解

核心代码分析

复制代码
tcp_epollser.c
cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/epoll.h>

typedef struct sockaddr*(SA);

// 添加fd到epoll
int add_fd(int epfd, int fd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    return epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}

// 从epoll移除fd
int del_fd(int epfd, int fd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    return epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
}

int main(int argc, char** argv)
{
    // 1. 创建监听socket
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 2. 绑定和监听
    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    bind(listfd, (SA)&ser, sizeof(ser));
    listen(listfd, 3);
    
    // 3. 创建epoll实例
    int epfd = epoll_create(100);
    
    // 4. 添加监听socket到epoll
    add_fd(epfd, listfd);
    
    struct epoll_event rev[100] = {0};
    
    while (1)
    {
        // 5. 等待事件
        int ep_ret = epoll_wait(epfd, rev, 100, -1);
        
        // 6. 处理事件
        for(int i = 0; i < ep_ret; i++)
        {
            if(rev[i].data.fd == listfd)  // 新连接
            {
                int conn = accept(listfd, (SA)&cli, &len);
                add_fd(epfd, conn);
            }
            else  // 数据可读
            {
                int conn = rev[i].data.fd;
                char buf[512] = {0};
                int ret = recv(conn, buf, sizeof(buf), 0);
                if(ret <= 0)  // 客户端断开
                {
                    del_fd(epfd, conn);
                    close(conn);
                }
                else
                {
                    // 处理并回复数据
                    send(conn, buf, strlen(buf), 0);
                }
            }
        }
    }
    close(listfd);
    return 0;
}

epoll模型特点

特点 说明
优点 1. 事件驱动,无需遍历所有fd 2. 支持边缘触发(ET)和水平触发(LT) 3. 内存拷贝少,性能高 4. 支持大量连接(万级别)
缺点 1. Linux特有,跨平台性差 2. 编程复杂度稍高
适用场景 高并发、大量连接场景

epoll相关函数

复制代码
// 创建epoll实例
int epoll_create(int size);  // size已废弃,但需>0

// 控制epoll事件
int epoll_ctl(int epfd, int op, int fd, 
              struct epoll_event *event);

// 等待事件
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

// epoll_event结构
struct epoll_event {
    uint32_t     events;    // EPOLLIN, EPOLLOUT等
    epoll_data_t data;      // 用户数据
};

union epoll_data {
    void    *ptr;
    int      fd;            // 常用fd
    uint32_t u32;
    uint64_t u64;
};

epoll触发模式

复制代码
// 水平触发(默认) - 只要缓冲区有数据就会触发
ev.events = EPOLLIN;

// 边缘触发 - 只在状态变化时触发一次
ev.events = EPOLLIN | EPOLLET;

// 同时监听读和写事件
ev.events = EPOLLIN | EPOLLOUT;

epoll 模型工作流程

fork多进程模型

核心代码分析

复制代码
tcp_forkser.c 
cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

typedef struct sockaddr*(SA);

// 子进程退出处理函数
void myhandle(int num)
{
    wait(NULL);  // 回收子进程
}

int main(int argc, char** argv)
{
    // 注册SIGCHLD信号处理,避免僵尸进程
    signal(SIGCHLD, myhandle);
    
    // 创建监听socket
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 绑定和监听
    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    bind(listfd, (SA)&ser, sizeof(ser));
    listen(listfd, 3);
    
    while (1)
    {
        // 接受连接
        int conn = accept(listfd, (SA)&cli, &len);
        
        // 创建子进程处理连接
        pid_t pid = fork();
        
        if (pid > 0)  // 父进程
        {
            close(conn);  // 父进程关闭连接socket
            // 由信号处理函数回收子进程
        }
        else if (0 == pid)  // 子进程
        {
            close(listfd);  // 子进程关闭监听socket
            
            // 处理客户端通信
            while (1)
            {
                char buf[512] = {0};
                int ret = recv(conn, buf, sizeof(buf), 0);
                if (ret <= 0) break;
                
                // 处理并回复数据
                send(conn, buf, strlen(buf), 0);
            }
            close(conn);
            exit(0);  // 子进程退出
        }
        else
        {
            perror("fork");
            continue;
        }
    }
    close(listfd);
    return 0;
}

fork模型特点

特点 说明
优点 1. 编程简单直观 2. 进程间隔离性好 3. 稳定性高(一个进程崩溃不影响其他)
缺点 1. 资源消耗大 2. 进程间通信复杂 3. 创建销毁开销大
适用场景 连接数较少,需要高稳定性的场景

关键点说明

复制代码
// 1. 父子进程文件描述符继承
// fork()后,子进程继承父进程所有打开的文件描述符
// 需要及时关闭不需要的fd,避免资源泄露

// 2. 僵尸进程处理
// 子进程退出后,父进程需要wait()回收
// 使用signal(SIGCHLD, SIG_IGN)可让内核自动回收

// 3. 父子进程地址空间
// fork()后,子进程获得父进程地址空间的拷贝
// 父子进程内存空间独立,修改互不影响

fork 模型工作流程

pthread多线程模型

核心代码分析

复制代码
tcp_threadser.c 
cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <semaphore.h>

typedef struct sockaddr*(SA);
sem_t sem_cli;  // 信号量,用于线程同步

// 线程处理函数
void* th(void* args)
{
    int conn = *(int*)args;
    sem_post(&sem_cli);  // 通知主线程参数已复制
    
    // 设置线程为分离状态,线程退出后自动回收资源
    pthread_detach(pthread_self());
    
    // 处理客户端通信
    while (1)
    {
        char buf[512] = {0};
        int ret = recv(conn, buf, sizeof(buf), 0);
        if (ret <= 0) break;
        
        // 处理并回复数据
        send(conn, buf, strlen(buf), 0);
    }
    close(conn);
    return NULL;
}

int main(int argc, char** argv)
{
    // 创建监听socket
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 绑定和监听
    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    bind(listfd, (SA)&ser, sizeof(ser));
    listen(listfd, 3);
    
    // 初始化信号量
    sem_init(&sem_cli, 0, 0);
    
    while (1)
    {
        // 接受连接
        int conn = accept(listfd, (SA)&cli, &len);
        
        // 创建线程处理连接
        pthread_t tid;
        pthread_create(&tid, NULL, th, &conn);
        
        // 等待线程复制conn参数
        sem_wait(&sem_cli);
    }
    
    close(listfd);
    sem_destroy(&sem_cli);
    return 0;
}

线程模型特点

特点 说明
优点 1. 创建销毁开销小 2. 共享内存,通信方便 3. 资源消耗相对较小
缺点 1. 编程复杂度高 2. 线程安全问题 3. 一个线程崩溃可能影响整个进程
适用场景 需要共享数据、频繁创建销毁的场景

线程同步机制

复制代码
// 1. 信号量(semaphore)
sem_t sem;
sem_init(&sem, 0, 0);  // 初始化,初始值0
sem_wait(&sem);        // P操作,等待信号量
sem_post(&sem);        // V操作,释放信号量
sem_destroy(&sem);     // 销毁

// 2. 互斥锁(mutex)
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);

// 3. 条件变量(condition variable)
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
pthread_cond_wait(&cond, &mutex);
pthread_cond_signal(&cond);
pthread_cond_destroy(&cond);

线程属性

复制代码
// 设置线程为分离状态
pthread_detach(thread_id);

// 或创建时指定分离属性
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, th_func, arg);

TCP客户端

统一客户端代码

所有模型的客户端代码基本相同:

复制代码
cli.c 
cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 2. 连接服务器
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");  // 服务器IP
    
    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    
    // 3. 通信
    int i = 5;
    while (i)
    {
        char buf[512] = "this is tcp test";
        send(sockfd, buf, strlen(buf), 0);
        
        bzero(buf, sizeof(buf));
        recv(sockfd, buf, sizeof(buf), 0);
        printf("ser:%s\n", buf);
        
        sleep(1);
        i--;
    }
    
    // 4. 关闭
    close(sockfd);
    return 0;
}

客户端关键点

复制代码
// 1. 连接服务器
// connect()会阻塞直到连接建立或失败

// 2. 数据收发
// send()和recv()默认是阻塞的
// 可以设置为非阻塞模式

// 3. 错误处理
// 连接失败、发送失败、接收失败都需要处理

// 4. 资源释放
// 确保socket被正确关闭

总结与对比

五种模型对比

模型 并发处理方式 资源消耗 编程复杂度 适用场景 性能
select 轮询所有fd 中等 简单 连接数<1000
epoll 事件通知 中等 高并发场景
fork 多进程 简单 稳定优先 中等
pthread 多线程 中等 复杂 需要共享数据
原始模型 单线程阻塞 简单 学习/测试 很低

模型对比图

选择建议

  1. 学习入门:先从简单的fork模型开始

  2. 小规模应用:select或fork模型

  3. 高并发服务:epoll模型

  4. 计算密集型:多线程模型

  5. 稳定要求高:多进程模型

通用优化建议

复制代码
// 1. 设置socket选项
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

// 2. 设置非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

// 3. 设置超时
struct timeval tv;
tv.tv_sec = 5;  // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

// 4. 优雅关闭
shutdown(sockfd, SHUT_RDWR);  // 先关闭读写
close(sockfd);                // 再关闭socket

常见问题处理

复制代码
// 1. 地址已在使用
// 设置SO_REUSEADDR选项

// 2. 大量TIME_WAIT
// 调整内核参数或设置SO_REUSEADDR

// 3. 连接数限制
// 调整系统文件描述符限制
// ulimit -n 65535

// 4. 内存泄漏
// 确保所有socket都被正确关闭
// 使用valgrind检查内存泄漏

编译命令汇总

复制代码
# 通用编译命令
gcc -o server server.c -lpthread  # 需要线程库
gcc -o client client.c

# 带调试信息
gcc -g -o server server.c -lpthread

# 优化编译
gcc -O2 -o server server.c -lpthread

# 指定标准
gcc -std=c99 -o server server.c -lpthread
相关推荐
ss2732 小时前
Java定时任务:ScheduledThreadPoolExecutor
开发语言·python
jacGJ2 小时前
记录学习--Windows常用命令
学习
张火火isgudi2 小时前
VMware Debian 挂载 Windows 文件夹至 Debian 目录
linux·运维·windows·debian
我可以将你更新哟2 小时前
【PyQT-4】QListWidget列表控件、QComboBox下拉列表控件、QTableWidget表格控件
开发语言·python·pyqt
TheSumSt2 小时前
Python丨课程笔记Part1:Python基础入门部分
开发语言·笔记·python·学习方法
神算大模型APi--天枢6462 小时前
2025 国产算力破局后,大模型训练数据集如何实现 “合规采集 + 高效清洗”?
运维·服务器·人工智能·架构·硬件架构
superman超哥2 小时前
Rust 注释与文档注释:代码即文档的工程实践
开发语言·算法·rust·工程实践·rust注释与文档注释·代码即文档
航Hang*2 小时前
Photoshop 图形与图像处理技术——第4章:图层的应用
图像处理·笔记·ui·photoshop