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

【Linux】Socket编程TCP

目录

一、TCP相关的接口

二、Echo Server

多进程版

多线程版

线程池版

三、多线程远程命令执行


一、TCP相关的接口

  • listen函数

功能:将套接字设置为“监听模式”,使其能够接受客户端的连接请求。

原型:int listen(int sockfd, int backlog);

参数说明:

  • sockfd:套接字的文件描述符。
  • backlog:指定在拒绝新连接之前,系统允许的最大未接受连接数量(即连接队列的长度)。例如:backlog = 6,服务器最多可排队6个未处理的连接请求。

返回值:成功返回0,失败返回-1 并设置errno。

注意:

  • 在TCP协议中,服务器需要先调用bind绑定地址和端口,然后调用listen开始监听。
  • accept函数

功能:服务器端接受客户端的连接请求。

原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

  • sockfd:监听套接字的文件描述符(通过socket()创建且以调用listen()进入监听状态)
  • addr:指向sockaddr结构体的指针,用于存储客户端地址信息(IP和端口)。设为NULL表示不获取地址。
  • addrlen:是一个传入参数,传入的是调用者提供的,缓冲区addr的长度以及缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满缓冲区)。

返回值:成功返回新创建的通信套接字描述符(非负整数),用于后续数据传输;失败返回-1并设置errno。

注意:

  • 必须在listen之后再调用accept函数。
  • 当服务器调用accept函数时没有客户端的连接请求,就阻塞等待直到有客户端连接请求。
  • connect函数

功能:用于客户端套接字发起连接请求,目标是与指定服务器建立可靠的TCP连接。

原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:套接字描述符
  • addr:用于指定服务器的IP地址和端口号
  • addrlen:表示addr结构体的长度

返回值:成功返回0,表示连接成功建立;失败返回-1并设置errno。

  • recv函数

功能:用于从已连接的套接字接收数据,常用语TCP协议通信。

原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:套接字描述符
  • buf:指向缓冲区的指针,用于存储接收到的数据。
  • len:缓冲区大小,表示可以接收的最大字节数。
  • flags:标志位,控制接收行为,常见值如0(默认行为)或MSG_WAITALL(等待所有数据到达)

返回值:成功返回接收到的字节数,失败返回-1并设置errno。

  • send函数

功能:用于通过已连接的套接字发送数据(TCP协议)。

原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:已建立连接的套接字描述符(由socket()创建且connect()成功的套接字)
  • buf:指向待发送数据缓冲区的指针
  • len:待发送数据的字节长度
  • flags:控制发送行为的标志位(常用值):0:默认阻塞模式;MSG_DONTWAIT:非阻塞发送;MSG_OOB:发送带外数据(紧急数据);MSG_NOSIGNAL:禁止生成SIGPIPE信号

返回值:成功返回实际发送的字节数(可能小于len),失败返回-1并设置errno。

二、Echo Server

