今天开始正式补充完善webServer服务器的内容!想到哪写到哪吧,回头再做整合。

标准C库IO函数

项目难点

1
2
3
4
5
/*
1、如何处理接收到的HTTP请求
2、如何填写HTTP响应
3、如何建立网络连接传输数据
*/

HTTP请求处理

http_conn头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class http_conn{
public:
http_conn() {}
~http_conn() {}
static int m_epollfd;
static int m_user_count;

public:
void init(int sockfd, const so0ckaddr_in &addr); // 初始化连接
void close_conn(bool real_close = true); // 关闭连接,关于需要传入real_close参数的原因,后面会讲到,预留问题

private:
int m_sockfd; // 发起http请求的sockfd
sockaddr_in m_address; // 发起http请求的socket地址
}

我们在http_conn类里主要设置五个对外的接口:

  • 初始化新接受的连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void init(int sockfd, const sockaddr_in &addr);
    /*
    我们会把所有事件注册到一张内核事件表上,因此定义一个内核事件就好了。static int m_epollfd;
    同时我们会统计当前连接数,同样是使用一个静态变量,所有实例对象共享。static int m_user_count;
    每建立一个新连接,m_user_count就会加1;
    当我们向内核事件表注册一个事件时,我们需要考虑我们所要监听的事件类型,这里我们考虑:
    读事件(EPOLLIN)、边沿触发模式(EPOLLET)、以及EPOLLRDHUP(检测TCP对端连接的关闭或者半关闭状态)
    为了配合ET模式和多线程,我们需要做两个操作,第一是将所监听的文件描述符设置为非阻塞的,第二需要设置为EPOLLONESHOT类型
    注意:我们对m_epollfd和m_user_count的初始化分别为
    */
    int m_epollfd = -1;
    int m_user_count = 0;

    设置非阻塞函数

    1
    2
    3
    4
    5
    6
    7
    // 代码示例:Linux高性能服务器编程 p113
    int setnonblocking(int sockfd){
    int old_option = fcntl(sockfd, F_GETFL); // 获取文件描述符旧的状态标志
    int new_option = old_option | O_NONBLOCK; // 设置非阻塞标志
    fcntl(sockfd, F_SETFL, new_option);
    return old_option; // 返回文件描述符旧的状态标志,以便日后恢复该状态标志
    }

    设置感兴趣事件类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 我们在向内核事件表注册新事件的时候,需要指定自己对这个文件描述符上发生的什么事件感兴趣
    // 可读?可写?
    void addfd(int epollfd, int fd, bool one_shot){
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    if(one_shot){
    event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
    }

    初始化函数代码实现

    1
    2
    3
    4
    5
    6
    void http_conn::init(int sockfd, const sockaddr_in &addr){
    m_sockfd = sockfd;
    m_address = addr;
    addfd(m_epollfd, sockfd, true);
    m_user_count++;
    }
  • 关闭连接

    1
    2
    3
    4
    5
    6
    7
    void close_conn(bool real_close);
    /*
    我们可以思考一下有关关闭连接需要涉及到哪些操作。
    1、首先,如果一个连接关闭了,我们需要将其从内核事件表上移除
    2、当前连接数目也会减一
    这里我们准备先实现一个从内核事件表上移除文件描述符的函数(void removefd),在实现关闭连接
    */

    移除文件描述符

    1
    2
    3
    4
    void removefd(int epollfd, int fd){
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
    }

    关闭连接

    1
    2
    3
    4
    5
    6
    7
    void http_conn::close_conn(bool real_close){
    if(real_close && m_sockfd != -1){
    removefd(m_epollfd, m_sockfd);
    m_sockfd = -1;
    m_user_count--;
    }
    }
  • 处理客户请求

    1
    2
    3
    4
    5
    6
    void process();
    /*
    关于如何处理客户连接请求的问题,我们从最原始的地方出发。
    首先,你了解一个http请求的基本格式吗?因为我们只有在了解http请求的通用格式后才知道如何对其进行解析
    下面,我将展示一个最基本的GET请求格式
    */
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    GET /path/to/resource?param1=value1&param2=value2 HTTP/1.1
    Host: www.example.com
    User-Agnet: Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    # 关于这个GET请求的详细解释我提一个chatgpt的解释在这,可以阅读一下
    下面是逐行详细解释:
    1. `GET /path/to/resource?param1=value1&param2=value2 HTTP/1.1`
    - `GET`: 这是HTTP请求的方法。`GET` 方法用于请求指定的资源。与POST相比,GET请求是只读的,并且用于获取数据而不是发送数据。
    - `/path/to/resource`: 这是请求的资源路径,通常是文件或者其他资源的位置。
    - `?`: 这个符号表示URL的查询部分的开始。
    - `param1=value1&param2=value2`: 这是查询字符串。在此例中,有两个参数,`param1`和`param2`,它们的值分别是`value1`和`value2`。`&`符号用于分隔查询参数。
    - `HTTP/1.1`: 表示使用的HTTP版本,这里是1.1。

    2. `Host: www.example.com`
    - `Host`: 这是HTTP头的名称。它指定了请求的目标主机和域名。
    - `www.example.com`: 请求的目标域名。

    3. `User-Agent: Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion`
    - `User-Agent`: 这是HTTP头的名称。它描述了发出请求的用户代理的类型,通常是浏览器。
    - `Mozilla/5.0`: 这是用户代理的一般标记。虽然名为Mozilla,但它并不仅仅代表Mozilla浏览器,大多数浏览器都会以这种方式标识。
    - `(platform; rv:geckoversion)`: 这部分提供了关于用户代理的详细信息,例如它在哪个平台上运行。
    - `Gecko/geckotrail`: 这是Gecko渲染引擎的标识及其版本。
    - `Firefox/firefoxversion`: 表示用户代理是Firefox浏览器,后面跟着其版本。

    4. `Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8`
    - `Accept`: HTTP头名称,表示客户端可以处理的内容类型。
    - 该头的值列出了浏览器接受的MIME类型,按照优先级排序。例如,`text/html` 表示HTML文档,而`q=0.9`表示相对优先级。

    5. `Accept-Language: en-US,en;q=0.5`
    - `Accept-Language`: HTTP头名称,表示用户代理偏好的自然语言。
    - `en-US,en`: 这指示用户代理首先希望接收美国英语的内容,其次是英语。

    6. `Accept-Encoding: gzip, deflate, br`
    - `Accept-Encoding`: HTTP头名称,表示用户代理可以接受的内容编码。
    - `gzip, deflate, br`: 这些是可以接受的编码方法,用于内容压缩。

    7. `Connection: keep-alive`
    - `Connection`: HTTP头名称,表示是否持续连接。
    - `keep-alive`: 表示浏览器希望服务器保持连接,以便于后续的请求可以复用相同的TCP连接。

    这个请求大体上是一个典型的HTTP GET请求,由HTTP方法、资源路径、HTTP版本、多个头字段组成。每个头字段都有其特定的语义和目的。

    现在我们知道了HTTP请求格式了,那么到底如何解析它呢?这里就要引入一种叫做“有限状态机”的方法了,有关这个方法的具体描述与实现,大家可以看我的另一篇文章。

  • 非阻塞读操作

  • 非阻塞写操作

第一个知识点:iovec

这里先介绍一个iovec结构体,因为我们在写HTTP响应的时候需要用到。

1
2
3
4
5
6
7
8
#include <sys/uio.h>

struct iovec{
ptr_t iov_base;
size_t iov_len;
};

// struct iovec结构体,指针成员iov_base指向一个缓冲区,这个缓冲区是存放read_v所接收的数据或者write_v将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度和实际写入的长度。
1
2
int readv(int fd, const struct iovec *vector, int count);
int writev(int fd, const struct iovec *vector, int count);

下面给出一个应用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <sys/uio.h>

int main(){
static char part1[] = "This is from writev";
static int part2 = 65;
static char part3[] = "[";

struct iovec iov[3];

iov[0].iov_base = part3;
iov[0].iov_len = strlen(part3);

iov[1].iov_base = part1;
iov[1].iov_len = strlen(part1);

iov[2].iov_base = &part2;
iov[2].iov_len = sizeof(int);

writev(1, iov, 3);

printf("\n");

return 0;
}

第二个知识点:va_list, vsnprintf

参考链接:https://blog.csdn.net/dengzhilong_cpp/article/details/54944676

参考链接:https://blog.csdn.net/luliplus/article/details/124123219

以上是今天要写代码的基础知识,下面开始正式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*写HTTP响应*/
bool http_conn::write(){
int temp = 0;
int bytes_have_send = 0;
int bytes_to_send = m_write_idx;
if(bytes_to_send == 0){
modfd(m_epollfd, m_sockfd, EPOLLIN);
init();
return true;
}

while(1){
temp = writev(m_sockfd, m_iv, m_iv_count);
if(temp <= -1){
if(errno == EAGAIN){
modfd(m_epollfd, m_sockfd, EPOLLIN);
return true;
}
unmap();
return false;
}

bytes_to_send -= temp;
bytes_have_send += temp;
if(bytes_to_send <= bytes_have_send){
unmap();
if(m_linger){
init();
modfd(m_epollfd, m_sockfd, EPOLLIN);
return true;
}
else{
modfd(m_epollfd, m_sockfd, EPOLLIN);
return false;
}
}
}
}

面试时被问到了关于如何将中缀表达式转换为后缀表达式,这里总结一下转换的步骤

  1. 遇到操作数,直接输出
  2. 栈为空时,遇到运算符,入栈
  3. 遇到左括号,将其入栈
  4. 遇到右括号,执行出栈操作,并将出栈的元素输出,直到弹出栈的是左括号,左括号不输出
  5. 遇到其他运算符“+”,“-”,“*”,“/”时,弹出所有优先级大于或等于该运算符的栈顶元素,然后将该运算符入栈
  6. 最终将栈中的元素依次出栈,输出

(啊!先写到这吧,为了弄个图片弄了好久)

参考链接:https://blog.csdn.net/y_16041527/article/details/79684188

我们今天来实现一个简易版的智能指针吧!拖了好久啦,以后会把vector、string的简易版也给补上,gigigi

我们可以先想想shared_ptr最大的特点,就是当他的引用计数为0时,便会自动释放所指对象和析构。所以一个关键点就是这个引用计数怎么设置?static?不可以!static变量同属于一个类的所有对象,这样就会导致不管指的是不是同一个对象,引用计数都相同。所以我们这里决定采用一个指针来进行引用计数。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <iostream>
#include <mutex>
#include <string>

template <typename T>
class shared_ptr{
public:
explicit shared_ptr(T* sPtr = nullptr): sPtr_(sPtr){
if(sPtr_){
useCount_ = new int(1);
sMutex_ = new std::mutex;
}
}

~shared_ptr(){
release();
}

shared_ptr(const shared_ptr<T>& sp){
useCount_ = sp.useCount_;
sPtr_ = sp.sPtr_;
sMutex_ = sp.sMutex_;
addUsecount();
}

shared_ptr<T>& operator=(const shared_ptr<T>& sp){
if(sPtr_ != sp.sPtr_){
release();
sPtr_ = sp.sPtr_;
useCount_ = sp.useCount_;
sMutex_ = sp.sMutex_;
addUsecount();
}
return *this;
}

T& operator*(){
return *sPtr_;
}

T* operator->(){
return sPtr_;
}

int useCount(){
return *useCount_;
}

T* get(){
return sPtr_;
}
private:
void addUsecount(){
sMutex_->lock();
++(*useCount_);
sMutex_->unlock();
}

void release(){
bool deleteFlag = false;
sMutex_->lock();
if(--(*useCount_) == 0){
delete useCount_;
delete sPtr_;
deleteFlag = true;
}
sMutex_->unlock();
if(deleteFlag){
delete sMutex_;
}
}
private:
int* useCount_;
T* sPtr_;
std::mutex* sMutex_;
};


int main(){
shared_ptr<std::string> p1(new std::string("hello"));
shared_ptr<std::string> p2 = p1;
//std::cout << p2.useCount() << std::endl;
shared_ptr<std::string> p3(p2);
std::cout << p3.useCount() << std::endl;
std::cout << p2.useCount() << std::endl;
p2 = p3;
std::cout << p3.useCount() << std::endl;
std::cout << p2.useCount() << std::endl;
return 0;
}

插入结果图片有点问题,最近没时间弄,各位自己跑一下验证一下,有错误记得说!!!

new和malloc的区别

  • 属性的区别

    new/delete:这两个是C++中的关键字;

    malloc/free:这两个是库函数;

  • 使用上的区别

    malloc:申请空间需要显式填入申请内存的大小;

    new:无需显式填入申请内存的大小,new会根据new的类型分配内存;

  • 返回类型的区别

    new操作符内存分配成功,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,故new是符合类型安全性的操作符。

    malloc内存分配成功返回的是void*指针,需要通过强制类型转换,转换成我们需要的类型。

    所以C++中new比malloc安全可靠。

  • 分配失败的区别

    malloc分配失败会返回NULL,我们可以通过判断返回值是否是NULL得知是否分配成功。

    new分配失败会抛出bad_alloc异常。

  • 扩张内存的区别

    malloc有内存扩张机制(通过realloc实现)。

    new没有扩张内存机制。

https://zhuanlan.zhihu.com/p/338489910

如何避免内存泄漏

  • 明确动态内存使用范围:在程序中使用动态内存时,需要明确该内存的使用范围,确保在不需要使用该内存时能够及时释放内存。
  • 使用RAII技术:RAII(Resource Acquisition Is Initialization)是C++中一种常用的资源管理技术,它利用了C++对象的构造函数和析构函数自动调用的特性,在对象的构造函数中申请资源,在对象的析构函数中释放资源,从而避免资源泄漏问题。
  • 使用智能指针:智能指针可以自动管理动态内存的分配和释放。

线程池的数量一般怎么设置

线程池中线程数量的设置主要考虑两个方面:

  • I/O密集型

  • 如果说任务是耗时I/O型,比如涉及数据库、文件的读写,网络通信等任务,这种任务的特点是不会特别消耗CPU资源,但是我们需要考虑到I/O操作耗时较长。这种情况一般会将线程数设置的比较大,达到了CPU核心数的很多倍。因为如果线程数设置的比较少,会造成CPU计算资源的浪费。

  • CPU密集型

  • 对于CPU密集型任务,线程数不宜设置的过多,因为过多的线程都会去抢占CPU资源,就会产生不必要的上下文切换,反而会造成整体性能的下降

线程数通用计算公示:线程数 = CPU核心数 * (1 + I/O耗时 / CPU耗时)

来了一个新任务,线程池是怎么工作的(本答案有待商榷)

当有一个新任务到来时,线程池会先判断是否有空闲线程,如果有,则将任务分配给空闲线程;

如果此时线程池里没有空线程,则先将任务放任务任务队列,待有空闲线程之后,再从任务队列中取出任务。

完美转发

在我的理解里,完美转发 = std::forward + 万能引用 + 引用折叠。首先引用折叠机制为T&&类型的万能引用中的模板参数T赋予了一个恰到好处的值,而我们用T去指明std::forward的模板参数,从而使得std::forward返回的是正确的类型(这里关于返回的是正确的类型,应该去看std::forward实现的源码,知乎链接如下:https://zhuanlan.zhihu.com/p/369203981)

去掉std::forward会咋样

可能会导致在传递参数的时候丢失类型信息,从而导致编译器无法正确推断模板类型或者在模板类型推断中发生错误。

讲一下C++中的虚继承

基类的析构函数为什么是虚函数?

如果基类的虚构函数不是虚函数,当我们定义一个父类指针指向子类对象时,最后子类的析构函数不会调用,导致内存泄漏。

struct和union的区别(默写)

C++内存布局/程序分段(默写)

了解shared_ptr吗?如果让你手写一个shared_ptr,你会怎么设计

shared_ptr是智能指针里面的共享指针,即多个指针指向同一个内存。每多一个指针指向这片内存,引用计数加1。当对象的引用计数减少为0时,对象会自动析构,对应内存被自动释放。智能指针是模板类,而不是指针。

设计:将shared_ptr定义为一个模板类,包括两个成员:模板类指针和一个指向引用计数的指针。指向引用计数类型的指针应该包括这些成员函数:增加计数、减少计数、返回现有计数。一个私有成员就是用于计数的变量。共享指针模板类的构造函数接受一个模板类型指针,并且需要声明为explicit,表示必须直接初始化。另外就是拷贝构造函数和移动构造函数。拷贝构造需要注意拷贝的对象的指针是否为nullptr,如果不是nullptr,则需要增加引用计数。移动构造函数需要注意将传入的右值引用对象的指针置空,引用计数清零。析构函数有两个判断条件,第一个最后一个指向对象的指针需要为非nullptr并且此时引用计数减一后为0。然后就是重载*、->、bool,还有一个函数用于获得指针get()。动态转换(可考虑)。

介绍一下TCP,TCP粘包如何解决

解决办法:

  • 发送端:使用TCP_NODELAY关闭Nagle算法,但是如果不是时延敏感的应用尽量不要关闭
  • 接收端:没法解决,只能交给应用端解决
  • 应用层:有三种解决办法
    • 只发送固定包长的数据包,但是这个方法基本不用,灵活性太差
    • 指定标识结尾,比如
    • 包头加包体,包头一般是固定长度,并且里面有一个字段可以告知我们接下来的包体有多大

回调函数是什么,回调函数的本质

回调函数允许我们将一个函数(或函数对象)作为参数传递给另一个函数,并在需要的时候由后者调用执行。回调函数定义了在特定事件或条件满足时应该执行的操作。主调函数是接受回调函数作为参数的函数。

进程和线程的区别

  1. 进程
    • 进程拥有独立内存空间和系统资源
    • 进程之间相互独立,一个进程的崩溃通常不会影响其他进程
    • 创建、销毁和切换进程开销比较大
  2. 线程
    • 一个进程可以包含多个线程,所有线程共享相同的地址空间和系统资源
    • 线程之间可以直接读写进程内的共享数据,执行起来更高效
    • 线程的创建、销毁和切换开销比较小

GET和POST的区别

GET用于获取资源,参数通过URL传递,不适合传输敏感信息,幂等,有缓存,传输数据的大小受限于URL的长度。POST用于提交数据,参数通过请求体传递,适合传输敏感信息,不幂等,无缓存,没有数据大小限制。

HTTP和HTTPS

  1. HTTP
    • HTTP是一种应用层协议,用于在Web浏览器和Web服务器之间传输超文本和其他资源
    • HTTP是明文传输的协议,意味着数据在传输过程中是未加密的,容易被窃听和篡改
    • HTTP默认使用80端口号
  2. HTTPS
    • HTTPS是HTTP协议的安全版本,加强了数据传输的安全性和保密性
    • HTTPS使用了SSL/TLS协议进行数据加密和身份认证
    • HTTPS默认使用443端口通信

C++ 11新特性

  • auto关键字

  • 智能指针

  • lambda表达式 参考链接(https://blog.51cto.com/u_15323899/5785594)

    Lambda表达式是C++11引入的一种新特性,它允许在代码中定义匿名函数。虽然在使用上非常简洁和方便,但lambda表达式背后的实现相对复杂。以下是lambda表达式在底层的工作原理:

    1. 转换为类:
      • 当你定义一个lambda表达式,编译器会为你生成一个匿名类(也称为闭包类型)。这个类将会覆盖函数调用操作符,使得该对象可以像函数一样被调用。
      • 如果lambda表达式捕获了外部的局部变量(例如通过值或引用),这些变量将会被添加为该匿名类的成员。
    2. 成员变量:
      • 为了支持捕获,生成的闭包类型可能会包含成员变量。如果使用值捕获,那么这些成员变量将存储捕获的变量的副本;如果使用引用捕获,那么成员变量将存储相应变量的引用。
    3. 函数调用操作符重载:
      • 生成的类会覆盖函数调用操作符operator(). 这个操作符的实现就是lambda表达式的主体。
    4. 构造函数:
      • 该匿名类的构造函数会初始化所有捕获的变量。根据捕获方式(值或引用)来复制或绑定这些变量。
    5. 生成的类是只移动构造的:
      • 这意味着你不能按常规方式复制lambda表达式,但可以移动它。

    下面是一个简单的lambda表达式的例子以及一个可能的简化版本的匿名类表示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Lambda
    auto lambda = [x](int y) { return x + y; };

    // Possible representation by the compiler
    class __anonymous {
    private:
    int x;
    public:
    __anonymous(int _x) : x(_x) {}
    int operator()(int y) const {
    return x + y;
    }
    };

    要注意的是,这只是一个简化的表示。实际编译器生成的代码会比这更复杂,尤其是当涉及到更高级的特性(如泛型、mutable关键字或捕获列表)时。

    最后,虽然从概念上讲,lambda表达式是转换为类的,但这并不意味着性能会受到影响。优化后的编译器通常会内联这些生成的类和函数调用操作符,从而消除由于间接调用导致的任何额外开销。

  • 右值引用和移动语义

lambda表达式的使用场景

lambda表达式提供了一种简洁、方便的方式来创建匿名对象。在一些需要传递简单函数对象的场景下,使用lambda表达式可以避免额外的函数对象类。

vector迭代器失效的原因

vector底层的实现是一个动态数组,vector里面存储的元素都是连续的,一旦比如删除一个元素,后面的所有元素都需要移动。我们可以考虑一种极端情况,删除最后一个元素,此时指向原vector数组的最后一个元素的迭代器就没指向任何元素了,如何此时我们访问这个迭代器所指向的元素,就会导致未定义行为,所以就会判定迭代器失效。其实所有改变vector大小的操作,都会导致vector迭代器失效。

map的底层数据结构

红黑树参考链接:https://www.jianshu.com/p/e136ec79235c

map是有序容器,底层数据结构是红黑树,时间复杂度为log(n)。红黑树的前身可以说是二叉搜索树。但是二叉搜索树最坏的情况下树的高度为n,那么就导致时间复杂度为o(n), 所以便衍生出来了平衡二叉树。其实红黑树的五大特性就是为了保持二叉搜索树的平衡。保证时间复杂度稳定在o(logn)。

unordered_map底层数据结构

unordered_map是无序容器,底层使用哈希表实现的。

TCP握手为什么是三次握手,两次握手为什么不行呢

这里有两个大点,第一个字面意思很好理解,第二点举个例子就通透了,面试时最好全部答上来

  • 为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤

  • 防止已失效的连接请求又传送到服务器端,因而产生错误

    只有采用三次握手可以减少服务端的资源浪费。解释如下: 例如,客户端向服务端发送请求同步报文A,因为网络阻塞等原因,服务端没有收到同步报文A,所以没有发送同步确认报文。过了一段时间,客户端没有收到服务端的确认报文,重新向服务端发送请求同步报文B,服务端接收到报文B后,向客户端发送同步确认报文,客户端接收到确认报文后,向服务端发送确认报文,建立连接。数据传输完毕后,连接断开。客户端进入close状态,此时服务端收到之前的报文A,向客户端返回同步确认报文。如果使用两次握手,服务端回应后不确认客户端的状态,连接建立成功。服务端会长时间等待客户端发送数据,连接长期保持,会造成资源浪费。当多个客户端产生这种情况,服务器就会等待多个客户端的响应,连接数量过多,之后的客户端请求,服务器无法响应。造成服务器处于瘫痪状态。 只有使用三次握手,当服务端收到确认报文后,保证当前时刻,客户端可以发送数据时,才能建立有意义的连接。当客户端一段时间不发送数据时,服务器应自动断开连接,来节省自身连接的客户端数量,减少资源的浪费。

HTTP状态码

HTTP状态码是在HTTP协议中用于表示服务器对请求的处理结果的三位数字代码。HTTP状态码的分类如下:

  1. 1xx(信息性状态码):表示服务器已接收请求,需要客户端继续操作。

  2. 2xx(成功状态码):表示服务器成功处理了请求。

  3. 3xx(重定向状态码):表示请求需要进一步的操作,通常用于重定向。

  4. 4xx(客户端错误状态码):表示客户端发出的请求有误。

  5. 5xx(服务器错误状态码):表示服务器在处理请求时发生了错误。

以下是一些常见的HTTP状态码示例:

  1. 200 OK:请求成功,服务器成功处理了请求。

  2. 201 Created:请求成功,服务器已成功创建了资源。

  3. 204 No Content:请求成功,但服务器没有新的信息返回。

  4. 400 Bad Request:请求错误,服务器不理解或无法处理请求。

  5. 401 Unauthorized:请求需要用户认证,未提供有效的认证信息。

  6. 403 Forbidden:请求被服务器拒绝,没有访问权限。

  7. 404 Not Found:请求的资源不存在。

  8. 500 Internal Server Error:服务器内部错误,无法完成请求。

  9. 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效的响应。

  10. 503 Service Unavailable:服务器暂时不可用,通常是由于过载或维护。

这只是HTTP状态码中的一部分,实际中可能还会遇到其他状态码。状态码的具体含义可参考HTTP协议的官方文档或RFC文档。不同的状态码代表了服务器对请求的不同响应结果,客户端根据状态码可以做出相应的处理。

使用智能指针了,普通指针还能用吗?

个人觉得使用了智能指针后不推荐使用普通指针。因为使用了智能指针后,再使用普通指针指向相同对象,并不会增加引用计数,等智能指针引用计数变为0的时候,对象被销毁,此时普通指针就变为了野指针。

什么时候使用智能指针,什么时候使用普通指针 (CHATGPT)

使用智能指针还是普通指针取决于你的代码需求和内存管理需求。以下是一些指导原则:

使用智能指针的情况:

  1. 动态分配内存:如果你需要动态分配内存,尤其是在使用new关键字时,最好使用智能指针来管理这些资源。智能指针会自动在不再需要时释放内存,避免了内存泄漏的问题。

  2. 所有权传递:当多个地方共享一个资源并且需要明确的所有权传递时,智能指针特别有用。例如,使用std::shared_ptr来共享资源,并在最后一个引用释放时自动删除资源。

  3. 异常安全:在使用普通指针的代码中,如果在动态分配资源后发生异常,可能会导致资源泄漏。使用智能指针可以更好地处理异常情况,确保资源在退出作用域时得到正确释放。

  4. 容器存储:如果要在STL容器中存储指针类型,建议使用智能指针。STL容器在元素添加或删除时可能会复制或移动元素,使用智能指针可以正确管理资源的所有权。

使用普通指针的情况:

  1. 接口和传统API:某些接口和传统的C API可能要求使用原始指针,此时你无法使用智能指针。在这种情况下,你应该小心管理内存,确保手动释放资源。

  2. 与C代码交互:当与C语言库或代码进行交互时,普通指针是常用的方式,因为C语言不支持智能指针。

  3. 临时指针:对于一些临时指针,其生命周期很短且不需要动态内存管理时,可以使用普通指针。

  4. 最低开销:在某些性能关键的场景,智能指针可能带来额外的开销,此时使用普通指针可能更合适。但请注意,在性能优化前确保使用智能指针没有影响到代码的正确性和可维护性。

综上所述,智能指针在大多数情况下是推荐的内存管理方式,特别是在现代C++代码中。然而,有些特定的场景可能需要使用普通指针,但在这种情况下你需要格外小心以避免内存泄漏和悬挂指针问题。

Hash_map原理

Hash_map基于Hash_map(哈希表)。

哈希表基本原理:使用一个下标范围较大的数组来储存元素。那我们怎么根据关键字知道它应该放在数组的哪个位置呢?这就通过哈希函数(散列函数)来解决。哈希函数使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应。但是,不能保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这就产生了哈希冲突。因此“直接定址”和“解决冲突”是哈希表的两大特点。

哈希冲突的解决主要有以下四种方法:开放地址法,再哈希法,链地址法和建立公共溢出区。关于这四种方法的解释见知乎链接 https://zhuanlan.zhihu.com/p/29520044

C++多态介绍一下

C++多态主要包括重载、虚函数、模板。重载包括函数重载和运算符重载,编译期,即编译器在编译阶段就会根据函数调用的上下文来决定使用哪一个重载版本。虚函数是在运行期。重载和模板属于静态多态,虚函数属于动态多态。静态多态与动态多态靠编译期与运行期区分。

虚函数、纯虚函数原理,虚表什么时候建立的 https://zhuanlan.zhihu.com/p/37331092

  • 虚函数,在类成员方法的声明(不是定义)语句前加“virtual”,如virtual void func
  • 纯虚函数,在虚函数后加“=0”,如virtual void func = 0
  • 对于虚函数,子类可以(也可以不)重写基类的虚函数,该行为称之为override
  • 对于纯虚函数,子类必须提供纯虚函数的个性化实现

在派生子类中对虚函数和纯虚函数的个性化实现,都体现了多态特性,但区别在于:

  • 子类如果不提供虚函数的实现,将会自动调用基类的缺省虚函数实现
  • 子类如果不提供纯虚函数的实现,将会编译失败

当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数则由指针指向的实际类型决定

实现虚函数表的关键就是虚函数表指针,这个指针指向一张名为虚函数表的表,为表中的数据则为函数指针,存储了虚函数具体实现所对应的位置。另外,当一个类有多个虚函数时,仍然只有一个虚函数指针,而此时的虚函数表里会有多个函数指针,因此,虚函数实现的过程是:通过对象内存中的虚函数指针vptr找到虚函数表vtbl,再通过vtbl中的函数指针找到对应虚函数的实现区域并进行调用。所以虚函数的调用时由指针所指向内存块的具体类型决定的。

构造函数和析构函数可以是虚函数吗?

答案是:构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。

首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,也即还没有进行初始化,此时vptr是没有值的,也就无法通过vptr找到作为构造函数和虚函数所在的代码区,所以构造函数只能以普通函数的形式存放在类所指定的代码区中。

而对于析构函数,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类base的析构函数。而当继承的时候,通常派生类会在基类的基础上定义自己的成员,此时我们当然希望可以调用派生类的析构函数对新定义的成员也进行析构。

map为啥用红黑树不用avl树?

  • 平衡调整次数更少
  • 内存使用更少:AVL树需要存储额外的平衡因子信息
  • 更适合于频繁的插入和删除操作

怎么判断map里key值存不存在

c.at(k) 访问关键字为k的元素,如果k不在c中,返回一个out_of_range异常

c.find(k) 如果存在,返回一个迭代器,指向关键字为k的元素;如果不存在,返回尾后迭代器

c.count(k) 不存在返回0,存在返回k关键字的数量

多进程通信方式

管道、命名管道、消息队列、信号量、共享内存、套接字、RPC

管道:一种最简单的进程间通信方式,通常用于父子进程间通信。管道中数据只能朝一个方向流动,即一方读另一方写。管道通过系统调用pipe()创建。

命名管道:一种更通用的进程间通信方式,它可以在无关的进程之间进行通信。不同于管道,命名管道通过文件系统中的路径名来标识。命名管道可以通过系统调用 mkfifo() 创建。

信号量:讲好sem_init(), sem_wait(), sem_post()系统整体运作过程就好了

套接字:服务器监听一个ip+端口,客户端访问连接

TCP和UDP的区别

TCP UDP
可靠性 可靠 不可靠
连接 面向连接 无连接
数据传输方式 字节流 数据报
双工性 全双工 一对一、一对多、多对一、多对多
流量控制 滑动窗口
拥塞控制 慢启动、拥塞避免、快速重传、快速回复
效率
传输速度

TCP/IP协议组

IP协议、TCP协议、UDP协议、ICMP协议、ARP协议、RARP协议

OSI七层

物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

socket编程recv函数

返回0:对方关闭连接

返回相应的接收数据大小

返回错误码:传输出现错误

排序算法

快排思想:选定基准元素,小的放一边,大的放一边,最后分治

HTTP状态码

  1. 1XX - 信息状态码:
    • 100 Continue:服务端已经收到了客户端请求,继续发送剩余部分
  2. 2XX - 成功状态码:
    • 200 OK:请求成功,服务器成功处理了请求
    • 201 Created:请求成功,并创建了资源
    • 204 No Content:请求成功,但没有返回内容
  3. 3XX - 重定向状态码:
    • 301 Moved Permanently:永久重定向,请求的资源被永久移到了新位置
    • 302 Found:临时重定向,请求的资源被临时移到了新位置
    • 304 Not Modified:客户端缓存资源仍然有效,未修改
  4. 4XX - 客户端错误状态码:
    • 400 Bad Request:客户端请求错误,服务器无法理解
    • 401 Unauthorized:请求要求身份验证,客户端未提供有效的身份信息
    • 403 Forbidden:服务器拒绝请求,没有访问权限
    • 404 Not Found:请求的资源不存在
  5. 5XX - 服务器错误状态码:
    • 500 Internet Server Error:服务器内部错误,无法完成请求
    • 502 Bad Gateway:作为网关或代理的服务器从上游服务器收到无效响应
    • 503 Service Unavailable:服务器暂时过载或维护中,无法处理请求
    • 504 Gateway Timeout:作为网关或代理服务器未及时从上游服务器接收响应

DNS解析过程

DNS就是域名解析服务,查询过程依次递增。本地域名解析(操作系统首先会查询本地DNS缓存) —–》本地域名服务器查询 —–》根域名服务器查询 —–》顶级域名服务器查询 —–》权威域名服务器查询 —–》返回结果。DNS查询和应答报文具有相同格式,主要区别在于某些字段的标识。下面讲几个重要的字段。

  • 16位标识:用于标记一对DNS查询和应答,以此区分一个DNS应答是哪一个DNS查询的回应
  • 16位标志
    • QR:查询报文(0)/ 应答报文(1)
    • opcode:标准查询(0)/ 反向查询(1)就是是通过域名获取ip地址还是通过ip地址获取域名

TCP为什么需要四次挥手,三次挥手有什么问题

  1. 客户端最后一个ACK可能会丢失,这样服务端就无法正常进入CLOSED状态。于是B会重传请求释放的报文,而此时如果A已经关闭了,那就收不到B的重传请求,就会导致B无法正常释放。而如果A还在等待时间内,就会收到B的重传,然后进行应答,这样B就可以进入CLOSED状态
  2. 如果三次挥手的话,服务端收到来自客户端的FIN请求后,需要同时回复ACK和发送FIN断开连接请求。但是在TCP连接中是有一个半关闭状态的,也就是服务端其实还是可以继续发送数据的,如果三次挥手,就无法做到服务器再发送数据了。

TCP传输为什么是可靠的

  • 确认与重传:如果一定时间内未收到ACK,则会重传
  • 序列号与顺序性:TCP为每一个数据包分配一个序列号,接收端会进行数据包重组
  • 流量控制
  • 拥塞控制
  • 连接管理:三次握手与四次挥手
  • 超时与重试

线程的状态

新建、就绪、运行、阻塞、等待、终止

死锁产生的条件

死锁发生的必要条件,通常被称为死锁的四个条件,分别是:

  1. 互斥条件(Mutual Exclusion): 指某个资源在一段时间内只能被一个线程或进程占用,其他线程或进程需要等待资源释放才能继续执行。
  2. 请求与保持条件(Hold and Wait): 指线程在保持至少一个资源的同时,还请求其他资源,而这些资源可能被其他线程占用,导致请求阻塞。
  3. 不剥夺条件(No Preemption): 指资源只能由占有它的线程显式释放,其他线程不能强行抢占资源。
  4. 循环等待条件(Circular Wait): 指多个线程形成一个循环,每个线程都在等待下一个线程所持有的资源,导致一个闭环的等待状态。

死锁解决方案

  • 系统重新启动
  • 撤销进程、剥夺资源
  • 进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处。

树的知识

https://oi-wiki.org/ds/bplus-tree/

Linux API调用返回值汇总

int pthread_create:成功时返回0,失败时返回错误码

void pthread_exit:不会失败

int pthread_join ( pthread_t thread, void** retval):成功时返回0,失败时返回错误码。错误码:EDEADLK:可能引起死锁,比如两个线程互相针对对方调用pthread_join,或者线程对自身调用pthread_join;EINVAL:目标线程是不可回收的,或者已经有其他线程在回收该目标线程;ESRCH:目标线程不存在。

int sem_init( sem_t* sem, int pshared, unsigned int value )

int sem_destroy( sem_t* sem )

int sem_wait( sem_t* sem)

int sem_trywait( sem_t* sem )

int sem_post( sem_t* sem ):以上五个成功时返回0, 失败时返回-1并设置errno

DNS的解析过程

DNS是实现域名和IP地址相互映射的一个分布式数据库。DNS解析过程主要包括以下步骤

  1. 本地缓存查询:当用户通过浏览器访问某域名时,浏览器会首先在自己的缓存里查询是否有该域名对应的IP地址。
  2. 本地系统查询:查看本计算机系统Host文件DNS缓存是否有对应DNS缓存
  3. 查看路由器缓存
  4. 查询ISP DNS缓存:也称本地域名服务器查询,计算机会向你的ISP(互联网服务提供商)分配的本地域名服务器发出查询请求
  5. 根域名服务器
  6. 顶级域名服务器:根域名服务器并不会直接返回用户IP地址,而是会指向查询请求对应的顶级域名服务器
  7. 权威域名服务器:顶级域名服务器会指向域名的权威域名服务器,这些服务器管理特定的域名的DNS服务。

TCP传输为什么是可靠的

总结下来主要包括三个方面:

  • 检验和
  • 确认应答(ACK和序列号一应一答)
  • 超时重传

NAT协议和跨域(不了解)

GET和POST区别

最明显的一个区别就是GET请求会把参数放在URL(统一资源定位符)中,POST会把参数放在请求体中。

数据大小限制:GET请求把参数放在URL中,GET请求数据大小收到URL长度限制。POST请求则不会

幂等性:GET请求每次相同请求返回的结果都一样,多次重复请求不会对资源产生影响。POST请求则相反

安全性:POST更安全,参数没有暴露在URL上

缓存:GET请求可以被浏览器缓存,POST请求不会被浏览器缓存

写一段死锁的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int a = 0, b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

void* another(void* arg){
pthread_mutex_lock(&mutex_a);
printf("in child thread, get mutex a\n");
sleep(5);
a++;
pthread_mutex_lock(&mutex_b);
b += a;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
pthread_exit(NULL);
}

int main(){
pthread_t id;

pthread_mutex_init(&mutex_a, NULL);
pthread_mutex_init(&mutex_b, NULL);
pthread_create(&id, NULL, another, NULL);

pthread_mutex_lock(&mutex_b);
printf("in parent thread, get mutex b\n");
sleep(5);
++b;
pthread_mutex_lock(&mutex_a);
a += b;
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);

pthread_join(id, NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);

printf("pro is end\n");

return 0;
}

删除字符串头尾空格代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>

int main(){
std::string s;
std::string tmp;
getline(std::cin, s); //这里很重要哦,如果用cin,“hello world”会被截断成hello

for(int i = 0; i < s.size(); ++i){
if(s[i] == ' '){
continue;
}

s = s.substr(i, s.size() - i);
break;
}

for(int i = s.size() - 1; i >= 0; --i){
if(s[i] == ' '){
continue;
}

s = s.substr(0, i + 1);
break;
}

std::cout << s << std::endl;

return 0;
}

shared_ptr是线程安全的吗?https://cloud.tencent.com/developer/article/1654442

  • 多线程同时读一个shared_ptr对象是安全的
  • 多线程同时对一个shared_ptr对象进行读和写是不安全的,需要加锁

这里给出一个伪代码的简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shared_ptr<Foo> g(new Foo1);   // 线程之间共享的shared_ptr
shared_ptr<Foo> x; // 线程A的局部变量
shared_ptr<Foo> n(new Foo2); // 线程B的局部变量
---------------------------------------------------------
线程A
x = g;
---------------------------------------------------------
线程B
g = n;
---------------------------------------------------------
测试场景:

线程A
智能指针x 读取Foo1,然后还重置Foo1计数。

线程 B:
销毁了Foo1
线程A
重置计数时,foo1已经被销毁。

mysql读写锁怎么实现 mysql 读写锁和互斥锁的区别 https://blog.51cto.com/u_16099299/7031260

  • 请你讲述一下互斥锁机制,以及互斥锁和读写锁的区别
    • 互斥锁(mutex),用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
    • 读写锁(rwlock),分为读锁和写锁。处于读操作的时候,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其他获得写锁失败的线程将会进入睡眠状态,直到写锁释放时被唤醒。注意;写锁会阻塞其他读写锁。当有一个线程获得写锁在写时,读锁也不能被其他线程获取;写者优于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。 读写锁:区分读者和写者,而互斥锁不区分,互斥锁只允许同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
  • Linux中的四种锁机制
    • 互斥锁:在同一时间内只允许一个线程访问对象
    • 读写锁:同一时间内允许多个读者同时读对象,同一时间内只允许一个写者访问对象,且当有一个线程获得写操作的时候,读锁也不能被其他的线程获取,写者优于读者,唤醒时先唤醒写者。
    • 自旋锁:在任何时刻内自能有一个线程访问资源,但是当获取锁操作失败时,不会进入睡眠状态而是会原地自旋,直到锁被释放,这样减少了线程从睡眠状态到被唤醒状态的资源消耗,在加锁时间短暂的情况下使用会提高效率。但是加锁时间过长会非常浪费CPU
    • RCU(read-copy-update):在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改,然后在将老数据update成新数据。在RCU的时候读者几乎不需要同步开销,即不需要获取锁,也不适用原子指令,不会导致竞争因此不用考虑死锁问题了。但是对于写者的同步开销比较大,他需要复制被修改的数据,还必须使用锁机制同步并行其他写者的改操作,在有大量读操作,少量写操作的时候使用。

delete关键字的作用

  1. 删除特殊的成员函数:在C++11中可以使用delete来显示的阻止编译器自动生成某些特殊的成员函数。通过在类的声明中将这些特殊函数标记为delete,可以防止这些函数隐式的生成。

    1
    2
    3
    4
    5
    6
    class MyClass {
    public:
    MyClass() = delete; // 阻止生成默认构造函数
    MyClass(const MyClass&) = delete; // 阻止生成拷贝构造函数
    // ...
    };
  2. 删除特定的函数重载:在函数重载时,可以使用delete关键字来标记某个特定的函数重载,防止特定的重载函数被调用。

    1
    2
    void someFunction(int x);
    void someFunction(double x) = delete; // 阻止调用带有 double 参数的函数

define和const的区别

  • 就起作用的阶段而言:#define是在编译的预处理阶段起作用,而const是在编译、运行时起作用;
  • 就起作用的方式而言:#define只是简单的字符串替换,没有类型检查,而const有类型检查,避免相应的错误;
  • 就存储方式而言:#define只是进行展开,有多少地方使用,就有多少替换。const定义的只读变量在程序运行过程中只有一份备份;

写时拷贝(COW) https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

在linux里,fork()会产生一个子进程,子进程与父进程用的是相同的物理空间,也就是说两者的虚拟空间不同,但对应的物理空间是一个。当父/子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。这里的相应段你能理解不?其实就是一个程序一般分为哪些段,主要可以分为四大段:栈、堆、数据段、代码段;再细分的话可以加一个bss段,也就是用于存放程序中未初始化的全局变量的一块内存区域。

还有一个细节问题就是,fork之后内核会将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,造成不必要的复制,导致效率的下降。可以这么理解,可能子进程执行的代码并不需要写,完全可以使用原共享段,而父进程反而会改变(fork之后),如果父进程先执行,则子进程就要COW。

coredump,gdb怎么定位

整体流程如下:

1
g++ -g source.cpp -o source
1
ulimit -c unlimited
1
./source
1
gdb ./source ./core
1
where(在gdb下输入)

epoll

关于epoll问题在这暂且不多写,只需记住内核事件表,selectpoll都是轮询,事件复杂度:epoll(o1),其他(on)

selectpoll必须遍历内核事件表上所有已注册的文件描述符以找到其中的就绪者;epoll仅遍历就绪的文件描述符。

static的作用

  1. 在函数体内,被声明为静态的变量在这一函数被调用过程中维持其值不变。

    这句话其实是想说如果在函数内部声明了一个静态变量,那么这个变量在函数调用期间不会被重新初始化。它的生命周期是从程序开始到程序结束,但其作用域仍然限制在该函数内。这里给一段示例代码最好理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <stdio.h>

    void count(){
    static int num = 0;
    num++;
    std::cout << "num is: " << num << std::endl;
    }

    int main(){
    count(); // num = 1
    count(); // num = 2 正常情况下,如果是非static,num会被重新置0,但这里并没有体现出来。
    count(); // num = 3
    return 0;
    }
  2. 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。

    这句话就是表面意思,很好理解,这里给一个代码帮助理解

    moduleA.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>

    static int secretValue = 42;

    void printSecretValue(){
    std::cout << secretValue << std::endl;
    }

    void modifySecretValue(int value){
    secretValue = value;
    }

    moudleB.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    extern void printSecretValue();
    extern void modifySecretValue(int value);

    int main(){
    printSecretValue();
    modifySecretValue(100);

    // std::cout << secretValue << std::endl;
    // 记住,这句话是会导致编译错误的,模块B不能直接访问模块A的变量。
    }
  3. 在模块内,一个被声明为静态的函数只可被本模块内的其他函数调用。也就是,这个函数被限制在声明它的模块的本地范围内使用。

    这里的意思其实就和第二点差不多了,也给一个简单示例代码吧

    moduleA.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>

    // 这是一个静态函数,只在moduleA.cpp中可见
    static void secretFunction() {
    std::cout << "Inside secret function of moduleA!" << std::endl;
    }

    // 公开的函数,可以被其他模块调用
    void publicFunction() {
    std::cout << "Inside public function of moduleA." << std::endl;
    secretFunction();
    }

    moduleB.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>

    extern void publicFunction();

    int main() {
    publicFunction(); // 可以调用这个函数

    // 不能调用 secretFunction,因为它在moduleA.cpp中是静态的
    // secretFunction(); // 这一行会导致编译错误

    return 0;
    }

    在上面的示例中,moduleA.cpp定义了一个静态函数secretFunction。在moduleB.cpp中,我们可以调用publicFunction,但当我们尝试调用secretFunction时,会导致编译错误,因为secretFunction只在moduleA.cpp中可见。

有关static的必要补充

  • 修饰成员变量

    用static修饰类的数据成员,使其成为类的全局变量,会被类的所有对象共享,包括派生类对象。所有对象只维持同一个实例。因此类的static成员必须进行类外初始化,而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。因为const修饰的变量无法修改。

  • 修饰成员函数

    用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问static成员变量。注意:正是因为static修饰的成员函数不含this指针,而const修饰的成员函数会添加一个隐藏的参数const this*,所以这两种语义是矛盾的,所以在成员函数上不能同时用他们。

  • this指针理解

    对象就是大房子,成员就是房子里的物件,this是一个指着房子的指针,如果要获取对象的成员也就是大房子内的物件,需要使用this指针加->这个符号。

三种智能指针

智能指针是在栈中的一个类,用于管理堆上分配的内存。传统的C/C++对于堆上内存的开辟释放,需要程序手动管理,而智能指针是一个类,有构造函数和析构函数,在超出作用范围后,程序会自动调用析构函数释放其管理的指针指向的内存,不需要手动释放。

  • shared_ptr 共享智能指针,多个智能指针可以指向同一个对象,对象的资源在最后一个指针销毁时释放,通过引用计数来判断是否是最后一个智能指针。
  • unique_ptr 独占智能指针,同一时刻只有一个智能指针可以指向该对象,如果要安全重用该指针,标准库函数std::move()可以将unique_ptr赋值给另一个unique_ptr。
  • weak_ptr 弱智能指针,不会增加shared_ptr的引用计数,可以避免两个shared_ptr相互引用的死锁问题。weak_ptr只能用shared_ptr或者另一个weak_ptr构造,通过lock()方法weak_ptr可以转化为shared_ptr。

下面给出代码示例

unique_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <memory>
#include <string.h>

class test{
public:
std::string str;
~test(){ std::cout << "test::~test()" << std::endl; }
};

int main(){
std::unique_ptr<test> p1(new test());
p1->str = "test string";
std::cout << "p1->str: " << p1->str << std::endl;
std::unique_ptr<test> p2;
p2 = std::move(p1);
// std::cout << p1->str << std::endl; p1此时已经访问不到资源了
std::cout << "p2->str: " << p2->str << std::endl;
return 0;
}

// 运行结果
// p1->str: test string
// p2->str: test string
// test::~test() 析构函数发生了调用,自动释放资源

shared_ptr造成的死锁问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include<memory>
using namespace std;

class B;
class A{
public:
shared_ptr<B> a;
~A(){
cout << "A::~A()" << endl;
}
};


class B{
public:
shared_ptr<A> b;
~B(){
cout << "B::~B()" << endl;
}
};
/*******前边两个类中相互引用,此时引入weak_ptr解决死锁问题*/

int main()
{
shared_ptr<A> p1(new A());
shared_ptr<B> p2(new B());
p1->a = p2;
p2->b = p1;//这种情况发生死锁,两个shared_ptr计数都不会为0,资源不会被释放
cout << "use_count():" << p1.use_count() << endl;
cout << "use_count():" << p2.use_count() << endl;
return 0;
}

// 具体解释一下死锁的产生主要原因是引用计数无法降为0。
/*
首先,来理解下为什么这种情况会导致资源不能被释放:

1. 当你创建`p1`和`p2`时,它们都有一个引用计数为1。
2. 当执行`p1->a = p2;`,`p2`的引用计数增加到2,因为现在有两个`shared_ptr`对象(`p1->a`和`p2`)指向同一个`B`对象。
3. 当执行`p2->b = p1;`,`p1`的引用计数增加到2,因为现在有两个`shared_ptr`对象(`p2->b`和`p1`)指向同一个`A`对象。

到此,我们得到了一个环状结构:`p1`指向`A`对象,`A`对象内部的`shared_ptr`指向`B`对象,`B`对象内部的`shared_ptr`又指向`A`对象。这就形成了循环引用。

当`main`函数返回时,`p1`和`p2`的析构函数将被调用,但是它们的引用计数都不会降为0,因为循环引用。所以,`A`和`B`对象的析构函数永远不会被调用,这就导致了资源泄漏。

要解决这个问题,可以使用`weak_ptr`来打破循环引用。你可以将其中一个类的`shared_ptr`成员变量替换为`weak_ptr`。这样,`weak_ptr`不会增加引用计数,从而避免了循环引用的问题。当你需要从`weak_ptr`获取一个`shared_ptr`时,可以使用`lock`方法。
*/

weak_prt解决循环引用问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <memory>
using namespace std;

class B;

class A {
public:
shared_ptr<B> a;
~A() {
cout << "A::~A()" << endl;
}
};

class B {
public:
string str = "hello";
weak_ptr<A> b; // 将 shared_ptr 替换为 weak_ptr
~B() {
cout << "B::~B()" << endl;
}
};

int main() {
shared_ptr<A> p1(new A());
shared_ptr<B> p2(new B());

p1->a = p2;
p2->b = p1; // 这里不会增加 p1 的引用计数

cout << "p1 use_count(): " << p1.use_count() << endl; // 输出 1
cout << "p2 use_count(): " << p2.use_count() << endl; // 输出 2

return 0; // 当 main 返回时,p1 和 p2 都会被析构,它们所指向的对象也会被正确地销毁
}

STL内存池机制

  • 第一级配置器

    第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。

  • 第二级配置器

    在STL的第二级配置器中多了一些机制,避免太多小区造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。

    如果要分配的区块大于128bytes,则移交给第一级配置器处理。 如果要分配的区块小于128bytes,则以 内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。

    参考连接:https://blog.csdn.net/a987073381/article/details/52245795

索引选B+树的原因

个人感觉主要就是探讨B+树和B树之间的区别。 参考链接:https://www.cnblogs.com/JCpeng/p/15231338.html

服务端TIME_WAIT过多的危害及解决办法

危害

  • 导致大量socket连接端口被占用无法释放,导致系统停转
  • 处理大量的TIME_WAIT状态的连接会占用更多的CPU时间,导致服务器响应时间增加

解决办法

  • 调整系统内核参数
  • 调整短连接为长连接

set底层数据结构

set底层数据结构是红黑树。关于map和set的一些问题列举如下:

为何map和set的插入删除效率比用其他序列容器高? 因为对于关联容器来说,不需要做内存拷贝和内存移动。因为map和set容器内部所有元素都是以节点的方式来存储,父节点和子节点。因此插入和删除的时候都是指针的指向换来换去,并没有内存移动。

为何每次插入/删除后以前保存的迭代器不会失效 在这里,迭代器就相当于指向节点的指针,内存没有变,指向内存的指针当然不会失效。不同于map/set,vector就会失效,比如需要扩容的时候,需要一块更大的内存,就必须把之前的内存释放,申请更大的内存,复制已有的数据元素到新的内存。

为何map和set不能像vector一样有个reserve函数来预分配数据 不懂!预留本问题

当数据元素增多时(10000到20000个比较),map和set的插入和搜索速度变化如何? map和set使用的是二分查找,举例来说就是,16个元素查四次,32个元素查5次,10000个也就是14次,20000也就15次。

mapreduce原理(不懂,预留)

socket哪些操作会产生阻塞

accept():在等待客户端的连接请求时会阻塞。如果没有客户端尝试连接,调用这个函数的进程或者线程会被挂起。

connect():当客户端尝试与远程主机建立连接时,此操作会阻塞,知道连接成功或失败

recv():当数据从网络到达时,如果没有数据可读,函数调用就会被挂起,阻塞

send():当发送缓冲区已满,并且无法接受更多数据发送到网络时,操作阻塞。

申请一块大内存和一块小内存的效率是一样的吗?

不一样,申请一块小内存通常是不需要一级配置器,小块内存通常由内存分配器从预先分配的内存池中分配,这些池被称为bins,这种分配通常非常快,因为它只是涉及到从已存在的内存池中返回一个指针。大的内存通常要调用mmap()

TCP慢启动和拥塞控制

TCP连接建立好之后,CWND(congestion window,拥塞窗口)被设置为初始值IW(initial window),其大小为2~4个SMSS(TCP报文段的最大长度,仅指数据部分),新的linux内核加大了这个值。此时发送端最多能发送IW字节的数据,此后发送端每收到一个确认,CWND就按照如下公式增长: CWND+ = min(N, SMSS) 其中N是此次确认中包含的之前未被确认的字节数。 如果不施加其他手段,慢启动必然使得CWND增长的很快,所以TCP拥塞控制中有另外一个非常重要的变量:慢启动门限。当CWND超过慢启动门限值时,TCP将进入拥塞避免阶段。

发送端判断发生拥塞的依据

  • 传输超时
  • 接收到重复的确认报文段

Linux下的POSIX互斥锁和条件变量

牢记这几个函数:

1
2
3
4
5
pthread_mutex_init(&mutex, NULL);
pthread_mutex_destory(&mutex);
pthread_mutex_lock(&mutex);
pthread_mutex_trylock(&mutex);
pthread_mutex_unlock(&mutex);
1
2
3
4
5
pthread_cond_init(&cond, NULL);
pthread_cond_destory(&cond);
pthread_cond_broadcast(&cond);
pthread_cond_signal(&cond);
pthread_cond_wait(&cond, &mutex);

条件变量要配合互斥锁使用。这里挂一个讲的比较清晰明了的博客链接(https://punmy.cn/2018/06/07/%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F%EF%BC%88Condition%20Variables%EF%BC%89.html),下面是使用互斥锁加条件变量实现的生产者-消费者模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <pthread.h>
#include <stdio.h>

static int value = 0;
static pthread_mutex_t mutex_;
static pthread_cond_t condition_;
static pthread_t notifying_thread;

void setup(){
pthread_mutex_init(&mutex_, NULL);
pthread_cond_init(&condition_, NULL);
}

void destory(){
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&condition_);
}

void* producer(void *args){
pthread_mutex_lock(&mutex_);
while(value != 10){
value++;
}
notifying_thread = pthread_self();
pthread_mutex_unlock(&mutex_);
pthread_cond_signal(&condition_);
return NULL;
}

void waitCondition(){
pthread_mutex_lock(&mutex_);
while(value != 10){
pthread_cond_wait(&condition_, &mutex_);
}
printf("value is 10, the pthread is: %lu\n", (unsigned long)notifying_thread);
pthread_mutex_unlock(&mutex_);
}

void* consumer(void *args){
waitCondition();
return NULL;
}

int main(){
pthread_t a1;
pthread_t a2;

setup();

pthread_create(&a1, NULL, producer, NULL);
pthread_create(&a2, NULL, consumer, NULL);

pthread_join(a1, NULL);
pthread_join(a2, NULL);

destory();

return 0;
}

写一个生产者/消费者模型(采用信号量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <semaphore.h>
#include <queue>
#include <pthread.h>
#include <unistd.h>

const int MAX_BUFFERS = 10;
std::queue<int> buffer;

sem_t empty;
sem_t full;
pthread_mutex_t mutex_;

void *producer(void *arg){
int num;
while(true){
num = rand() % 100;
sem_wait(&empty);
pthread_mutex_lock(&mutex_);
buffer.push(num);
std::cout << "Produced" << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex_);
sem_post(&full);
}
return nullptr;
}

void *comsumer(void *arg){
int num;
while(true){
sem_wait(&full);
pthread_mutex_lock(&mutex_);
num = buffer.front();
buffer.pop();
std::cout << "Comsumed" << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex_);
sem_post(&empty);
}
return nullptr;
}


int main(){
pthread_t proThread, comPthread;

pthread_mutex_init(&mutex_, NULL);

sem_init(&empty, 0, MAX_BUFFERS);
sem_init(&full, 0, 0);

pthread_create(&proThread, NULL, producer, NULL);
pthread_create(&comPthread, NULL, comsumer, NULL);

pthread_join(proThread, NULL);
pthread_join(comPthread, NULL);

pthread_mutex_destroy(&mutex_);

sem_destroy(&empty);
sem_destroy(&full);

return 0;
}

基类的析构函数为什么是虚函数

当基类的析构函数不是虚函数可能会导致派生类对象的析构函数不被调用。从而引发资源泄露或者其他未定义行为。下面给出示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

class Base{
public:
Base() { std::cout << "Base obj is constructed" << std::endl; }
// ~Base() { std::cout << "Base obj is deconstructed" << std::endl; }
virtual ~Base() { std::cout << "Base obj is deconstructed" << std::endl; }
};

class Derived : public Base{
public:
Derived(){
num_ = new int[10];
std::cout << "Derived is constructed" << std::endl;
}

~Derived(){
std::cout << "Derived is deconstructed" << std::endl;
}
private:
int *num_;
};

int main(){
Base *base = new Derived();
delete base;
return 0;
}

为什么基类不使用虚函数会导致派生类的析构函数不调用(除了写的内容,最好再加上有关虚函数表、指针的内容)

当一个基类没有虚析构函数,并且通过基类指针删除一个派生类对象时,只有基类的析构函数会被调用。这是因为编译器在编译时期决定了调用哪个析构函数,而不是在运行时期,这种机制被称为“静态绑定”。

如果基类的析构函数是虚函数,那么编译器会在运行时决定要调用哪个析构函数,这种机制称为“动态绑定”或“多态”。

拷贝构造函数为什么要用引用

避免拷贝构造函数无限递归下去!那传引用为什么行呢?回忆一下引用定义,传引用人家根本就不会涉及到构造函数这玩意,形参相当于实参的一个别名。

左右值的差别

C++中的虚继承

c++中的虚继承主要是为了解决多继承情况中存在的二义性问题。比如,现在类A为基类,类B和类C继承自类A,类D继承自类B和类C,假设此时A中有一个名为x的变量且B和C都定义了x,如果此时D直接访问x会产生二义性问题。因为不清楚是A->B->D还是A->C->D。此时如果让类B和类C虚继承自A,就使得在派生类中只保留有一份成员变量x,解决了二义性问题。 参考链接: http://c.biancheng.net/view/2280.html https://zhuanlan.zhihu.com/p/41309205(虚继承、虚函数都讲了,十分详细)

C++中的forward函数

左值引用和右值引用的结果都是左值,无法通过引用区分原本变量的左右值,forward可以保持变量的原本的左右值属性,帮助我们区分。

如何判断TCP断开

  1. 发送心跳消息
  2. 利用recv/send函数的返回值
    • recv返回0代表对方关闭了连接
    • recv返回-1并且errno==EAGAIN或者EWOULDBLOCK时,表示没有数据可读,可以稍后再试。
    • send返回非负值:
      • send返回的是实际发送的字节数,这可能会少于你请求发送的字节数(缓冲区已满)。
      • 如果没有可用的缓冲区空间,那么在非阻塞模式下,send可能会返回EAGAINEWOULDBLOCK
    • send返回-1:
      • recv类似,这表示出现了错误。
      • 可以使用perrorstrerror或检查errno来确定具体的错误原因。
      • 常见的错误原因包括EPIPE(对端已关闭,本端仍尝试发送数据导致的”Broken pipe”错误)和ECONNRESET(连接被对端重置)。
  3. 利用tcp自带的keepalive机制

如何查看系统的最大进程数和线程数

1
2
cat /proc/sys/kernel/pid_max    # 最大进程数
cat /proc/sys/kernel/threads-max # 最大线程数

git rebase和git merge的区别(https://joyohub.com/2020/04/06/git-rebase/)

C++ calss 和 struct的区别

class默认访问类型是private,struct默认访问类型是public

指针和引用的区别

  1. 指针是一个变量,它保存了另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。
  2. 指针可以被重新赋值,指向新的对象;引用绑定对象后不能更改。
  3. 指针可以为nullptr;引用必须初始化。

为什么模板类的声明和实现不能分别写在.h文件和.cpp文件中

首先,模板不是传统意义上的代码。它更像是编译器的“代码生成工具”。当你使用一个特定的模板类型(例如std::vector<int>),编译器会为你生成这种类型的实例代码。为了做到这一点,编译器需要能够看到模板的完整定义。注意:这就是模板和普通类的最大区别。普通类在编译阶段并没有(并不依赖于)编译时的类型特化,只需要在链接阶段将所有.cpp文件链接在一起就好了。而如果模板分开写,其他使用模板的.cpp文件在编译时找不到定义,导致链接错误。

动态链接与静态链接

静态链接(.a/.lib):把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。如果多个程序使用相同的库,那么该库的代码在每个程序中都会被复制,这浪费了磁盘和内存空间。但是所有代码都在可执行文件中,没有运行时链接开销。

动态链接(.so/.ddl):动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。多份代码可以共享同一份库代码。

pragma pack编译宏

pragma pack用于指定内存对齐值。

vector底层实现原理

理解vector的实现主要考虑四个东西就好了:三个指针加动态内存申请。

三个指针:

  • first_:指向vector容器对象起始地址的位置
  • last_:指向vector容器对象中当前最后一个元素的末尾字节
  • end_:指向vector容器所占内存空间的最后一个字节

动态内存申请:

当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  3. 最后将旧的内存空间释放。

常量指针和指针常量

常量指针:记忆方法(常量的指针),指针指向的对象是一个常量,只能读取指针指向的内容,不能修改指针指向的内容。

指针常量:指针本身是一个常量,不能修改指针指向的地址,可以修改指针指向地址的内容。

C++中一些特别的关键字(default,override、final、volatile)

参考链接:https://blog.csdn.net/u011947630/article/details/103062773

字节算法题:小于N的最大数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<string>
#include<vector>
#include<iostream>
#include <algorithm>

using namespace std;

int ans;
int value = 14132;
string valueStr = to_string(value);
vector<int> nums = {1,4,9};

bool dfs(int index, bool judge, int temp) {

if(index == valueStr.size()) {
ans = temp;
return true;
}
if(judge) {
return dfs(index + 1, true, temp * 10 + (nums[nums.size() - 1]));
} else {
int val = valueStr[index] - '0';
for(int i = nums.size() - 1; i >= 0; --i) {
if(val == nums[i]) {
if(dfs(index + 1, false, temp * 10 + nums[i])) {
return true;
}
} else if (val > nums[i]) {
if(dfs(index + 1, true, temp * 10 + nums[i])) {
return true;
}
}
}
//最终结果为空
if(index != 0) {
return false;
}
//少了一位,显然小了
return dfs(index + 1, true, temp);
}
}

int main()
{

dfs(0, false, 0);
cout << ans << endl;
return 0;
}

如果同时有大量客户并发建立连接,服务器端有什么机制进行处理

  1. 多进程和多线程
  2. 连接池
  3. 异步I/O
  4. 负载均衡
  5. 限流
  6. 短连接与长连接

HTTP和TCP之间的关系

  1. HTTP是一个应用层协议,TCP是一个传输层协议

  2. HTTP依赖于TCP来进行数据的传输。比如,当你使用浏览器访问一个网页时,背后发生的是HTTP请求和响应的交换,而这些HTTP数据包是通过TCP连接发送的。

  3. HTTP/1.0对于每一个请求-响应都会新建一个TCP连接,使用完之后就关闭

    HTPP/1.1引入了keep-alive机制,允许在单个TCP连接上发送多个HTTP请求和响应

    HTTP/2进一步扩展了这种机制,允许在单个TCP连接上同时多路复用多个HTTP请求和响应

HTTP从请求到得到结果的过程

  1. 域名解析
    • 客户端(通常是浏览器)首先检查URL是否包含域名(例如 www.example.com)。
    • 如果包含,客户端首先会查找其DNS缓存是否已经有该域名的IP地址。
    • 如果没有,客户端会发起一个DNS查询到配置的DNS服务器,以获取对应的IP地址。
  2. 建立TCP连接
    • 使用从DNS查询得到的IP地址,客户端尝试与服务器的80端口(HTTP)或443端口(HTTPS)建立一个TCP连接。
    • 这涉及到TCP三次握手过程。
  3. (HTTPS的情况)SSL/TLS握手
    • 如果是HTTPS请求,一旦TCP连接建立,客户端和服务器会进行SSL/TLS握手来建立加密的通信通道。
  4. 发送HTTP请求
    • 连接建立后,客户端会发送HTTP请求报文。这包括请求行(例如 GET /path HTTP/1.1)、请求头和(对于某些请求如POST)请求体。
  5. 服务器处理请求
    • 服务器接收到请求后,由其HTTP服务软件(如Apache、Nginx等)处理。
    • 服务器可能会根据请求路径查询文件、与数据库交互或调用其他服务,以生成响应。
  6. 服务器发送响应
    • 服务器生成响应后,它将响应报文发送回客户端。响应通常包括状态行(例如 HTTP/1.1 200 OK)、响应头和响应体。
    • 对于动态内容,如由PHP、Python或Node.js等后端语言生成的页面,服务器可能会进行一些额外的处理来生成响应内容。
  7. 客户端处理响应
    • 客户端(如浏览器)接收响应,并基于响应内容采取相应的行动。
    • 如果响应是一个HTML页面,浏览器会开始解析HTML,并可能发起其他请求来获取页面上的资源,如图片、CSS、JavaScript文件等。
  8. 关闭连接
    • 一旦数据交换完成,通常会关闭TCP连接。然而,在HTTP/1.1中,默认使用keep-alive,意味着连接可以被复用,从而减少后续请求的延迟。

https和http的区别是什么,https具体是怎么做的

HTTPS和HTTP唯一的区别就是HTTPS使用TLS/SSL来加密普通的HTTP请求和响应。

关于https具体是如何做的,这里有一个参考博文链接,写的超好(参考链接:https://www.runoob.com/w3cnote/http-vs-https.html)

gdb中,如何查看每个线程相关的信息

在 gdb 中,可以使用 info threads 命令查看当前进程中的所有线程信息。这个命令会列出每个线程的编号、状态(如运行、挂起等)以及当前所在的函数名称。

1
2
3
4
5
6
(gdb) info threads
Id Target Id Frame
2 Thread 0x7ffff7fc8700 (LWP 13627) "main" __libc_start_main (argc=1, argv=0x7fffffffe4c8, env=0x7fffffffe4d8,
auxvec=0x7fffffffe4d8, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe4c8) at ../csu/libc-start.c:310
3 Thread 0x7ffff77c7700 (LWP 13628) "main" foo () at test.c:6
4 Thread 0x7ffff6fc6700 (LWP 13629) "main" bar () at test.c:11

你还可以使用 thread <thread-id> 命令来切换到指定的线程,然后使用其他 gdb 命令来查看线程的信息或调试线程。

例如,你可以输入 thread 3 命令切换到编号为 3 的线程,然后输入 bt 命令来查看线程的调用堆栈:

1
2
3
4
5
6
7
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff77c7700 (LWP 13628))]
#0 foo () at test.c:6
(gdb) bt
#0 foo () at test.c:6
#1 0x00007ffff7bbb830 in start_thread (arg=<optimized out>) at pthread_create.c:486
#2 0x00007ffff78f95fd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

构造函数中能调用虚函数吗,如果调用虚函数,会出现什么问题,写程序验证,并从原理上分析原因

构造函数从语法上调用虚函数没有任何问题,但这样做可能并不会达到预期的结果。因为在构造函数执行期间,对象的虚表仍在被设置,因此调用的虚函数版本可能并不是我们想要的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class Base{
public:
Base(){
std::cout << "Base construct\n";
call();
}

virtual void call() const{
std::cout << "Base call\n";
}

virtual ~Base() {}
};

class Derived : public Base{
public:
Derived(){
std::cout << "Derived construct\n";
call();
}

virtual void call() const override{
std::cout << "Derived call\n";
}

virtual ~Derived() {}
};

int main(){
Derived d;
return 0;
}

对于上面这段程序,可能我们期望得到的结果是

1
2
3
4
Base constructor
Derived call
Derived constructor
Derived call

但我们实际得到的结果是

1
2
3
4
Base constructor
Base call
Derived constructor
Derived call

原因是在Base构造函数执行时,Derived部分的对象还没有完全构造出来,因此,此时vtable仍然指向Base类的vtable。因此,当在Base构造函数中调用call虚函数时,他会调用Base版本,而不是Derived版本。

这里延伸一下不要在析构函数中调用虚函数的原因:析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。

迭代器的实现

参考链接:https://www.cnblogs.com/wengle520/p/12492708.html

epoll红黑树的作用

简单点讲就是在内核事件表上快速注册/删除所需连接文件描述符。下面两篇文章基本阐述了这个问题,好好看!

https://cloud.tencent.com/developer/article/1862671

https://zhuanlan.zhihu.com/p/366955699

用户态和内核态分别会做什么,怎么切换的

内核空间主要负责操作系统内核线程以及用户程序系统调用。

用户空间主要负责用户程序的非系统调用。

从用户态切换到内核态主要有三种方式:

  1. 系统调用:系统调用本身就是中断,但是软件中断,跟硬中断不同。系统调用机制是使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断。
  2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,会触发由当前运行进程切换到处理此异常的内核相关进程中
  3. 外围设备中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序。

有一个面试问题:I/O频繁发生内核态和用户态切换,怎么解决?

答案:使用用户进程缓冲区。

用户进程缓冲区

你看一些程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。所以说:用户缓冲区的目的就是是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。除了在进程中设计缓冲区,内核也有自己的缓冲区。

内核缓存区

当一个用户进程要从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。但若是内核缓冲区中没有数据,内核会把对数据块的请求,加入到请求队列,然后把进程挂起,为其它进程提供服务。等到数据已经读取到内核缓冲区时,把内核缓冲区中的数据读取到用户进程中,才会通知进程,当然不同的IO模型,在调度和使用内核缓冲区的方式上有所不同。

参考链接:

https://blog.csdn.net/qq_42052956/article/details/111562280

https://cloud.tencent.com/developer/article/2131401

进程的创建需要系统分配什么资源

根据其他博客里写的底层源码来看,分配给一个进程的东西太多啦,我们挑几个记一下:内存、CPU处理时间、输入输出设备、存储空间。

参考链接:

https://blog.csdn.net/lvyibin890/article/details/82193900

https://juejin.cn/s/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%BB%99%E8%BF%9B%E7%A8%8B%E5%88%86%E9%85%8D%E4%BB%80%E4%B9%88%E8%B5%84%E6%BA%90

HTTPS加密方式:对称加密、非对称加密

对称加密:密钥只有一个,加密解密为同一个密码。利用这种加密方式时必须把密钥也发送给对方,密钥在传输过程中被窃取,也就失去了加密的意义。

非对称加密:密钥成对出现,公钥加密需要私钥解密,私钥加密需要公钥解密。

参考链接:

https://www.runoob.com/w3cnote/http-vs-https.html

https://www.itwork.club/2022/05/27/why-did-https-use-mixed-encryption-algorithm/

线程的三种状态

就绪态、运行态、阻塞态

析构函数里能不能抛异常?为什么?

析构函数从语法上是可以抛出异常的,但是这样做很危险,请尽量不要这要做。原因在《More Effective C++》中提到两个:

(1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

(2)通常异常发生时,c++的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding),因发生异常而逐步退出复合语句和函数定义的过程,被称为栈展开。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。

两个类里有相同的成员,区别在于一个有虚函数,一个没有,哪个内存大?

有虚函数的大,因为有虚函数的类的会包含一个虚函数指针。注意不同操作系统指针大小不一样。

参考链接:https://blog.csdn.net/luolaihua2018/article/details/110736211

数据报和数据流的区别?

可以这么简单理解:

假设现在有100字节数据,面向数据流和面向数据报的连接可以分别怎么样处理呢?

  • 数据流是发送端可以调用10次write,每次10字节。接收端调用一次read。也可以是发送端调用一次write,接收端调用10次read。数据流是没有边界的
  • 数据报则是一次write就需要一次read,是有边界的。

在分布式系统中,如果某个节点宕机了咋办?

做一个数据副本策略,把每一台机器上的数据做几个副本的冗余,放在别的机器上。万一说某一台机器宕机,没事啊,因为其他机器上还有他的副本。

自旋锁

所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁。

自旋锁实现参考链接:https://blog.csdn.net/jeffasd/article/details/80661804

线程同步的方式有哪些

互斥锁、信号量、条件变量、自旋锁、读写锁、屏障

开始补上MPRPC项目的代码实现,从头开始———-day1

简介:项目基于muduo高性能网络库 + protobuf开发,所以命名为mprpc。

技术栈

  • 集群和分布式概念及原理
  • RPC远程过程调用原理及实现
  • Protobuf数据序列化和反序列化协议
  • Zookeeper分布式一致性协调服务应用及编程(服务注册中心,方便寻找哪个服务在哪台服务器上)
  • muduo网络库编程
  • conf配置文件读取
  • CMake构建项目集成编译环境

集群和分布式(搞懂为什么要分布式)

集群:每一台服务器独立运行一个工程的所有模块

分布式:一个工程拆分了很多模块,每一个模块独立部署运行在一个服务器主机上,所有服务器协同工作共同提供服务,每一台服务器称作分布式的一个节点,根据节点的并发要求,对一个节点可以再做节点模块集群部署。

如图中server所示,一个聊天系统包括不同模块:用户管理、好友管理、群组管理、消息管理以及后台管理五个模块。每个模块有自己的特定业务,举个例子,用户管理模块包括用户注册、用户登录、用户注销等。

现在让你提出一些单机聊天服务器的缺陷,你能想到哪些?

  1. 受限于硬件资源,聊天服务器所能承受的用户的并发量,比如端口用光了。
  2. 假设这个单机聊天服务器体系很庞大,项目编译需要两个小时!如果现在消息管理模块有一个小bug,需要改动几行,然后导致需要重新编译、部署整个项代码。
  3. 系统中,有些模块是属于CPU密集型,有些模块是I/O密集型的,造成各模块对于硬件资源的需求是不一样的。既然受限于硬件资源,我们就多部署几台服务器,横向增加服务器数量。但并未解决问题2和3。

集群的优缺点如下,优点:用户的并发量提升了。缺点:项目代码还是需要整体重新编译,而且需要进行多次部署。

现在我们看红色圈,我们把不同模块分类部署在不同服务器上。所有服务器共同构成一个聊天系统,这就是分布式。现在我们将不同服务器分别视为不同分布式节点。比如用户管理模块和消息管理模块对并发要求高,我们可以进行扩容,再部署几台服务器用于用户管理和消息管理(根据节点的并发要求,对一个节点可以再做节点模块集群部署)。分布式系统针对问题2,每个模块独立部署独立运行,哪个模块有bug,我只需要重新编译部署那个模块,其他模块还能正常运行。问题3就很明显了,视不同要求配置不同服务器。

那分布式就全是优点吗?下面我们说说关于分布式设计的难点。

  1. 大系统的软件模块该怎么划分。
  2. 各模块之间怎么访问?集群服务器所有模块运行在一个进程里,不同模块之间访问简便。而分布式各模块都运行在不同的进程里,那服务器1的模块怎么调用服务器2上的模块的一个业务方法呢?我们这个项目所做的就是封装这种远程调用过程,方便用户调用,也就是程序员方便使用我们写的项目(MPRPC)。

RPC通信原理———-day2

通过上图我们可以看到,我们设计的框架主要由以下部分和流程组成:

  • 发起调用端(caller):调用方需要将调用的函数名、参数打包(序列化),并通过网络发送出去。这里打算采用muduo网络库。
  • 接收端(callee):接收方接收到包后,将包里的内容反序列化,就能知道调用哪一个函数、传入的参数是啥,然后返回值依旧是序列化之后通过网络发送回去,发送端接收到后反序列化,得到具体的返回值。
  • 我们的框架主要是实现图中的黄绿部分。
    1. 黄色部分是rpc方法参数的打包和解析,也就是数据的序列化与反序列化,通过protobuf完成。
    2. 在图中,有一些东西没画进去,比如我们将黄色块分别取名为client-stub和server-stub。这两个stub都是执行数据的序列化与反序列化。
    3. 如果远程调用过程中函数执行出错,我们可以返回一些错误码,防止接收端读取错误的返回值。
    4. 绿色部分:网络部分,包括寻找rpc服务主机,发起rpc调用请求和响应rpc调用结果,使用muduo网络库和Zookeeper服务配置中心(专门做服务发现)。

protobuf相对于json的好处:

1、protobuf是二进制存储;xml和json都是文本存储

2、protobuf不需要存储额外的信息;json通过key-value存储数据

  • json:name: “zhang san”, pwd: “123456”

  • protobuf: “zhangsan” “123456”

项目环境搭建

文件组成

  • bin:可执行文件
  • build:项目编译文件
  • lib:项目库文件
  • src:源文件
  • test:测试代码
  • example:框架代码使用范例
  • CMakeLists.txt:顶层的
  • cmake文件
  • README.md:项目自述文件
  • autonbuild.sh:一键编译脚本

Ubuntu protobuf环境搭建

首先在GitHub下载源代码,源码包中src/README.md有详细的安装说明,也可以按照如下步骤安装:

  1. 解压压缩包:unzip protobuf-master.zip
  2. 进入解压后的文件夹:cd protobuf-master
  3. 安装所需工具:sudo apt-get install autoconf automake libtool curl make g++ unzip
  4. 自动生成configure配置文件:./autogen.sh
  5. 配置环境:./configure
  6. 编译源代码(时间比较长):make
  7. 安装:sudo make install
  8. 刷新动态库:sudo ldconfig
  9. 测试:看能否正常执行protoc命令(直接去终端执行)

源码下载地址:https://github.com/protocolbuffers/protobuf

muduo库安装

关于muduo库安装,强烈推荐按照施老师的步骤来(参考链接:https://blog.csdn.net/QIANGWEIYUAN/article/details/89023980),可以不用看我下面的安装步骤,不完整!!!

  1. 安装依赖:sudo apt-get install libbost-dev libbost-test-dev sudo apt-get install libcurl4-openssl-dev libc-ares-dev
  2. 拉文件:git clone https://github.com/chenshuo/muduo.git
  3. ./build.sh

muduo库是否安装成功的测试:muduo_test.cpp实现了一个简单的echo server

编译:

1
g++ -std=c++11 muduo_test.cpp -lmuduo_net -lmuduo_base -lpthread -o muduo_test

执行:

一个shell终端执行

1
./muduo_test 

另一个shell终端执行

1
telnet 127.0.0.1 8032

如下图所示则安装成功

创建文件(循序渐进,现在需要哪些文件夹就创建哪些)

首先,我们可以以自己的名字缩写作为后缀创建一个mprpc文件夹,比如我创建的

1
mkdir mprpc_zcl

接下来我们需要在mprpc_zcl文件夹里创建以下这些文件

1
2
3
4
5
6
7
8
9
cd mprpc_zcl
mkdir bin
mkdir build
mkdir example
mkdir lib
mkdir src
mkdir test
touch autobuild.sh
touch CMakeLists.txt

好啦,第一件该做的事我们已经完成啦!

protobuf实践讲解(一)

首先我们写一个protobuf测试文件来了解一下protobuf的基本使用。

1
2
3
4
5
# test文件夹下创建protobuf文件夹,在protobuf文件夹里创建test.proto文件和main.cpp文件
cd test/
mkdir protobuf
cd protobuf/
touch main.cpp test.proto

首先编写test.proto文件,必要的注释我会放在代码块里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
syntax = "proto3"; // 声明protobuf版本

package fixbug; // 声明了代码所在的包(对于C++来说就是namespace)

/*
message就是消息类型,protobuf就是将我们封装好的数据类型进行序列化与反序列化。
定义登录消息类型:名字 + 密码
string name = 1; 1 代表第一个字段
注意,你的name等变量需要使用什么类型,string要指明。
这里的string是protobuf里的string类型,不是c++里的string类型
我们编译protobuf文件的时候需要使用这样的命令编译:protoc test.proto --cpp_out=./ (=后面是编译生成文件的输出路径)
编译后会生成一个LoginRequest类(这里仅拿LoginRequest举例),类有两个成员:string name 和 string pwd。
如果序列化成功,就把序列化的结果放在了send_str里面了,serializeToString是protobuf编译后人家已经帮我们写好的方法,我们直接用就好了。
*/


// 定义登录请求消息类型 name pwd
message LoginRequest{
string name = 1;
string pwd = 2;
}

// 定于登录响应消息类型 错误码:errcode,错误消息:errmsg, 成功与否:success
message LoginResponse{
int32 errcode = 1;
string errmsg = 2;
bool success = 3;
}

接下来,编写main.cpp进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "test.pb.h"
#include <iostream>
#include <string>
// using namespace fixbug

/*
protobuf是一个动态库,需要链接,所以整个编译命令为:g++ main.cpp test.pb.cc -lprotobuf
*/

int main(){

// 封装了login请求对象的数据
fixbug::LoginRequest req;
req.set_name("zhangsan");
req.set_pwd("123456");

// 对象数据序列化
std::string send_str;
if(req.SerializeToString(&send_str)){
std::cout << send_str.c_str() << std::endl;
}

// 从send_str反序列化一个login请求对象
fixbug::LoginRequest reqB;
if(reqB.ParseFromString(send_str)){
std::cout << reqB.name() << std::endl;
std::cout << reqB.pwd() << std::endl;
}

return 0;
}

protobuf实践讲解(二)

这一小节我们主要引入了两个新知识:

  • 在一个消息类型中定义另外一个消息类型
  • 列表

必要的注释我已经放在代码块里了,大家可以看看,有不对的地方请谅解并麻烦指出来。

首先是test.proto文件的更新版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
syntax = "proto3"; // 声明protobuf版本

package fixbug; // 声明了代码所在的包(对于C++来说就是namespace)

/*
message就是消息类型,protobuf就是将我们封装好的数据类型进行序列化与反序列化。
定义登录消息类型:名字 + 密码
string name = 1; 1 代表第一个字段
注意,你的name等变量需要使用什么类型,string要指明。
这里的string是protobuf里的string类型,不是c++里的string类型
我们编译protobuf文件的时候需要使用这样的命令编译:protoc test.proto --cpp_out=./ (=后面是编译生成文件的输出路径)
编译后会生成一个LoginRequest类(这里仅拿LoginRequest举例),类有两个成员:string name 和 string pwd。
如果序列化成功,就把序列化的结果放在了send_str里面了,serializeToString是protobuf编译后人家已经帮我们写好的方法,我们直接用就好了。
*/

message ResultCode{
int32 errcode = 1;
bytes errmsg = 2;
}

// 定义登录请求消息类型 name pwd
// 这里有一个点可以注意一下,一般把string定义成bytes,string也没错,但bytes效率更高
// 因为你定义成string,protobuf还是要转换成bytes
message LoginRequest{
bytes name = 1;
bytes pwd = 2;
}

// 定于登录响应消息类型 错误码:errcode,错误消息:errmsg, 成功与否:success
/*
注意:对于protobuf的消息类型里面定义的成员变量本身又是另外一个消息类型的话,
他都会提供一个mutable用于改变其成员
*/
message LoginResponse{
ResultCode result = 1;
bool success = 2;
}

// 在protobuf里还有一个常用的叫列表
message GetFriendListsRequest{
uint32 userid = 1;
}

message User{
bytes name = 1;
uint32 age = 2;
enum Sex{
MAN = 0;
WOMAN = 1;
}
Sex sex = 3;
}

message GetFriendListsReponse{
// 这里我们注意到,我们每次定义响应的时候好像都会定义errcode和errmsg字段
// 所以我们决定将其封装起来(ResultCode),便于使用
// int32 errcode = 1;
// bytes errmsg = 2;
ResultCode result = 1;

// 返回的肯定是一个列表,如何表示呢?
repeated User friend_list = 2; // 定义了一个列表类型
}

main.cpp更新版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include "test.pb.h"
#include <iostream>
#include <string>
// using namespace fixbug

/*
protobuf是一个动态库,需要链接,所以整个编译命令为:g++ main.cpp test.pb.cc -lprotobuf
*/

int main(){

// 封装了login请求对象的数据
fixbug::LoginRequest req;
req.set_name("zhangsan");
req.set_pwd("123456");

// 对象数据序列化
std::string send_str;
if(req.SerializeToString(&send_str)){
std::cout << send_str.c_str() << std::endl;
}

// 从send_str反序列化一个login请求对象
fixbug::LoginRequest reqB;
if(reqB.ParseFromString(send_str)){
std::cout << reqB.name() << std::endl;
std::cout << reqB.pwd() << std::endl;
}

fixbug::LoginResponse rsp;
fixbug::ResultCode *rc = rsp.mutable_result();
rc->set_errcode(1);
rc->set_errmsg("登陆处理失败了");

std::string recv_str;
if(rsp.SerializeToString(&recv_str)){
// 好像设置的errcode并没有输出,暂时不知道为啥
std::cout << recv_str.c_str() << std::endl;
}

fixbug::GetFriendListsReponse rsp2;
fixbug::ResultCode *rc2 = rsp2.mutable_result();
rc2->set_errcode(0);

fixbug::User *user1 = rsp2.add_friend_list();
user1->set_name("zhang san");
user1->set_age(20);
user1->set_sex(fixbug::User::MAN);

fixbug::User *user2 = rsp2.add_friend_list();
user2->set_name("li si");
user2->set_age(18);
user2->set_sex(fixbug::User::MAN);

std::string recv_str2;
if(rsp2.SerializeToString(&recv_str2)){
std::cout << "2" << recv_str2.c_str() << std::endl;
}

std::string user_str;
if(user1->SerializeToString(&user_str)){
std::cout << "1" << user_str.c_str() << std::endl;
}

std::cout << rsp2.friend_list_size() << std::endl;

return 0;
}

protobuf实践讲解(三)

这一节我们主要讲了如何引入函数名。因为我们只传输参数给远程rpc服务器是肯定不够的,远程rpc服务器还需要知道我们需要调用哪种方法,因此我们需要使用service在远程rpc服务器上注册函数?(不知道这个理解对不对)同时,方便之后函数调用方传输函数名,里面有一个ServiceDescriptor类型的指针,用于访问方法的各个属性。代码更新如下,主要更新了test.proto文件,main文件没有更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
syntax = "proto3"; // 声明protobuf版本

package fixbug; // 声明了代码所在的包(对于C++来说就是namespace)

option cc_generic_services = true;

/*
message就是消息类型,protobuf就是将我们封装好的数据类型进行序列化与反序列化。
定义登录消息类型:名字 + 密码
string name = 1; 1 代表第一个字段
注意,你的name等变量需要使用什么类型,string要指明。
这里的string是protobuf里的string类型,不是c++里的string类型
我们编译protobuf文件的时候需要使用这样的命令编译:protoc test.proto --cpp_out=./ (=后面是编译生成文件的输出路径)
编译后会生成一个LoginRequest类(这里仅拿LoginRequest举例),类有两个成员:string name 和 string pwd。
如果序列化成功,就把序列化的结果放在了send_str里面了,serializeToString是protobuf编译后人家已经帮我们写好的方法,我们直接用就好了。
*/

message ResultCode{
int32 errcode = 1;
bytes errmsg = 2;
}

// 定义登录请求消息类型 name pwd
// 这里有一个点可以注意一下,一般把string定义成bytes,string也没错,但bytes效率更高
// 因为你定义成string,protobuf还是要转换成bytes
message LoginRequest{
bytes name = 1;
bytes pwd = 2;
}

// 定于登录响应消息类型 错误码:errcode,错误消息:errmsg, 成功与否:success
/*
注意:对于protobuf的消息类型里面定义的成员变量本身又是另外一个消息类型的话,
他都会提供一个mutable用于改变其成员
*/
message LoginResponse{
ResultCode result = 1;
bool success = 2;
}

// 在protobuf里还有一个常用的叫列表
message GetFriendListsRequest{
uint32 userid = 1;
}

message User{
bytes name = 1;
uint32 age = 2;
enum Sex{
MAN = 0;
WOMAN = 1;
}
Sex sex = 3;
}

message GetFriendListsReponse{
// 这里我们注意到,我们每次定义响应的时候好像都会定义errcode和errmsg字段
// 所以我们决定将其封装起来(ResultCode),便于使用
// int32 errcode = 1;
// bytes errmsg = 2;
ResultCode result = 1;

// 返回的肯定是一个列表,如何表示呢?
repeated User friend_list = 2; // 定义了一个列表类型
}

/*
我们在上面已经完成了参数和反回值传输,但我们仅将函数参数传过去,远程服务器并不知道我们要调用哪种方法啊!
protobuf没有rpc通信功能,只进行序列化与反序列化。但我们依旧要在protobuf里完成rpc方法类型的定义描述。
这就需要使用protobuf的service功能,注意此时需要加入option选项, option cc_generic_services = true; 表示生成service服务类和rpc方法描述
*/
// 不管是message还是service,最后都会生成class类。注意message只生成一个,service会生成两个,一个class UserServiceRpc,一个class UserServiceRpc_stub
service UserServiceRpc{
rpc Login(LoginRequest) returns(LoginResponse);
rpc GetFriendLists(GetFriendListsRequest) returns(GetFriendListsReponse);
}

本地服务如何发布成RPC服务(一)

这一块算是正式跨入项目第一步,我们通过业务出发,如果要实现我们所需要的具体需求应该怎么办,直接给出更新后的代码,必要的注释都在代码里

这里先给大家看一个本节之后的文件组成情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.
├── CMakeLists.txt
├── autobuild.sh
├── bin
├── build
├── example
│ ├── CMakeLists.txt
│ ├── callee
│ │ ├── CMakeLists.txt
│ │ └── userservice.cpp
│ ├── caller
│ ├── user.pb.cc
│ ├── user.pb.h
│ └── user.proto
├── lib
├── src
└── test
└── protobuf
├── a.out
├── main.cpp
├── test.pb.cc
├── test.pb.h
└── test.proto

9 directories, 13 files
# 最上面的小点代表根目录:mprpc_zcl

更新后的mprpc_zcl/CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 设置cmake的最低版本和项目名称
cmake_minimum_required(VERSION 3.0)
project(mprpc_zcl)

# 设置项目可执行文件输出的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

# 设置项目库文件输出路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

# 设置项目编译头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_SOURCE_DIR}/example)

# 设置项目库文件搜索路径
link_directories(${PROJECT_SOURCE_DIR}/lib)

# src里面放的是框架代码
# add_subdirectory(src)

# example里面放的是rpc服务的使用者和消费者,业务代码
add_subdirectory(example)

更新后的mprpc_zcl/example/user.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
syntax = "proto3";

package fixbug;

option cc_generic_services = true;

message ResultCode{
int32 errcode = 1;
bytes errmsg = 2;
}

message LoginRequest{
bytes name = 1;
bytes pwd = 2;
}

message LoginResponse{
ResultCode result = 1;
bool success = 2;
}

service UserServiceRpc{
rpc Login_rpc(LoginRequest) returns(LoginResponse);
}

// 注意,编写完之后直接使用protoc user.proto --cpp_out=./编译

更新后的mprpc_zcl/example/CMakeLists.txt

1
add_subdirectory(callee)

更新后的mprpc_zcl/example/callee/userservice.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
我个人觉得这个项目应该从这里开始讲解,我们现在本地有一个用户登录Login_Local方法(隶属于UserService类)。
我们自己在本地调用那肯定很方便,创建一个UserService类对象,直接调用Login_Local方法即可
可是,如果现在有另外一个进程或者另外一台机器想调用我们这个方法怎么办呢?所以我们就要结合谷歌protobuf提供给我们的工具了
这个文件写的就是rpc服务提供者文件,我们在别人想要调用这个本地方法之前需要做哪些基础工作

1、proto文件经过protobuf编译后生成了UserServiceRpc类
2、我们需要在userservice.cpp文件里对这个类里的方法进行重写
3、注意:message会变为类的成员,service会生成一个类,service里的内容会生成方法
4、现在的问题就是:我们重写过后,远方是怎么调用的呢?
*/

#include <iostream>
#include <string>
#include "../user.pb.h"

class Userservice : public fixbug::UserServiceRpc // 使用在rpc服务发布端(rpc服务提供者)
{
public:
bool Login_local(std::string name, std::string pwd){
std::cout << "doing local service: Login" << std::endl;
std::cout << "name:" << name << " pwd:" << pwd;
return true;
}

// 注意,作用过程是这样的,当远端将请求发送过来,先会被我们的rpc框架接受,我们rpc框架根据接收到的参数,函数名等数据
// 匹配到了我们重写的这个函数,然后调用了这个函数。
// 所以这一块不属于框架的代码,是我们要使用这个框架必须自己写的代码,实现自己需要的功能。
void Login_rpc(::google::protobuf::RpcController* controller,
const ::fixbug::LoginRequest* request,
::fixbug::LoginResponse* response,
::google::protobuf::Closure* done)
{
// 暂时还没写完
}
};

int main(){
return 0;
}

更新后的mprpc_zcl/example/callee/CMakeLists.txt

1
2
set(SRC_LIST userservice.cpp ../user.pb.cc)
add_executable(provider ${SRC_LIST})

本地服务如何发布成RPC服务(二)

这一节必要的注释我都放在了代码里,以及思考的过程,这一节只对userservice.cpp进行了更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
我个人觉得这个项目应该从这里开始讲解,我们现在本地有一个用户登录Login_Local方法(隶属于UserService类)。
我们自己在本地调用那肯定很方便,创建一个UserService类对象,直接调用Login_Local方法即可
可是,如果现在有另外一个进程或者另外一台机器想调用我们这个方法怎么办呢?所以我们就要结合谷歌protobuf提供给我们的工具了
这个文件写的就是rpc服务提供者文件,我们在别人想要调用这个本地方法之前需要做哪些基础工作

1、proto文件经过protobuf编译后生成了UserServiceRpc类
2、我们需要在userservice.cpp文件里对这个类里的方法进行重写
3、注意:message会变为类的成员,service会生成一个类,service里的内容会生成方法
4、现在的问题就是:我们重写过后,远方是怎么调用的呢?
*/

#include <iostream>
#include <string>
#include "../user.pb.h"

class Userservice : public fixbug::UserServiceRpc // 使用在rpc服务发布端(rpc服务提供者)
{
public:
bool Login_local(std::string name, std::string pwd){
std::cout << "doing local service: Login" << std::endl;
std::cout << "name:" << name << " pwd:" << pwd;
return true;
}

// 注意,作用过程是这样的,当远端将请求发送过来,先会被我们的rpc框架接受,我们rpc框架根据接收到的参数,函数名等数据
// 匹配到了我们重写的这个函数,然后调用了这个函数。
// 所以这一块不属于框架的代码,是我们要使用这个框架必须自己写的代码,实现自己需要的功能。
// 现在的问题是,我callee端收到了参数和函数名,怎么匹配的呢?
void Login_rpc(::google::protobuf::RpcController* controller,
const ::fixbug::LoginRequest* request,
::fixbug::LoginResponse* response,
::google::protobuf::Closure* done)
{
// 框架给业务上报了请求参数LoginRequest,应用获取相应数据做本地业务
std::string name = request->name();
std::string pwd = request->pwd();

// 做本地业务
bool login_result = Login_local(name, pwd);

// 把响应写入,包括错误码、错误消息、返回值。我们不需要管序列化与反序列化,这个是框架来做的
fixbug::ResultCode *code = response->mutable_result();
code->set_errcode(0);
code->set_errmsg("");
response->set_success(login_result);

// 执行回调操作,我们可以跳进::google::protobuf::Closure类去看看,里面的run是纯虚函数,需要我们进行重新写,那么run应该实现什么功能呢?
// 其实就是执行响应对象数据的序列化与网络发送
done->Run();
}

/*
总结一下,初始准备步骤:
写proto文件 ------》继承生成的类,重写类里的方法
*/
};

int main(){


return 0;
}

Mprpc框架基础类设计

这一节我们从服务发布方的需求出发,比如我需要发布一个rpc服务,我需要做什么?我们考虑思路是这样的:假设现在框架写好了,当远端将请求发送过来,先会被我们的rpc框架接受,我们rpc框架根据接收到的参数,函数名等数据。匹配到了我们重写的这个函数,然后调用了这个函数。所以这一块不属于框架的代码,是我们要使用这个框架必须自己写的代码,实现自己需要的功能。现在的问题是,我callee端收到了参数和函数名,怎么匹配的呢?

我们现在做好了服务发布方的基础工作,我们现在需要思考一个问题:我怎么能让别人想用我们写的rpc框架呢?答案只有一个,就是越简单越好。

  1. 先进行框架初始化操作
  2. 框架里提供了用于发布服务的类

这一节更新的文件如下

新加入src/include/mprpcapplication.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once


// 负责框架的初始化操作
class MprpcApplication{
public:
static void Init(int argc, char **argv);
static MprpcApplication &GetInstance();
private:
MprpcApplication(){}
MprpcApplication(const MprpcApplication&) = delete;
MprpcApplication(MprpcApplication&&) = delete;
};

新加入src/include/mprpcprovider.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once
#include "google/protobuf/service.h"


// 框架提供的专门服务发布rpc服务的网络对象类
class RpcProvider{
public:
// 这个是框架提供给外部使用的,可以发布rpc方法的函数接口
void NotifyService(google::protobuf::Service *service);

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void Run();
};

新加入src/mprpcapplication.cpp

1
2
3
4
5
6
7
8
9
10
#include "include/mprpcapplication.h"

void MprpcApplication::Init(int argc, char **argv){

}

MprpcApplication &MprpcApplication::GetInstance(){
static MprpcApplication app;
return app;
}

新加入src/mprpcprovider.cpp

1
2
3
4
5
6
7
8
9
10
#include "include/mprpcprovider.h"

void RpcProvider::NotifyService(google::protobuf::Service *service){

}

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run(){

}

更新了example/callee/userservice.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
我个人觉得这个项目应该从这里开始讲解,我们现在本地有一个用户登录Login_Local方法(隶属于UserService类)。
我们自己在本地调用那肯定很方便,创建一个UserService类对象,直接调用Login_Local方法即可
可是,如果现在有另外一个进程或者另外一台机器想调用我们这个方法怎么办呢?所以我们就要结合谷歌protobuf提供给我们的工具了
这个文件写的就是rpc服务提供者文件,我们在别人想要调用这个本地方法之前需要做哪些基础工作

1、proto文件经过protobuf编译后生成了UserServiceRpc类
2、我们需要在userservice.cpp文件里对这个类里的方法进行重写
3、注意:message会变为类的成员,service会生成一个类,service里的内容会生成方法
4、现在的问题就是:我们重写过后,远方是怎么调用的呢?
*/

#include <iostream>
#include <string>
#include "../user.pb.h"
#include "/home/zcl/mprpc_zcl/src/include/mprpcapplication.h"
#include "/home/zcl/mprpc_zcl/src/include/mprpcprovider.h"

class UserService : public fixbug::UserServiceRpc // 使用在rpc服务发布端(rpc服务提供者)
{
public:
bool Login_local(std::string name, std::string pwd){
std::cout << "doing local service: Login" << std::endl;
std::cout << "name:" << name << " pwd:" << pwd;
return true;
}

// 注意,作用过程是这样的,当远端将请求发送过来,先会被我们的rpc框架接受,我们rpc框架根据接收到的参数,函数名等数据
// 匹配到了我们重写的这个函数,然后调用了这个函数。
// 所以这一块不属于框架的代码,是我们要使用这个框架必须自己写的代码,实现自己需要的功能。
// 现在的问题是,我callee端收到了参数和函数名,怎么匹配的呢?
void Login_rpc(::google::protobuf::RpcController* controller,
const ::fixbug::LoginRequest* request,
::fixbug::LoginResponse* response,
::google::protobuf::Closure* done)
{
// 框架给业务上报了请求参数LoginRequest,应用获取相应数据做本地业务
std::string name = request->name();
std::string pwd = request->pwd();

// 做本地业务
bool login_result = Login_local(name, pwd);

// 把响应写入,包括错误码、错误消息、返回值。我们不需要管序列化与反序列化,这个是框架来做的
fixbug::ResultCode *code = response->mutable_result();
code->set_errcode(0);
code->set_errmsg("");
response->set_success(login_result);

// 执行回调操作,我们可以跳进::google::protobuf::Closure类去看看,里面的run是纯虚函数,需要我们进行重新写,那么run应该实现什么功能呢?
// 其实就是执行响应对象数据的序列化与网络发送
done->Run();
}

/*
总结一下,初始准备步骤:
写proto文件 ------》继承生成的类,重写类里的方法
*/
};

/*
我们现在做好了服务发布方的基础工作,我们现在需要思考一个问题:我怎么能让别人想用我们写的rpc框架呢?答案只有一个,就是越简单越好。
1、先进行框架初始化操作
2、框架里提供了用于发布服务的类
*/

int main(int argc, char **argv){
// argc和argv是写ip地址和端口号配置文件这些的

// 调用框架的初始化操作
MprpcApplication::Init(argc, argv);

// provider是一个rpc网络服务对象。把UserService对象发布到rpc节点上
// 可能会有很多用户同时使用Rpcprovider,所以这一块必须做到高并发,使用muduo网络库
RpcProvider provider;
provider.NotifyService(new UserService());

// 启动一个rpc服务发布节点, Run以后,进程进入阻塞状态,等待远程的rpc调用请求
provider.Run();
return 0;
}

Mprpc框架项目动态库编译

在init的时候,我们希望用户的输入是这样的:./provider -i config.conf(config.conf是配置文件,自动读取网络服务器和配置中心的ip地址和端口号)。

这节涉及到一个函数:int getopt(int argc, char * const argv[], const char *optstring); 我先给出这个函数的详细解释,方便解读下面的代码

首先,让我们看看getopt函数:

getopt函数用于解析命令行参数。其原型如下:

1
int getopt(int argc, char * const argv[], const char *optstring);

  • argcargv是从main函数传递过来的命令行参数数量和参数值。
  • optstring是一个字符串,表示我们期望的选项。例如,如果我们期望一个-i选项,那么optstring就会是"i:"。冒号表示-i后面必须跟一个参数值。

函数每次调用都会返回一个字符,这个字符表示被解析到的选项。如果选项后面跟有参数值(如-i value),那么这个值可以通过optarg全局变量获得。当所有选项都被解析完毕后,getopt返回-1。

现在,让我们回到你的代码片段:

1
2
3
4
int c;
while ((c = getopt(argc, argv, "i:")) != -1) {
// ...
}

这个while循环的目的是持续解析命令行参数,直到所有选项都被解析完毕。

  • c = getopt(argc, argv, "i:"):这里,getopt被调用,并返回值赋给c。如果有-i选项,c会等于字符'i'

  • c != -1:这个条件检查c是否不等于-1。如果c等于-1,那么说明所有选项都已经被解析完毕,while循环结束。

while循环的内部,你可能会基于c的值做一些操作,例如:

1
2
3
4
5
6
7
8
9
switch (c) {
case 'i':
// do something with optarg, which contains the value after -i
break;
// possibly handle other options
case '?':
// handle unknown option
break;
}

简而言之,这个代码片段用getopt函数在命令行参数中寻找-i选项,并将找到的值存储在optarg中。

在使用 getopt 函数来解析命令行参数时,你可能会遇到几种特定的返回值。当你在 optstring 中指定了一个选项后跟冒号(如 “i:”),这意味着该选项需要一个参数。

对于 getopt 的返回值:

  • 如果一个选项被发现,并且它有一个关联的参数(例如 -i <value>),那么 getopt 返回该选项字符。
  • 如果一个选项被发现,但它缺少一个关联的参数(例如仅仅 -i 而没有后续值),并且在 optstring 中该选项后面有一个冒号,那么 getopt 返回 ':'
  • 如果找到一个不在 optstring 中的选项,或者找到一个不应有参数但却有参数的选项,那么 getopt 返回 '?'

所以,case ':'switch 语句中处理的是缺少参数的选项情况。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int c;
while ((c = getopt(argc, argv, "i:")) != -1) {
switch (c) {
case 'i':
// handle the -i option with its argument in optarg
break;
case ':': // Missing option argument
fprintf(stderr, "Option -%c requires an argument.\n", optopt);
break;
case '?': // Unknown option
fprintf(stderr, "Unknown option: -%c\n", optopt);
break;
}
}

在上述代码中,如果用户只输入 -i 而没有提供参数,程序将输出 “Option -i requires an argument.”。

新加入src/CMakeLists.txt

1
2
aux_source_directory(. SRC_LIST)
add_library(mprpc SHARED ${SRC_LIST}) # 创建一个动态库,方便用户调用

更新example/callee/CMakeLists

1
2
3
set(SRC_LIST userservice.cpp ../user.pb.cc)
add_executable(provider ${SRC_LIST})
target_link_libraries(provider mprpc protobuf) # 链接我们上面创建的mprpc动态库和protobuf库

更新mprpcapplication.cpp文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "include/mprpcapplication.h"
#include <iostream>
#include <cstdlib>
#include <unistd.h>

void showArgHelp(){
std::cout << "format: command -i <configfile>" << std::endl;
}

void MprpcApplication::Init(int argc, char **argv){
if(argc < 2){
showArgHelp();
exit(EXIT_FAILURE);
}

int c = 0;
std::string config_file;

while((c = getopt(argc, argv, "i:")) != -1){
switch(c){
case 'i':
config_file = optarg;
break;
case '?':
std::cout << "invalid args!" << std::endl;
showArgHelp();
exit(EXIT_FAILURE); // 配置文件都没加载进来就不要break啦,直接退出运行吧
case ':':
std::cout << "need config_file" << std::endl;
showArgHelp();
exit(EXIT_FAILURE);
default:
break;
}
}

// 开始加载配置文件了 rpcserver_ip= rpcserver_port= zookeeper_ip= zookeeper_port=
}

MprpcApplication &MprpcApplication::GetInstance(){
static MprpcApplication app;
return app;
}

Mprpc配置文件的加载(一)

这一节我们主要讲的是如何加载配置文件,换句话说就是解析配置文件。首先我们规定了配置文件的标准格式,如下所示:

1
2
3
4
5
6
7
8
# rpc节点的ip地址
rpcserverip = 127.0.0.1
# rpc节点的port端口号
rpcserverport = 8000
# zk的IP地址
zookeeperip = 127.0.0.1
# zk的port端口号
zookeeperport = 5000

这一节引入了MprpcConfig类,我们考虑两部分:解析配置文件 + 查询配置信息。一个自然而然要思考的问题就是如何查询配置信息呢?我们这里考虑的是通过一个map映射即通过键找值。具体如何操作看代码就能了解了。

新加入配置文件/bin/test.conf

1
2
3
4
5
6
7
8
# rpc节点的ip地址
rpcserverip = 127.0.0.1
# rpc节点的port端口号
rpcserverport = 8000
# zk的IP地址
zookeeperip = 127.0.0.1
# zk的port端口号
zookeeperport = 5000

新加入/src/include/mprpcconfig.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once

#include <unordered_map>
#include <string>

class MprpcConfig{
public:
// 负责解析加载配置文件
void loadConfigFile(const char *config_file);
// 查询配置项信息
std::string load(const std::string &key);
private:
// 每解析到一组ip---port,就insert进m_configMap
std::unordered_map<std::string, std::string> m_configMap;
};

新加入/src/mprpcconfig.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "include/mprpcconfig.h"
#include <iostream>
#include <string>

// 负责解析加载配置文件
void MprpcConfig::loadConfigFile(const char *config_file){
FILE *pf = fopen(config_file, "r");
if(pf == nullptr){
std::cout << config_file << " is not exist!" << std::endl;
exit(EXIT_FAILURE);
}

// 1、注释 2、正确的配置项通过=判断 3、去掉开头多余的空格
while(!feof(pf)){
char buf[512] = {0};
fgets(buf, 512, pf);

// 去掉字符串前面的空格
// 转换成字符串便于后续操作,因为字符串里有很多函数
std::string src_buf(buf);
int idx = src_buf.find_first_not_of(' ');
if(idx != -1){
// 说明字符串前面有空格
src_buf = src_buf.substr(idx, src_buf.size() - idx);
}
// 去掉字符串后面多余的空格
idx = src_buf.find_last_not_of(' ');
if(idx != -1){
src_buf = src_buf.substr(0, idx);
}

// 判断#的注释
if(src_buf[0] == '#' || src_buf.empty()){
continue;
}

// 解析配置项
idx = src_buf.find('=');
if(idx == -1){
// 配置项不合法
continue;
}

std::string key;
std::string value;
key = src_buf.substr(0, idx);
value = src_buf.substr(idx + 1, src_buf.size() - idx);
m_configMap.insert({key, value});
}
}
// 查询配置项信息
std::string MprpcConfig::load(const std::string &key){
auto it = m_configMap.find(key);
if(it == m_configMap.end()){
return "";
}
return it->second;
}

更新/src/include/mprpcapplication.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once


// 负责框架的初始化操作
#include "mprpcconfig.h"

class MprpcApplication{
public:
static void Init(int argc, char **argv);
static MprpcApplication &GetInstance();
private:
static MprpcConfig m_config;
MprpcApplication(){}
MprpcApplication(const MprpcApplication&) = delete;
MprpcApplication(MprpcApplication&&) = delete;
};

更新/src/mprpcapplication.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "include/mprpcapplication.h"
#include <iostream>
#include <cstdlib>
#include <unistd.h>

MprpcConfig MprpcApplication::m_config;

void showArgHelp(){
std::cout << "format: command -i <configfile>" << std::endl;
}

void MprpcApplication::Init(int argc, char **argv){
if(argc < 2){
showArgHelp();
exit(EXIT_FAILURE);
}

int c = 0;
std::string config_file;

while((c = getopt(argc, argv, "i:")) != -1){
switch(c){
case 'i':
config_file = optarg;
break;
case '?':
showArgHelp();
exit(EXIT_FAILURE);
case ':':
showArgHelp();
exit(EXIT_FAILURE);
default:
break;
}
}

// 开始加载配置文件了 rpcserver_ip= rpcserver_port= zookeeper_ip= zookeeper_port=
// 这是我们规定的配置文件的标准格式,因为我们后面解析的配置文件也是这个格式
m_config.loadConfigFile(config_file.c_str());
std::cout << "rpcserverip:" << m_config.load("reserverip") << std::endl;
std::cout << "rpcserverport:" << m_config.load("reserverport") << std::endl;
std::cout << "zookeeperip:" << m_config.load("zookeeperip") << std::endl;
std::cout << "zookeeperport:" << m_config.load("zookeeperport") << std::endl;
}

MprpcApplication &MprpcApplication::GetInstance(){
static MprpcApplication app;
return app;
}

Mprpc配置文件的加载(二)

在上一节中好像忘记告诉大家如何编译测试代码了。不知道大家还记不记得我们的可执行文件都是放在bin目录里的,所以我们需要先进入bin目录,即cd bin/。

然后为了我们就可以在终端执行

1
./provider -i test.conf

从这个编译命令就可以看出来我们为什么要把test.conf文件和provider都放在bin目录下,这样方便我们编译。然后你们可以测试一下上一节的代码,是有bug的!然后我们需要进行gdb调试,进行gdb调试的话,我们需要在最外层的CMakeLists.txt文件加一行这个代码

1
set(CMAKE_BUILD_TYPE "Debug")

bug出现在了mprpcconfig.cpp文件里,你们自己调试一下哈,我下面给出更新后的本节所有代码

更新后的CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 设置cmake的最低版本和项目名称
cmake_minimum_required(VERSION 3.0)
project(mprpc_zcl)

# gdb调试选项
set(CMAKE_BUILD_TYPE "Debug")

# 设置项目可执行文件输出的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

# 设置项目库文件输出路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

# 设置项目编译头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/src/include)
include_directories(${PROJECT_SOURCE_DIR}/example)

# 设置项目库文件搜索路径
link_directories(${PROJECT_SOURCE_DIR}/lib)

# src里面放的是框架代码
add_subdirectory(src)

# example里面放的是rpc服务的使用者和消费者,业务代码
add_subdirectory(example)

更新后的/src/include/mprpcconfig.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once

#include <unordered_map>
#include <string>

class MprpcConfig{
public:
// 负责解析加载配置文件
void loadConfigFile(const char *config_file);
// 查询配置项信息
std::string load(const std::string &key);
private:
std::unordered_map<std::string, std::string> m_configMap;

// 去掉字符串前后的空格
void Trim(std::string &src_buf);
};

更新后的mprpcconfig.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "include/mprpcconfig.h"
#include <iostream>
#include <string>

// 负责解析加载配置文件
void MprpcConfig::loadConfigFile(const char *config_file){
FILE *pf = fopen(config_file, "r");
if(pf == nullptr){
std::cout << config_file << " is not exist!" << std::endl;
exit(EXIT_FAILURE);
}

// 1、注释 2、正确的配置项通过=判断 3、去掉开头多余的空格
while(!feof(pf)){
char buf[512] = {0};
fgets(buf, 512, pf);

// 去掉字符串前面的空格
// 转换成字符串便于后续操作,因为字符串里有很多函数
std::string read_buf(buf);
Trim(read_buf);

// 判断#的注释
if(read_buf[0] == '#' || read_buf.empty()){
continue;
}

// 解析配置项
int idx = read_buf.find('=');
if(idx == -1){
// 配置项不合法
continue;
}

std::string key;
std::string value;
key = read_buf.substr(0, idx);
Trim(key);
int endidx = read_buf.find('\n', idx);
value = read_buf.substr(idx + 1, endidx - idx - 1);
Trim(value);
m_configMap.insert({key, value});
}
}
// 查询配置项信息
std::string MprpcConfig::load(const std::string &key){
auto it = m_configMap.find(key);
if(it == m_configMap.end()){
return "";
}
return it->second;
}

// 去掉字符串前后的空格
void MprpcConfig::Trim(std::string &src_buf){
int idx = src_buf.find_first_not_of(' ');
if(idx != -1){
// 说明字符串前面有空格
src_buf = src_buf.substr(idx, src_buf.size() - idx);
}
// 去掉字符串后面多余的空格
idx = src_buf.find_last_not_of(' ');
if(idx != -1){
src_buf = src_buf.substr(0, idx + 1);
}
}

开发RpcProvider的网络服务

是这样的,我们现在已经能够读取到配置文件里的信息了,包括哪些信息呢?还记得吗?来一起回顾一下:rpc服务的ip地址和port端口号,zookeeper的ip地址和port端口号。我们现在回到这个配置文件这里,我们从这个角度出发,我们读取配置文件就是为了获取rpc服务发布节点的ip地址和port端口号,我们是为了什么读取呢?是为了让客户端能够接入,能够调用我们发布的这个rpc服务方法。我们之前在mprpcapplication里说过,为了简便用户的操作,我们提供了一个RpcProvoder类来发布服务节点。所以,这节我们要做的就是通过muduo网络库让配置文件里的rpc服务节点运行起来,可以接收客户端的连接请求。Let‘s go!

新加入mprpcprovider.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma once
#include "google/protobuf/service.h"
#include "mprpcapplication.h"
#include <memory>
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>
#include <muduo/net/TcpConnection.h>

// 框架提供的专门服务发布rpc服务的网络对象类
class RpcProvider{
public:
// 这个是框架提供给外部使用的,可以发布rpc方法的函数接口
void NotifyService(google::protobuf::Service *service);

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void Run();

private:
// 组合了EventLoop
muduo::net::EventLoop m_eventLoop;

// 新的socket连接回调
void OnConnection(const muduo::net::TcpConnectionPtr&);

// 已建立连接用户的读写事件回调
void OnMessage(const muduo::net::TcpConnectionPtr&, muduo::net::Buffer*, muduo::Timestamp);
};

新加入mprpcprovider.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "include/mprpcprovider.h"
#include "include/mprpcapplication.h"
#include <string>
#include <functional>

void RpcProvider::NotifyService(google::protobuf::Service *service){

}

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run(){
// 你启动一个rpc网络服务节点,总要获取ip地址和port端口号吧
// 所以这里是获取ip地址和port端口号
std::string ip = MprpcApplication::GetInstance().getConfig().load("rpcserverip");
uint16_t port = atoi(MprpcApplication::GetInstance().getConfig().load("rpcserverport").c_str());
muduo::net::InetAddress address(ip, port);

// 创建TcpServer对象
muduo::net::TcpServer server(&m_eventLoop, address, "RpcProvider");
// 绑定连接回调和消息读写回调方法 分离了网络代码和业务代码
server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));
server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 设置muduo库线程数量,自动分配I/O线程和工作线程
server.setThreadNum(4);

std::cout << "RpcProvider start service at ip:" << ip << " port:" << port << std::endl;

// 启动网络服务
server.start();
m_eventLoop.loop();
}

