文件传输设计思路

设计思路

文件传输必须满足以下几个条件:

  • 限制文件大小(不超过4G)
  • 长连接传输(效率高,支持大文件)
  • 客户端和服务器都知道传输进度,以保证支持断点续传(后续实现)
  • 先实现服务器单线程处理版本,在实现多线程处理版本

如遇问题可添加我的微信

<img src="https://cdn.llfc.club/wechat.jpg" alt="img" style="zoom: 33%;" />

也可以去我得哔哩哔哩主页查看项目视频详细讲解

B站主页 https://space.bilibili.com/271469206

客户端

客户端还是采用聊天项目客户端封装的TcpClient, 只是修改了发送逻辑

  1. //发送数据槽函数
  2. void TcpClient::slot_send_msg(quint16 id, QByteArray body)
  3. {
  4. //如果连接异常则直接返回
  5. if(_socket->state() != QAbstractSocket::ConnectedState){
  6. emit sig_net_error(QString("断开连接无法发送"));
  7. return;
  8. }
  9. //获取body的长度
  10. quint32 bodyLength = body.size();
  11. //创建字节数组
  12. QByteArray data;
  13. //绑定字节数组
  14. QDataStream stream(&data, QIODevice::WriteOnly);
  15. //设置大端模式
  16. stream.setByteOrder(QDataStream::BigEndian);
  17. //写入ID
  18. stream << id;
  19. //写入长度
  20. stream << bodyLength;
  21. //写入包体
  22. data.append(body);
  23. //发送消息
  24. _socket->write(data);
  25. }

这里着重叙述以下,发送的格式是id + bodyLength + 文件流数据

其中id 为2字节,bodyLength为4字节,之后就是传输的文件流

https://cdn.llfc.club/1732450428990.jpg

slot_send_msg是槽函数,和 sig_send_msg信号连接

  1. //连接 发送数据信号和槽函数
  2. connect(this, &TcpClient::sig_send_msg, this, &TcpClient::slot_send_msg);

客户端在发送数据的时候调用

  1. void TcpClient::sendMsg(quint16 id,QByteArray data)
  2. {
  3. //发送信号,统一交给槽函数处理,这么做的好处是多线程安全
  4. emit sig_send_msg(id, data);
  5. }

客户端在打开文件对话框后选择文件,接下来,点击发送会将文件切分成固定大小的报文发送

  1. void MainWindow::on_uploadBtn_clicked()
  2. {
  3. ui->uploadBtn->setEnabled(false);
  4. // 打开文件
  5. QFile file(_file_name);
  6. if (!file.open(QIODevice::ReadOnly)) {
  7. qWarning() << "Could not open file:" << file.errorString();
  8. return;
  9. }
  10. // 保存当前文件指针位置
  11. qint64 originalPos = file.pos();
  12. QCryptographicHash hash(QCryptographicHash::Md5);
  13. if (!hash.addData(&file)) {
  14. qWarning() << "Failed to read data from file:" << _file_name;
  15. return ;
  16. }
  17. _file_md5 = hash.result().toHex(); // 返回十六进制字符串
  18. // 读取文件内容并发送
  19. QByteArray buffer;
  20. int seq = 0;
  21. QFileInfo fileInfo(_file_name); // 创建 QFileInfo 对象
  22. QString fileName = fileInfo.fileName(); // 获取文件名
  23. qDebug() << "文件名是:" << fileName; // 输出文件名
  24. int total_size = fileInfo.size();
  25. int last_seq = 0;
  26. if(total_size % MAX_FILE_LEN){
  27. last_seq = (total_size/MAX_FILE_LEN)+1;
  28. }else{
  29. last_seq = total_size/MAX_FILE_LEN;
  30. }
  31. // 恢复文件指针到原来的位置
  32. file.seek(originalPos);
  33. while (!file.atEnd()) {
  34. //每次读取2048字节发送
  35. buffer = file.read(MAX_FILE_LEN);
  36. QJsonObject jsonObj;
  37. // 将文件内容转换为 Base64 编码(可选)
  38. QString base64Data = buffer.toBase64();
  39. //qDebug() << "send data is " << base64Data;
  40. ++seq;
  41. jsonObj["md5"] = _file_md5;
  42. jsonObj["name"] = fileName;
  43. jsonObj["seq"] = seq;
  44. jsonObj["trans_size"] = buffer.size() + (seq-1)*MAX_FILE_LEN;
  45. jsonObj["total_size"] = total_size;
  46. if(buffer.size() < MAX_FILE_LEN){
  47. jsonObj["last"] = 1;
  48. }else{
  49. jsonObj["last"] = 0;
  50. }
  51. jsonObj["data"]= base64Data;
  52. jsonObj["last_seq"] = last_seq;
  53. QJsonDocument doc(jsonObj);
  54. auto send_data = doc.toJson();
  55. TcpClient::Inst().sendMsg(ID_UPLOAD_FILE_REQ, send_data);
  56. //startDelay(500);
  57. }
  58. //关闭文件
  59. file.close();
  60. }

