Linux编程: 10、线程池与初识网络编程

今天我计划通过一个小型项目,系统讲解线程池与网络编程的核心原理及实践。项目将围绕 “利用线程池实现高并发网络通信” 这一核心需求展开,具体设计如下:

  • 为保证线程安全,线程池采用单例模式设计,确保全局唯一实例避免资源竞争;
  • 技术栈采用 C/C++ 混合编程:网络通信、线程管理、同步锁等底层操作直接调用 Linux 系统接口(贴合 Linux 编程的 C 语言风格),其他业务逻辑及封装层则使用 C++ 实现,兼顾底层效率与代码的可维护性。

一、什么是线程池

线程池是一种线程管理机制,简单来说,它是一组预先创建好的线程的集合,这些线程在程序启动时或需要时被初始化并待命,当有任务到来时,线程池会分配一个空闲线程来处理任务,任务完成后线程不会被销毁,而是回到线程池等待下一个任务。

它的核心作用是避免频繁创建和销毁线程带来的性能开销。因为线程的创建、切换、销毁都需要操作系统内核进行资源调度,在高并发场景下(比如大量网络请求、频繁的任务处理),如果每次处理任务都新建线程,会导致系统资源(CPU、内存)被大量消耗,甚至引发系统不稳定。

线程池的主要组成部分通常包括:

  • 线程队列:存储所有等待或正在运行的线程
  • 任务队列:存放待处理的任务,当没有空闲线程时,新任务会暂时进入队列等待
  • 管理机制:负责线程的创建、回收、任务分配以及线程池大小的动态调整(可选)

在实际应用中,线程池可以高效处理大量并发连接:当新的网络请求到达时,无需为每个请求单独创建线程,而是直接交给线程池中的空闲线程处理,既保证了响应速度,又避免了资源浪费。

 

1. 线程池基础配置

图中 “线程池” 里标注了线程数量:3,意思是线程池预先创建并维护着 3 个空闲线程,随时准备处理任务。

2. 任务提交与分配

右侧的任务 1、任务 2、任务 3,代表 3 个待处理的任务。因为线程池有 3 个空闲线程,所以这 3 个任务会直接分配给线程池里的空闲线程,同时开始执行。

3. 任务排队等待

任务 4到来时,线程池里的 3 个线程已经被前面的任务占用了(都在忙)。此时,任务 4 会进入阻塞状态,被放进线程池的 “任务队列” 里,等待有线程处理完任务后,再重新变成空闲线程来执行它


二、项目开始前的配置文件

我这里用的编辑器是vscode,调试器是lldb,lsp是clangd,操作系统是centos10,也用了cmake,当然,你们也可以用其它的编译环境

1、launch.json

{"version": "0.2.0","configurations": [{"type": "lldb","request": "launch","name": "Debug","program": "${workspaceFolder}/bin/server",  "args": [],"cwd": "${workspaceFolder}","internalConsoleOptions": "neverOpen","console": "integratedTerminal"}]
}

2、.clang-format

BasedOnStyle: LLVM
AccessModifierOffset: -2       # 访问修饰符(public/private)缩进减少 2 格
AlignAfterOpenBracket: Align   # 开括号后的内容对齐
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
AllowShortFunctionsOnASingleLine: Empty  # 空函数允许单行
BraceWrapping:AfterClass: trueAfterControlStatement: falseAfterEnum: trueAfterFunction: trueAfterNamespace: falseAfterStruct: trueAfterUnion: trueBeforeCatch: trueBeforeElse: trueIndentBraces: falseSplitEmptyFunction: trueSplitEmptyRecord: trueSplitEmptyNamespace: true
BreakBeforeBraces: Custom
BreakConstructorInitializers: BeforeComma
ColumnLimit: 80
ConstructorInitializerAllOnOneLineOrOnePerLine: true
Cpp11BracedListStyle: true
DerivePointerAlignment: false
IndentCaseLabels: true
IndentWidth: 4
PointerAlignment: Left
SpaceBeforeAssignmentOperators: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInParentheses: false
Standard: Cpp11
TabWidth: 4
UseTab: Never