void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr&){

}

void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr&, muduo::net::Buffer*, muduo::Timestamp){

}

RpcProvider发布服务方法(一)

我们想一下,我们发布的rpc服务节点运行起来阻塞着等待客户的请求连接。假设现在有一个客户端将函数及其参数全部传递过来了,框架应该怎么做匹配呢?框架怎么就能做到这个函数名就匹配这个函数呢?对,有人应该想到了,使用map。我现在的想法也是map。我们将服务与函数对应起来(回忆一下:proto文件里服务生成类,函数就是类成员函数),所以要先限定服务,在限定函数。所以我们使用map将服务函数对应起来就好了。

更新mprpcprovider.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#pragma once
#include "google/protobuf/service.h"
#include "mprpcapplication.h"
#include <memory>
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>
#include <muduo/net/TcpConnection.h>
#include <google/protobuf/descriptor.h>
#include <unordered_map>
#include <string>
#include <functional>


// 框架提供的专门服务发布rpc服务的网络对象类
class RpcProvider{
public:
// 这个是框架提供给外部使用的,可以发布rpc方法的函数接口
void NotifyService(google::protobuf::Service *service);

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void Run();

private:
// 组合了EventLoop
muduo::net::EventLoop m_eventLoop;

// service服务类型信息
struct ServiceInfo{
google::protobuf::Service *m_service; // 保存服务对象
std::unordered_map<std::string, const google::protobuf::MethodDescriptor*> m_methodMap; // 保存服务方法
};
std::unordered_map<std::string, ServiceInfo> m_serviceMap; // 可不止一个服务类型哦,所以也要建立一个映射到不同服务的表

// 新的socket连接回调
void OnConnection(const muduo::net::TcpConnectionPtr&);

// 已建立连接用户的读写事件回调
void OnMessage(const muduo::net::TcpConnectionPtr&, muduo::net::Buffer*, muduo::Timestamp);
};

