今天我计划通过一个小型项目,系统讲解线程池与网络编程的核心原理及实践。项目将围绕 “利用线程池实现高并发网络通信” 这一核心需求展开,具体设计如下:
- 为保证线程安全,线程池采用单例模式设计,确保全局唯一实例避免资源竞争;
- 技术栈采用 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;
}
-
构造函数
ThreadPool::ThreadPool(size_t pool_size)
- 初始化列表中设置了线程池大小
_pool_size
和停止标志_is_stop
(初始为true
表示未运行) - 调用
pthread_mutex_init
和pthread_cond_init
初始化了互斥锁和条件变量(线程同步的核心工具) - 最后调用
start()
方法启动线程池(实际创建并启动工作线程)
- 初始化列表中设置了线程池大小
-
单例获取函数
ThreadPool::getInstance()
- 使用 C++11 的
static
局部变量特性实现单例模式 - 局部静态变量
instance
会在第一次调用时初始化,且保证线程安全 - 每次调用都返回同一个实例的引用,确保整个程序中只有一个线程池实例
- 使用 C++11 的
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;}}
}
-
worker 函数
这是线程的入口函数(符合 pthread 库对线程函数的要求),作用是:- 通过
static_cast
将传入的void*
参数转换为线程池实例指针 - 调用线程池的
work()
方法,让线程进入实际的任务处理循环
它相当于一个适配层,将 pthread 库的 C 风格函数接口与 C++ 的类方法衔接起来。
- 通过
-
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);}
}
-
启动状态检查
首先判断_is_stop
标志,如果线程池已经处于运行状态(_is_stop
为false
),则直接返回,避免重复启动。 -
初始化准备
将_is_stop
设为false
(标记线程池进入运行状态),并通过_threads.reserve(_pool_size)
预先为存储线程 ID 的容器分配内存,提升后续插入效率。 -
创建工作线程
循环_pool_size
次(线程池预设的线程数量),每次调用pthread_create
创建一个线程:- 线程入口函数为
worker
(之前实现的线程工作函数) - 传入
this
指针作为当前线程池实例指针,让工作线程能访问线程池的任务队列等资源
- 线程入口函数为
-
错误处理
若线程创建失败(pthread_create
返回非 0),则通过perror
打印错误信息,将_is_stop
重置为true
(标记线程池停止),并退出函数。 -
记录线程 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);
}
-
停止状态检查
首先判断_is_stop
标志,如果线程池已经处于停止状态,则直接返回,避免重复停止操作。 -
触发停止机制
- 将
_is_stop
设为true
(标记线程池进入停止状态) - 调用
pthread_cond_broadcast(&_cond)
唤醒所有等待在条件变量上的工作线程(避免线程一直阻塞在等待任务的状态)
- 将
-
回收工作线程
遍历存储线程 ID 的_threads
容器,通过pthread_join
等待每个工作线程执行完毕并回收资源,最后清空容器。这一步确保所有线程都正常退出,避免僵尸线程。 -
清理任务队列
- 加锁保护任务队列操作
- 循环清空队列中剩余的未执行任务,逐个释放任务对象的内存
- 解锁完成清理
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);
}
-
参数与状态检查
先判断线程池是否已停止(_is_stop
为true
)或任务指针为空,若满足任一条件则直接返回,避免向已停止的线程池添加任务或添加无效任务。 -
线程安全的任务入队
- 加锁(
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);
}
-
调用 stop () 方法
首先调用stop()
,确保线程池在销毁前已经停止运行:包括唤醒所有工作线程、回收线程资源、清理未执行的任务等(这些逻辑已在stop()
中实现)。这一步是为了避免线程池对象销毁后,仍有线程在后台运行或资源未释放的情况。 -
销毁同步机制
- 调用
pthread_mutex_destroy(&_mutex)
销毁互斥锁,释放其占用的系统资源 - 调用
pthread_cond_destroy(&_cond)
销毁条件变量,同样释放相关系统资源
- 调用
四、服务器和客户端的通信流程
1、服务器端(像收件邮箱服务器)
- 新建 socket → 架起 “邮件接收系统”,准备收邮件
- bind 绑定 → 确定自己叫
xxx@qq.com
,让别人能找到 - listen 监听 → 开通 “同时收多封邮件” 功能,别一来就挤崩
- accept 等待 → 守着等你点 “发送”,接住你的邮件请求
- read/write 收发 → 收你发的邮件内容,还能回 “已收到” 提示
- close 关闭 → 这次发信结束,等下次你再发
2、客户端(像你用邮箱发信)
- 新建 socket → 打开手机邮箱 App,准备发邮件
- connect 连接 → 填对方邮箱点 “发送”,主动找服务器
- read/write 收发 → 写邮件、发出去,还能收到 “发送成功”
- 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;
}
-
创建服务器套接字
通过socket(AF_INET, SOCK_STREAM, 0)
创建 TCP 套接字(SOCK_STREAM
表示流式协议,即 TCP),失败则返回错误。 -
设置套接字选项
调用setsockopt
设置SO_REUSEADDR
选项,允许端口在服务器重启后快速重用(避免因端口未完全释放导致的启动失败)。 -
绑定地址和端口
- 初始化
server_addr
结构体,指定 IPv4 协议(AF_INET
)、监听所有网卡(INADDR_ANY
)和端口(_port
,通过htons
转换为网络字节序) - 调用
bind
将套接字与地址端口绑定,失败则关闭套接字并返回。
- 初始化
-
开始监听连接
listen(_server_sock, 5)
启动监听,设置等待连接的队列长度为 5(最多同时有 5 个客户端在队列中等待处理)。 -
循环接受客户端连接
- 进入
while(!_is_stop)
循环,持续等待客户端连接 accept
函数阻塞等待新连接,成功后返回客户端套接字client_sock
和客户端地址信息- 打印客户端 IP 地址,标识新连接建立
- 进入
-
用线程池处理连接
将客户端套接字封装成NetworkTask
任务,通过线程池的addTask
方法提交给线程池处理,实现高并发(避免为每个连接单独创建线程)。
3、服务器停止运行
void Server::stop()
{_is_stop = true;close(_server_sock);
}
-
设置停止标志
将_is_stop
设为true
,用于终止start()
方法中接受连接的循环(while(!_is_stop)
),让服务器退出等待新连接的状态。 -
关闭服务器套接字
调用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);
}
-
初始化缓冲区
创建BUFSIZ
大小的字符数组buff
(系统默认缓冲区大小,通常为 8192 字节),并初始化为 0,用于临时存储从客户端读取的数据。 -
进入通信循环
通过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
输出错误详情并退出循环。
- 读取数据:调用
-
清理资源
循环结束后,调用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 ..