当前位置: 首页 > article >正文

Linux与UDP应用2:简易聊天室

UDP应用2:简易聊天室

本篇介绍

在前面的基本使用过程中已经完成了本地和网络通信,既然一个人和一台服务器可以进行通信,那么多个人连接一台服务器也可以和这台服务器实现通信。在这个基础上,如果服务器可以将某个人发给服务器的消息转发给所有连接到当前服务器的客户端,就可以实现一个人发消息,其他人看到消息的效果,这就是简易聊天室的原理

本篇就是利用UDP的操作实现上面提到的简易聊天室,其中客户端向服务端发送消息,服务端将收到的消息转发给所有连接到当前服务器的客户端

需要注意,本次实现基于前面封装的服务端和客户端

主要功能实现思路

在实现具体功能之前,先了解本次设计聊天室的主要功能的实现思路:

既然要实现多个用户连接一台服务器,那么每一个用户就需要有相关的属性,并且多个用户还需要进行管理,所以必须先对用户模块进行设计

本次设计的用户模块可以采用观察者模式。观察者模式是一种行为型设计模式,用于定义对象之间的一对多依赖关系。当一个对象的状态发生变化时,所有依赖于它的对象都会自动收到通知并更新,其有下面的核心对象:

  1. 主题(Subject):被观察的对象。维护一组观察者,并提供添加、删除和通知观察者的方法
  2. 观察者(Observer):监听主题状态变化的对象。定义一个更新接口,用于接收主题的通知

根据上面的概念,考虑接下来的设计思路

首先是设计基类,在观察者模式中,基类可以分为两种:

  1. 主题基类:声明主题需要实现的三个方法,在本次聊天室中分别对应着:添加用户、删除用户和将消息转发给所有用户
  2. 观察者基类:声明观察者在检测到状态变化时需要做出的行为,在本次聊天室中对应着:将指定的信息发给自己

在本次实现中,实际上是由服务器调用转发函数,此时就会调用主题实现类中的转发函数,在转发函数中,每一个观察者都会收到服务器传递的信息,此时每一个观察者携带着该信息调用自己的更新函数,在更新函数中,每一个观察者就会将指定信息根据自己的IP地址发送给自己,这样就相当于是服务器将信息转发给所有在线的客户端。简单来说,就是通过服务器从没有收到信息到接收到某一信息这一状态的变化间接更新所有的客户端

接着,有了基类就需要有对应的实现类,那么对应的就是观察者实现类和主题实现类,主题实现类需要对所有的观察者进行管理,实现类只需要实现需要的更新方法即可,具体见后面的解析

设计完用户模块之后,就是服务器端的处理,本次设计的聊天室服务器需要完成下面的内容:

  1. 当用户第一次向服务器发送信息时需要将用户添加为观察者
  2. 一个用户向服务器发送信息时,服务器需要将这条信息转发给所有连接到当前服务器的用户
  3. 当用户发送一条固定消息(本次默认为字符串quit)时,服务器需要删除从在线用户列表中删除该用户

对于第一个任务,服务器可以完全胜任,因为完全就是调用主题实现类的添加方法,这一步不会有很大的消耗,但是对于第二个任务,因为socketfd是双工的,那么就会存在既接收信息又分发信息,而分发信息需要遍历到每一个观察者,此时只有服务器一个线程,那么这一个线程开销就会非常大,所以为了避免这个问题就可以考虑使用线程池完成分发任务

需要注意,本次会使用前面封装的单例线程池

实现用户模块

创建观察者基类

观察者基类最关键的方法就是更新数据,所以基本结构如下:

// 观察者基类
class UserObserver
{
public:virtual ~UserObserver() = default;virtual void sendMessage() = 0;
};

创建观察者实现类

既然是观察者实现类,那么少不了的就是对虚函数进行重写,在前面的主要功能中提到过,观察者的主要方法是将消息根据自己的IP地址,所以基本结构代码如下:

// 观察者实现类
class User : public UserObserver
{
public:virtual void sendMessage(int sockfd, const std::string& message) override{}
};

但是,为了这个类需要用于不同的用户,所以还需要相关的属性和构造方法,本次考虑用户可以有IP地址、端口和用户名作为属性,在构造时需要用户传递IP地址、端口和用户名:

需要注意,本次将使用前面封装的struct sockaddr_in结构