发送时数据字段分别为:

  • 文件md5 : 以后用来做断点续传校验

  • name : 文件名

  • seq: 报文序列号,类似于TCP序列号,自己定义的,服务器根据这个序列号组合数据写入文件。
  • trans_size: 当前已经传输的大小

  • total_size: 传输文件的总大小。

客户端需要接受服务器返回的消息更新进度条

  1. //接受服务器发送的信息
  2. void TcpClient::slot_ready_read()
  3. {
  4. //读取所有数据
  5. QByteArray data = _socket->readAll();
  6. //将数据缓存起来
  7. _buffer.append(data);
  8. //处理收到的数据
  9. processData();
  10. }

处理消息更新进度条

  1. void TcpClient::processData()
  2. {
  3. while(_buffer.size() >= TCP_HEAD_LEN){
  4. //先取出八字节头部
  5. auto head_byte = _buffer.left(TCP_HEAD_LEN);
  6. QDataStream stream(head_byte);
  7. //设置为大端模式
  8. stream.setByteOrder(QDataStream::BigEndian);
  9. //读取ID
  10. quint16 msg_id;
  11. stream >> msg_id;
  12. //读取长度
  13. quint32 body_length;
  14. stream >> body_length;
  15. if(_buffer.size() >= TCP_HEAD_LEN+body_length){
  16. //完整的消息体已经接受
  17. QByteArray body = _buffer.mid(TCP_HEAD_LEN,body_length);
  18. //去掉完整的消息包
  19. _buffer = _buffer.mid(TCP_HEAD_LEN+body_length);
  20. // 解析服务器发过来的消息
  21. QJsonDocument jsonDoc = QJsonDocument::fromJson(body);
  22. if(jsonDoc.isNull()){
  23. qDebug() << "Failed to create JSON doc.";
  24. this->_socket->close();
  25. return;
  26. }
  27. if(!jsonDoc.isObject()){
  28. qDebug() << "JSON is not an object.";
  29. this->_socket->close();
  30. return;
  31. }
  32. //qDebug() << "receive data is " << body;
  33. // 获取 JSON 对象
  34. QJsonObject jsonObject = jsonDoc.object();
  35. emit sig_logic_process(msg_id, jsonObject);
  36. }else{
  37. //消息未完全接受,所以中断
  38. break;
  39. }
  40. }
  41. }

单线程逻辑服务器

我们先讲解单线程处理收包逻辑的服务器,以后再给大家将多线程的。

服务器要配合客户端,对报文头部大小做修改

  1. //头部总长度
  2. #define HEAD_TOTAL_LEN 6
  3. //头部id长度
  4. #define HEAD_ID_LEN 2
  5. //头部数据长度
  6. #define HEAD_DATA_LEN 4
  7. // 接受队列最大个数
  8. #define MAX_RECVQUE 2000000
  9. // 发送队列最大个数
  10. #define MAX_SENDQUE 2000000

其余逻辑和我们在网络编程中讲的IocontextPool模型服务器一样