// Comm.hpp#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>enum{Usage_Err = 1,Socket_Err,Bind_Err,Listen_Err
};#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
// nocopy.hpp#pragma onceclass nocopy
{
public:nocopy(){}nocopy(const nocopy&) = delete;nocopy& operator=(const nocopy&) = delete;~nocopy(){}
};
// TcpServer.hpp#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"using namespace LogModule;const static int default_backlog = 6;
const static int default_listenfd = -1;class TcpServer : public nocopy
{
public:TcpServer(uint16_t port, int backlog = default_backlog):_port(port), _listenfd(default_listenfd), _backlog(default_backlog), _running(false){}void Init() {// 创建socket_listenfd = socket(AF_INET, SOCK_STREAM, 0);if(_listenfd < 0) {LOG(LogLevel::ERROR) << "create socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Socket_Err);}LOG(LogLevel::INFO) << "create socket success, sockfd: " << _listenfd;// bindstruct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY;local.sin_family = AF_INET;local.sin_port = htons(_port);ssize_t n = bind(_listenfd, CONV(&local), sizeof(local));if(n < 0) {LOG(LogLevel::ERROR) << "socket bind error, errno: " << errno << ", error string: " << strerror(errno);exit(Bind_Err);}LOG(LogLevel::INFO) << "bind socket success!!!";// 设置 socket 为监听状态n = listen(_listenfd, _backlog);if(n < 0) {LOG(LogLevel::ERROR) << "listen socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Listen_Err);}LOG(LogLevel::INFO) << "listen socket success!!!";}void Start() {_running = true;while(_running) {// 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listenfd, CONV(&peer), &len);if(sockfd < 0) {LOG(LogLevel::WARNING) << "accept socket error, errno: " << errno << ", error string: " << strerror(errno);continue;}LOG(LogLevel::INFO) << "accept socket success, sockfd: " << sockfd;// 提供服务Server(sockfd);close(sockfd);}}
private:void Server(int sockfd) {char buffer[4096];// 一直进行IO操作while(true) {// 可以使用文件的读写接口直接进行通信ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0) {buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo = "server echo# ";echo += buffer;write(sockfd, echo.c_str(), echo.size());}else if(n == 0) { // 说明读到结尾,关闭连接LOG(LogLevel::INFO) << "client quit...";break;}else { // 文件读取出现错误LOG(LogLevel::ERROR) << "read sockfd error, errno: " << errno << ", error string: " << strerror(errno);break;}}}
private:uint16_t _port;int _listenfd;int _backlog;bool _running;
};
// TcpServer.cc#include "TcpServer.hpp"int main(int argc, char* argv[])
{if(argc != 2) {std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;return 1;}uint16_t port = atoi(argv[1]);TcpServer ts(port);ts.Init();ts.Start();return 0;
}
// TcpClient.cc#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include "Comm.hpp"int main(int argc, char* argv[])
{if(argc != 3) {std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}uint16_t port = std::atoi(argv[2]);std::string ip = argv[1];int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0) {std::cerr << "create socket error" << std::endl;return 2;}// 跟UDP一样,需要bind,但不需要用户显示bind,client系统随机端口// 发起连接时,client会被系统自动进行本地bind// connectstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &server.sin_addr);int n = connect(sockfd, CONV(&server), sizeof(server)); // 自动bindif(n < 0) {std::cerr << "connect error" << std::endl;return 3;}while(true) {std::string inbuffer;std::cout << "Please Enter: ";std::getline(std::cin, inbuffer);ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());if(n > 0) {char buffer[4096];ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);if(m > 0) {buffer[m] = 0;std::cout << "get a echo message: " << buffer << std::endl;}else break;}else break;}return 0;
}

由于客户端不需要固定的端口号,因此不用调用 bind 函数,客户端端口由内核自动分配。

注意:

  • 客户端不是不允许调用bind,只是没有必要显示调用bind固定一个端口号。否则在同一台机器上启动多个客户端,机会出现端口号被占用导致无法正确建立连接。
  • 服务器也不是必须调用bind,但如果服务器不调用bind,内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

测试多个连接的情况

我们在启动一个客户端,尝试连接服务器,发现第二个客户端不能正确的和服务器进行通信。

原因:是因为我们 accept 一个请求后,就一直在 while 中循环尝试 read,没有继续调用 accept,导致无法接受新的请求。

我们当前的TCP服务器只能与一个客户端通信,这是不科学的。

多进程版

对于每个请求,创建子进程的方式来支持多连接。

// TcpServer.hpp#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/wait.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"using namespace LogModule;const static int default_backlog = 6;
const static int default_listenfd = -1;class TcpServer : public nocopy
{
public:TcpServer(uint16_t port, int backlog = default_backlog):_port(port), _listenfd(default_listenfd), _backlog(default_backlog), _running(false){}void Init() {// 创建socket_listenfd = socket(AF_INET, SOCK_STREAM, 0);if(_listenfd < 0) {LOG(LogLevel::ERROR) << "create socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Socket_Err);}LOG(LogLevel::INFO) << "create socket success, sockfd: " << _listenfd;// bindstruct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY;local.sin_family = AF_INET;local.sin_port = htons(_port);ssize_t n = bind(_listenfd, CONV(&local), sizeof(local));if(n < 0) {LOG(LogLevel::ERROR) << "socket bind error, errno: " << errno << ", error string: " << strerror(errno);exit(Bind_Err);}LOG(LogLevel::INFO) << "bind socket success!!!";// 设置 socket 为监听状态n = listen(_listenfd, _backlog);if(n < 0) {LOG(LogLevel::ERROR) << "listen socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Listen_Err);}LOG(LogLevel::INFO) << "listen socket success!!!";}void Start() {_running = true;while(_running) {// 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listenfd, CONV(&peer), &len);if(sockfd < 0) {LOG(LogLevel::WARNING) << "accept socket error, errno: " << errno << ", error string: " << strerror(errno);continue;}LOG(LogLevel::INFO) << "accept socket success, sockfd: " << sockfd;ProcessConnect(sockfd);}}
private:void ProcessConnect(int sockfd) {int id = fork();if(id < 0) {LOG(LogLevel::ERROR) << "fork error";close(sockfd);return;}else if(id == 0) {// 子进程close(_listenfd);if(fork() > 0) exit(0); // 子进程创建一个子进程,并退出,让创建出的子进程变为孤儿进程,由系统管理// 孤儿进程// 提供服务Server(sockfd);close(sockfd);exit(0);}else {close(sockfd);waitpid(id, nullptr, 0);}}void Server(int sockfd) {char buffer[4096];// 一直进行IO操作while(true) {// 可以使用文件的读写接口直接进行通信ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0) {buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo = "server echo# ";echo += buffer;write(sockfd, echo.c_str(), echo.size());}else if(n == 0) { // 说明读到结尾,关闭连接LOG(LogLevel::INFO) << "client quit...";break;}else { // 文件读取出现错误LOG(LogLevel::ERROR) << "read sockfd error, errno: " << errno << ", error string: " << strerror(errno);break;}}}
private:uint16_t _port;int _listenfd;int _backlog;bool _running;
};

多线程版

// TcpServer.hpp#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"using namespace LogModule;const static int default_backlog = 6;
const static int default_listenfd = -1;class TcpServer : public nocopy
{
public:TcpServer(uint16_t port, int backlog = default_backlog):_port(port), _listenfd(default_listenfd), _backlog(default_backlog), _running(false){}void Init() {// 创建socket_listenfd = socket(AF_INET, SOCK_STREAM, 0);if(_listenfd < 0) {LOG(LogLevel::ERROR) << "create socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Socket_Err);}LOG(LogLevel::INFO) << "create socket success, sockfd: " << _listenfd;// bindstruct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY;local.sin_family = AF_INET;local.sin_port = htons(_port);ssize_t n = bind(_listenfd, CONV(&local), sizeof(local));if(n < 0) {LOG(LogLevel::ERROR) << "socket bind error, errno: " << errno << ", error string: " << strerror(errno);exit(Bind_Err);}LOG(LogLevel::INFO) << "bind socket success!!!";// 设置 socket 为监听状态n = listen(_listenfd, _backlog);if(n < 0) {LOG(LogLevel::ERROR) << "listen socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Listen_Err);}LOG(LogLevel::INFO) << "listen socket success!!!";}void Start() {_running = true;while(_running) {// 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listenfd, CONV(&peer), &len);if(sockfd < 0) {LOG(LogLevel::WARNING) << "accept socket error, errno: " << errno << ", error string: " << strerror(errno);continue;}LOG(LogLevel::INFO) << "accept socket success, sockfd: " << sockfd;pthread_t tid;pthread_create(&tid, nullptr, ProcessConnect, (void*)&sockfd);}}static void Server(int sockfd) {char buffer[4096];// 一直进行IO操作while(true) {// 可以使用文件的读写接口直接进行通信ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0) {buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo = "server echo# ";echo += buffer;write(sockfd, echo.c_str(), echo.size());}else if(n == 0) { // 说明读到结尾,关闭连接LOG(LogLevel::INFO) << "client quit...";break;}else { // 文件读取出现错误LOG(LogLevel::ERROR) << "read sockfd error, errno: " << errno << ", error string: " << strerror(errno);break;}}}
private:static void* ProcessConnect(void* arg) {pthread_detach(pthread_self());int* sockptr = static_cast<int*>(arg);TcpServer::Server(*sockptr);close(*sockptr);delete sockptr;return nullptr;}
private:uint16_t _port;int _listenfd;int _backlog;bool _running;
};

线程池版

#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"
#include "ThreadPool.hpp"using namespace LogModule;const static int default_backlog = 6;
const static int default_listenfd = -1;class TcpServer : public nocopy
{
public:TcpServer(uint16_t port, int backlog = default_backlog):_port(port), _listenfd(default_listenfd), _backlog(default_backlog), _running(false){}void Init() {// 创建socket_listenfd = socket(AF_INET, SOCK_STREAM, 0);if(_listenfd < 0) {LOG(LogLevel::ERROR) << "create socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Socket_Err);}LOG(LogLevel::INFO) << "create socket success, sockfd: " << _listenfd;// bindstruct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY;local.sin_family = AF_INET;local.sin_port = htons(_port);ssize_t n = bind(_listenfd, CONV(&local), sizeof(local));if(n < 0) {LOG(LogLevel::ERROR) << "socket bind error, errno: " << errno << ", error string: " << strerror(errno);exit(Bind_Err);}LOG(LogLevel::INFO) << "bind socket success!!!";// 设置 socket 为监听状态n = listen(_listenfd, _backlog);if(n < 0) {LOG(LogLevel::ERROR) << "listen socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Listen_Err);}LOG(LogLevel::INFO) << "listen socket success!!!";}void Start() {using task_t = std::function<void()>;_running = true;while(_running) {// 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listenfd, CONV(&peer), &len);if(sockfd < 0) {LOG(LogLevel::WARNING) << "accept socket error, errno: " << errno << ", error string: " << strerror(errno);continue;}LOG(LogLevel::INFO) << "accept socket success, sockfd: " << sockfd;task_t t = std::bind(&TcpServer::Server, sockfd);ThreadPool<task_t>::GetInstance()->Push(t);}}static void Server(int sockfd) {char buffer[4096];// 一直进行IO操作while(true) {// 可以使用文件的读写接口直接进行通信ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0) {buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo = "server echo# ";echo += buffer;write(sockfd, echo.c_str(), echo.size());}else if(n == 0) { // 说明读到结尾,关闭连接LOG(LogLevel::INFO) << "client quit...";break;}else { // 文件读取出现错误LOG(LogLevel::ERROR) << "read sockfd error, errno: " << errno << ", error string: " << strerror(errno);break;}}}
private:uint16_t _port;int _listenfd;int _backlog;bool _running;
};

三、多线程远程命令执行

// Command.hpp#pragma once#include <iostream>
#include <string>
#include <unordered_set>
#include <unistd.h>
#include <sys/socket.h>class Command
{
public:Command(int sockfd):_sockfd(sockfd){// 只允许少量命令执行,只是做一个简单的示范,有兴趣可以和之前实现的简单的shell程序结合_hash_com.insert("ls");_hash_com.insert("ls -l");_hash_com.insert("pwd");_hash_com.insert("whoami");}bool IsExist(std::string command) {return _hash_com.find(command) != _hash_com.end();}std::string Excute(std::string &command) {if(!IsExist(command)) return std::string();FILE* fp = popen(command.c_str(), "r");if(fp == nullptr) return std::string();char buffer[1024];std::string res;while(fgets(buffer, sizeof(buffer), fp))res += buffer;pclose(fp);return res;}std::string RecvCommand() {char command[1024];ssize_t n = recv(_sockfd, command, sizeof(command) - 1, 0);if(n > 0) {command[n] = 0;return command;}else return std::string();}void SendCommand(std::string res) {if(res.empty()) res = "Empty";send(_sockfd, res.c_str(), res.size(), 0);}
private:int _sockfd;std::string _command;std::unordered_set<std::string> _hash_com;
};
// TcpServer.hpp#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"
#include "ThreadPool.hpp"
#include "Command.hpp"using namespace LogModule;const static int default_backlog = 6;
const static int default_listenfd = -1;class TcpServer : public nocopy
{
public:TcpServer(uint16_t port, int backlog = default_backlog):_port(port), _listenfd(default_listenfd), _backlog(default_backlog), _running(false){}void Init() {// 创建socket_listenfd = socket(AF_INET, SOCK_STREAM, 0);if(_listenfd < 0) {LOG(LogLevel::ERROR) << "create socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Socket_Err);}LOG(LogLevel::INFO) << "create socket success, sockfd: " << _listenfd;// bindstruct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY;local.sin_family = AF_INET;local.sin_port = htons(_port);ssize_t n = bind(_listenfd, CONV(&local), sizeof(local));if(n < 0) {LOG(LogLevel::ERROR) << "socket bind error, errno: " << errno << ", error string: " << strerror(errno);exit(Bind_Err);}LOG(LogLevel::INFO) << "bind socket success!!!";// 设置 socket 为监听状态n = listen(_listenfd, _backlog);if(n < 0) {LOG(LogLevel::ERROR) << "listen socket error, errno: " << errno << ", error string: " << strerror(errno);exit(Listen_Err);}LOG(LogLevel::INFO) << "listen socket success!!!";}void Start() {using task_t = std::function<void()>;_running = true;while(_running) {// 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listenfd, CONV(&peer), &len);if(sockfd < 0) {LOG(LogLevel::WARNING) << "accept socket error, errno: " << errno << ", error string: " << strerror(errno);continue;}LOG(LogLevel::INFO) << "accept socket success, sockfd: " << sockfd;task_t t = std::bind(&TcpServer::Server, sockfd);ThreadPool<task_t>::GetInstance()->Push(t);}}static void Server(int sockfd) {while(true) {Command com(sockfd);std::string req = com.RecvCommand();if(req.empty()) break;std::string resp = com.Excute(req);com.SendCommand(resp);}// char buffer[4096];// // 一直进行IO操作// while(true) {//     // 可以使用文件的读写接口直接进行通信//     ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);//     if(n > 0) {//         buffer[n] = 0;//         std::cout << "client say# " << buffer << std::endl;//         std::string echo = "server echo# ";//         echo += buffer;//         write(sockfd, echo.c_str(), echo.size());//     }//     else if(n == 0) { // 说明读到结尾,关闭连接//         LOG(LogLevel::INFO) << "client quit...";//         break;//     }//     else { // 文件读取出现错误//         LOG(LogLevel::ERROR) << "read sockfd error, errno: " << errno << ", error string: " << strerror(errno);//         break;//     }// }}
private:uint16_t _port;int _listenfd;int _backlog;bool _running;
};

http://www.dtcms.com/a/506860.html

相关文章:

  • Debian编译Qt5
  • [3-03-01].第07节:搭建服务 - 服务重构cloud-consumer-ocommon
  • Ubuntu Certbot版本查询失败?Snap安装后报错终极修复指南(通用版)
  • Kafka底层解析:可靠性与高性能原理
  • 分布式链路追踪中的上下文传播与一致性维护技术
  • 为已有nextjs项目添加supabase数据库,不再需要冗余后端
  • 网站建设怎样上传程序微信网站搭建多少钱
  • rabbitmq在微服务中配置监听开关
  • 下一代时序数据库标杆:Apache IoTDB架构演进与AIoT时代的数据战略
  • k8s中的控制器
  • Blender入门学习02
  • 动态规划的“数学之魂”:从DP推演到质因数分解——巧解「只有两个键的键盘」
  • Blender入门学习01
  • 网站开发word文档精品简历模板网站
  • WrenAI:企业级AI数据分析平台技术解析
  • 【Processing】椭圆眼珠鼠标跟随
  • 工业显示器在矿用挖掘机中的应用
  • 济南企业网站开发网站建设域名
  • 【深度学习计算机视觉】14:实战Kaggle比赛:狗的品种识别(ImageNet Dogs)
  • 基于k8s的Python的分布式深度学习训练平台搭建简单实践
  • 网站服务器地址在哪里看前端工程师是做网站吗
  • 基于SpringBoot的环保行为记录与社区互动平台(Vue+MySQL)
  • 洛谷 P3392 涂条纹-普及-
  • 【 柒个贰航空旅游-注册安全分析报告-无验证方式导致安全隐患】
  • CentOS 7 安装 MySQL 8
  • Java 数据类型分类
  • 定制高端网站建设设计上传网站图片不显示
  • 无人机路径规划与定位技术原理及实现详解
  • 自己做公司网站适用于手机的网站怎么建设
  • 解决前端多标签页通信:BroadcastChannel