3、CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(server)set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)# 输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)# 头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)file(GLOB_RECURSE SOURCES "${PROJECT_SOURCE_DIR}/src/*.*")# 可执行文件
add_executable(server ${SOURCES} main.cpp)# 链接线程库(必须!)
target_link_libraries(server pthread)

4、项目结构


三、实现单例线程池

1、任务的基类:Task.h

#ifndef TASK_H
#define TASK_Hclass Task
{
public:virtual ~Task() = default;virtual void run() = 0;
};#endif  // !TASK_H

2、线程池的相关接口:ThreadPool.h

#ifndef THREAD_POOL_H
#define THREAD_POOL_H#include "Task.h"
#include <atomic>
#include <cstddef>
#include <cstdio>
#include <pthread.h>
#include <queue>
#include <vector>class ThreadPool
{public:ThreadPool(const ThreadPool&)           = delete;ThreadPool operator=(const ThreadPool&) = delete;~ThreadPool();static ThreadPool& getInstance();// 添加任务到线程池void addTask(Task* task);private:ThreadPool(size_t pool_size = 3);// 工作线程函数static void* worker(void* arg);// 工作循环函数void work();// 开始\停止void start();void stop();private:size_t                 _pool_size;  // 线程池数量std::vector<pthread_t> _threads;    // 线程数组std::queue<Task*>      _tasks;      // 任务队列pthread_cond_t         _cond;       // 条件变量pthread_mutex_t        _mutex;      // 锁std::atomic<bool>      _is_stop;    // 是否暂停
};#endif  // !THREAD_POOL_H

3、构造函数与获取单例对象

ThreadPool::ThreadPool(size_t pool_size): _pool_size(pool_size), _is_stop(true)
{// 初始化互斥量和条件变量pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);// 线程池开始运行start();
}ThreadPool& ThreadPool::getInstance() {static ThreadPool instance;return instance;
}
  1. 构造函数 ThreadPool::ThreadPool(size_t pool_size)

    • 初始化列表中设置了线程池大小_pool_size和停止标志_is_stop(初始为true表示未运行)
    • 调用pthread_mutex_initpthread_cond_init初始化了互斥锁和条件变量(线程同步的核心工具)
    • 最后调用start()方法启动线程池(实际创建并启动工作线程)
  2. 单例获取函数 ThreadPool::getInstance()

    • 使用 C++11 的static局部变量特性实现单例模式
    • 局部静态变量instance会在第一次调用时初始化,且保证线程安全
    • 每次调用都返回同一个实例的引用,确保整个程序中只有一个线程池实例

4、工作线程函数与工作循环函数

