
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [1 ~> TCP](#1 ~> TCP)
-
- [1.1 文件目录树和指令手册](#1.1 文件目录树和指令手册)
- [1.2 为什么会有两个sockfd?](#1.2 为什么会有两个sockfd?)
- [1.3 多线程 / 线程池:为什么要"解耦"](#1.3 多线程 / 线程池:为什么要“解耦”)
- [1.4 代码架构](#1.4 代码架构)
- [1.5 Service函数:数据的"加工厂"](#1.5 Service函数:数据的“加工厂”)
- [1.6 为什么会bind(绑定)失败?](#1.6 为什么会bind(绑定)失败?)
- [1.7 理解一下TCP流程](#1.7 理解一下TCP流程)
- [1.8 TCP Socket 编程目前已有知识体系整理](#1.8 TCP Socket 编程目前已有知识体系整理)
- [2 ~> TCP代码演示](#2 ~> TCP代码演示)
-
- [2.1 服务端(TcpEchoServer.cc和TcpEchoServer.hpp)](#2.1 服务端(TcpEchoServer.cc和TcpEchoServer.hpp))
- [2.2 客户端(TcpEchoClient.cc)](#2.2 客户端(TcpEchoClient.cc))
- [2.3 运行结果](#2.3 运行结果)
- 结尾

1 ~> TCP
1.1 文件目录树和指令手册

1.2 为什么会有两个sockfd?

这怎么理解?我们讲一个故事就好理解了。
五一出去旅行,到旅游景点必然要吃饭,有各种饭庄、餐厅,这些饭庄也卷,卷到什么程度?专门派出一个人站在路口拉客,张三是"好再来鱼庄"的顶级销售:

我和朋友来西湖溜达的时候就被张三忽悠瘸了,拉进鱼庄吃饭了,进鱼庄的时候张三吆喝一声:"来客人了,后厨来个服务员",这时候出来一个李四,给我和朋友提供服务,有需求就找服务员李四,当我们吃饭的时候张三在干什么?继续忽悠新客人。这时候后厨又出来一个王五,为新客人服务。张三的核心工作是拉客,张三每拉一个客人,就会有一个服务员单独为新客人服务,李四和王五是服务客人(真正提供服务的服务员)------张三、李四、王五都是服务员,只不过张三的核心工作是拉客的销售。

别人怎么知道我的服务器连上了呢?IP地址端口号就是客人。

我们一般把传入的socket叫做这个socket是监听套接字,返回的叫做IO套接字。监听套接字只需要一个,而IO套接字会越来越多。
当我们写代码的时候,IO套接字的文件描述符(服务员,李四、王五......)会不会打满我不关心;我们关心监听套接字(张三)和客户端进行通信(销售推销)。
1.3 多线程 / 线程池:为什么要"解耦"

1.4 代码架构

1.5 Service函数:数据的"加工厂"

1.6 为什么会bind(绑定)失败?

1.7 理解一下TCP流程

1.8 TCP Socket 编程目前已有知识体系整理

2 ~> TCP代码演示
2.1 服务端(TcpEchoServer.cc和TcpEchoServer.hpp)
TcpEchoServer.cc
cpp
#include "TcpEchoServer.hpp"
#include <iostream>
#include <memory>
#include <string>
void Usage(std::string procname)
{
std::cout << "Usage: " << procname << "ServerPort" << std::endl;
}
// ./tcp_echo_server 8080
int main(int argc,char *argv[])
{
// 输出一个简单的手册
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
// 将日志输出方式设置为 输出到屏幕(终端/控制台) ,而不是写入文件
ENABLE_CONSOLE_LOG_STRATEGY();
// 智能指针
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>(port);
tsvr->Init();
tsvr->Start();
return 0;
}
TcpEchoServer.hpp
包括了四个版本的写法:
- 单进程
- 多进程
- 多线程
- 线程池
cpp
#ifndef __TcpEchoServer_HPP
#define __TcpEchoServer_HPP
#include <iostream>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
// 使用原生的线程
#include <pthread.h>
// 使用自己封装的线程池(还有两个附加的小挂件:互斥锁和条件变量,都要加上)
#include "ThreadPool.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
// 这样未来我就可以定义任务、构建任务类型
#include <functional>
using task_t = std::function<void()>;
// 网络通信三剑客
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 自己封装的
#include "Logger.hpp"
#include "InetAddr.hpp" // 今天不做主机序列和网络序列的转换工作了,直接把客户端地址拿过来
// 调用日志,命名空间
using namespace LogModule;
// 定义一个默认全局的端口号
static uint16_t gdefaultport = 8080;
// 设置全连接个数
static const int gbacklog = 32;
class TcpEchoServer
{
private:
// sockfd: 既可以支持读,又可以支持写, TCP socket也是全双工的.
// 通过这个套接字给对应的客户端提供服务
void Service(int sockfd,InetAddr client)
{
// 我想让这个服务一直进行
while(true)
{
// TCP可以直接采用文件的接口,不用网络接口,我今天先直接验证read/write
char inbuffer[1024];
// 1.读取数据
ssize_t n = read(sockfd,inbuffer,sizeof(inbuffer) - 1); // 默认字符串
if(n > 0) // 读取成功
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << client.StringAddress() << " say# " << inbuffer; // 客户端给我发了个什么消息
}
else if(n == 0) // 返回值为0表示读到文件结尾,= 0相当于客户端退出了
{
LOG(LogLevel::INFO) << client.StringAddress() << "close sockfd" << sockfd << ", me too!";
}
else // 读取客户端异常,但是这个日志等级要注意,一个客户端出错不影响其他客户端(餐厅吃饭,一个客人出问题不影响其他客人吃饭)
{
LOG(LogLevel::ERROR) << "read sockfd error" << sockfd;
break;
}
// 2.加工处理数据
std::string echo_string = "server echo# ";
echo_string += inbuffer;
// 3.写回数据
n = write(sockfd,echo_string.c_str(),echo_string.size());
if(n <= 0)
{
LOG(LogLevel::ERROR) << "write sockfd error" << sockfd;
break;
}
}
}
public:
TcpEchoServer(uint16_t port = gdefaultport) : _port(port),_listsockfd(-1)
{}
void Init()
{
// 1.创建套接字
_listsockfd = socket(AF_INET,SOCK_STREAM,0); // IPPROTO_TCP,设置0就知道是TCP了
// 创建套接字失败
if(_listsockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error: " << _listsockfd;
exit(2);
}
LOG(LogLevel::INFO) << "socket success: " << _listsockfd; // 这里012被占用,就是3
// 2.bind:设置服务器的socket信息
struct sockaddr_in local;
// 把local字段清零
memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 这里使用的是构造函数传入的_port
// 把主机序列转成网络序列
local.sin_addr.s_addr = htonl(INADDR_ANY);
// local.sin_addr = inet_addr(_ip);
int n = bind(_listsockfd,(const sockaddr *)&local,sizeof(local));
if(n < 0)
{
// 如果bind返回值小于0,表示绑定失败
LOG(LogLevel::FATAL) << "bind error: " << _listsockfd;
exit(3); // 直接记录FATAL日志,并且退出进程
}
LOG(LogLevel::INFO) << "bind success: " << _listsockfd;
// 3.将socket设置为监听状态
n = listen(_listsockfd,gbacklog);
if(n < 0)
{
LOG(LogLevel::FATAL) << "listen error" << _listsockfd;
exit(4);
}
LOG(LogLevel::INFO) << "listen success" << _listsockfd;
}
// 设计一个内部类:要它线程完成任务(传sockfd);线程的数据也要传
class ThreadData
{
public:
// 还是写一下构造函数
ThreadData(int sockfd,InetAddr addr,TcpEchoServer *owner) // owner要初始化一下
:_sockfd(sockfd),
_address(addr),
_owner(owner)
{}
public:
int _sockfd;
InetAddr _address;
// Service属于类内方法,static里面不能直接使用
TcpEchoServer *_owner;
};
// 定义ThreadRun,里面有this指针,要加static
static void* Threadrun(void *args)
{
// 主线程不能阻塞,那要怎么做?
// 线程分离 --> detach这个接口就是为今天这种情况准备的!
pthread_detach(pthread_self());
// 调用的时候要能够拿到sockfd
ThreadData *td = static_cast<ThreadData *>(args);
// Service属于类内方法,static里面不能直接使用-->必须加一个函数成员owner
td->_owner->Service(td->_sockfd,td->_address); // 这样就可以使用方法了
delete td;
return nullptr;
}
void Start()
{
// // 最佳实践
// signal(SIGCHLD,SIG_IGN); // 直接对子进程退出信号做忽略
// 死循环
while(true)
{
struct sockaddr_in clientaddr; // 客户端地址
socklen_t len = sizeof(clientaddr); // 客户端地址长度,提供的缓冲区大小
int sockfd = accept(_listsockfd,(struct sockaddr *)&clientaddr,&len); // 强转
if(sockfd < 0)
{
// 张三推销被拒绝会一蹶不振吗?拉客失败最多warning,不影响后面的拉客动作,继续accept
LOG(LogLevel::WARNING) << "accpet error";
continue;
}
// 获取地址了,就知道客户端连接了,得获取一下(IP地址和端口号)
InetAddr clientaddress(clientaddr);
LOG(LogLevel::INFO) << "get a new link: " << clientaddress.StringAddress() << " sockfd: " << sockfd;
// sleep(1);
// 处理连接,进行IO通信
// // Version0 -- 不会被使用的单进程版本
// Service(sockfd, clientaddress);
// // Version1 -- 多进程版本优化
// pid_t id = fork();
// if(id < 0) // 创建失败
// {
// LOG(LogLevel::ERROR) << "fork error";
// close(sockfd);
// }
// else if(id == 0)
// {
// // 子进程,拷贝文件描述符表,从而和父进程看到同一批文件
// if(fork() > 0) // > 0才是父进程退出
// exit(0);
// // 孙子进程
// Service(sockfd,clientaddress);
// exit(0);
// }
// else
// { }
// // 关闭没有时序问题,各管各的,不影响
// close(sockfd);
// // 父进程
// pid_t rid = waitpid(id,nullptr,0);
// (void)rid;
// // Version2 -- 多线程版本
// // --> 多线程这里不能关闭任何自己不用的sockfd <--
// // 其实我大可以使用以前我自己封装的线程,但是整理我直接用原生的线程,这样会更直观
// // 我不想创建进程,我要使用多线程降低创建进程的开销
// pthread_t tid;
// // pthread_create(&tid,nullptr);
// // 以new的方式新建,其它线程也能够看见
// ThreadData *td = new ThreadData(sockfd,clientaddress,this);
// pthread_create(&tid,nullptr,Threadrun,(void*)td);
// // // 这里的join是阻塞的!主线程不能卡在这里,主线程要继续去获取新链接才可以啊!
// // pthread_join(tid,nullptr);
// Version3 -- 创建线程池(单例模式) -- 是bug吗?其实是应用场景方面的限制
// [](){} -- lambda
// 按值捕获 sockfd 和 clientaddress,捕获 this 指针
ThreadPool<task_t>::GetInstance()->Enqueue([sockfd,clientaddress,this](){
Service(sockfd,clientaddress); // 这样线程池中的线程就能访问到客户端 socket 和地址信息
});
}
}
~TcpEchoServer()
{}
private:
uint16_t _port;
// 监听套接字
int _listsockfd;
};
#endif
2.2 客户端(TcpEchoClient.cc)
先这样写:
cpp
#include <iostream>
#include <string>
int main()
{}
2.3 运行结果
这里的运行结果我就直接给大家展示艾莉丝用Excalidraw Whiteboard(非常好用的一款画图板,比Win11自带的好用多了------艾莉丝用Win11画图板画思维导图的时候好几次花了很多时间好不容易画完了结果卡死未保存,给艾莉丝整emo了,就在草莓熊的推荐下使用了Excalidraw Whiteboard,确实是很不错的一款画图软件)绘制的思维导图中代码运行演示模块的截图啦。
单进程版本
cpp
// Version0 -- 不会被使用的单进程版本
Service(sockfd, clientaddress);

多进程版本
cpp
// Version1 -- 多进程版本优化
pid_t id = fork();
if(id < 0) // 创建失败
{
LOG(LogLevel::ERROR) << "fork error";
close(sockfd);
}
else if(id == 0)
{
// 子进程,拷贝文件描述符表,从而和父进程看到同一批文件
if(fork() > 0) // > 0才是父进程退出
exit(0);
// 孙子进程
Service(sockfd,clientaddress);
exit(0);
}
else
{ }
// 关闭没有时序问题,各管各的,不影响
close(sockfd);
// 父进程
pid_t rid = waitpid(id,nullptr,0);
(void)rid;
多进程的优化

多进程写法的特点

多进程写法存在反噬

多线程版本
cpp
// Version2 -- 多线程版本
// --> 多线程这里不能关闭任何自己不用的sockfd <--
// 其实我大可以使用以前我自己封装的线程,但是整理我直接用原生的线程,这样会更直观
// 我不想创建进程,我要使用多线程降低创建进程的开销
pthread_t tid;
// pthread_create(&tid,nullptr);
// 以new的方式新建,其它线程也能够看见
ThreadData *td = new ThreadData(sockfd,clientaddress,this);
pthread_create(&tid,nullptr,Threadrun,(void*)td);
// // 这里的join是阻塞的!主线程不能卡在这里,主线程要继续去获取新链接才可以啊!
// pthread_join(tid,nullptr);

线程池版本
cpp
// Version3 -- 创建线程池(单例模式) -- 是bug吗?其实是应用场景方面的限制
// [](){} -- lambda
// 按值捕获 sockfd 和 clientaddress,捕获 this 指针
ThreadPool<task_t>::GetInstance()->Enqueue([sockfd,clientaddress,this](){
Service(sockfd,clientaddress); // 这样线程池中的线程就能访问到客户端 socket 和地址信息
});

线程池这里的是BUG吗?其实是应用场景方面的限制!
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux网络】Linux 网络编程入门:UDP Socket 编程(下)
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