// 观察者实现类
class User : public UserObserver
{
public:User(uint16_t port, std::string ip, std::string name): _name(name), _sa_in(port, ip){}// ...private:std::string _name;SockAddrIn _sa_in;
};

实现观察者实现类重写方法

在前面的主要功能中提到过,观察者的主要方法是将消息根据自己的IP地址发给自己,所以实现如下:

virtual void sendMessage(int sockfd, const std::string &message) override
{// 打印日志LOG(LogLevel::INFO) << "Client: " << _sa_in.getIp() << ":" << _sa_in.getPort() << "send message: " << message;// 发送信息给自己ssize_t = sendto(sockfd, message.c_str(), message.size(), 0, &_sa_in, _sa_in.getLength());
}

创建主题基类

根据前面的主要功能实现思路可以知道主题基类有三个主要方法:添加用户、删除用户和将消息转发给所有用户,所以代码如下:

// 主题基类
class UserManagerSubject
{
public:virtual ~UserManagerSubject() = default;// 添加方法virtual void addUser(const User &user) = 0;// 删除方法virtual void delUser(const User &user) = 0;// 通知方法virtual void dispatchMessage(int sockfd, const std::string &message) = 0;
};

创建主题实现类

根据上面的基类实现对应的方法,但是除了有对应的方法外,还需要有一个用于管理所有用户的结构,本次考虑使用list结构,基本代码如下:

// 主题实现类
class UserManager : public UserManagerSubject
{
public:UserManager(){}// 实现添加方法virtual void addUser(const User &user) override{}// 实现删除方法virtual void delUser(const User &user) override{}// 通知方法virtual void dispathMessage(int sockfd, const std::string &message) override{}private:std::list<std::shared_ptr<User>> _u_list;
};

实现主题实现类重写方法

主题实现类中一共三个方法,下面针对这三个方法进行分别实现:

添加方法

对于第一个方法,既然是添加数据,那么肯定涉及到访问链表,因为后面会存在服务器线程访问这个链表,线程池中的线程也会访问这个链表,所以为了保证线程安全,每一个线程在访问链表时需要先申请锁,申请锁成功后再修改链表中的内容,所以在此基础之上还需要一个互斥锁成员

接着在主要逻辑中,因为是插入用户,所以首先需要判断插入的用户是否存在,如果存在就插入,否则就不插入。但是这里涉及到一个问题,需要判断用户是否存在就必然涉及到比较用户的属性,在前面观察者实现类中一共有两个属性,一个是字符串,这个类型的属性已经重载了比较运算符,所以可以直接比较,但是对于封装的结构SockAddrIn并没有重载对应的操作符,所以此处还需要在观察者实现类中实现==运算符重载函数

判断两个SockAddrIn类成员是否相同主要是判断端口和IP地址是否相同,注意不能本次不建议单独判断IP地址或者端口,假设只判断IP地址,那么如果当前用户与服务器断开再连接就会出现误判断,但是实际上这个用户是已经退出再添加的。同理,如果只是判断端口,那么可能存在两个端口相同的客户端连接服务器,当其中一个客户端连接服务器,第二个用户再连接也会出现误判断。所以在SockAddrIn类需要实现下面的方法:

// 重载==
bool operator==(const SockAddrIn &s)
{return _ip == s._ip && _port == s._port;
}

接着,为了比较两个用户是否是同一个用户,还需要在User类中重载==运算符,比较方式是比较SockAddrIn对象,即:

// 重载==
bool operator==(const User &u)
{return _sa_in == u._sa_in;
}

有了这两个重载函数之后,就可以比较两个用户是否相同。回到上面的逻辑,有了比较两个用户是否相等的方式后,只要判断插入的用户不存在于链表中,那么就可以插入,即:

// 实现添加方法
virtual void addUser(const User &user) override
{// 先申请锁MutexGuard guard(_mutex);// 确保用户不存在for (auto &u : _u_list)if (*u == user)return;// 不存在时插入_u_list.push_back(std::make_shared<User>(user));
}

上面的代码使用到了默认的拷贝构造,因为需要深拷贝的结构string有对应的拷贝构造函数,而uint16_t本质是一个内置类型,所以直接拷贝也不会有问题

删除方法

删除方法的实现步骤就是找到指定的用户将其从链表中移除即可,此处可以考虑使用remove_if接口,该接口会将指定的用户节点移动到链表的最后,之后只需要删除最后一个节点即可