void* ThreadPool::worker(void* arg) {auto* pool = static_cast<ThreadPool*>(arg);pool->work(); return nullptr;
}void ThreadPool::work()
{while(!_is_stop) {// 加锁pthread_mutex_lock(&_mutex);// 只有运行时且任务队列为空时才会等待while(!_is_stop && _tasks.empty()) {pthread_cond_wait(&_cond, &_mutex);}// 运行时若停止了,则退出if(_is_stop) {pthread_mutex_unlock(&_mutex);break;}auto* task = _tasks.front();_tasks.pop();pthread_mutex_unlock(&_mutex);if(task) {task->run();delete task;}}
}
  1. worker 函数
    这是线程的入口函数(符合 pthread 库对线程函数的要求),作用是:

    • 通过static_cast将传入的void*参数转换为线程池实例指针
    • 调用线程池的work()方法,让线程进入实际的任务处理循环

    它相当于一个适配层,将 pthread 库的 C 风格函数接口与 C++ 的类方法衔接起来。

  2. work 函数
    这是线程的核心工作循环,实现了 "等待 - 取任务 - 执行" 的逻辑:

    • 循环判断:通过!_is_stop控制线程是否退出
    • 加锁保护:操作任务队列前先加互斥锁_mutex,保证线程安全
    • 等待机制:当任务队列为空且线程池未停止时,通过pthread_cond_wait让线程进入等待状态(释放锁并阻塞,直到被唤醒)
    • 退出检查:被唤醒后先判断是否需要停止,若需停止则解锁并退出循环
    • 处理任务:从队列取第一个任务,解锁后执行任务的run()方法,完成后释放任务对象
    • 线程复用:任务执行完后回到循环开头,继续等待新任务,实现线程的复用

5、线程池开启函数

void ThreadPool::start()
{// 如果已经是启动状态]if(!_is_stop) {return;}_is_stop = false;_threads.reserve(_pool_size);for(size_t i = 0; i < _pool_size; i++) {pthread_t pth;if(pthread_create(&pth, nullptr,worker, this) != 0){perror("create thread failed");_is_stop = true;return;}_threads.push_back(pth);}
}
  1. 启动状态检查
    首先判断_is_stop标志,如果线程池已经处于运行状态(_is_stopfalse),则直接返回,避免重复启动。

  2. 初始化准备
    _is_stop设为false(标记线程池进入运行状态),并通过_threads.reserve(_pool_size)预先为存储线程 ID 的容器分配内存,提升后续插入效率。

  3. 创建工作线程
    循环 _pool_size 次(线程池预设的线程数量),每次调用pthread_create创建一个线程:

    • 线程入口函数为worker(之前实现的线程工作函数)
    • 传入this指针作为当前线程池实例指针,让工作线程能访问线程池的任务队列等资源
  4. 错误处理
    若线程创建失败(pthread_create返回非 0),则通过perror打印错误信息,将_is_stop重置为true(标记线程池停止),并退出函数。

  5. 记录线程 ID
    成功创建的线程 ID(pth)会被存入_threads容器,便于后续管理(如停止线程池时回收线程)。

6、线程池停止函数

void ThreadPool::stop()
{// 如果已停止if(_is_stop) return;_is_stop = true;pthread_cond_broadcast(&_cond);// 等待所有线程结束for(pthread_t& pth : _threads) {pthread_join(pth, nullptr);}_threads.clear();// 清空任务队列pthread_mutex_lock(&_mutex);while (!_tasks.empty()){auto* task = _tasks.front();_tasks.pop();if(task) {delete task;}}pthread_mutex_unlock(&_mutex);
}
  1. 停止状态检查
    首先判断_is_stop标志,如果线程池已经处于停止状态,则直接返回,避免重复停止操作。

  2. 触发停止机制

    • _is_stop设为true(标记线程池进入停止状态)
    • 调用pthread_cond_broadcast(&_cond)唤醒所有等待在条件变量上的工作线程(避免线程一直阻塞在等待任务的状态)
  3. 回收工作线程
    遍历存储线程 ID 的_threads容器,通过pthread_join等待每个工作线程执行完毕并回收资源,最后清空容器。这一步确保所有线程都正常退出,避免僵尸线程。

  4. 清理任务队列

    • 加锁保护任务队列操作
    • 循环清空队列中剩余的未执行任务,逐个释放任务对象的内存
    • 解锁完成清理

7、添加任务进任务队列

void ThreadPool::addTask(Task* task)
{if(_is_stop || !task) return;pthread_mutex_lock(&_mutex);_tasks.push(task);pthread_cond_signal(&_cond);        // 唤醒一个线程pthread_mutex_unlock(&_mutex);
}
  1. 参数与状态检查
    先判断线程池是否已停止(_is_stoptrue)或任务指针为空,若满足任一条件则直接返回,避免向已停止的线程池添加任务或添加无效任务。

  2. 线程安全的任务入队

    • 加锁(pthread_mutex_lock):确保多线程同时添加任务时,对任务队列_tasks的操作是线程安全的
    • 入队(_tasks.push(task)):将新任务添加到任务队列尾部
    • 唤醒线程(pthread_cond_signal):发送信号唤醒一个正在等待的工作线程(之前在work方法中通过pthread_cond_wait等待的线程),通知有新任务可处理
    • 解锁(pthread_mutex_unlock):释放锁,允许其他线程操作任务队列

8、析构函数

ThreadPool::~ThreadPool()
{stop();pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);
}
  1. 调用 stop () 方法
    首先调用stop(),确保线程池在销毁前已经停止运行:包括唤醒所有工作线程、回收线程资源、清理未执行的任务等(这些逻辑已在stop()中实现)。这一步是为了避免线程池对象销毁后,仍有线程在后台运行或资源未释放的情况。

  2. 销毁同步机制

    • 调用pthread_mutex_destroy(&_mutex)销毁互斥锁,释放其占用的系统资源
    • 调用pthread_cond_destroy(&_cond)销毁条件变量,同样释放相关系统资源