更新mprpcprovider.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "include/mprpcprovider.h"
#include "include/mprpcapplication.h"

/*
这里是框架提供给外部使用的,可以发布rpc方法的函数接口
这里有一个很重要的指针(protobuf提供给我们获取服务及函数的):google::protobuf::ServiceDescriptor *pserviceDesc

*/
void RpcProvider::NotifyService(google::protobuf::Service *service){
ServiceInfo service_info;

// 获取服务对象的描述信息
const google::protobuf::ServiceDescriptor *pserviceDesc = service->GetDescriptor();
// 获取服务的名字
std::string service_name = pserviceDesc->name();
// 获取服务对象service的方法的数量
int methodCnt = pserviceDesc->method_count();

std::cout << "service_name: " << service_name << std::endl;

for(int i = 0; i < methodCnt; ++i){
// 获取了服务对象指定下标的服务方法的描述(抽象描述)
const google::protobuf::MethodDescriptor *pmethodDesc = pserviceDesc->method(i);
std::string method_name = pmethodDesc->name();
service_info.m_methodMap.insert({method_name, pmethodDesc});
std::cout << "method_name: " << method_name << std::endl;
}
service_info.m_service = service;
m_serviceMap.insert({service_name, service_info});
}

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run(){
// 你启动一个rpc网络服务节点,总要获取ip地址和port端口号吧
// 所以这里是获取ip地址和port端口号
std::string ip = MprpcApplication::GetInstance().getConfig().load("rpcserverip");
uint16_t port = atoi(MprpcApplication::GetInstance().getConfig().load("rpcserverport").c_str());
muduo::net::InetAddress address(ip, port);

// 创建TcpServer对象
muduo::net::TcpServer server(&m_eventLoop, address, "RpcProvider");
// 绑定连接回调和消息读写回调方法 分离了网络代码和业务代码
server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));
server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 设置muduo库线程数量,自动分配I/O线程和工作线程
server.setThreadNum(4);