服务器收到报文头后调用LogicSystem来处理

  1. void CSession::AsyncReadBody(int total_len)
  2. {
  3. auto self = shared_from_this();
  4. asyncReadFull(total_len, [self, this, total_len](const boost::system::error_code& ec, std::size_t bytes_transfered) {
  5. try {
  6. if (ec) {
  7. std::cout << "handle read failed, error is " << ec.what() << endl;
  8. Close();
  9. _server->ClearSession(_session_id);
  10. return;
  11. }
  12. if (bytes_transfered < total_len) {
  13. std::cout << "read length not match, read [" << bytes_transfered << "] , total ["
  14. << total_len<<"]" << endl;
  15. Close();
  16. _server->ClearSession(_session_id);
  17. return;
  18. }
  19. memcpy(_recv_msg_node->_data , _data , bytes_transfered);
  20. _recv_msg_node->_cur_len += bytes_transfered;
  21. _recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
  22. cout << "receive data is " << _recv_msg_node->_data << endl;
  23. //此处将消息投递到逻辑队列中
  24. LogicSystem::GetInstance()->PostMsgToQue(make_shared<LogicNode>(shared_from_this(), _recv_msg_node));
  25. //继续监听头部接受事件
  26. AsyncReadHead(HEAD_TOTAL_LEN);
  27. }
  28. catch (std::exception& e) {
  29. std::cout << "Exception code is " << e.what() << endl;
  30. }
  31. });
  32. }

我们知道LogicSystem会将消息投递到队列里,然后单线程处理, 服务器LogicSystem注册上传逻辑

  1. void LogicSystem::RegisterCallBacks() {
  2. _fun_callbacks[ID_TEST_MSG_REQ] = [this](shared_ptr<CSession> session, const short& msg_id,
  3. const string& msg_data) {
  4. Json::Reader reader;
  5. Json::Value root;
  6. reader.parse(msg_data, root);
  7. auto data = root["data"].asString();
  8. std::cout << "recv test data is " << data << std::endl;
  9. Json::Value rtvalue;
  10. Defer defer([this, &rtvalue, session]() {
  11. std::string return_str = rtvalue.toStyledString();
  12. session->Send(return_str, ID_TEST_MSG_RSP);
  13. });
  14. rtvalue["error"] = ErrorCodes::Success;
  15. rtvalue["data"] = data;
  16. };
  17. _fun_callbacks[ID_UPLOAD_FILE_REQ] = [this](shared_ptr<CSession> session, const short& msg_id,
  18. const string& msg_data) {
  19. Json::Reader reader;
  20. Json::Value root;
  21. reader.parse(msg_data, root);
  22. auto data = root["data"].asString();
  23. //std::cout << "recv file data is " << data << std::endl;
  24. Json::Value rtvalue;
  25. Defer defer([this, &rtvalue, session]() {
  26. std::string return_str = rtvalue.toStyledString();
  27. session->Send(return_str, ID_UPLOAD_FILE_RSP);
  28. });
  29. // 解码
  30. std::string decoded = base64_decode(data);
  31. auto seq = root["seq"].asInt();
  32. auto name = root["name"].asString();
  33. auto total_size = root["total_size"].asInt();
  34. auto trans_size = root["trans_size"].asInt();
  35. auto file_path = ConfigMgr::Inst().GetFileOutPath();
  36. auto file_path_str = (file_path / name).string();
  37. std::cout << "file_path_str is " << file_path_str << std::endl;
  38. std::ofstream outfile;
  39. //第一个包
  40. if (seq == 1) {
  41. // 打开文件,如果存在则清空,不存在则创建
  42. outfile.open(file_path_str, std::ios::binary | std::ios::trunc);
  43. }
  44. else {
  45. // 保存为文件
  46. outfile.open(file_path_str, std::ios::binary | std::ios::app);
  47. }
  48. if (!outfile) {
  49. std::cerr << "无法打开文件进行写入。" << std::endl;
  50. return 1;
  51. }
  52. outfile.write(decoded.data(), decoded.size());
  53. if (!outfile) {
  54. std::cerr << "写入文件失败。" << std::endl;
  55. return 1;
  56. }
  57. outfile.close();
  58. std::cout << "文件已成功保存为: " << name << std::endl;
  59. rtvalue["error"] = ErrorCodes::Success;
  60. rtvalue["total_size"] = total_size;
  61. rtvalue["seq"] = seq;
  62. rtvalue["name"] = name;
  63. rtvalue["trans_size"] = trans_size;
  64. };
  65. }

收到上传消息后写入文件。

多线程逻辑服务器

多线程逻辑服务器主要是为了缓解单线程接受数据造成的瓶颈,因为单线程接收数据,就会影响其他线程接收数据,所以考虑引入线程池处理收到的数据。

在多线程编程中我们讲过划分多线程设计的几种思路:

  1. 按照任务划分,将不同的任务投递给不同的线程
  2. 按照线程数轮询处理
  3. 按照递归的方式划分

很明显我们不是做二分查找之类的算法处理,所以不会采用第三种。