四、服务器和客户端的通信流程

1、服务器端(像收件邮箱服务器)

  1. 新建 socket → 架起 “邮件接收系统”,准备收邮件
  2. bind 绑定 → 确定自己叫 xxx@qq.com ,让别人能找到
  3. listen 监听 → 开通 “同时收多封邮件” 功能,别一来就挤崩
  4. accept 等待 → 守着等你点 “发送”,接住你的邮件请求
  5. read/write 收发 → 收你发的邮件内容,还能回 “已收到” 提示
  6. close 关闭 → 这次发信结束,等下次你再发

2、客户端(像你用邮箱发信)

  1. 新建 socket → 打开手机邮箱 App,准备发邮件
  2. connect 连接 → 填对方邮箱点 “发送”,主动找服务器
  3. read/write 收发 → 写邮件、发出去,还能收到 “发送成功”
  4. close 关闭 → 发完关 App,结束这次发信

3、总结

网络通信操作邮箱发信类比通俗理解
socket打开邮箱 App / 搭建邮箱系统准备通信工具 / 服务
bind确定邮箱域名(如 qq.com )给服务定地址,让人找得到
listen开通 “同时收信” 队列限制并发,避免系统被挤爆
accept邮箱服务器 “接住” 你的邮件服务器受理客户端的连接请求
connect你点 “发送”,发起发信客户端主动连服务器
read/write写邮件、发邮件、收提示双方互相收发数据内容
close发完信关 App / 退出页面结束本次通信,释放资源

五、实现简易服务器

1、服务器相关接口:Server.h

#ifndef SERVER_H
#define SERVER_H#include <atomic>
class Server
{
public:Server(int port): _port(port), _is_stop(true) {}bool start();void stop();private:int _server_sock;int _port;std::atomic<bool> _is_stop;
};#endif // !SERVER_H

2、服务器开始运行

bool Server::start()
{// 1. 创建服务器套接字// AF_INET: 使用IPv4协议// SOCK_STREAM: 使用TCP协议(面向连接的流式传输)// 0: 使用默认协议(TCP)_server_sock = socket(AF_INET, SOCK_STREAM, 0);if(_server_sock  == -1) {  // 套接字创建失败std::cerr << "create socket failed" << std::endl;return false;}// 2. 设置套接字选项:允许端口重用// 解决服务器重启时"地址已在使用"的问题(端口未完全释放)int opt = 1;  // 选项值:1表示启用该选项// SOL_SOCKET: 通用套接字级别// SO_REUSEADDR: 允许端口重用选项if (setsockopt(_server_sock, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt)) == -1){std::cerr << "Failed to set socket options" << std::endl;close(_server_sock);  // 失败时关闭已创建的套接字return false;}// 3. 绑定套接字到指定地址和端口sockaddr_in server_addr;          // 存储服务器地址信息的结构体server_addr.sin_family = AF_INET; // 使用IPv4协议// 监听所有可用网络接口(服务器可能有多个网卡)server_addr.sin_addr.s_addr = INADDR_ANY;// 将端口号从主机字节序转换为网络字节序(大端序)server_addr.sin_port = htons(_port);// 绑定操作:将套接字与地址信息关联if(bind(_server_sock, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {std::cerr << "Failed to bind socket" << std::endl;close(_server_sock);  // 失败时释放资源return false;}// 4. 开始监听连接请求// 第二个参数5: 最大等待连接队列长度(超过的连接会被拒绝)if(listen(_server_sock, 5) == -1) {std::cerr << "Failed to listen on socket" << std::endl;close(_server_sock);  // 失败时释放资源return false;}// 5. 进入主循环,持续接受客户端连接_is_stop = false;  // 重置停止标志,开始运行while (!_is_stop) {sockaddr_in client_addr;      // 存储客户端地址信息socklen_t client_len = sizeof(client_addr);  // 地址结构体长度// 阻塞等待客户端连接,成功后返回客户端专属套接字int client_sock = accept(_server_sock, (sockaddr*)&client_addr, &client_len);if(client_sock == -1) {  // 接受连接失败if(!_is_stop) {  // 非主动停止时才打印错误std::cerr << "Failed to accept connection" << std::endl;}continue;  // 继续等待下一个连接}// 打印新连接的客户端IP地址std::cout << "New connection from " << inet_ntoa(client_addr.sin_addr)<< std::endl;// 6. 将客户端连接交给线程池处理// 创建网络任务(封装客户端套接字),添加到线程池任务队列// 线程池会自动分配空闲线程处理该连接,实现并发处理ThreadPool::getInstance().addTask(new NetworkTask(client_sock));}return true;
}
  1. 创建服务器套接字
    通过socket(AF_INET, SOCK_STREAM, 0)创建 TCP 套接字(SOCK_STREAM表示流式协议,即 TCP),失败则返回错误。

  2. 设置套接字选项
    调用setsockopt设置SO_REUSEADDR选项,允许端口在服务器重启后快速重用(避免因端口未完全释放导致的启动失败)。

  3. 绑定地址和端口

    • 初始化server_addr结构体,指定 IPv4 协议(AF_INET)、监听所有网卡(INADDR_ANY)和端口(_port,通过htons转换为网络字节序)
    • 调用bind将套接字与地址端口绑定,失败则关闭套接字并返回。
  4. 开始监听连接
    listen(_server_sock, 5)启动监听,设置等待连接的队列长度为 5(最多同时有 5 个客户端在队列中等待处理)。

  5. 循环接受客户端连接

    • 进入while(!_is_stop)循环,持续等待客户端连接
    • accept函数阻塞等待新连接,成功后返回客户端套接字client_sock和客户端地址信息
    • 打印客户端 IP 地址,标识新连接建立
  6. 用线程池处理连接
    将客户端套接字封装成NetworkTask任务,通过线程池的addTask方法提交给线程池处理,实现高并发(避免为每个连接单独创建线程)。