std::cout << "RpcProvider start service at ip:" << ip << " port:" << port << std::endl;

// 启动网络服务
server.start();
m_eventLoop.loop();
}

void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr&){

}

void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr&, muduo::net::Buffer*, muduo::Timestamp){

}

接下来就是要处理当rpc服务节点接收到来自客户端的已经序列化的请求该如何处理。首先,我们框架内部RpcProvider和RpcConsumer协商好之间通信用的protobuf数据类型,这样才方便序列化与反序列化。我们考虑将服务名+方法名作为头部字段,同时,为了防止后面的参数与下一次请求产生粘包问题,我们需要在头部字段里声明参数的大小。另外还有一个问题就是,我们如何分离出头部字段和参数字段呢?一个方法就是指明头部字段有多长。所以,我们的代码更新如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include "include/mprpcprovider.h"
#include "include/mprpcapplication.h"
#include "mprpcheader.pb.h"

/*
这里是框架提供给外部使用的,可以发布rpc方法的函数接口
这里有一个很重要的指针(protobuf提供给我们获取服务及函数的):google::protobuf::ServiceDescriptor *pserviceDesc

*/
void RpcProvider::NotifyService(google::protobuf::Service *service){
ServiceInfo service_info;

// 获取服务对象的描述信息
const google::protobuf::ServiceDescriptor *pserviceDesc = service->GetDescriptor();
// 获取服务的名字
std::string service_name = pserviceDesc->name();
// 获取服务对象service的方法的数量
int methodCnt = pserviceDesc->method_count();

std::cout << "service_name: " << service_name << std::endl;

for(int i = 0; i < methodCnt; ++i){
// 获取了服务对象指定下标的服务方法的描述(抽象描述)
// 注意,这里建立map表都是依据proto文件建立的!!!思考一下哦!
const google::protobuf::MethodDescriptor *pmethodDesc = pserviceDesc->method(i);
std::string method_name = pmethodDesc->name();
service_info.m_methodMap.insert({method_name, pmethodDesc});
std::cout << "method_name: " << method_name << std::endl;
}
service_info.m_service = service;
m_serviceMap.insert({service_name, service_info});
}

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run(){
// 你启动一个rpc网络服务节点,总要获取ip地址和port端口号吧
// 所以这里是获取ip地址和port端口号
std::string ip = MprpcApplication::GetInstance().getConfig().load("rpcserverip");
uint16_t port = atoi(MprpcApplication::GetInstance().getConfig().load("rpcserverport").c_str());
muduo::net::InetAddress address(ip, port);

// 创建TcpServer对象
muduo::net::TcpServer server(&m_eventLoop, address, "RpcProvider");
// 绑定连接回调和消息读写回调方法 分离了网络代码和业务代码
server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));
server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 设置muduo库线程数量,自动分配I/O线程和工作线程
server.setThreadNum(4);