现在考虑第二种,如果客户端发送一个很大的文件,客户端将文件切分为几个小份发送,服务器通过iocontext池接受数据, 将接受的数据投递到线程池。

我们知道线程池处理任务是不分先后顺序的,只要投递到队列中的都会被无序取出处理。

https://cdn.llfc.club/1732945106584.jpg

会造成数据包处理的乱序,当然可以最后交给一个线程去组合,统一写入文件,这么做的一个弊端就是如果文件很大,那就要等待完全重组完成才能组合为一个统一的包,如果文件很大,这个时间就会很长,当然也可以暂时缓存这些数据,每次收到后排序组合,比较麻烦。

所以这里推荐按照任务划分。

按照任务划分就是按照不同的客户端做区分,一个客户端传输的数据按照文件名字的hash值划分给不同的线程单独处理,也就是一个线程专门处理对应的hash值的任务,这样既能保证有序,又能保证其他线程可以处理其他任务,也有概率会命中hash同样的值投递给一个队列,但也扩充了并发能力。

https://cdn.llfc.club/1732948742965.jpg

因为我们之前的逻辑处理也是单线程,所以考虑在逻辑层这里做一下解耦合,因为这个服务只是用来处理数据接受,不涉及多个连接互相访问。所以可以讲logic线程扩充为多个,按照sessionid将不同的逻辑分配给不同的线程处理。

https://cdn.llfc.club/1732952125218.jpg

多线程处理逻辑

LogicSystem中添加多个LogicWorker用来处理逻辑

  1. typedef function<void(shared_ptr<CSession>, const short &msg_id, const string &msg_data)> FunCallBack;
  2. class LogicSystem:public Singleton<LogicSystem>
  3. {
  4. friend class Singleton<LogicSystem>;
  5. public:
  6. ~LogicSystem();
  7. void PostMsgToQue(shared_ptr < LogicNode> msg, int index);
  8. private:
  9. LogicSystem();
  10. std::vector<std::shared_ptr<LogicWorker> > _workers;
  11. };

实现投递逻辑

  1. LogicSystem::LogicSystem(){
  2. for (int i = 0; i < LOGIC_WORKER_COUNT; i++) {
  3. _workers.push_back(std::make_shared<LogicWorker>());
  4. }
  5. }
  6. LogicSystem::~LogicSystem(){
  7. }
  8. void LogicSystem::PostMsgToQue(shared_ptr < LogicNode> msg, int index) {
  9. _workers[index]->PostTask(msg);
  10. }

每一个LogicWorker都包含一个线程,这样LogicWorker可以在独立的线程里处理任务

  1. class LogicWorker
  2. {
  3. public:
  4. LogicWorker();
  5. ~LogicWorker();
  6. void PostTask(std::shared_ptr<LogicNode> task);
  7. void RegisterCallBacks();
  8. private:
  9. void task_callback(std::shared_ptr<LogicNode>);
  10. std::thread _work_thread;
  11. std::queue<std::shared_ptr<LogicNode>> _task_que;
  12. std::atomic<bool> _b_stop;
  13. std::mutex _mtx;
  14. std::condition_variable _cv;
  15. std::unordered_map<short, FunCallBack> _fun_callbacks;
  16. };

LogicWorker启动一个线程处理任务

  1. LogicWorker::LogicWorker():_b_stop(false)
  2. {
  3. RegisterCallBacks();
  4. _work_thread = std::thread([this]() {
  5. while (!_b_stop) {
  6. std::unique_lock<std::mutex> lock(_mtx);
  7. _cv.wait(lock, [this]() {
  8. if(_b_stop) {
  9. return true;
  10. }
  11. if (_task_que.empty()) {
  12. return false;
  13. }
  14. return true;
  15. });
  16. if (_b_stop) {
  17. return;
  18. }
  19. auto task = _task_que.front();
  20. task_callback(task);
  21. _task_que.pop();
  22. }
  23. });
  24. }