3、服务器停止运行

void Server::stop()
{_is_stop = true;close(_server_sock);
}
  1. 设置停止标志
    _is_stop设为true,用于终止start()方法中接受连接的循环(while(!_is_stop)),让服务器退出等待新连接的状态。

  2. 关闭服务器套接字
    调用close(_server_sock)关闭服务器监听套接字,这会导致阻塞在accept()函数上的服务器线程被唤醒并退出,使服务器无法再接受新连接。

4、相关的接口函数

#include <sys/socket.h>int socket(int domain, int type, int protocol);功能:创建一个套接字参数1AF_INET  网络套接字(不同主机通过通络进行通信)AF_UNIX  文件系统套接字(本机内多进程之间通信)参数2
指定套接字的特性:当参数1是AF_INET是,参数可以选择:SOCK_STREAM 数据流服务,是面向连接的,更可靠的,使用TCP协议SOCK_DGRAM 数据报服务,使用UDP协议参数3
0: 表示使用默认的协议参数2为SOCK_STREAM的默认协议就是TCP参数3为SOCK_DGRAM的默认协议就是UDP返回值:成功,返回套接字对应的文件描述符;失败,返回-1
#include <sys/socket.h>int bind(int socket,const struct sockaddr *address,socklen_t address_len);功能:把套接字和地址绑定
参数1:服务器套接字
参数2:服务器的地址
参数3:参数2的长度
返回值:成功,返回0;失败,返回-1
#include <sys/socket.h>int listen(int socket, int backlog);功能:创建套接字队列服务器正在处理一个客户端的请求时,后续的客户请求就被放入队列等待处理。如果队列中等待处理的请求数超过参数 2,连接请求就会被拒绝。返回值:成功,返回 0;失败,返回-1
#include <sys/socket.h>int accept(int socket,struct sockaddr *restrict address,socklen_t *restrict address_len);功能:等待客户端的请求,直到有客户端接入。
参数1:服务器套接字
参数2:被接入服务器的客户端的地址
参数3:客户端地址的长度(注意,是一个指针)
返回值:成功,返回一个对应的客户端套接字失败,返回-1
#include <sys/socket.h>int connect(int socket,const struct sockaddr *address,socklen_t address_len);功能:客户端向指定的服务器发起连接请求。
参数1:套接字
参数2:服务器地址
参数3:地址的长度
返回值:成功,返回 0;失败,返回-1
#include <sys/socket.h>int setsockopt(int socket, int level, int option_name,const void *option_value, socklen_t option_len);功能:设置套接字的属性选项
参数1:要设置的套接字
参数2:选项级别(如SOL_SOCKET表示通用套接字选项)
参数3:具体选项(如SO_REUSEADDR表示允许端口重用)
参数4:选项值的指针
参数5:选项值的长度
返回值:成功返回0;失败返回-1