std::cout << "RpcProvider start service at ip:" << ip << " port:" << port << std::endl;

// 启动网络服务
server.start();
m_eventLoop.loop();
}

void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr& conn){
if(!conn->connected()){
// 和rpc client的连接断开了
conn->shutdown();
}
}

/*
在框架内部,RpcProvider和RpcConsumer协商好之间通信用的protobuf数据类型
service_name method_name args 定义proto的message类型,进行数据头的序列化与反序列化
service_name method_name args_size(args_size是为了防止粘包问题,指定参数长度)
16UserServiceLogin_rpc16zhang san123456

header_size + header_str + args_size + args_str
*/
void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buffer, muduo::Timestamp){
// 接收到来自客户端的请求
std::string recv_buf = buffer->retrieveAllAsString();

// 从字符流中读取前四个字节的内容
uint32_t header_size = 0;
recv_buf.copy((char*)&header_size, 4, 0);

// 根据header_size读取数据头的原始字符流,反序列化数据,得到rpc请求的详细信息
std::string rpc_header_str = recv_buf.substr(4, header_size);
mprpc::RpcHeader rpcHeader;
std::string servcie_name;
std::string method_name;
uint32_t args_size;
if(rpcHeader.ParseFromString(rpc_header_str)){
servcie_name = rpcHeader.service_name();
method_name = rpcHeader.method_name();
args_size = rpcHeader.args_size();
}
else{
std::cout << "rpc_header_str:" << rpc_header_str << " parse error!" << std::endl;
return;
}

// 获取rpc方法参数的字符流数据
std::string args_str = recv_buf.substr(4 + header_size, args_size);

std::cout << "---------------------------" << std::endl;
std::cout << "header_size: " << header_size << std::endl;
std::cout << "rpc_header_str: " << rpc_header_str << std::endl;
std::cout << "servcie_name: " << servcie_name << std::endl;
std::cout << "method_name: " << method_name << std::endl;
std::cout << "args_str: " << args_str << std::endl;
std::cout << "---------------------------" << std::endl;
}