当然要提前注册好任务

  1. void LogicWorker::RegisterCallBacks()
  2. {
  3. _fun_callbacks[ID_TEST_MSG_REQ] = [this](shared_ptr<CSession> session, const short& msg_id,
  4. const string& msg_data) {
  5. Json::Reader reader;
  6. Json::Value root;
  7. reader.parse(msg_data, root);
  8. auto data = root["data"].asString();
  9. std::cout << "recv test data is " << data << std::endl;
  10. Json::Value rtvalue;
  11. Defer defer([this, &rtvalue, session]() {
  12. std::string return_str = rtvalue.toStyledString();
  13. session->Send(return_str, ID_TEST_MSG_RSP);
  14. });
  15. rtvalue["error"] = ErrorCodes::Success;
  16. rtvalue["data"] = data;
  17. };
  18. _fun_callbacks[ID_UPLOAD_FILE_REQ] = [this](shared_ptr<CSession> session, const short& msg_id,
  19. const string& msg_data) {
  20. Json::Reader reader;
  21. Json::Value root;
  22. reader.parse(msg_data, root);
  23. auto seq = root["seq"].asInt();
  24. auto name = root["name"].asString();
  25. auto total_size = root["total_size"].asInt();
  26. auto trans_size = root["trans_size"].asInt();
  27. auto last = root["last"].asInt();
  28. auto file_data = root["data"].asString();
  29. Json::Value rtvalue;
  30. Defer defer([this, &rtvalue, session]() {
  31. std::string return_str = rtvalue.toStyledString();
  32. session->Send(return_str, ID_UPLOAD_FILE_RSP);
  33. });
  34. // 使用 std::hash 对字符串进行哈希
  35. std::hash<std::string> hash_fn;
  36. size_t hash_value = hash_fn(name); // 生成哈希值
  37. int index = hash_value % FILE_WORKER_COUNT;
  38. std::cout << "Hash value: " << hash_value << std::endl;
  39. FileSystem::GetInstance()->PostMsgToQue(
  40. std::make_shared<FileTask>(session, name, seq, total_size,
  41. trans_size, last, file_data),
  42. index
  43. );
  44. rtvalue["error"] = ErrorCodes::Success;
  45. rtvalue["total_size"] = total_size;
  46. rtvalue["seq"] = seq;
  47. rtvalue["name"] = name;
  48. rtvalue["trans_size"] = trans_size;
  49. rtvalue["last"] = last;
  50. };
  51. }

处理逻辑

  1. void LogicWorker::task_callback(std::shared_ptr<LogicNode> task)
  2. {
  3. cout << "recv_msg id is " << task->_recvnode->_msg_id << endl;
  4. auto call_back_iter = _fun_callbacks.find(task->_recvnode->_msg_id);
  5. if (call_back_iter == _fun_callbacks.end()) {
  6. return;
  7. }
  8. call_back_iter->second(task->_session, task->_recvnode->_msg_id,
  9. std::string(task->_recvnode->_data, task->_recvnode->_cur_len));
  10. }

比如对于文件上传,ID_UPLOAD_FILE_REQ就调用对应的回调,在回调函数里我们再次将要处理的任务封装好投递到文件系统

  1. FileSystem::GetInstance()->PostMsgToQue(
  2. std::make_shared<FileTask>(session, name, seq, total_size,
  3. trans_size, last, file_data),
  4. index
  5. );

文件系统和逻辑系统类似,包含一堆FileWorker

  1. class FileSystem :public Singleton<FileSystem>
  2. {
  3. friend class Singleton<FileSystem>;
  4. public:
  5. ~FileSystem();
  6. void PostMsgToQue(shared_ptr <FileTask> msg, int index);
  7. private:
  8. FileSystem();
  9. std::vector<std::shared_ptr<FileWorker>> _file_workers;
  10. };

实现投递逻辑

  1. FileSystem::~FileSystem()
  2. {
  3. }
  4. void FileSystem::PostMsgToQue(shared_ptr<FileTask> msg, int index)
  5. {
  6. _file_workers[index]->PostTask(msg);
  7. }
  8. FileSystem::FileSystem()
  9. {
  10. for (int i = 0; i < FILE_WORKER_COUNT; i++) {
  11. _file_workers.push_back(std::make_shared<FileWorker>());
  12. }
  13. }

定义文件任务

  1. class CSession;
  2. struct FileTask {
  3. FileTask(std::shared_ptr<CSession> session, std::string name,
  4. int seq, int total_size, int trans_size, int last,
  5. std::string file_data) :_session(session),
  6. _seq(seq),_name(name),_total_size(total_size),
  7. _trans_size(trans_size),_last(last),_file_data(file_data)
  8. {}
  9. ~FileTask(){}
  10. std::shared_ptr<CSession> _session;
  11. int _seq ;
  12. std::string _name ;
  13. int _total_size ;
  14. int _trans_size ;
  15. int _last ;
  16. std::string _file_data;
  17. };