六、实现网络工作类

1、网络工作类的接口:NetworkTask.h

#ifndef NETWORK_TASK_H
#define NETWORK_TASK_H
#include "Task.h"class NetworkTask: public Task
{
public:NetworkTask(int sock) :_client_sock(sock) {}void run() override;private:int _client_sock;
};#endif // !NETWORK_TASK_H

2、实现run函数

#include "NetworkTask.h"
#include <sys/socket.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cstring>void NetworkTask::run() {char buff[BUFSIZ] = {0};while (1) {size_t read_bytes = read(_client_sock, buff, BUFSIZ - 1);if (read_bytes > 0) {std::cout << "接收: " << buff << std::endl;std::string response ="接收到" + std::to_string(strlen(buff)) + "个字符";int ret = write(_client_sock, response.data(), response.size());if (ret == -1) {perror("Error writing to socket");break;}memset(buff, 0, BUFSIZ);  // 清空缓冲区,准备下次读取}else if (read_bytes == 0) {// 客户端正常关闭连接std::cout << "客户端主动关闭连接" << std::endl;break;}else {// 读取错误perror("Error reading from socket");break;}}// 循环结束后关闭close(_client_sock);
}
  1. 初始化缓冲区
    创建BUFSIZ大小的字符数组buff(系统默认缓冲区大小,通常为 8192 字节),并初始化为 0,用于临时存储从客户端读取的数据。

  2. 进入通信循环
    通过while(1)开启无限循环,持续处理与客户端的交互:

    • 读取数据:调用read(_client_sock, buff, BUFSIZ-1)从客户端套接字读取数据,最多读取BUFSIZ-1字节(预留 1 字节给字符串结束符)。
    • 处理有效数据:若read_bytes>0(成功读取到数据):
      • 打印接收的内容到服务器控制台。
      • 构造响应字符串(格式为 “接收到 X 个字符”),通过write函数发送给客户端。
      • 若写入失败(ret==-1),打印错误并退出循环。
      • memset清空缓冲区,为下一次读取做准备。
    • 客户端断开连接:若read_bytes==0(客户端主动关闭连接),打印提示信息并退出循环。
    • 读取错误:若read_bytes<0(读取失败,如网络异常),用perror输出错误详情并退出循环。
  3. 清理资源
    循环结束后,调用close(_client_sock)关闭客户端套接字,释放该连接占用的资源。


 七、main函数与测试文件

1、主函数入口: main.cpp