同样,为了确保多个线程下的线程安全,同样需要先申请锁,再进行删除操作:

// 实现删除方法
virtual void delUser(const User &user) override
{MutexGuard guard(_mutex);auto pos = std::remove_if(_u_list.begin(), _u_list.end(), [&user](const std::shared_ptr<User> &u){ return *u == user; });_u_list.erase(pos, _u_list.end());
}
通知方法

在主要功能实现思路部分重点提到了通知方法的本质就是让每一个客户端自己个自己发消息,所以只需要遍历用户链表,让每一个成员调用自己的发送更新方法即可

同样,为了确保多个线程下的线程安全,同样需要先申请锁,再进行删除操作:

// 通知方法
virtual void dispatchMessage(int sockfd, const std::string &message) override
{MutexGuard guard(_mutex);for (auto &u : _u_list)u->sendMessage(sockfd, message);
}

修改客户端模块

添加用户名字

本次实现的方式是用户通过命令行输入端口号、IP地址和名字的方式连接服务器,所以需要在创建客户端对象时指定端口号、IP地址和名字,对应地需要在客户端类中添加一个_name成员,并在构造函数中对其进行初始化:

class UdpClient
{
public:UdpClient(std::string name, /* ... */): // ..., _name(name){// ...}// ...private:// ...std::string _name; // 客户端名字
};

更改消息类型

因为服务器需要拿到用户的名字和信息,所以本次考虑客户端向服务端发送带有name:message格式的字符串,根据这个思路将拼接后的字符串发送给服务端:

// 1.1 读取输入信息
std::string message;
getline(std::cin, message);// 1.2 整合数据
std::string userinfo = _name + ":" + message;// 1.3 发送数据
ssize_t ret = sendto(_socketfd, userinfo.c_str(), userinfo.size(), 0, &_sa_in, _sa_in.getLength());if (ret < 0)LOG(LogLevel::WARNING) << "Client send failed";

修改客户端主函数逻辑

将客户端修改为必须输入IP地址、端口号和名字,如下:

// 获取到用户输入的端口和IP地址
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);// 获取用户名字
std::string name = argv[3];// 创建客户端对象——用户自定义端口和IP地址
client = std::make_shared<UdpClient>(name, ip, port);// 启动客户端
client->start();

修改服务器模块

完成了用户管理模块,接下来就是处理服务器应该执行的任务,在主要功能实现思路部分提到,服务器主要有两个任务:

  1. 接收到用户消息后添加用户
  2. 将收到的消息转发给所有在线用户
  3. 收到固定消息时移除用户

对于第一个任务,服务器直接处理并无压力,但是对于第二个任务需要借助线程池。所以下面根据这个思路对已有的服务器模块进行修改

接收到用户消息后添加用户

在之前的服务器模块中,当服务器调用recvfrom时,如果返回值不为0,就说明服务器已经收到消息,那么只需要在这个判断中添加「接收到用户消息后添加用户」的逻辑即可

确定了修改位置,接下来思考如何处理添加用户。在前面主题实现类中存在着一个添加用户的方法,根据观察者模式的特点,添加用户本质就属于添加观察者,所以服务器添加用户本质就是调用主题实现类中的添加观察者方法。但是现在的问题是,服务器并不知道如何调用这个方法,所以根据前面实现翻译软件的思路,可以考虑在创建服务器对象时要求传递对应地添加方法,所以此时就需要一个成员变量用于接收传递的函数,基本结构如下:

using add_user_t = std::function<void(const User &)>;// ...class UdpServer
{
public:UdpServer(add_user_t addUser, /* ... */): // ...,_addUser(addUser){// ...}// ...private:// ...add_user_t _addUser; // 添加用户函数
};

服务器有了添加用户的函数后,接下来的问题就是用户从哪来,因为用户在上线时传递了名字和信息构成的字符串,所以在服务器收到这个信息时需要将名字和信息进行分离,再将名字和收到的IP地址和端口构造成一个User对象,这样服务器就可以知道用户的相关信息从而正确调用添加用户的函数:

// 1. 接收客户端信息
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t length = sizeof(peer);
ssize_t ret = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&peer), &length);if (ret > 0)
{// 1. 根据收到的消息构建User对象SockAddrIn netUser(peer);// 切割字符串std::string fullInfo = buffer;// 获取名字auto pos = fullInfo.find(":", 0);std::string name = fullInfo.substr(0, pos);// 获取消息std::string message = fullInfo.substr(pos + 1, fullInfo.size());User user(netUser.getPort(), netUser.getIp(), name);// 添加用户_addUser(user);
}

将收到的消息转发给所有在线用户

当服务器收到消息时,除了需要将用户添加到链表中以外,还需要分发用户发送的消息,此时就需要借助线程池减小服务器单一线程的工作量,所以首先需要创建线程池。因为前面设计的线程池需要一个任务函数,所以在创建之前需要考虑线程池执行的任务,前面已经提到线程池需要执行派发任务,但是服务器本身没有这个任务,所以和添加用户一样,需要在创建服务器对象时给定,对应的还需要一个成员用于接收这个任务:

using dispatch_msg_t = std::function<void(int, const std::string &)>;class UdpServer
{
public:UdpServer(/* ... */, dispatch_msg_t dispatchMsg, /* ... */): // ..., _dispath_message(dispathMsg){// ...}// ...private:// ...add_user_t _addUser;            // 添加用户函数dispatch_msg_t _dispatch_message; // 分发消息函数
};

有了分发消息的函数之后,接下来就可以创建线程池。需要注意的是,在线程池的实现内部,每一个线程执行的是一个无参的任务函数,但是分发任务函数是存在形参的,所以还需要使用绑定将需要的参数固定在分发消息函数,再将其传递给一个无参的函数类型,最后将该类型作为任务类型派发给线程池即可:

需要注意,启动线程池不要在循环中启动,可能会出现服务器刚上线就收消息导致的服务器崩溃问题

using task_t = std::function<void()>;class UdpServer
{
public:UdpServer(/* ... */): // ...{// ..._tp = ThreadPool<task_t>::getInstance();if (!_tp){LOG(LogLevel::ERROR) << "线程池启动失败";return;}_tp->startThreads();// ...}// 启动服务器void start(){if (!_isRunning){_isRunning = true;while (true){// ...if (ret > 0){// ...// 2. 创建线程池并添加任务task_t task = std::bind(_dispatch_message, _socketfd, message);_tp->pushTasks(task);}}}}// ...private:// ...std::shared_ptr<ThreadPool<task_t>> _tp;// ...
};

收到固定消息时移除用户

本次默认服务器收到固定的消息是quit字符串。当客户端想要退出时必须要结束对应的进程,所以可以考虑使用信号捕捉的方式,本次以捕捉ctrl+c的信号为例

首先,客户端需要对ctrl+c对应的信号进行捕捉,即一旦收到该信号时就向服务器端发送固定的字符串quit提示服务端指定用户退出,再结束该客户端。为了可以看到客户端的名字、端口与IP地址结构和套接字,可以考虑提供获取名字、端口+IP地址和套接字的接口,并且将客户端指针作为全局对象确保信号捕捉函数可以看到该指针对象:

=== “客户端主函数”

std::shared_ptr<UdpClient> client;void quit(int sig)
{(void)sig;std::string userinfo = client->getName() + ":" + "quit";int ret = sendto(client->getSocketfd(), userinfo.c_str(), userinfo.size(), 0, &client->getSockAddrIn(), client->getSockAddrIn().getLength());LOG(LogLevel::INFO) << client->getName() << "退出群聊";exit(0);
}int main(int argc, char *argv[])
{// 捕捉2号新号signal(2, quit);// ...
}

=== “客户端类”

 // 获取名字std::string getName(){return _name;}// 获取IP地址+端口号SockAddrIn getSockAddrIn(){return _sa_in;}// 获取socketfdint getSocketfd(){return _socketfd;}

接着,在服务器端需要对收到的字符串进行判断,如果收到的字符串为quit,说明需要删除该用户,调用主题实现类中的delUser方法,思路与前面两个功能的思路一致,此处不再赘述:

using del_user_t = std::function<void(const User &)>;class UdpServer
{
public:UdpServer(add_user_t addUser, dispatch_msg_t dispatchMsg, del_user_t delUser, uint16_t port = default_port): // ..., _delUser(delUser){// ...}// 启动服务器void start(){if (!_isRunning){_isRunning = true;while (true){// 1. 接收客户端信息// ...if (ret > 0){// ...// 1.1 判断是删除还是添加if (strcmp(message.c_str(), "quit") == 0){// 删除用户_delUser(user);}else{// 添加用户_addUser(user);}// ...}}}}// ...private:// ...del_user_t _delUser; // 删除用户函数
};

