📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! 📢本文由 JohnKi 原创,首发于 CSDN🙉 📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
- 🏳️🌈一、TcpServer.cpp
- 🏳️🌈二、TcpServer.hpp
-
- 2.1 枚举错误情况
- 2.2 基本结构
- 2.3 构造函数、析构函数
- 2.4 初始化方法
- 2.5 循环监听
-
- 2.5.1 server 0 – 单执行流版本
- 2.5.2 server 1 – 多进程版本
- 2.5.3 server 2 – 多线程版本
- 2.5.4 server 3 – 内存池
- 🏳️🌈三、TcpClient.cpp
- 👥总结
前面几篇文章中使用UDP协议实现了相关功能 ,这篇使用TCP协议实现客户端与服务端的通信
相比与UDP协议,TCP协议更加可靠,也更加复杂!与UDP类似,我们先写主函数,然后实现相关函数!
🏳️🌈一、TcpServer.cpp
服务端主函数使用智能指针构造Server对象,然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!
#include "TcpServer.hpp"
int main(int argc, char* argv[]){
if(argc != 2){
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
Die(1);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->InitServer();
tsvr->Loop();
return 0;
}
🏳️🌈二、TcpServer.hpp
2.1 枚举错误情况
与 UDP 同样的,我们先枚举错误情况,将其放在 common.hpp 中
enum {
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
2.2 基本结构
我们这里先实现最基本的 TCP 服务器,基本成员有端口号,文件描述符,与运行状态
class TcpServer{
public:
TcpServer(){}
void InitServer(){}
void Loop(int sockfd){}
~TcpServer(){}
private:
int _listensockfd; // 监听socket
uint16_t _port;
bool _isrunning;
};
2.3 构造函数、析构函数
构造函数初始化成员变量,析构函数无需处理!
- 这里需要为端口号设置一个默认值 – 8080
static const uint16_t gport = 8080;
// 构造函数
TcpServer(uint16_t port = gport)
:_port(port),_sockfd(gsockfd),_isrunning(false){}
// 析构函数
~TcpServer(){}
2.4 初始化方法
初始化函数主要分为三步:
获取连接需要使用 listen 函数(将套接字设置为监听模式,以便能够接受进入的连接请求)
·listen· 需要设置一个队列,用来保存等待连接地客户端,我们可以事先设置一个 #define BACKLOG 8 来定义这个队列长度
void InitServer() {
// 1. 创建 socket
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0) {
LOG(LogLevel::ERROR) << "create socket error: " << strerror(errno);
Die(2);
}
LOG(LogLevel::INFO) << "create sockfd success: " << _listensockfd;
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
// 2. 绑定 socket
if (::bind(_listensockfd, CONV(&local), sizeof(local)) < 0) {
LOG(LogLevel::ERROR) << "bind socket error: " << strerror(errno);
Die(3);
}
LOG(LogLevel::INFO) << "bind sockfd success: " << _listensockfd;
// 3. 因为 tcp 是面向连接的,tcp需要未来不断地获取连接
// listen 就是监听连接的意思,所以需要设置一个队列,来保存等待连接的客户端
// 队列的长度为 8,表示最多可以有 8 个客户端等待连接
// listen(int sockfd, int backlog)
// sockfd 就是之前创建的 socket 句柄
// backlog 就是队列的长度
// 返回值:成功返回 0,失败返回 -1
if (::listen(_listensockfd, BACKLOG) < 0) {
LOG(LogLevel::ERROR) << "listen socket error: " << strerror(errno);
Die(4);
}
LOG(LogLevel::INFO) << "listen sockfd success: " << _listensockfd;
}
我们先将 Loop() 函数设计成死循环,验证一下 初始化函数 的正确性
// 测试
void Loop() {
_isrunning = true;
while (_isrunning) {
sleep(1);
}
_isrunning = false;
}
2.5 循环监听
执行服务函数主要分为两步:
2. 执行服务 (前提是获取到新连接)
2.5.1 server 0 – 单执行流版本
工作机制:
- 单线程通过while(1)循环依次处理每个客户端请求,处理完一个请求后才能处理下一个。
注意:tcp协议可以直接使用read,write函数读写文件描述符的内容(因为tcp是面向字节流的)!
// server – 0 单执行流版本
// 工作机制:单线程通过while(1)循环依次处理每个客户端请求,处理完一个请求后才能处理下一个。
void Loop() {
_isrunning = true;
while (_isrunning) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 1. 获取新连接
int sockfd = ::accept(_listensockfd, CONV(&client), &len);
if (sockfd < 0) {
LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);
continue;
}
InetAddr cli(client);
LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr()
<< " sockfd: " << sockfd;
// 获取成功
Server(sockfd, cli);
}
}
void Server(int sockfd, InetAddr& cli) {
// 长服务
while (true) {
char inbuffer[1024]; // 当作字符串
// 1. 读文件
// read
// 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
// 返回值:成功返回读入的字节数,失败返回 -1
ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) – 1);
if (n > 0) {
inbuffer[n] = 0;
LOG(LogLevel::INFO)
<< "get msg from " << cli.AddrStr() << " msg: " << inbuffer;
std::string echo_string = "[server echo]# ";
echo_string += inbuffer;
// 2. 写文件
write(sockfd, echo_string.c_str(), echo_string.size());
} else if (n == 0) {
LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";
break;
} else {
LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);
break;
}
}
::close(sockfd);
}
2.5.2 server 1 – 多进程版本
工作机制:
- 父进程通过fork()为每个新连接创建子进程,子进程处理完请求后退出。
- 但是进程创建/销毁开销大,高并发时资源耗尽
void Loop() {
_isrunning = true;
while (_isrunning) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 1. 获取新连接
int sockfd = ::accept(_listensockfd, CONV(&client), &len);
if (sockfd < 0) {
LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);
continue;
}
InetAddr cli(client);
LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr()
<< " sockfd: " << sockfd;
// 获取成功
pid_t id = fork();
if (id == 0) { // 子进程
::close(_listensockfd); // 关闭监听套接字(子进程不需要监听)
// 孙子进程的创建
if (fork() > 0)
exit(0); // 父进程(子进程)退出,孙子进程继续运行
// 孙子进程执行实际服务逻辑
Server(sockfd, cli);
exit(0);
}
// 父进程(主进程)
::close(sockfd); // 关闭连接套接字(父进程不处理具体业务)
int n = waitpid(id, nullptr, 0); // 等待子进程退出
if (n > 0) {
LOG(LogLevel::INFO) << "wait chid success";
}
}
_isrunning = false;
}
void Server(int sockfd, InetAddr& cli) {
// 长服务
while (true) {
char inbuffer[1024]; // 当作字符串
// 1. 读文件
// read
// 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
// 返回值:成功返回读入的字节数,失败返回 -1
ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) – 1);
if (n > 0) {
inbuffer[n] = 0;
LOG(LogLevel::INFO)
<< "get msg from " << cli.AddrStr() << " msg: " << inbuffer;
std::string echo_string = "[server echo]# ";
echo_string += inbuffer;
// 2. 写文件
write(sockfd, echo_string.c_str(), echo_string.size());
} else if (n == 0) {
LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";
break;
} else {
LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);
break;
}
}
::close(sockfd);
}
双重 fork() 的核心目的
- 若子进程直接处理请求并退出,父进程需通过 waitpid 回收资源,否则子进程会成为僵尸进程。
- 高并发场景下,父进程可能因频繁调用 waitpid 而阻塞,无法及时处理新连接。
- 解决方案:
- 子进程创建孙子进程后立即退出,孙子进程成为 孤儿进程,由 init 进程(PID=1)接管并自动回收资源。
- 父进程只需等待子进程(短暂存在)退出,避免阻塞。
- 父进程的 waitpid 仅需等待子进程(而非孙子进程),子进程退出速度极快(仅执行 fork() 和 exit()),父进程迅速返回 accept 循环。
- 提升服务器并发处理能力。
2.5.3 server 2 – 多线程版本
工作机制:
- 为每个新连接分配独立的线程处理业务逻辑
// server – 2 多线程版本
// 为每个新连接分配独立的线程处理业务逻辑
// (Loop 函数)通过 accept 循环监听新连接,为每个新连接创建线程(pthread_create)传递连接信息给子线程(通过 ThreadDate 结构体)
// (Execute 函数)调用 pthread_detach 分离自身(避免主线程调用 pthread_join)执行 Server 函数处理具体业务逻辑线程结束后自动释放资源(通过 delete td)
// (Server 函数)循环读取客户端数据(read),处理业务逻辑(示例中的回显服务),发送响应(write)关闭连接(close(sockfd))
void Loop(){
_isrunning = true;
while(_isrunning){
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 1. 获取新连接
int sockfd = ::accept(_listensockfd, CONV(&client), &len);
if(sockfd < 0){
LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);
continue;
}
InetAddr cli(client);
LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr() << " sockfd: " << sockfd;
// 获取成功
pthread_t tid;
ThreadDate* td = new ThreadDate(sockfd, this, cli);
// pthread_create 第一个参数是线程id,第二个参数是线程属性,第三个参数是线程函数,第四个参数是线程函数参数
pthread_create(&tid, nullptr, Execute, td);
}
_isrunning = false;
}
// 线程函数参数对象
class ThreadDate{
public:
int _sockfd;
TcpServer* _self;
InetAddr _addr;
public:
ThreadDate(int sockfd, TcpServer* self, const InetAddr& addr)
: _sockfd(sockfd), _self(self), _addr(addr)
{}
};
// 线程函数
static void* Execute(void* args){
ThreadDate* td = static_cast<ThreadDate*>(args);
// 子线程结束后由系统自动回收资源,无需主线程调用 pthread_join
pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
td->_self->Server(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
void Server(int sockfd, InetAddr& cli){
// 长服务
while(true){
char inbuffer[1024]; // 当作字符串
// 1. 读文件
// read 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
// 返回值:成功返回读入的字节数,失败返回 -1
ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) – 1);
if(n > 0){
inbuffer[n] = 0;
LOG(LogLevel::INFO) << "get msg from " << cli.AddrStr() << " msg: " << inbuffer;
std::string echo_string = "[server echo]# ";
echo_string += inbuffer;
// 2. 写文件
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if(n == 0){
LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";
break;
}
else{
LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);
break;
}
}
::close(sockfd);
}
2.5.4 server 3 – 内存池
将执行服务的函数入线程池队列,该函数需要是参数为空和返回值为void的函数,因此需要bind绑定函数!
using task_t = std::function<void(int sockfd, InetAddr& cli)>;
// server – 3 内存池版本
void Loop() {
_isrunning = true;
while (_isrunning) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 1. 获取新连接
int sockfd = ::accept(_listensockfd, CONV(&client), &len);
if (sockfd < 0) {
LOG(LogLevel::ERROR) << "accept socket error: " << strerror(errno);
continue;
}
InetAddr cli(client);
LOG(LogLevel::INFO) << "accept new connection from " << cli.AddrStr()
<< " sockfd: " << sockfd;
// 获取成功
task_t task = std::bind(&TcpServer::Server, this, sockfd, cli);
ThreadPool<task_t>::getInstance()->Equeue(task);
}
_isrunning = false;
}
void Server(int sockfd, InetAddr& cli) {
// 长服务
while (true) {
char inbuffer[1024]; // 当作字符串
// 1. 读文件
// read
// 函数的第一个参数是文件描述符,第二个参数是读入缓冲区,第三个参数是读入的字节数
// 返回值:成功返回读入的字节数,失败返回 -1
ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) – 1);
if (n > 0) {
inbuffer[n] = 0;
LOG(LogLevel::INFO)
<< "get msg from " << cli.AddrStr() << " msg: " << inbuffer;
std::string echo_string = "[server echo]# ";
echo_string += inbuffer;
// 2. 写文件
write(sockfd, echo_string.c_str(), echo_string.size());
} else if (n == 0) {
LOG(LogLevel::INFO) << "client " << cli.AddrStr() << " closed";
break;
} else {
LOG(LogLevel::ERROR) << "read socket error: " << strerror(errno);
break;
}
}
::close(sockfd);
}
🏳️🌈三、TcpClient.cpp
#include "TcpClient.hpp"
int main(int argc, char* argv[]){
if(argc != 3){
std::cerr << "Usage: " << argv[0] << " <server_ip> <server_port>" << std::endl;
Die(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建套接字
// AF_INET: IPv4协议
// SOCK_STREAM: TCP协议
// 0: 表示默认协议
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0){
std::cerr << "Create socket error: " << std::strerror(errno) << std::endl;
Die(2);
}
// 2. 设置服务器地址
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // IPv4协议
server.sin_port = htons(serverport); // 端口号
// 这句话表示将字符串形式的IP地址转换为网络字节序的IP地址
// inet_pton函数的作用是将点分十进制的IP地址转换为网络字节序的IP地址
// 这里的AF_INET表示IPv4协议
// 这里的serverip.c_str()表示IP地址的字符串形式
// &server.sin_addr表示将IP地址存储到sin_addr成员变量中
::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // IP地址
// 3. 与服务器建立连接
int n = ::connect(sockfd, CONV(&server), sizeof(server));
if(n < 0){
std::cerr << "Connect error: " << std::strerror(errno) << std::endl;
Die(5);
}
// 4. 发送消息
while(true){
std::string mag;
std::cout << "Enter# ";
std::getline(std::cin, mag);
write(sockfd, mag.c_str(), mag.size());
char echo_buf[1024];
n = read(sockfd, echo_buf, sizeof(echo_buf));
if(n > 0){
echo_buf[n] = 0;
std::cout << "Echo: " << echo_buf << std::endl;
}
else{
break;
}
}
// 5. 关闭套接字
::close(sockfd);
return 0;
}
👥总结
本篇博文对 【Linux网络】各版本TCP服务器构建 – 从理解到实现 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~
评论前必须登录!
注册