#include "Server.h"
#include <cstdlib>
#include <iostream>int main()
{try{Server s(8000);std::cout << "Starting server..." << std::endl;if (!s.start()) {std::cerr << "Failed to start server" << std::endl;exit(1);}std::cout << "Server is running. Press Enter to stop..." << std::endl;std::cin.get();// 停止服务器和线程池s.stop();std::cout << "Server stopped successfully" << std::endl;}catch (const std::exception& e){std::cerr << "Error: " << e.what() << std::endl;return 1;}return 0;
}

2、测试用的客户端: test.c

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>int main()
{// 创建客户端套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 设置服务器地址struct sockaddr_in address;address.sin_family      = AF_INET;address.sin_addr.s_addr = inet_addr("192.168.1.10");address.sin_port        = htons(8000);int ret = connect(sockfd, (struct sockaddr*)&address, sizeof(address));if (ret == -1) {perror("connect failed.");exit(1);}// 接收用户输入char buff[BUFSIZ];printf("Please input: ");fgets(buff, sizeof(buff), stdin);buff[strlen(buff) - 1] = 0;// 向服务器发送数据write(sockfd, buff, strlen(buff) + 1);// 读取服务器发回的数据read(sockfd, buff, sizeof(buff));printf("Received: %s\n", buff);// 关闭套接字close(sockfd);return 0;
}

3、运行结果:

// cmake编译后可调试的命令
cmake -DCMAKE_BUILD_TYPE=Debug ..

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93887.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93887.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

藏云阁 Logo 库(开源项目SVG/PNG高清Logo)

在日常技术方案设计、架构图绘制或PPT制作中&#xff0c;常常会遇到一些问题&#xff0c;比如&#xff1a; 找不到统一风格的开源项目组件图标&#xff0c;PPT中的logo五花八门下载的图标分辨率不足&#xff0c;放大后模糊失真不同来源的图标颜色风格冲突&#xff0c;破坏整体…

从0开始学习R语言--Day64--决策树回归

对于没有特征或者说需要寻找另类关系的数据集&#xff0c;我们通常会用聚合或KNN近邻的方法来分类&#xff0c;但这样的分类或许在结果上是好的&#xff0c;但是解释性并不好&#xff0c;有时候我们甚至能看到好的结果反直觉&#xff1b;而决策树回归做出的结果&#xff0c;由于…

B+树高效实现与优化技巧

B树的定义 一颗M阶B树T,满足以下条件 每个结点至多拥有M课子树 根结点至少拥有两颗子树 除了根结点以外,其余每个分支结点至少拥有M/2课子树 所有的叶结点都在同一层上 有k棵子树的分支结点则存在k-1个关键字,关键字按照递增顺序进行排序 关键字数量满足 ceil( M/2 ) - 1 &…

Android 基础入门学习目录(持续更新)

四大组件 Activity&#xff1a; Service&#xff1a; BroadcastReceiver&#xff1a; ContentProvider&#xff1a; UI 与交互开发 常见的UI布局和UI控件 样式与主题 Fragment Intent 数据存储 自定义View和自定义Group 自定义View 自定义ViewGroup 事件分发 Key…

Linux移动大量文件命令

背景 使用 mv 命令报“/bin/mv: 参数列表过长”&#xff0c;也是第一遇到&#xff0c;查了一下&#xff0c;最后用rsync命令解决了。还好每台服务器&#xff0c;都必装rsync了&#xff0c;记录如下。 命令 nohup rsync -av --remove-source-files --progress /public/tmp/video…

SQL中的HAVING用法

HAVING 是 SQL 中专门对 “分组之后的聚合结果” 再做筛选的子句。 它一般跟在 GROUP BY 后面&#xff0c;不能单独使用&#xff0c;作用类似于分组版的 WHERE。✅ 1. 语法位置 SELECT 列1, 聚合函数(列2) AS 别名 FROM 表 GROUP BY 列1 HAVING 聚合条件; -- 这里写对聚合…

【Halcon 】Halcon 实战:如何为 XLD 模板添加极性信息以提升匹配精度?

Halcon 实战&#xff1a;如何为 XLD 模板添加极性信息以提升匹配精度&#xff1f; 在使用 Halcon 进行模板匹配时&#xff0c;我们通常有两种方式创建模板&#xff1a; 基于图像灰度&#xff08;CreateScaledShapeModel&#xff09;基于轮廓 XLD&#xff08;CreateScaledShapeM…

grafana/lock-stack 日志 Pipeline 配置

前言 本文使用的是 grafana/loki-stack chart 抓取的 k8s 日志。其他 chart 配置都差不多。 日志问题 docker 容器运行时 pod 内原始日志 [cpu-4] Hello, 第 9788 次报时&#xff0c;时间&#xff1a;2025-08-01T06:35:420000 {"HOSTNAME":"cpu-4",&qu…

appium2.0+之PointerActions详解

以下内容在 夜神模拟器 上进行。 一、应用场景 一些针对手势的操作&#xff0c;比如滑动、长按、拖动等。可以将这些基本手势组合成一个相对复杂的手势。 二、使用步骤创建触摸输入设备&#xff08;模拟手指操作&#xff09; touch_input PointerInput(interaction.POINTER_TO…

Java HTTPS 请求失败排查与证书导入全过程

文章目录Java HTTPS 请求失败排查与证书导入全过程问题背景问题初步分析排查过程查看目标地址证书导入证书验证证书是否导入成功重启应用进一步验证&#xff1a;是否真的是证书问题&#xff1f;1. 浏览器访问2. 抓包工具验证&#xff08;如 Charles、Wireshark&#xff09;补充…

android APT技术

1&#xff0c;背景 对于注解的使用&#xff0c;想必大家都不陌生&#xff0c;它出现在我们的源码中&#xff0c;以及大部分框架中&#xff0c;比如ButterKnife、Arouter、Retrofit&#xff0c;但它们是有区别的&#xff0c;其中前2个是编译时注解&#xff0c;最后一个是运行时注…

MySQL 和 PostgreSQL综合比对分析汇总

面对大数据项目或其它类型项目中&#xff0c;面对关系型数据库选择一直是很总要的一点&#xff0c;本文针对MySQL 和 PostgreSQL进行综合比对分析汇总&#xff0c;内容仅供参考。MySQL 和 PostgreSQL 是两款主流的开源关系型数据库&#xff08;RDBMS&#xff09;&#xff0c;但…

Linux---make和makefile

一、基本概念1.是什么make是一条命令&#xff0c;makefile是一个文件2.对应在vs中按一下f5就能运行代码&#xff0c;在Linux中make就相当于f5&#xff0c;使用makefile来封装从而实现我&#xff0c; 想要的功能3.使用①创建makefile文件②编辑makefile解释&#xff1a;test.exe…

【DAB收音机】DAB收音机协议及其他资料汇总

目录[ETSI DAB标准协议文档](https://www.etsi.org/standards)Other DAB资料DAB收音机相关的专利DAB收音机相关的期刊及学位论文DAB开源项目代码仓库qt-dab工具welle.io工具dablin工具【eti广播工具】⚙️ 项目对比与选型建议Other 收音机资料Other资料ETSI DAB标准协议文档 官…

RabbitMQ的特点和消息可靠性保障

掌握RabbitMQ的核心知识&#xff0c;需从其特点和消息可靠性保障&#xff08;尤其是消息丢失解决方案&#xff09;两方面入手&#xff0c;以下是详细说明&#xff1a; 一、RabbitMQ的核心特点 RabbitMQ是基于AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;协议…

项目升级啦

公司要新做一个医疗行业的业务&#xff0c;经过业务端和产品端的评估该业务与公司已有的产品线关联不大&#xff0c;用户后续也不想在老系统那台老爷车上继续使用&#xff0c;话说老系统到现在差不多10年了&#xff0c;中间经历过的前后端开发者形形色色&#xff0c;维护者换了…

Android中页面生命周期变化

一、Activity切换的生命周期变化&#xff08;A启动B&#xff09;1. 标准流程&#xff08;B完全覆盖A&#xff09;完整生命周期路径&#xff1a;Activity A&#xff1a;onPause()&#xff1a;失去焦点&#xff0c;仍部分可见onStop()&#xff1a;完全不可见&#xff08;当B完全覆…

自动驾驶控制算法——PID算法

自动驾驶控制算法——PID算法 文章目录自动驾驶控制算法——PID算法一、PID 是什么&#xff1f;二、PID 原理2.1 **比例环节&#xff08;P&#xff09;**2.2 **积分环节&#xff08;I&#xff09;**2.3 **微分环节&#xff08;D&#xff09;**2.4 特点总结2.5 案例分析 —— 小…

Spring Boot 异步执行方式全解析:@Async、CompletableFuture 与 TaskExecutor 对比

在 Spring Boot 开发中&#xff0c;异步执行是提升系统性能的重要手段&#xff0c;尤其适用于处理耗时操作&#xff08;如日志记录、邮件发送、数据同步等&#xff09;。本文将深入对比 Spring Boot 中三种主流的异步实现方式 ——Async注解、手动CompletableFuture和直接使用T…

高效微调2:Prompt-Tuning原理与实战

高效微调2:Prompt-Tuning原理与实战 Prompt-Tuning原理介绍 代码 Prompt-Tuning原理介绍 Prompt-Tuning Prompt-Tuning的思想:冻结主模型全部参数,在训练数据前加入一小段Prompt,只训练Prompt的表示层,即一个Embedding模块。其中,Prompt.又存在两种形式,一种是hard promp…