修改服务器主函数逻辑

在创建服务器时,需要指定三个函数,分别是:添加函数、分发函数和删除函数,其他逻辑不变,以使用默认端口为例:

// 创建UserManager对象
std::shared_ptr<UserManager> usm = std::make_shared<UserManager>();
// 创建UdpServerModule对象
udp_server = std::make_shared<UdpServer>([&usm](const User &user){ usm->addUser(user); },[&usm](int sockfd, const std::string &message){ usm->dispatchMessage(sockfd, message); },[&usm](const User &user){ usm->delUser(user); });

客户端回显优化

默认情况下回显客户端的消息在发送之下,代码如下:

// 1.1 读取输入信息
std::string message;
LOG(LogLevel::INFO) << "请输入信息:";
getline(std::cin, message);// 1.2 整合数据
std::string userinfo = _name + ":" + message;// 1.3 发送数据
ssize_t ret = sendto(_socketfd, userinfo.c_str(), userinfo.size(), 0, &_sa_in, _sa_in.getLength());if (ret < 0)LOG(LogLevel::WARNING) << "Client send failed";// 2. 回显服务器的信息
struct sockaddr_in temp;
socklen_t length = sizeof(temp);
char buffer[1024] = {0};
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&temp), &length);if (n > 0)LOG(LogLevel::INFO) << "Received message from server: " << buffer;

但是这样会出现一个问题,因为socketfd是全双工的,即在收的同时还可以发送,所以就会出现sendMessage中的发送可以和recvfrom同时进行,而因为本次聊天室服务器并没有直接向客户端发送消息,导致客户端发送一条消息之后就会卡在recvfrom的位置。为了解决这个问题,需要再客户端部分使用两个线程,一个线程负责发送信息,另外一个线程负责发送信息,简单来说就是收和发分离