RpcProvider响应回调实现

回忆一下provider应该做的事,调用方法并返回response。所以这一节做的就是这个事情。注意request和response都是继承自google::protobuf::Message。

更新mprpcprovider.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#pragma once
#include "google/protobuf/service.h"
#include "mprpcapplication.h"
#include <memory>
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>
#include <muduo/net/TcpConnection.h>
#include <google/protobuf/descriptor.h>
#include <unordered_map>
#include <string>
#include <functional>


// 框架提供的专门服务发布rpc服务的网络对象类
class RpcProvider{
public:
// 这个是框架提供给外部使用的,可以发布rpc方法的函数接口
void NotifyService(google::protobuf::Service *service);

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void Run();

private:
// 组合了EventLoop
muduo::net::EventLoop m_eventLoop;

// service服务类型信息
struct ServiceInfo{
google::protobuf::Service *m_service; // 保存服务对象
std::unordered_map<std::string, const google::protobuf::MethodDescriptor*> m_methodMap; // 保存服务方法
};
std::unordered_map<std::string, ServiceInfo> m_serviceMap; // 可不止一个服务类型哦,所以也要建立一个映射到不同服务的表

// 新的socket连接回调
void OnConnection(const muduo::net::TcpConnectionPtr&);

// 已建立连接用户的读写事件回调
void OnMessage(const muduo::net::TcpConnectionPtr&, muduo::net::Buffer*, muduo::Timestamp);

// Closure的回调操作,用于序列化rpc的响应和网络发送
void SendRpcResponse(const muduo::net::TcpConnectionPtr&, google::protobuf::Message*);
};

更新mprpcprovider.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include "include/mprpcprovider.h"
#include "include/mprpcapplication.h"
#include "mprpcheader.pb.h"

/*
这里是框架提供给外部使用的,可以发布rpc方法的函数接口
这里有一个很重要的指针(protobuf提供给我们获取服务及函数的):google::protobuf::ServiceDescriptor *pserviceDesc

*/
void RpcProvider::NotifyService(google::protobuf::Service *service){
ServiceInfo service_info;

// 获取服务对象的描述信息
const google::protobuf::ServiceDescriptor *pserviceDesc = service->GetDescriptor();
// 获取服务的名字
std::string service_name = pserviceDesc->name();
// 获取服务对象service的方法的数量
int methodCnt = pserviceDesc->method_count();

std::cout << "service_name: " << service_name << std::endl;

for(int i = 0; i < methodCnt; ++i){
// 获取了服务对象指定下标的服务方法的描述(抽象描述)
// 注意,这里建立map表都是依据proto文件建立的!!!思考一下哦!
const google::protobuf::MethodDescriptor *pmethodDesc = pserviceDesc->method(i);
std::string method_name = pmethodDesc->name();
service_info.m_methodMap.insert({method_name, pmethodDesc});
std::cout << "method_name: " << method_name << std::endl;
}
service_info.m_service = service;
m_serviceMap.insert({service_name, service_info});
}

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run(){
// 你启动一个rpc网络服务节点,总要获取ip地址和port端口号吧
// 所以这里是获取ip地址和port端口号
std::string ip = MprpcApplication::GetInstance().getConfig().load("rpcserverip");
uint16_t port = atoi(MprpcApplication::GetInstance().getConfig().load("rpcserverport").c_str());
muduo::net::InetAddress address(ip, port);

// 创建TcpServer对象
muduo::net::TcpServer server(&m_eventLoop, address, "RpcProvider");
// 绑定连接回调和消息读写回调方法 分离了网络代码和业务代码
server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));
server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 设置muduo库线程数量,自动分配I/O线程和工作线程
server.setThreadNum(4);

