文章目录
91. TCP断开连接的时候为什么必须4次而不是3次?
92. 为什么要区分用户态和内核态?
93. 说说编写socket套接字的步骤
编写一个基于套接字(socket)的网络程序通常包括以下步骤,无论是客户端还是服务器都需要遵循这些步骤。下面分别说明服务器和客户端编写的步骤,这些是简单的代码示例,仅仅帮助大家去理解这个过程。
1. 服务器端编写步骤
1.1 创建套接字
使用 socket() 函数创建一个套接字。这个函数返回一个套接字描述符,用于后续的操作。
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
1.2 绑定套接字
将创建的套接字绑定到指定的 IP 地址和端口号上,使用 bind() 函数。
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
网络字节序是大端模式
address.sin_addr.s_addr = INADDR_ANY;指绑定端口到本地所有网络接口
1.3 监听连接
使用 listen() 函数使套接字进入监听状态,等待客户端连接请求。
if (listen(server_fd, 3) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
1.4 接受连接
使用 accept() 函数接受客户端的连接请求,返回一个新的套接字描述符,用于与客户端通信。
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
perror("accept");
close(server_fd);
exit(EXIT_FAILURE);
}
1.5 数据传输
使用 read() 和 write() 函数(或 recv() 和 send() 函数)进行数据的接收和发送。
char buffer[1024] = {0};
read(new_socket, buffer, 1024);
printf("Message from client: %s\n", buffer);
send(new_socket, "Hello from server", strlen("Hello from server"), 0);
1.6 关闭套接字
完成通信后,使用 close() 函数关闭套接字。
close(new_socket);
close(server_fd);
2. 客户端编写步骤
2.1 创建套接字
与服务器端类似,使用 socket() 函数创建一个套接字。
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
2.2 连接服务器
使用 connect() 函数将套接字连接到服务器端的指定 IP 地址和端口号。
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sock);
exit(EXIT_FAILURE);
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection Failed");
close(sock);
exit(EXIT_FAILURE);
}
2.3 数据传输
同样使用 read() 和 write() 或 recv() 和 send() 进行数据的发送和接收。
send(sock, "Hello from client", strlen("Hello from client"), 0);
char buffer[1024] = {0};
read(sock, buffer, 1024);
printf("Message from server: %s\n", buffer);
2.4 关闭套接字
完成通信后,使用 close() 函数关闭套接字。
close(sock);
94. 什么是大小端模式,编写代码区分大小端
如何检查自己的电脑 是大端还是小端?
第一种方法:
#include <iostream>
bool isLittleEndian() {
int num = 1;
// 数值 1 的 32 位表示是:0x00 00 00 01(从高到低 4 个字节)。
// 大端(big-endian)把最高有效字节放在最低地址,所以内存从低地址到高地址是:
// 00 00 00 01
// 于是 reinterpret_cast<char*>(&num) 指向的第一个字节就是 0x00
// 小端(little-endian)相反,把最低有效字节放在最低地址,所以是:
// 01 00 00 00
// 这时第一个字节是 0x01
// 换个更形象的例子:如果 num = 0x12 34 56 78,
// 大端内存排布(低→高地址):12 34 56 78
// 小端内存排布(低→高地址):78 56 34 12
char *c = reinterpret_cast<char*>(&num);
return *c == 1; // 如果最低有效字节在最低地址处,则为小端字节序
}
int main() {
if (isLittleEndian()) {
std::cout << "This is a little-endian system." << std::endl;
} else {
std::cout << "This is a big-endian system." << std::endl;
}
return 0;
}
第二种方法:
利用数据类型的存储方式来判断当前系统的字节序。常见的实现方法是使用 union 联合体来共享内存,并通过访问不同的成员来检查数据的存储顺序。
#include <iostream>
#include <cstdint>
// 定义一个联合体,包含一个整数和一个字符数组
// 联合体(union)的所有成员共享同一段内存;写一个成员,换个成员读,能看到相同内存里的原始字节
union {
// uint8_t:精确 8 位 无符号整数,范围 0 ~ 255。
// uint32_t:精确 32 位 无符号整数,范围 0 ~ 4,294,967,295。
uint32_t i; // 32 位整数
uint8_t c[4]; // 4 字节字符数组
} test;
int main() {
test.i = 0x12345678; // 将一个已知的 32 位整数存入联合体中
// 根据第一个字节的值判断大小端
if (test.c[0] == 0x78) {
std::cout << "小端模式 (Little-endian)" << std::endl;
} else if (test.c[0] == 0x12) {
std::cout << "大端模式 (Big-endian)" << std::endl;
} else {
std::cout << "无法确定字节序" << std::endl;
}
return 0;
}
95. 代码实现:实现简单的智能指针
下面是一个简单的智能指针实现的例子,用于管理动态分配的内存,避免内存泄漏。这个示例实现了一个类似于 std::shared_ptr 的简单智能指针,叫做 SimpleSmartPointer,它使用引用计数来管理对象的生命周期。
是在定义一个新对象 sp2,带着一个“初始值”sp1。
在 C++ 里,带初始值的定义叫“拷贝初始化(copy-initialization)”,它会调用拷贝构造函数(或能匹配的移动构造),不会调用赋值运算符。
赋值运算符只在对象已经存在之后再用 = 给它“换内容”时才会被调用。
SimpleSmartPointer<int> a(new int(10));
// 1) 拷贝初始化:调用拷贝构造函数
SimpleSmartPointer<int> b = a; // == SimpleSmartPointer<int> b(a);
// 2) 直接初始化:也调用拷贝构造函数
SimpleSmartPointer<int> c(a);
// 3) 先默认构造一个对象,再赋值:调用赋值运算符 operator=
SimpleSmartPointer<int> d; // 等价于 SimpleSmartPointer<int> d(nullptr);
d = a; // 这里才会走你的 operator=
#include <iostream>
// 简单智能指针类
template<typename T>
class SimpleSmartPointer {
private:
T* ptr; // 原生指针
unsigned* count; // 引用计数
public:
// 构造函数,接受一个原生指针
//这个explicit 主要目的是防止隐式类型转换。提高代码可读性和安全性
explicit SimpleSmartPointer(T* p = nullptr) : ptr(p) {
if (p) {
count = new unsigned(1); // 初始化引用计数为1
} else {
count = nullptr;
}
}
// 拷贝构造函数
SimpleSmartPointer(const SimpleSmartPointer<T>& sp) : ptr(sp.ptr), count(sp.count) {
if (count) {
(*count)++; // 增加引用计数
}
}
// 赋值运算符重载
SimpleSmartPointer<T>& operator=(const SimpleSmartPointer<T>& sp) {
if (this == &sp) {
return *this; // 防止自我赋值
}
// 释放当前资源
//--(*count):对 *count(引用计数值)进行自减操作,表示当前对象不再使用该资源
//如果 *count 为 0,说明已经没有其他智能指针对象在使用这个资源了,此时需要释放资源
if (count && --(*count) == 0) {
delete ptr;
delete count;
}
// delete ptr; 释放的是 ptr 指向的那块堆内存(并调用析构),并不会“删掉变量 ptr 本身”
// 赋值新资源
ptr = sp.ptr;
count = sp.count;
if (count) {
(*count)++;
}
return *this;
}
// 解引用运算符重载
// 返回类型:对 T 的引用。有了引用返回,*sp 就是一个可当左值用的对象(能读也能改)。
// 函数名是个特殊运算符函数:重载“一元解引用运算符 *”。
// 调用方式:*sp 等价于 sp.operator*()
// operator* 是运算符重载函数,重载的是“一元解引用运算符 *”(注意不是乘法;乘法是二元 *)。
// 这是一个成员函数,当你写 *sp 时,编译器会把它当作:
// sp.operator*() // 调用你这个函数
// ptr 是你类里存的裸指针(T*)。
// *ptr 是对这个裸指针的解引用,得到“那个 T 对象本身”。结合返回类型 T&,就把“托管对象”的引用交给了调用者。
T& operator*() const {
return *ptr;
}
// 指针访问运算符重载
// T* operator->() const { return ptr; }
// 把内部的裸指针 ptr(类型 Point*)拿出来,然后再用普通指针的 -> 去调用 print() / move() / 访问 x、y。所以 p->成员 就能像真指针那样用起来了
// 前提是裸指针对应的数据结构里面有定义成员变量或者成员函数
// operator->() 只有在 T 有成员时才有用;你现在用的是 SimpleSmartPointer<int>,int 没成员,所以用不上。给它换个有成员的类型,比如 Point,就能直接写 sp->成员/方法 了。
T* operator->() const {
return ptr;
}
// 返回当前共享计数;空指针时按惯例返回 0
unsigned use_count() const {
return count ? *count : 0;
}
// 获取原生指针
T* get() const {
return ptr;
}
// 析构函数 RAII
~SimpleSmartPointer() {
if (count && --(*count) == 0) {
delete ptr;
delete count;
}
}
};
// 测试函数
void testSimpleSmartPointer() {
// sp1.ptr 指向“int(10)”;
// sp1.count 指向一块 unsigned,其值 *count == 1
SimpleSmartPointer<int> sp1(new int(10)); // 创建一个智能指针,管理整数10
std::cout << "sp1: " << *sp1 << std::endl; // 输出sp1所指向的值
std::cout << "sp1.use_count() = " << sp1.use_count() << "\n";
{
// 拷贝构造函数 ← 就是被 “SimpleSmartPointer<int> sp2 = sp1;” 调用的这个
SimpleSmartPointer<int> sp2 = sp1; // sp2与sp1共享同一块内存
*sp2 = 33;
std::cout << "sp2: " << *sp2 << std::endl; // 输出sp2所指向的值
std::cout << "sp2.use_count() = " << sp2.use_count() << "\n";
std::cout << "sp1.use_count() = " << sp1.use_count() << "\n";
} // sp2超出作用域,引用计数减1
// 再次输出sp1所指向的值
std::cout << "sp1: " << *sp1 << std::endl;
std::cout << "sp1.use_count() = " << sp1.use_count() << "\n";
}
int main() {
testSimpleSmartPointer();
return 0;
}
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!