=== “启动客户端函数”

 void start(){if (!_isRunning){// 创建新线程执行获取消息的函数Thread t([this](){ getMessage(); });t.start();// ...}}

=== “获取消息函数”

void getMessage()
{LOG(LogLevel::DEBUG) << "新线程启动";while (true){// 2. 回显服务器的信息struct sockaddr_in temp;socklen_t length = sizeof(temp);char buffer[1024] = {0};ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&temp), &length);if (n > 0)LOG(LogLevel::INFO) << "Received message from server: " << buffer;}
}

触发添加用户逻辑优化

前面的设计中只有客户端发送一条消息之后才会添加用户,但是实际上只要用户上线了就应该可以算是连接成功,所以此处可以在用户上线时预先给服务器发送固定消息,一旦服务器收到该固定消息再决定添加用户,即:

=== “客户端启动函数”

// 启动客户端
void start()
{if (!_isRunning){// 创建新线程执行获取消息的函数// ...// 预先发送一条消息给服务器std::string online = _name + ":" + "online";ssize_t ret = sendto(_socketfd, online.c_str(), online.size(), 0, &_sa_in, _sa_in.getLength());// ...}
}

=== “服务器启动函数”

 // 启动服务器void start(){if (!_isRunning){// ...while (true){// 1. 接收客户端信息// ...if (ret > 0){// ...// 1.1 判断是删除还是添加if (strcmp(message.c_str(), "quit") == 0){// 删除用户_delUser(user);}else if (strcmp(message.c_str(), "online") == 0){// 添加用户_addUser(user);}// ...}}}}

整体显示优化

上面已经基本上可以实现通信功能,但是显示的效果并不好,因为输入和输出都显示在同一个显示器上,所以可以考虑将当前客户端收到的分发消息打印到文件中,正常的输出依旧还是在控制台,此处可以借助标准错误输出流,例如cerr

根据这个思路,对回显进行优化:

void getMessage()
{LOG(LogLevel::DEBUG) << "新线程启动";while (true){// ...if (n > 0)std::cerr << "收到消息:" << buffer << std::endl;}
}

另外,为了更好得显示用户上线和下线,可以在用户添加或者删除之后将携带的信息带上更具体的文字,例如:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){// ...if (ret > 0){// ...// 1.1 判断是删除还是添加if (strcmp(message.c_str(), "quit") == 0){// 删除用户_delUser(user);message = user.getName() + ":" + "(" + user.getSockAddrIn().getIp() + ":" + std::to_string(user.getSockAddrIn().getPort()) + ")" + " offline";}else if (strcmp(message.c_str(), "online") == 0){// 添加用户_addUser(user);message = user.getName() + ":" + "(" + user.getSockAddrIn().getIp() + ":" + std::to_string(user.getSockAddrIn().getPort()) + "):" + message;}// ...}}}
}

测试

在启动客户端之前,先通过下面的指令创建管道:

mkfifo 管道名称

启动客户端时输入下面的指令:

./客户端可执行程序名称 IP地址 端口号 名字 2> 管道名称

运行客户端和服务端即可看到收到的消息在管道中,输入的消息在标准输出中

相关文章:

Linux与UDP应用2:简易聊天室

UDP应用2&#xff1a;简易聊天室 本篇介绍 在前面的基本使用过程中已经完成了本地和网络通信&#xff0c;既然一个人和一台服务器可以进行通信&#xff0c;那么多个人连接一台服务器也可以和这台服务器实现通信。在这个基础上&#xff0c;如果服务器可以将某个人发给服务器的…...

张雪峰教育观点及争议分析

李升伟 整理 张雪峰&#xff08;网络常用名&#xff0c;本名张子彪&#xff09;是中国知名的考研辅导教师、教育领域自媒体人&#xff0c;因其幽默犀利的语言风格和直击痛点的教育观点走红网络。以下是对他的基本介绍及综合评价&#xff1a; --- ### **一、基本情况** 1. **个…...

从0开始的IMX6ULL学习篇——裸机篇之分析粗略IMX6ULL与架构

目录 简单的说一下Cortex-A7架构 讨论ARMv7a-cortex系列的运行模式 寄存器 后言 让我们到NXP的官网上扫一眼。 i.MX 6ULL应用处理器_Arm Cortex-A7单核&#xff0c;频率为900 MHz | NXP 半导体 我们先看CPU Platform&#xff0c;这个是我们的核心。 这里我们的芯片是基于Ar…...

面向实时性的超轻量级动态感知视觉SLAM系统

一、重构后的技术架构设计(基于ROS1 ORB-SLAM2增强) #mermaid-svg-JEJte8kZd7qlnq3E {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JEJte8kZd7qlnq3E .error-icon{fill:#552222;}#mermaid-svg-JEJte8kZd7qlnq3E .…...

Hue UI展示中文

个人博客地址&#xff1a;Hue UI展示中文 | 一张假钞的真实世界 如果使用开发分支代码如master分支&#xff09;编译安装&#xff0c;需要自己编译语言文件。例如Hue安装目录为“/opt/hue”&#xff0c;则安装后执行以下命令&#xff1a; $ cd /opt/hue $ make locales 如果…...

C#贪心算法

贪心算法&#xff1a;生活与代码中的 “最优选择大师” 在生活里&#xff0c;我们常常面临各种选择&#xff0c;都希望能做出最有利的决策。比如在超市大促销时&#xff0c;面对琳琅满目的商品&#xff0c;你总想用有限的预算买到价值最高的东西。贪心算法&#xff0c;就像是一…...

【新人系列】Python 入门专栏合集

✍ 个人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4dd; 专栏地址&#xff1a;https://blog.csdn.net/newin2020/category_12801353.html &#x1f4e3; 专栏定位&#xff1a;为 0 基础刚入门 Python 的小伙伴提供详细的讲解&#xff0c;也欢迎大佬们…...

SQL命令详解之数据的查询操作

目录 1 简介 2 基础查询 2.1 基础查询语法 2.2 基础查询练习 3 条件查询 3.1 条件查询语法 3.2 条件查询练习 4 排序查询 4.1 排序查询语法 4.2 排序查询练习 5 聚合函数 5.1 一般语法&#xff1a; 5.2 聚合函数练习 6 分组查询 6.1 分组查询语法 6.2 分组查询…...

序列化选型:字节流抑或字符串

序列化既可以将对象转换为字节流&#xff0c;也可以转换为字符串&#xff0c;具体取决于使用的序列化方式和场景。 转换为字节流 常见工具及原理&#xff1a;在许多编程语言中&#xff0c;都有将对象序列化为字节流的机制。例如 Python 中的 pickle 模块、Java 中的对象序列化…...

使用C#控制台调用本地部署的DeepSeek

1、背景 春节期间大火的deepseek&#xff0c;在医疗圈也是火的不要不要的。北京这边的医院也都在搞“deepseek竞赛”。友谊、北医三院等都已经上了&#xff0c;真是迅速啊&#xff01; C#也是可以进行对接&#xff0c;并且非常简单。 2、具体实现 1、使用Ollama部署DeepSeek…...

Linux环境安装Nginx及版本升级指南

Linux环境安装Nginx及版本升级指南 一、安装Nginx 1. 安装前准备 # 更新系统软件包&#xff08;Ubuntu/Debian&#xff09; sudo apt update && sudo apt upgrade -y# CentOS/RHEL sudo yum update -y2. 安装依赖库 # Ubuntu/Debian sudo apt install -y curl wget…...

选开源CMS建站系统时,插件越多越好吗?

在选择开源CMS建站系统时&#xff0c;插件数量并不是唯一的衡量标准&#xff0c;更不能简单地说“插件越多就越好”&#xff0c;还是需要综合评估来考虑选择结果&#xff0c;以下是有关选择开源CMS系统时对插件数量的考量。 插件数量的优势插件数量可能带来的问题功能丰富性&a…...

Windows对比MacOS

Windows对比MacOS 文章目录 Windows对比MacOS1-环境变量1-Windows添加环境变量示例步骤 1&#xff1a;打开环境变量设置窗口步骤 2&#xff1a;添加系统环境变量 2-Mac 系统添加环境变量示例步骤 1&#xff1a;打开终端步骤 2&#xff1a;编辑环境变量配置文件步骤 3&#xff1…...

使用 Python 实现基于 AGA8 GERG - 2008 方程计算掺氢天然气压缩因子的示例代码

AGA8 GERG - 2008 方程是用于计算天然气混合物热力学性质的一种方法&#xff0c;下面是一个使用 Python 实现基于 AGA8 GERG - 2008 方程计算掺氢天然气压缩因子的示例代码。需要注意的是&#xff0c;AGA8 GERG - 2008 方程非常复杂&#xff0c;完整实现需要大量的系数和详细的…...

开源绝版经典小游戏合集

随着生活节奏的日益加快&#xff0c;我们常常需要一些小游戏来缓解疲惫的身心。过去&#xff0c;Windows 7自带的扫雷、蜘蛛纸牌等小游戏深受大家喜爱&#xff0c;但随着系统的更新换代&#xff0c;这些经典游戏逐渐淡出了人们的视野。我也曾花费不少时间寻找这些游戏&#xff…...

给虚拟机配置IP

虚拟机IP这里一共有三个地方要设置&#xff0c;具体说明如下&#xff1a; &#xff08;1&#xff09;配置vm虚拟机网段 如果不进行设置&#xff0c;每次启动机器时都可能是随机的IP&#xff0c;不方便我们后续操作。具体操作是&#xff1a;点击编辑→虚拟网络编辑器 选择VMne…...

YOLOv12以注意力机制为核心的架构:主要特点、创新点、使用方法

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...

GD32F450 使用

GB32F450使用 1. 相关知识2. 烧写程序3. SPI3.1 spi基础3.2 spi代码 4. 串口4.1 串口引脚4.2 串口通信代码 问题记录1. 修改晶振频率 注意&#xff1a;GD32F450 总共有三种封装形式&#xff0c;本文所述的相关代码和知识&#xff0c;均为 GD32F450IX 系列。 1. 相关知识 参数配…...

Linux 动静态库和_make_进度条(一)

文章目录 一、如何理解条件编译二、动静态库1. 理论2. 实践3. 解决普通用户的sudo问题4. 技术上理解库 三、make和make_file 一、如何理解条件编译 1. gcc code.c -o code -DM 命令行级别的宏定义预处理的本质就是修改编辑我们的文本代码 头文件展开到源文件中去注释宏替换条…...

Android 图片压缩详解

在 Android 开发中,图片压缩是一个重要的优化手段,旨在提升用户体验、减少网络传输量以及降低存储空间占用。以下是几种主流的图片压缩方法,结合原理、使用场景和优缺点进行详细解析。 效果演示 直接先给大家对比几种图片压缩的效果 质量压缩 质量压缩:根据传递进去的质…...

C# 牵手DeepSeek:打造本地AI超能力

一、引言 在人工智能飞速发展的当下&#xff0c;大语言模型如 DeepSeek 正掀起新一轮的技术变革浪潮&#xff0c;为自然语言处理领域带来了诸多创新应用。随着数据隐私和安全意识的提升&#xff0c;以及对模型部署灵活性的追求&#xff0c;本地部署 DeepSeek 成为众多开发者和…...

普通人高效使用DeepSeek指南?

李升伟 整理 DeepSeek&#xff08;深度求索&#xff09;作为一款智能搜索引擎或AI工具&#xff0c;普通人可以通过以下方式高效利用它&#xff0c;提升学习、工作和生活效率&#xff1a; --- ### **一、基础功能&#xff1a;精准搜索** 1. **明确需求提问** 用自然语言…...

卢卡斯定理判断组合数奇偶(Codeforces Round 1006 (Div. 3)——F)

文章目录 组合数奇偶判断题意思路综上 组合数奇偶判断 【用杨辉三角阐释Lucas定理】https://www.bilibili.com/video/BV14F411P7ES?vd_source67186f29c3efb728bcff34035cf5aba2 这个视频可以简单的领会一下精神&#xff0c;卢卡斯定理也就是用于组合数取模。 奇偶性通过对2…...

ECharts组件封装教程:Vue3中的实践与探索

在日常的前端开发中,ECharts 作为一款强大且易用的图表库,被广泛应用于数据可视化场景。为了更好地在 Vue3 项目中复用 ECharts 功能,我们可以将其封装成一个组件。本文将带大家一步步实现 ECharts 的 Vue3 组件封装,并演示如何在父组件中调用和使用。 一、封装 ECharts 组…...

LLM中的Benchmark是什么

LLM中的Benchmark是什么 “DeepSeek推动价值重估Benchmark” DeepSeek这家公司或其相关技术的发展,促使Benchmark这家机构对相关资产或企业的价值进行重新评估。“Benchmark”在这里是一家研究机构或金融分析机构。 “Benchmark”常见的意思是“基准;水准点,基准点”,作…...

【新加坡】软件工程师工签政策、求职指南

文章目录 关键要点就业准证要求求职平台注意事项详细报告就业准证&#xff08;EP&#xff09;要求求职平台与投递渠道注意事项与求职建议表格&#xff1a;求职平台对比额外考虑关键引用 关键要点 去新加坡工作需要申请就业准证&#xff08;EP&#xff09;&#xff0c;通常要求…...

梯度下降法(Gradient Descent) -- 现代机器学习的血液

梯度下降法(Gradient Descent) – 现代机器学习的血液 梯度下降法是现代机器学习最核心的优化引擎。本文从数学原理、算法变种、应用场景到实践技巧&#xff0c;用三维可视化案例和代码实现揭示其内在逻辑&#xff0c;为你构建完整的认知体系。 优化算法 一、梯度下降法的定义…...

CMake宏定义管理:如何优雅处理第三方库的宏冲突

在C/C项目开发中&#xff0c;我们常常会遇到这样的困境&#xff1a; 当引入一个功能强大的第三方库时&#xff0c;却发现它定义的某个宏与我们的项目产生冲突。比如&#xff1a; 库定义了 BUFFER_SIZE 1024&#xff0c;而我们需要 BUFFER_SIZE 2048库内部使用 DEBUG 宏控制日志…...

【计算机网络】常见tcp/udp对应的应用层协议,端口

TCP 和 UDP 对应的常见应用层协议 &#x1f4cc; 基于 TCP 的应用层协议 协议全称用途默认端口HTTPHyperText Transfer Protocol超文本传输协议80HTTPSHTTP Secure加密的超文本传输协议443FTPFile Transfer Protocol文件传输协议&#xff08;20 传输数据&#xff0c;21 控制连…...

微服务学习(2):实现SpringAMQP对RabbitMQ的消息收发

目录 SpringAMQP是什么 为什么采用SpringAMQP SpringAMQP应用 准备springBoot工程 实现消息发送 SpringAMQP是什么 Spring AMQP是Spring框架下用于简化AMQP&#xff08;高级消息队列协议&#xff09;应用开发的一套工具集&#xff0c;主要针对RabbitMQ等消息中间件的集成…...