std::cout << "RpcProvider start service at ip:" << ip << " port:" << port << std::endl;

// 启动网络服务
server.start();
m_eventLoop.loop();
}

void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr& conn){
if(!conn->connected()){
// 和rpc client的连接断开了
conn->shutdown();
}
}

/*
在框架内部,RpcProvider和RpcConsumer协商好之间通信用的protobuf数据类型
service_name method_name args 定义proto的message类型,进行数据头的序列化与反序列化
service_name method_name args_size(args_size是为了防止粘包问题,指定参数长度)
16UserServiceLogin_rpc16zhang san123456

header_size + header_str + args_size + args_str
*/
void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buffer, muduo::Timestamp){
// 接收到来自客户端的请求
std::string recv_buf = buffer->retrieveAllAsString();

// 从字符流中读取前四个字节的内容
uint32_t header_size = 0;
recv_buf.copy((char*)&header_size, 4, 0);

// 根据header_size读取数据头的原始字符流,反序列化数据,得到rpc请求的详细信息
std::string rpc_header_str = recv_buf.substr(4, header_size);
mprpc::RpcHeader rpcHeader;
std::string servcie_name;
std::string method_name;
uint32_t args_size;
if(rpcHeader.ParseFromString(rpc_header_str)){
servcie_name = rpcHeader.service_name();
method_name = rpcHeader.method_name();
args_size = rpcHeader.args_size();
}
else{
std::cout << "rpc_header_str:" << rpc_header_str << " parse error!" << std::endl;
return;
}

// 获取rpc方法参数的字符流数据
std::string args_str = recv_buf.substr(4 + header_size, args_size);

std::cout << "---------------------------" << std::endl;
std::cout << "header_size: " << header_size << std::endl;
std::cout << "rpc_header_str: " << rpc_header_str << std::endl;
std::cout << "servcie_name: " << servcie_name << std::endl;
std::cout << "method_name: " << method_name << std::endl;
std::cout << "args_str: " << args_str << std::endl;
std::cout << "---------------------------" << std::endl;

// 获取servcie对象和method对象
auto it = m_serviceMap.find(servcie_name);
if(it == m_serviceMap.end()){
std::cout << servcie_name << " is not exist!" << std::endl;
return;
}

auto mit = it->second.m_methodMap.find(method_name);
if(mit == it->second.m_methodMap.end()){
std::cout << servcie_name << ":" << method_name << " is not exist!" << std::endl;
return;
}

google::protobuf::Service *service = it->second.m_service; // 获取seivice对象
const google::protobuf::MethodDescriptor *method = mit->second; // 获取method对像

google::protobuf::Message *request = service->GetRequestPrototype(method).New();
if(!request->ParseFromString(args_str)){
std::cout << "request parse error, content:" << args_str << std::endl;
return;
}
google::protobuf::Message *response = service->GetResponsePrototype(method).New();

// 给下面的method方法的调用,绑定一个Closure的回调函数
google::protobuf::Closure *done = google::protobuf::NewCallback<RpcProvider, const muduo::net::TcpConnectionPtr &, google::protobuf::Message *>(this, &RpcProvider::SendRpcResponse, conn, response);

// 在框架上根据远端rpc请求,调用当前rpc节点上发布的方法
service->CallMethod(method, nullptr, request, response, done);
}

// Closure的回调操作,用于序列化rpc的响应和网络发送
void RpcProvider::SendRpcResponse(const muduo::net::TcpConnectionPtr &conn, google::protobuf::Message *response){
std::string response_str;
if(response->SerializeToString(&response_str)){ // response进行序列化
// 序列化成功后,通过网络把rpc方法执行的结果发送回rpc的调用方
conn->send(response_str);
}
else{
std::cout << "serialize response_str error!" << std::endl;
}
conn->shutdown(); // 模拟http的短连接服务,由provider主动断开连接
}

RpcController

为什么需要这个呢?这里我们需要注意到一个问题,举个例子,在我们的callfriendservice.cpp文件中,当我们使用完stub调用函数GetFriendlist之后,就直接开始读取response了。但你有没有想过,如果在序列化、网络发送、反序列化、函数执行等过程中产生错误了呢?那我们根本就拿不到response或者说拿到的response是错误的。RpcController可以帮助我们记录一些rpc调用过程中的状态信息。

快速排序

tips:为什么快速排序算法把基准元素名称定义为pivot

在快速排序算法中,“pivot”(基准元素)是一个用来将数据集分割成两部分的元素。所有比基准元素小的元素都被放到它的左边,而所有比它大的元素都被放到它的右边。这就是为什么它被称为 “pivot”(枢轴),因为它在排序过程中起到了中心轴的作用,就像一个旋转门或者天平的支点那样。

在一次快速排序的分割操作中,我们从数组的一端开始,将所有比pivot小的元素放到左边,比pivot大的元素放到右边。这个过程称为分区操作(partitioning)。经过分区操作后,pivot元素会位于数组的某个位置,它左边的所有元素都不大于它,它右边的所有元素都不小于它,所以它就到了排序后应该在的位置。

然后我们可以递归地对pivot左边的元素和右边的元素分别进行快速排序,这样整个数组就会变得有序。

所以,pivot元素在快速排序中起到了关键的作用,它是算法的核心部分。

代码实现

以下是一个C++的快速排序实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <vector>

// 交换函数
void swap(int& a, int& b) {
int t = a;
a = b;
b = t;
}

// 快速排序的一次划分操作
int partition(std::vector<int>& arr, int low, int high) {
int pivot = arr[high]; // 选择最右边的元素作为基准元素
int i = low - 1;

for(int j = low; j <= high-1; j++) {
if(arr[j] < pivot) {
i++;
swap(arr[i], arr[j]);
}
}
swap(arr[i+1], arr[high]);
return (i + 1);
}

// 快速排序函数
void quickSort(std::vector<int>& arr, int low, int high) {
if(low < high) {
int pi = partition(arr, low, high); // 执行一次划分操作

quickSort(arr, low, pi - 1); // 对左侧子数组递归执行快速排序
quickSort(arr, pi + 1, high); // 对右侧子数组递归执行快速排序
}
}

// 主函数
int main() {
std::vector<int> arr = {10, 7, 8, 9, 1, 5};
int n = arr.size();
quickSort(arr, 0, n-1);
std::cout << "Sorted array: \n";
for(int i = 0; i < n; i++)
std::cout << arr[i] << " ";
std::cout << std::endl;
return 0;
}

在这个程序中,partition函数是快速排序的核心,它实现了一次划分操作。我们首先选择一个基准元素(这里选择的是数组最右侧的元素),然后将所有比基准元素小的元素移动到数组的左侧,比基准元素大的元素移动到数组的右侧。

quickSort函数是一个递归函数,它首先调用partition函数进行一次划分操作,然后对基准元素左侧和右侧的子数组分别递归调用quickSort函数进行排序。这个过程会一直递归下去,直到子数组的大小为1或0,此时子数组已经是有序的,递归结束。

在实现代码时犯了一个小错误

partition函数实现时,最后一步的交换过程中,应该是swap(arr[i+1], arr[high]),而我第一次实现时写成了swap(arr[i+1], pivot)。为什么这样不行呢?貌似看起来是合理的,因为我们在一开始就有语句:int pivot = arr[high];但是恰巧问题就是出现在了这里,因为我们要交换的是vector容器里的元素,我们在实现int pivot = arr[high];这个语句时pivot只是拿到了arr[high]的值,因此我们并没有实际交换arr[i+1]arr[high],所以排序后的结果肯定是错的。(因此,定义成这样int& pivot = arr[high],就可以这样写:swap(arr[i+1], pivot)

堆排序

堆排序是一种基于堆的排序算法。它使用了一种叫做堆的数据结构。堆有两种类型:最大堆和最小堆。最大堆的特性是父节点的值大于或等于其所有子节点的值,最小堆的特性是父节点的值小于或等于其所有子节点的值。

堆排序的基本步骤如下:

  1. 建立最大堆:将待排序序列构造成一个最大堆,这样就能保证整个序列的最大值就是堆顶的根节点。
  2. 交换数据:将根节点与最后一个元素交换位置,然后断开(排除)最后一个元素。
  3. 重建最大堆:通过调整使剩余元素重新构成最大堆。
  4. 重复步骤2~3,直到整个序列有序。

这里挂一个讲堆排序很好的博客,一定要耐心阅读!引用链接

堆排序c++实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void heapify(vector<int>& arr, int n, int i) {
int largest = i; //从largest往下开始建立大根堆,暂不考虑largset往上的数据
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && arr[left] > arr[largest]) {
largest = left;
}

if (right < n && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest);
}
}

void heapSort(vector<int>& arr) {
int n = arr.size();

for (int i = n / 2 - 1; i >= 0; --i) {
heapify(arr, n, i);
}

for (int i = n - 1; i >= 0; --i) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}

int main() {
vector<int> arr = { 10, 7, 8, 9, 1, 5, 5, 5, 5 };
heapSort(arr);
for (auto a : arr) {
cout << a << " ";
}
cout << endl;
return 0;
}
自己初步实现时没有彻底弄明白堆排序,所以有如下这个问题

为什么第一次需要

1
2
3
for (int i = n / 2 - 1; i >= 0; --i) {
heapify(arr, n, i);
}

而从i=n-1开始,就直接 heapify(arr, i, 0);就没有i=n/2-1的这个过程

这是因为我们在开始的时候是在创建最大堆。在创建最大堆的过程中,我们需要从最后一个非叶子节点(即 n / 2 - 1)开始,然后向前遍历到根节点(即 0),对每个节点进行下沉操作,确保其满足最大堆的性质。这是创建最大堆的过程。

然后,我们进入排序的步骤,每次将当前最大的元素(即堆顶元素)与当前堆的最后一个元素交换,然后断开最后一个元素(即排除最后一个元素,使堆的大小减小1),然后再将新的堆顶元素进行下沉操作,确保剩余元素还是一个最大堆。这个过程一直持续到整个堆的元素都被排除,即整个数组都被排序。

因此,在排序的过程中,我们不需要再从 n / 2 - 1 开始了,因为除了堆顶元素以外,其他元素都满足最大堆的性质,所以我们只需要将新的堆顶元素进行下沉操作即可,也就是 heapify(arr, i, 0);

1、HTTP常见的状态码

  • 1XX 提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
  • 2XX 成功,报文已经成功收到并被正确处理;
  • 3XX 重定向,资源位置发生变动,需要客户端重新发送请求;
  • 4XX 客户端错误,请求报文有误,服务器无法处理;
  • 5XX 服务器错误。服务器在处理请求时内部发生了错误;

2、HTTP请求和响应的各自组成部分

HTTP通信协议由请求(Request)和响应(Response)两部分构成。每个请求和响应都由头部和主体组成,其中头部包含了各种元数据,而主体则包含了实际的内容(如果有的话)。

HTTP请求

HTTP请求包括以下字段:

  1. 请求行(Request Line):请求行包括HTTP方法(比如GET、POST、PUT、DELETE等)、请求的URI、以及HTTP版本。
  2. 请求头(Request Headers):请求头包含了一系列的字段,每个字段都提供了一些关于请求的元数据。常见的请求头包括:
    • Host: 请求的目标主机名和端口号。
    • User-Agent: 发起请求的用户代理的信息,通常包括浏览器类型、版本、操作系统等信息。
    • Accept: 客户端可以处理的MIME类型。
    • Accept-Language: 客户端接受的语言。
    • Accept-Encoding: 客户端接受的内容编码,比如gzip。
    • Cookie: 客户端存储的用于服务器识别的cookie。
    • Content-Type: 如果请求包含了主体,这个字段描述了主体的MIME类型。
    • Content-Length: 如果请求包含了主体,这个字段描述了主体的长度。
  3. 请求主体(Request Body):不是所有的请求都包含主体。比如GET和HEAD请求就没有主体。但是POST和PUT请求通常会有主体,包含了要发送给服务器的数据。
HTTP响应

HTTP响应包括以下字段:

  1. 状态行(Status Line):状态行包括HTTP版本、状态码(比如200表示成功,404表示未找到等),以及状态描述。
  2. 响应头(Response Headers):响应头包含了一系列的字段,每个字段都提供了一些关于响应的元数据。常见的响应头包括:
    • Server: 发送响应的服务器的信息。
    • Content-Type: 响应主体的MIME类型。
    • Content-Length: 响应主体的长度。
    • Content-Encoding: 响应主体的内容编码,比如gzip。
    • Set-Cookie: 服务器想要设置在客户端的cookie。
    • Last-Modified: 资源的最后修改日期。
    • ETag: 资源的版本标识。
  3. 响应主体(Response Body):响应的主体包含了服务器返回的数据,比如HTML页面、图片、JSON数据等。

3、完整的HTTP请求和响应展示

让我们来看一下一对典型的HTTP请求和响应。

HTTP请求示例

假设你在浏览器中访问 http://www.example.com,你的浏览器可能会发送如下的GET请求:

1
2
3
4
5
6
7
GET / HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP响应示例

对于上述的请求,服务器可能会返回如下的响应:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Date: Mon, 23 May 2023 22:38:34 GMT
Server: Apache/2.4.1 (Unix)
Last-Modified: Sat, 20 May 2023 18:56:51 GMT
ETag: "2dcd3-2c-54b5a5e68b177"
Accept-Ranges: bytes
Content-Length: 44
Vary: Accept-Encoding
Content-Type: text/html

<html><body><h1>It works!</h1></body></html>

这个响应表示请求成功(状态码是200),服务器返回了一个简单的HTML页面作为响应主体。响应头还包含了一些其他的信息,比如服务器类型(Apache),最后修改日期,内容长度,内容类型等。

希望这些示例可以帮助你更好地理解HTTP请求和响应!

4、TCP的KeepAlive和HTTP的Keep-Alive

TCP的KeepAlive和HTTP的Keep-Alive虽然名字类似,但实际上是两个不同级别的概念,分别在TCP和HTTP协议层级中起作用。

TCP的KeepAlive

TCP的KeepAlive是一个底层的、可选的机制,其目的是为了检测和维护处于空闲状态的TCP连接。一旦启用,如果在特定的时间间隔内(通常在数小时)没有任何数据在TCP连接上进行交换,那么发送方就会发送一个KeepAlive数据包到接收方,而无需传输任何应用级别的数据。接收方需要对这个数据包做出响应。如果发送方在一定时间内没有收到响应,它将重发KeepAlive数据包,一般会尝试多次。如果仍然没有收到响应,发送方将假设连接已经断开,并将其关闭。

HTTP的Keep-Alive

HTTP的Keep-Alive是在HTTP 1.1引入的,用来允许单一的TCP连接被多个HTTP请求/响应共享,而不是每个请求/响应都重新建立一个新的连接。这显著提高了网络通信的效率,因为建立和关闭TCP连接需要时间和资源。

在HTTP 1.0中,每个HTTP请求/响应都需要一个新的TCP连接,这被称为非持久连接。而在HTTP 1.1中,默认启用了Keep-Alive,也就是说默认使用持久连接,除非明确指定”Connection: close”。

当使用HTTP Keep-Alive时,HTTP请求的头部会包含一个”Connection: keep-alive”字段,这告诉服务器客户端希望保持连接以便发送更多的请求。服务器的响应也会包含一个”Connection: keep-alive”字段,这表示服务器同意保持连接。

总的来说,这两个概念都关于连接的维护,但是工作在不同的层级。TCP的KeepAlive主要是为了检测和处理僵死连接,而HTTP的Keep-Alive则是为了提高效率,通过复用已存在的TCP连接来发送多个HTTP请求/响应。

队头阻塞

这里涉及到一个小概念需要说一下:可以想象,当我们开启Keep-Alive时,我们无需像以前那样,先发送A请求,等待服务器回应,再发送B请求…..一直如此运作下去。我们可以发送A请求后,无需等待服务器响应,紧接着发送B请求。但是服务器还是按照顺序响应,先响应A请求,完成后再响应B请求。其实这里很明显感觉到会有问题,如果迟迟收不到A响应,后面的响应就更收不到了,就会造成所谓的队头阻塞问题。

在使用HTTP长连接时,如果一个客户端完成了一个HTTP请求后不在发起请求,此时这个TCP连接一直占用不就会导致浪费资源吗?

对没错,所以为了避免资源浪费的情况,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。

比如设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

传输层协议有哪些

UDPTCP协议;

TCP协议的特点

面向连接的:采用TCP协议通信的双方必须通过三次握手建立连接才能开始数据的读写。完成数据交换以后,通信双方都必须断开连接,以释放内核的资源。

面向字节流的:当用户消息通过TCP协议传输时,TCP模块先将这些消息放入TCP发送缓冲区,发送的时候消息可能会被操作系统分组成多个TCP报文。注意,当我们调用send函数的时候,消息并没有真正被发送出去,只是被拷贝到了操作系统内核协议栈中。何时被真正发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件

可靠的:1、TCP采用发送应答机制,即TCP发送端发送每一个TCP报文段都要接收到对方的应答,才认为这个TCP报文段传输成功;2、TCP协议采用超时重传机制,发送端发送一个TCP报文段之后,开启定时器,如果在规定时间内没有接收到应答,则重传此TCP报文段;3、因为TCP报文段最终是以IP数据报发送的,IP数据报到达接受方的时候可能乱序、重复,TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层。

描述TCP头部结构的大致组成部分

16位源端口号、16位目标端口号、32位序号、32位确认号、4位头部长度、6位保留、6位标志位、16位窗口大小、16位校验和、16位紧急指针

简述各部分作用

源端口号代表的是是从发送端的哪一个端口发送出来的,目的端口号代表的是发送给接收端上层应用程序的哪一个端口。

TCP报文段的序号值等于系统初始化的某个随机值ISN加上该报文段在字节流中的偏移(偏移值)。

确认号就是接收到的TCP报文段的序号值加1。

4位头部长度代表TCP报文段整个头部长度有多少个32bit字(四字节)。

6位标志位分别为:URG(紧急指针是否有效)、ACK(确认号)、PSH(接收端应立即从TCP接收缓冲区中读走数据)、RST(表示要求对方重新建立连接)、SYN(请求建立一个连接)、FIN(通知对方本端要关闭连接)。

16位窗口大小是流量控制的一个手段,他告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据。

16位校验和由发送端填充,接收端采取CRC算法对接收到的TCP报文段验证。

TCP连接的建立和关闭

首先,通过数据展示一下三次握手:

发送端:seq 12345 (SYN) ————- 接收端:seq 56789 (SYN) ack 12346 ————- 发送端:ack 56790

接下来是四次握手,假设发送端首先发出了断开连接请求

发:seq 12346, ack 56790 (FIN) —— 收:ack 12347 —— 收:seq 56790, ack 12347 (FIN) ——- 发:ack 56791

实际上,四次握手中的收的第一次确认报文段可以省略,因为下一次收的TCP报文段里包含了ack。其实第一个ack是否应该出现取决于TCP的延迟确认特性。

TCP状态转移

服务器处于被动等待客户连接,可以简单描述一下当有客户端请求连接时,该连接服务器端的状态变化为:

LISTEN ————- SYN RCVD —————– ESTABLISHED.

当客户端主动关闭连接时,服务器端状态变化为:

CLOSE_WAIT ————– LAST_ACK.

请求连接时,客户端状态变化为,考虑connect系统调用成功

SYN_SENT ————- ESTABLISHED

注意,connect系统调用失败有三个原因:

1、connect连接的目标端口不存在(未被任何线程监听)

2、connect试图连接的端口处在TIME_WAIT状态

3、没有收到服务器端的应答报文

connect调用失败则返回CLOSE状态。

客户端请求关闭时,客户端状态变化为

FIN_WAIT_1 ———- (FIN_WAIT_2) ————– TIME_WAIT

TIME_WAIT状态好好看看书的3.4.2节,讲的很好

复位报文段

什么情况下接收端会回复复位报文段?

1、发送端访问一个不存在的窗口。

2、发送端访问的服务器端的窗口仍处于TIME_WAIT状态。

3、半打开状态向连接发送数据,会收到复位报文段。

收到复位报文段应该如何处理呢

收到复位报文段的机器应该断开连接或者重新发起连接。

半打开状态

假设现在服务器端(或者客户端)关闭或者异常终止了连接,客户端(或者服务器端)并没有接收到结束报文段,依旧保持连接状态,就称为半打开连接状态。如果此时客户端(或者服务器端)向此连接发送数据,会收到一个复位报文段的回应报文。

关于MSS的计算

首先需要了解两个名词,一个是MTUMaxMaximum Transmission Unit)是指网络传输中最大的数据包大小,MSSMaximum Segment Size)是指TCP协议中数据段的最大大小。在TCP协议中,MSS是由MTU减去IPTCP头部的长度得出的。

IP头部的长度通常为20个字节,TCP头部的长度通常为20个字节,所以MSS的计算方法为:

MSS= MTU - IP头部长度 - TCP头部长度

因此,在MTU为16436字节的情况下,MSS的计算公式为:

MSS = 16436 - 20 - 20 = 16396

其中,20是IP头部和TCP头部的长度之和,这是因为IPTCP协议都需要使用头部来传递各种控制信息,如源地址、目标地址、端口号、序列号、确认号等。因此,在TCP协议中,MSSMTU减去IPTCP头部长度的结果。

你知道c++三大特性吗?那你又是否清楚他们的特点呢?

c++三大特性分别为:封装、继承和多态

封装: 1、三个特点

结合性,即将属性(数据成员)和行为(成员函数)相结合

信息隐蔽性,利用接口机制隐蔽内部实现细节,只留下接口给外界调用

实现代码重用,在此举个例子解释所谓的实现代码重用:

例如,如果你需要在程序的多个地方进行文件读写操作,你可以创建一个名为FileHandler的类,将文件的打开、关闭、读取和写入等操作封装在该类中的成员函数中。然后,你可以在程序的各个地方实例化FileHandler对象,并调用其成员函数来执行文件操作,而不需要每次都编写打开、关闭、读取和写入的代码。

这种方式可以大大简化程序的编写和维护,并且当你需要对文件操作的逻辑进行修改时,只需要修改FileHandler类中的代码,而不需要修改所有调用该类的地方。

继承:

类的派生就是指从已有类产生新类的过程,原有类称为基类或父类,产生的新类称为子类或派生类,子类继承基类后,可以创建子类对象调用基类的函数和变量等。

多态:

多态(Polymorphism)是面向对象编程中的一个重要概念,它允许使用基类类型的指针或引用来调用派生类对象的特定方法。C++中的多态性是通过虚函数(virtual function)和动态绑定(dynamic binding)实现的。

在C++中,要实现多态,需要满足以下条件: 1. 基类(父类)中声明一个虚函数。 2. 派生类(子类)中重写(覆盖)这个虚函数,并使用关键字override进行标记。

例如,考虑一个基类Shape和两个派生类CircleRectangle的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

class Shape {
public:
virtual void draw() {
std::cout << "Drawing a shape." << std::endl;
}
};

class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();

shape1->draw(); // 调用派生类的方法:Drawing a circle.
shape2->draw(); // 调用派生类的方法:Drawing a rectangle.

delete shape1;
delete shape2;

return 0;
}

在上面的例子中,基类Shape声明了一个虚函数draw(),并且两个派生类CircleRectangle分别重写了这个函数。在main()函数中,我们创建了两个指向基类的指针shape1shape2,分别指向派生类CircleRectangle的对象。然后,通过基类指针调用draw()函数,由于这个函数是虚函数,实际调用的是派生类的版本,即产生了多态行为。

通过多态,我们可以使用统一的接口来处理不同的对象,提高了代码的灵活性和可维护性。此外,通过将基类指针或引用传递给函数或方法,我们可以在运行时确定要调用的具体实现,而不是在编译时静态地确定。

需要注意的是,在基类中使用关键字virtual来声明虚函数是很重要的,而派生类中的重写函数则使用关键字override进行标记,以确保正确的绑定和多态行为。

函数体内部声明一个string变量,未进行初始化,会进行默认初始化吗?

在c++中,如果你在函数体内部声明一个std::string变量而不进行初始化,该变量会被默认初始化。对于std::string,默认初始化意味着会创建一个空字符串。

例如:

1
2
3
4
void someFunction(){
std::string s;
// .....
}

在这个例子中,s 被默认初始化为一个空字符串。当你尝试打印 s 或者获取 s.length() 时,你会发现s是一个空字符串。

注意这与内置类型如intfloat等不同,这些类型如果没有明确初始化,它们的值是未定义的。

string属于内置类型吗?

是的,string 类型不属于内置类型。std::string 是 C++ 标准库中的一个类,它是对字符数组(C 风格字符串)的封装,提供了更加方便、安全的字符串操作功能。

内置类型(也称为基本类型或原始类型)是编程语言中预定义的、最基本的数据类型,例如 intfloatdoublechar 等。这些类型通常直接映射到计算机硬件上的表示,所以操作起来非常高效。

相比之下,std::string 是一个类,其对象由多个数据成员和成员函数组成,提供了对字符串的各种操作。虽然 std::string 不是内置类型,但它在 C++ 中被广泛使用,因为它提供了比 C 风格字符串更方便和安全的方式来处理字符串。

#pragma once的作用和#ifndef一样么

是的,#pragma once#ifndef(加上对应的 #define#endif)都用于防止头文件的重复包含。它们的作用相似,但实现方式有所不同。

  1. #pragma once#pragma once 是一种编译器指令,用于确保头文件只被包含一次。当编译器遇到 #pragma once 时,它会记录已经包含了该头文件,从而避免重复包含。这种方法简洁易懂,只需在头文件的开头添加一行代码。

    示例:

    1
    2
    3
    4
    // header.h
    #pragma once

    // ... 声明和定义

    请注意,#pragma once 不是 C++ 标准的一部分,但大多数现代编译器都支持它。

  2. #ifndef#define#endif: 这是一种被称为“包含保护”的技术,通过预处理器指令来防止头文件的重复包含。首先使用 #ifndef 检查一个特定的宏是否已定义,如果未定义,则使用 #define 定义宏,并将头文件的内容包含在 #ifndef#endif 之间。

    示例:

    1
    2
    3
    4
    5
    6
    7
    // header.h
    #ifndef HEADER_H
    #define HEADER_H

    // ... 声明和定义

    #endif // HEADER_H

    这种方法是 C++ 标准的一部分,因此具有更好的可移植性。

总的来说,#pragma once#ifndef 都可以防止头文件的重复包含,但实现方式不同。在实际项目中,你可以根据编译器支持和代码可移植性的需求来选择其中一种方法。

总结一下上面的:作用是一样的,关于ifndef的具体细节可以去看C++ primer第68页的有关介绍。

C++分离式编译

举例一个简单情况:

我在Chapter6.h头文件中声明了fact函数,在fact.cpp文件中定义了fact函数,并在factMain.cpp文件中使用了fact函数,为什么在factMain.cpp文件中,只需要引入Chapter6.h头文件就可以调用到fact函数,Chapter6.h头文件里不是只有一个声明吗?

解答:

Chapter6.h 头文件中只有 fact 函数的声明。然而,这就是在 C++ 中分离声明和定义的好处。你在头文件中声明函数(告诉编译器有一个名为 fact 的函数存在),然后在一个源文件(例如 fact.cpp)中定义该函数(提供函数的具体实现)。这样,其他源文件(例如 factMain.cpp)可以通过包含相应的头文件来获取函数的声明,从而能够调用该函数。

在编译阶段,编译器会分别编译 fact.cppfactMain.cpp,并生成相应的对象文件(例如 fact.ofactMain.o)。在 fact.cpp 中定义的 fact 函数的具体实现会在 fact.o 中。

然后在链接阶段,链接器会把所有的对象文件连接在一起,形成一个可执行文件。链接器会解决所有未解决的符号引用,例如在 factMain.o 中对 fact 函数的调用。因为 fact 函数在 fact.o 中定义了,所以链接器能够找到它,并正确地设置引用。

因此,尽管 Chapter6.h 头文件中只有 fact 函数的声明,但只要 fact 函数在其他地方被定义,那么在 factMain.cpp 中就可以通过包含 Chapter6.h 来调用 fact 函数。

为什么不可以同时用conststatic修饰成员函数。详细解释一下

C++中,conststatic都可以用于修饰成员函数,但它们有不同的含义,并且不能同时应用于同一个成员函数。下面我们来详细了解一下:

  1. const成员函数:如果一个成员函数被声明为const,那么它就不能修改对象的任何非静态数据成员或者调用任何非const成员函数。在const成员函数中,关键字const是在参数列表之后声明的。const成员函数可以被任何类型的对象调用,无论是const还是非const对象。

    例如:

    1
    2
    3
    4
    5
    class MyClass {
    public:
    void nonConstMethod() { /* 可以修改对象的状态 */ }
    void constMethod() const { /* 不能修改对象的状态 */ }
    };
  2. static 成员函数:static 成员函数与类的对象无关,不需要对象就可以调用。它不能访问类的非静态成员(包括数据成员和成员函数),因为非静态成员都是与特定对象关联的,而 static 成员函数与任何对象都无关。

    例如:

    1
    2
    3
    4
    5
    class MyClass {
    public:
    static void staticMethod() { /* 不能访问非静态成员 */ }
    void nonStaticMethod() { /* 可以访问非静态成员 */ }
    };

这就是为什么不能同时用 conststatic 修饰成员函数的原因:const 成员函数是基于对象的,它不能改变对象的状态,但可以访问对象的状态。而 static 成员函数是不基于对象的,它不能访问对象的状态。因此,conststatic 在这里是冲突的,不能同时用于同一个成员函数。

如果一个成员函数被声明为 const,那么它就不能修改对象的任何非静态数据成员或者调用任何非 const 成员函数。能举个例子吗?解释的更通俗一点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {
int data;

public:
MyClass() : data(0) {}

void increment() { ++data; } // 非const成员函数

int getValue() const { return data; } // const成员函数

void tryToModify() const {
// ++data; // 这里将导致编译错误,因为不能在const成员函数中修改非静态数据成员
// increment(); // 这里也将导致编译错误,因为不能在const成员函数中调用非const成员函数
}
};

在这个类中,increment() 是一个非 const 成员函数,它可以修改对象的状态。getValue() 是一个 const 成员函数,它不能修改对象的状态,只能返回对象的状态。

tryToModify() 也是一个 const 成员函数,试图在其中修改 data 或者调用 increment() 都会导致编译错误,因为这些操作都会改变对象的状态,而 const 成员函数不允许这样做。

通俗来说,const 成员函数就像一个诺言,它承诺我不会改变对象的状态。这样,你就可以放心地把对象传给这个函数,知道这个函数不会做任何坏事。这在编程中是非常有用的,因为它可以帮助你避免一些常见的错误,并使你的代码更容易理解和维护。

static 成员函数与类的对象无关,不需要对象就可以调用。这句话怎么理解呢?不需要对象如何调用呢?直接传参吗?能举个例子吗

static 成员函数是与类本身相关联的,而不是与类的特定实例(即对象)相关联。因此,你不需要创建一个类的实例就可以调用 static 成员函数。你可以直接使用类名和作用域解析运算符 :: 来调用 static 成员函数。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
static void staticMethod() {
std::cout << "Calling static method" << std::endl;
}
};

int main() {
MyClass::staticMethod(); // 不需要创建MyClass的实例就可以调用staticMethod
return 0;
}

在这个例子中,我们没有创建 MyClass 的实例就直接调用了 staticMethod。这是因为 staticMethod 是一个 static 成员函数,它与类本身相关联,而不是与类的特定实例相关联。

注意,static 成员函数不能访问类的非 static 成员(包括数据成员和成员函数),因为非 static 成员都是与特定对象关联的,而 static 成员函数与任何对象都无关。这也是为什么 static 成员函数不能有 this 指针的原因。

深拷贝和浅拷贝

在用户未自定义拷贝构造函数的情况下,系统会调用默认的拷贝构造函数,系统默认的拷贝构造函数执行浅拷贝。当数据成员中没有指针时,浅拷贝是可行的。

但当数据成员中出现指针时,如果依旧使用默认的拷贝构造函数,就会出现调用两次析构函数,造成悬空指针问题。

深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

问了一个很愚蠢的问题:我发现在定义一个类的时候,构造函数会对private里的类似于int,string和指针等类型显式或隐式的初始化,而一些函数只给出了声明,函数为什么不用给出定义?

C++中,类的定义通常分为两部分:声明和定义。声明是告诉编译器某个东西(例如变量、函数、类等)的存在,而定义则提供了这个东西的实际实现或分配了它的内存。

在类的定义中,构造函数对数据成员进行初始化,这是构造函数的主要目的。对于简单类型如intstring或者指针,可以在构造函数中显式初始化,也可以使用默认的初始化(对于类类型,调用其默认构造函数;对于内置类型,如果它们是静态存储期的对象,则进行值初始化,即初始化为0,否则不进行初始化)。

对于成员函数,只需要在类定义中给出声明,这是因为我们通常希望将函数的定义(实现)放在类定义之外,例如在一个.cpp文件中。这样可以将接口(即类的声明)与实现(即函数的定义)分离,使代码更清晰,易于理解和维护。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyClass.h
class MyClass {
public:
MyClass(int a); // 构造函数声明
void foo(); // 成员函数声明
private:
int data;
};

// MyClass.cpp
#include "MyClass.h"
#include <iostream>

MyClass::MyClass(int a) : data(a) {} // 构造函数定义

void MyClass::foo() { // 成员函数定义
std::cout << data << std::endl;
}

当然,也可以直接在类定义中提供成员函数的定义。这通常在函数体很短,或者希望函数被内联(inline)时这样做:

1
2
3
4
5
6
7
class MyClass {
public:
MyClass(int a) : data(a) {}
void foo() { std::cout << data << std::endl; }
private:
int data;
};

在这种情况下,foo函数会自动被编译器视为内联函数,可以减少函数调用开销,但可能增加代码大小。

数组不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

1
2
3
int a[] = {0, 1, 2};
int a2[] = a; //错误,不允许使用一个数组初始化另一个数组
a2 = a; //错误,不能把一个数组直接赋值给另外一个数组

标准库函数begin()和end()

数组毕竟不是类类型,因此这两个函数不是成员函数,正确的使用形式是把数组作为他们的参数传入:

1
2
3
int ia[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
int* beg = begin(ia); //指向ia首元素的指针
int* last = end(ia); //指向ia尾元素的下一位置的指针
创建一个vector容器存放string类型的对象

1、首先我们确定一下这个类要实现哪些功能,初步设计包括三个:

  • 实现push_back向容器尾后迭代器指定位置添加元素;
  • 实现size获取容器中存放元素个数的功能;
  • 实现capacity获取容器当前最大容量的功能;

同时,可以想到以上三个要在public里实现,作为对外的接口。

2、要实现以上这些功能(拷贝控制函数那些后面再实现)需要哪些变量呢?

  • 在一开始,我们需要获得一个原始内存,在这里我们采用一个allocator来获得原始内存;

  • 三个指针,分别指向vector容器的首元素、最后一个实际元素之后的位置、分配的内存末尾之后的位置;

3、拷贝控制函数的实现需要哪些工具函数

为了方便各种拷贝控制函数的实现,我们需要实现一些工具函数。拿拷贝构造函数举例来说,我们需要对一个vector容器对象进行拷贝,在拷贝构造函数里直接调用工具函数肯定会使得整个代码结构更加清晰明朗。

  • alloc_n_copy:分配内存,并拷贝一个给定范围中的元素;
  • free:销毁构造的元素并释放内存;
  • chk_n_alloc:保证StrVec至少有容纳一个新元素的空间。如果没有空间添加新元素,chk_n_alloc会调用reallocate来分配更多内存;
  • reallocate在内存用完时为StrVec分配新内存;
我决定先实现StrVec类的拷贝构造函数

我们可以将拷贝构造函数设想为将一个vector容器里的所有string拷贝到另一个vector里面。可以简单思考一下,在实现拷贝构造函数时最先遇到的两个问题:

  • 既然我要拷贝到另外一个vector里面,那我要如何分配这个新vector的空间?
  • 分配的新空间应该多大呢?

在分配空间时我们需要使用到c++里的模板类allocator(后面我会给出有关介绍),

返回类型+函数名+形参列表+const+函数体,请问这个const作用是什么

C++中,函数后面的 const 关键字用于说明函数是一个常量成员函数。这意味着该函数不能修改它所在类的任何非静态成员变量的值。换句话说,常量成员函数不能修改对象的状态。

例如,考虑以下类定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {
private:
int x;

public:
MyClass(int val) : x(val) {}

int getValue() const {
return x;
}

void setValue(int val) {
x = val;
}
};

在这个例子中,getValue() 是一个常量成员函数,因为它后面有 const 关键字。这意味着 getValue() 函数不能修改 MyClass 对象的任何成员变量的值(在这个例子中是 x)。如果你尝试在 getValue() 函数中修改 x 的值,编译器会报错。

常量成员函数通常用于定义不应改变对象状态的操作。这有助于提高代码的安全性和易读性,因为读者可以明确知道哪些函数不会修改对象的状态。

另外,常量成员函数可以在常量对象上调用,非常量成员函数则不能。这使得你可以在需要保证对象不被修改的情况下,仍然可以调用其上的一些成员函数。

常量成员函数可以在常量对象上调用,非常量成员函数则不能。这句话的意思是常量对象不能调用非常量成员函数?

常量对象(即被 const 修饰的对象)只能调用常量成员函数。这是因为常量对象一旦初始化,其值就不能再被改变。因此,为了保证常量对象的状态不被改变,编译器禁止在常量对象上调用可能会改变对象状态的非常量成员函数。

举一个例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {
public:
int x;

MyClass(int val) : x(val) {}

int getValue() const { return x; } // 常量成员函数
void setValue(int val) { x = val; } // 非常量成员函数
};

int main() {
const MyClass obj(10); // 声明一个常量对象
std::cout << obj.getValue() << std::endl; // 合法,因为getValue是常量成员函数
obj.setValue(20); // 非法,编译错误。因为setValue是非常量成员函数,不能在常量对象上调用
return 0;
}

在上述代码中,obj 是一个常量对象。尽管 setValue()MyClass 的一个成员函数,但你不能在 obj 上调用它,因为 setValue() 可能会改变对象的状态,这违反了 obj 是常量的事实。但你可以在 obj 上调用 getValue(),因为它是一个常量成员函数,不会改变对象的状态。

总结:常量对象不能调用非常量成员函数,非常量对象可以调用常量成员函数。(c++ primer p231:引入const成员函数)

封装定义

封装是指保护类的成员不被随意访问的能力,通过把类的实现细节设置为private,我们就能完成类的封装,实现类的接口与实现分离。

封装的两个重要优点:

1、用户代码不会无意间破坏封装对象的状态;

2、被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码;

友元

类允许其他类或者函数访问它的非公有成员(私有成员),方法是将其他类或者函数定义成友元,使用关键字friend

注意:

友元只能出现在类定义的内部,在类内出现的具体位置不受约束。友元不是类的成员,不受它所在区域访问控制级别的约束。

右值引用

右值引用主要用来解决两个问题:

1、临时对象非必要的昂贵的拷贝操作

2、在模板函数中如何按照参数的实际类型进行转发

c++11中所有的值必属于左值、将亡值和纯右值之一。

右值引用绑定了右值,让临时右值的生命周期延长了,可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构

右值引用类型既可能是左值也可能是右值,如T&& t,这里的t既可能是左值也可能是右值。如下所示

1
2
3
4
5
6
7
template<typename T>
void f(T&& t){}

f(10); //t是右值

int x = 10;
f(x); //t是左值

贴一个超级详细的连接

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <cerrno>
#include <vector>
#include <fcntl.h>
#include <assert.h>
#include <sys/epoll.h>
#include <thread>
#define PORT 5000
#define SERVER_IP "127.0.0.1"
#define MAX_EVENTS 5

void set_nonblocking(int sockfd){
int flags = fcntl(sockfd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flags);
}

int main(){
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
assert(listen_fd >= 1);
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &address.sin_addr);
int ret = bind(listen_fd, (struct sockaddr*)(&address), sizeof(address));
assert(ret != -1);
int ret1 = listen(listen_fd, 5);
assert(ret1 != -1);
set_nonblocking(listen_fd);

int epoll_fd = epoll_create1(0);
assert(epoll_fd != -1);

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_fd;

if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1){
std::cerr << "Failed to add file descriptor to epoll" << std::endl;
close(epoll_fd);
return 1;
}

struct epoll_event events[MAX_EVENTS];

while(true){
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for(int i = 0; i < nfds; ++i){
if(events[i].data.fd == listen_fd){
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int sock_fd = accept(listen_fd, (struct sockaddr*)(&client), &client_len);
set_nonblocking(sock_fd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = sock_fd;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev) == -1){
std::cerr << "Failed to add client socket file descriptor to epoll" << std::endl;
}
}
else{
int client_fd = events[i].data.fd;
char buf[1024] = {0};
int recv_size;

while((recv_size = recv(client_fd, buf, sizeof(buf), 0)) > 0){
std::cout << "服务端接收到的消息为:" << buf << std::endl;
send(client_fd, buf, sizeof(buf), 0);
}

if(recv_size == 0 || (recv_size == -1 && errno != EAGAIN && errno != EWOULDBLOCK)){
if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, &ev) == -1){
std::cerr << "Failed delete the client_fd" << std::endl;
}
close(client_fd);
}
}
}
}
close(listen_fd);
close(epoll_fd);
return 0;
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <cerrno>
#include <vector>
#include <fcntl.h>
#include <assert.h>
#include <sys/epoll.h>
#include <thread>
#include <mutex>
#define PORT 5000
#define SERVER_IP "127.0.0.1"
#define MAX_EVENTS 5
std::mutex mtx;

int i = 0;

void client_thread_function(){
int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr);
if(connect(client_sockfd, (struct sockaddr*)(&serv_addr), sizeof(serv_addr))){
std::cerr << "Connection failed!" << std::endl;
close(client_sockfd);
return;
}
char buf[1024] = {0};
memset(buf, 0, sizeof(buf));
strcpy(buf, "Hello, world!");
int send_size = send(client_sockfd, buf, strlen(buf), 0);
if(send_size < 0) std::cerr << "Error sending data: " << strerror(errno) << std::endl;
int recv_size = recv(client_sockfd, buf, send_size, 0);
if(recv_size < 0) {
std::cerr << "Error recving data: " << strerror(errno) << std::endl;
} else {
std::unique_lock<std::mutex> lock(mtx);
i += 1;
std::cout << "客户端" << i << "接收信息为:" << buf << std::endl;
}
close(client_sockfd);
}

int main(){
const int thread_num = 5;
std::vector<std::thread> client_threads;
for(int i = 0; i < thread_num; ++i){
client_threads.push_back(std::thread(client_thread_function));
}

for(auto& t : client_threads){
t.join();
}

return 0;
}


运行时问题及结果截图

在服务端与客户端均运行起来时,会发现程序并不能完整运行完,可能运行三条/一条/直接卡住,一条没有等情况,截图如下:

0条echo,直接卡住不动

如上图所示,就显示一条echo回声消息,服务端跟客户端就都卡在这不动了。

0条echo,直接卡住不动

解决方案(我并不清楚怎么就解决了!)

将ET边缘触发模式改为LT水平触发模式就OK了,但从ET结果来看,程序貌似阻塞住了,但哪里会产生阻塞呢?十分不理解?还是其他问题呢(并非阻塞)?

0%