实现文件工作者

  1. class FileWorker
  2. {
  3. public:
  4. FileWorker();
  5. ~FileWorker();
  6. void PostTask(std::shared_ptr<FileTask> task);
  7. private:
  8. void task_callback(std::shared_ptr<FileTask>);
  9. std::thread _work_thread;
  10. std::queue<std::shared_ptr<FileTask>> _task_que;
  11. std::atomic<bool> _b_stop;
  12. std::mutex _mtx;
  13. std::condition_variable _cv;
  14. };

构造函数启动线程

  1. FileWorker::FileWorker():_b_stop(false)
  2. {
  3. _work_thread = std::thread([this]() {
  4. while (!_b_stop) {
  5. std::unique_lock<std::mutex> lock(_mtx);
  6. _cv.wait(lock, [this]() {
  7. if (_b_stop) {
  8. return true;
  9. }
  10. if (_task_que.empty()) {
  11. return false;
  12. }
  13. return true;
  14. });
  15. if (_b_stop) {
  16. break;
  17. }
  18. auto task = _task_que.front();
  19. _task_que.pop();
  20. task_callback(task);
  21. }
  22. });
  23. }

析构需等待线程

  1. FileWorker::~FileWorker()
  2. {
  3. _b_stop = true;
  4. _cv.notify_one();
  5. _work_thread.join();
  6. }

投递任务

  1. void FileWorker::PostTask(std::shared_ptr<FileTask> task)
  2. {
  3. {
  4. std::lock_guard<std::mutex> lock(_mtx);
  5. _task_que.push(task);
  6. }
  7. _cv.notify_one();
  8. }

因为线程会触发回调函数保存文件,所以我们实现回调函数

  1. void FileWorker::task_callback(std::shared_ptr<FileTask> task)
  2. {
  3. // 解码
  4. std::string decoded = base64_decode(task->_file_data);
  5. auto file_path = ConfigMgr::Inst().GetFileOutPath();
  6. auto file_path_str = (file_path / task->_name).string();
  7. auto last = task->_last;
  8. //std::cout << "file_path_str is " << file_path_str << std::endl;
  9. std::ofstream outfile;
  10. //第一个包
  11. if (task->_seq == 1) {
  12. // 打开文件,如果存在则清空,不存在则创建
  13. outfile.open(file_path_str, std::ios::binary | std::ios::trunc);
  14. }
  15. else {
  16. // 保存为文件
  17. outfile.open(file_path_str, std::ios::binary | std::ios::app);
  18. }
  19. if (!outfile) {
  20. std::cerr << "无法打开文件进行写入。" << std::endl;
  21. return ;
  22. }
  23. outfile.write(decoded.data(), decoded.size());
  24. if (!outfile) {
  25. std::cerr << "写入文件失败。" << std::endl;
  26. return ;
  27. }
  28. outfile.close();
  29. if (last) {
  30. std::cout << "文件已成功保存为: " << task->_name << std::endl;
  31. }
  32. }

测试效果

https://cdn.llfc.club/1732955339237.jpg

源码链接

https://gitee.com/secondtonone1/boostasio-learn/tree/master/network/day26-multithread-res-server

热门评论

热门文章

  1. 解密定时器的实现细节

    喜欢(566) 浏览(3401)
  2. C++ 类的继承封装和多态

    喜欢(588) 浏览(4794)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(11669)
  4. windows环境搭建和vscode配置

    喜欢(587) 浏览(2659)
  5. slice介绍和使用

    喜欢(521) 浏览(2441)

最新评论

  1. golang 函数介绍 secondtonone1:函数是go中的一等公民,作为新兴语言,go摒弃了面向对象的一些糟粕,采取接口方式编程,而接口方式编程都是基于函数的,参数为interface,进而达到泛型作用,比如sort排序,只需要传入的参数满足sort所需interface的规定即可,需实现Len, Swap, Less三个方法,只要实现了这三个方法都可以用来做sort排序的参数。
  2. 聊天项目(13) 重置密码功能 Doraemon:万一一个用户多个邮箱呢 有可能的
  3. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  4. asio多线程模式IOThreadPool secondtonone1:这么优秀吗
  5. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。

个人公众号

个人微信