后端技术22-从轮询到WebSocket:实时通信的性能提升100倍,10万并发在线!WebSocket聊天室的设计与实现

「知识图谱生成工具」:一键将文件夹内容变身为交互式知识图谱的免安装桌面工具(文末附免费下载链接)-CSDN博客

还在为消息延迟发愁?还在用定时轮询折磨服务器?这篇文章,带你从协议原理到分布式实战,彻底搞懂WebSocket。

目录


开篇:那些年,我们踩过的实时通信坑

还记得第一次做聊天功能的时候,我信心满满地写了个AJAX轮询:

setInterval(() => {
    fetch('/api/messages').then(res => res.json()).then(updateUI);
}, 1000);

当时觉得:完美!每秒刷新一次,用户肯定感觉不到延迟。

直到产品上线第三天,服务器CPU飙到90%,数据库连接池被打爆,老板拿着监控截图问我:“为什么我们的小聊天室能把8核16G的服务器干趴下?”

那一刻我才明白:轮询不是实时通信,是服务器杀手。

今天这篇文章,我会把这些年踩过的坑、学到的经验,全部倒给你。从协议原理到分布式架构,从代码实战到性能优化,保证你看完能直接上手,搭建一个能支撑10万并发的聊天系统。

目标很明确:消息延迟 < 100ms,并发连接 10万+。

坐稳了,发车。


一、WebSocket协议原理:一次握手,终身朋友

1.1 握手过程:HTTP的"伪装"

WebSocket最妙的地方在于:它借用了HTTP来完成握手,然后" hijack "了这条连接。

客户端                                    服务器
   |                                         |
   |  GET /chat HTTP/1.1                     |
   |  Host: server.example.com               |
   |  Upgrade: websocket                     |
   |  Connection: Upgrade                    |
   |  Sec-WebSocket-Key: dGhlIHNhbXBsZQ==    |
   |  Sec-WebSocket-Version: 13              |
   |---------------------------------------->|
   |                                         |
   |         HTTP/1.1 101 Switching Protocols|
   |         Upgrade: websocket              |
   |         Connection: Upgrade             |
   |         Sec-WebSocket-Accept: s3pPLMBi  |
   |<----------------------------------------|
   |                                         |
   |======= WebSocket连接建立成功 ===========|
   |                                         |

关键点:

  • Upgrade: websocket 告诉服务器:“我要升级协议”
  • Sec-WebSocket-Key 是客户端生成的Base64随机数
  • 服务器用魔法字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接后SHA1哈希,再Base64编码返回

1.2 帧格式:轻量级的二进制协议

握手完成后,数据以**帧(Frame)**的形式传输:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - -+
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

核心字段解释:

字段说明
FIN是否为最后一帧
opcode0x1=文本帧, 0x2=二进制帧, 0x8=关闭, 0x9=ping, 0xA=pong
MASK客户端必须置1,服务端必须置0
Payload len数据长度,0-125直接表示,126=后续2字节,127=后续8字节

1.3 心跳机制:保活与检测

TCP连接会超时,NAT会断开空闲连接。WebSocket通过Ping/Pong帧来维持心跳:

客户端                    服务器
   |                        |
   |--- Ping帧 (opcode=0x9) ->|
   |                        |
   |<- Pong帧 (opcode=0xA) ---|
   |                        |
   |    (每隔30-60秒一次)    |

最佳实践:

  • 客户端发送Ping,服务端回复Pong
  • 超时时间通常设置为60-120秒
  • 连续3次未收到Pong,认为连接断开

二、WebSocket vs 长轮询 vs SSE:选谁?

做实时通信,除了WebSocket,还有长轮询和SSE。三者怎么选?

┌─────────────────────────────────────────────────────────────┐
│                    实时通信方案对比                          │
├──────────────┬─────────────┬─────────────┬──────────────────┤
│     特性      │   轮询      │   长轮询    │   SSE/WebSocket  │
├──────────────┼─────────────┼─────────────┼──────────────────┤
│   实时性      │   差(秒级)  │   较好      │   极好(毫秒级)   │
│   服务器压力  │   极高      │   高        │   低             │
│   兼容性      │   最好      │   好        │   WebSocket需IE10+│
│   双向通信    │   否        │   否        │   WebSocket支持  │
│   实现复杂度  │   简单      │   中等      │   中等           │
└──────────────┴─────────────┴─────────────┴──────────────────┘

2.1 轮询(Polling):简单但暴力

// 客户端每秒请求一次
setInterval(() => {
    fetch('/api/messages').then(updateUI);
}, 1000);

问题:

  • 大量无效请求(90%返回空数据)
  • 消息延迟0-1000ms不等
  • 服务器QPS爆炸

2.2 长轮询(Long Polling):改进版

function longPoll() {
    fetch('/api/messages/wait').then(res => {
        updateUI(res);
        longPoll(); // 递归继续
    });
}
longPoll();

服务端hold住连接,有新消息才返回。

问题:

  • 仍然需要频繁建立/断开HTTP连接
  • 服务器需要维护大量挂起连接
  • 消息量大时退化为普通轮询

2.3 SSE(Server-Sent Events):服务器推送

const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (e) => updateUI(e.data);

适用场景:

  • 单向推送(股票行情、新闻feed)
  • 需要自动重连和事件ID追踪

局限:

  • 只能服务器→客户端单向
  • 浏览器连接数限制(HTTP/1.1下6个/域名)

2.4 WebSocket:双向实时通信

结论:需要双向实时通信,选WebSocket。


三、实战:Spring Boot + WebSocket构建聊天室

3.1 项目结构

websocket-chat/
├── src/main/java/com/example/chat/
│   ├── config/
│   │   └── WebSocketConfig.java          # 配置类
│   ├── handler/
│   │   └── ChatWebSocketHandler.java     # 消息处理器
│   ├── manager/
│   │   └── SessionManager.java           # 连接管理
│   ├── service/
│   │   └── MessageRouter.java            # 消息路由
│   └── ChatApplication.java
├── src/main/resources/
│   └── static/
│       └── index.html                    # 前端页面
└── pom.xml

3.2 依赖配置

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.43</version>
    </dependency>
</dependencies>

3.3 WebSocket配置

// WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Autowired
    private ChatWebSocketHandler chatHandler;
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "/ws/chat")
                .setAllowedOrigins("*")
                .addInterceptors(new AuthHandshakeInterceptor());
    }
    
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        // 关键:设置超时时间
        container.setMaxSessionIdleTimeout(600000L); // 10分钟
        return container;
    }
}

3.4 连接管理:SessionManager

// SessionManager.java
@Component
@Slf4j
public class SessionManager {
    
    // 用户ID -> Session映射
    private final ConcurrentHashMap<String, WebSocketSession> userSessions = 
        new ConcurrentHashMap<>();
    
    // Session ID -> 用户ID映射(用于断开时查找)
    private final ConcurrentHashMap<String, String> sessionUserMap = 
        new ConcurrentHashMap<>();
    
    // 在线用户数统计
    private final AtomicInteger onlineCount = new AtomicInteger(0);
    
    public void addSession(String userId, WebSocketSession session) {
        WebSocketSession oldSession = userSessions.put(userId, session);
        if (oldSession != null && oldSession.isOpen()) {
            try {
                oldSession.close(CloseStatus.POLICY_VIOLATION);
                log.info("用户[{}]被踢下线", userId);
            } catch (IOException e) {
                log.error("关闭旧会话失败", e);
            }
        }
        sessionUserMap.put(session.getId(), userId);
        int count = onlineCount.incrementAndGet();
        log.info("用户[{}]上线,当前在线:{}", userId, count);
    }
    
    public void removeSession(String sessionId) {
        String userId = sessionUserMap.remove(sessionId);
        if (userId != null) {
            userSessions.remove(userId);
            int count = onlineCount.decrementAndGet();
            log.info("用户[{}]下线,当前在线:{}", userId, count);
        }
    }
    
    public WebSocketSession getSession(String userId) {
        return userSessions.get(userId);
    }
    
    public Collection<WebSocketSession> getAllSessions() {
        return userSessions.values();
    }
    
    public int getOnlineCount() {
        return onlineCount.get();
    }
}

3.5 消息处理器

// ChatWebSocketHandler.java
@Component
@Slf4j
public class ChatWebSocketHandler extends TextWebSocketHandler {
    
    @Autowired
    private SessionManager sessionManager;
    
    @Autowired
    private MessageRouter messageRouter;
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        String userId = getUserIdFromSession(session);
        sessionManager.addSession(userId, session);
        
        // 发送欢迎消息
        sendMessage(session, new ChatMessage("system", "欢迎进入聊天室!"));
        
        // 广播用户上线通知
        broadcast(new ChatMessage("notice", userId + " 加入了聊天室"));
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        String userId = sessionManager.getUserIdBySessionId(session.getId());
        String payload = message.getPayload();
        
        log.debug("收到消息[{}]: {}", userId, payload);
        
        try {
            ChatMessage chatMessage = JSON.parseObject(payload, ChatMessage.class);
            chatMessage.setFrom(userId);
            chatMessage.setTimestamp(System.currentTimeMillis());
            
            // 路由消息
            messageRouter.route(chatMessage);
        } catch (Exception e) {
            log.error("消息解析失败", e);
            sendMessage(session, new ChatMessage("error", "消息格式错误"));
        }
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        String userId = sessionManager.getUserIdBySessionId(session.getId());
        sessionManager.removeSession(session.getId());
        broadcast(new ChatMessage("notice", userId + " 离开了聊天室"));
    }
    
    private void sendMessage(WebSocketSession session, ChatMessage message) {
        if (session.isOpen()) {
            try {
                session.sendMessage(new TextMessage(JSON.toJSONString(message)));
            } catch (IOException e) {
                log.error("发送消息失败", e);
            }
        }
    }
    
    private void broadcast(ChatMessage message) {
        String payload = JSON.toJSONString(message);
        sessionManager.getAllSessions().forEach(session -> {
            try {
                session.sendMessage(new TextMessage(payload));
            } catch (IOException e) {
                log.error("广播消息失败", e);
            }
        });
    }
}

3.6 消息路由

// MessageRouter.java
@Component
@Slf4j
public class MessageRouter {
    
    @Autowired
    private SessionManager sessionManager;
    
    public void route(ChatMessage message) {
        switch (message.getType()) {
            case "broadcast":
                // 广播给所有用户
                broadcast(message);
                break;
            case "private":
                // 私聊
                sendToUser(message.getTo(), message);
                break;
            case "group":
                // 群聊(从Redis获取群成员)
                sendToGroup(message.getGroupId(), message);
                break;
            default:
                log.warn("未知消息类型: {}", message.getType());
        }
    }
    
    private void sendToUser(String userId, ChatMessage message) {
        WebSocketSession session = sessionManager.getSession(userId);
        if (session != null && session.isOpen()) {
            try {
                session.sendMessage(new TextMessage(JSON.toJSONString(message)));
            } catch (IOException e) {
                log.error("发送私聊消息失败", e);
            }
        } else {
            // 用户离线,存储离线消息到Redis
            storeOfflineMessage(userId, message);
        }
    }
}

3.7 前端代码

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket聊天室</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        #messages { border: 1px solid #ccc; height: 400px; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
        .message { margin: 5px 0; padding: 8px; border-radius: 4px; }
        .system { background: #fff3cd; color: #856404; }
        .user { background: #d4edda; color: #155724; }
        .input-area { display: flex; gap: 10px; }
        input[type="text"] { flex: 1; padding: 10px; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
        button:hover { background: #0056b3; }
        #status { margin-bottom: 10px; padding: 5px; border-radius: 4px; }
        .connected { background: #d4edda; }
        .disconnected { background: #f8d7da; }
    </style>
</head>
<body>
    <h1>WebSocket实时聊天室</h1>
    <div id="status" class="disconnected">● 未连接</div>
    <div id="messages"></div>
    <div class="input-area">
        <input type="text" id="userId" placeholder="输入用户ID" value="user_1">
        <input type="text" id="messageInput" placeholder="输入消息...">
        <button onclick="connect()">连接</button>
        <button onclick="sendMessage()">发送</button>
    </div>

    <script>
        let ws = null;
        let reconnectTimer = null;
        const RECONNECT_INTERVAL = 3000;
        
        function connect() {
            const userId = document.getElementById('userId').value;
            ws = new WebSocket(`ws://localhost:8080/ws/chat?userId=${userId}`);
            
            ws.onopen = () => {
                updateStatus(true);
                console.log('WebSocket连接成功');
                if (reconnectTimer) {
                    clearTimeout(reconnectTimer);
                    reconnectTimer = null;
                }
            };
            
            ws.onmessage = (event) => {
                const msg = JSON.parse(event.data);
                displayMessage(msg);
            };
            
            ws.onclose = () => {
                updateStatus(false);
                console.log('WebSocket连接关闭,准备重连...');
                reconnectTimer = setTimeout(connect, RECONNECT_INTERVAL);
            };
            
            ws.onerror = (error) => {
                console.error('WebSocket错误:', error);
            };
        }
        
        function sendMessage() {
            const input = document.getElementById('messageInput');
            const message = {
                type: 'broadcast',
                content: input.value,
                timestamp: Date.now()
            };
            
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.send(JSON.stringify(message));
                input.value = '';
            }
        }
        
        function displayMessage(msg) {
            const div = document.createElement('div');
            div.className = `message ${msg.from === 'system' ? 'system' : 'user'}`;
            div.innerHTML = `<strong>${msg.from}:</strong> ${msg.content} 
                <small style="color:#999">${new Date(msg.timestamp).toLocaleTimeString()}</small>`;
            document.getElementById('messages').appendChild(div);
            document.getElementById('messages').scrollTop = 999999;
        }
        
        function updateStatus(connected) {
            const status = document.getElementById('status');
            status.className = connected ? 'connected' : 'disconnected';
            status.innerHTML = connected ? '● 已连接' : '● 未连接';
        }
        
        // 回车发送
        document.getElementById('messageInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') sendMessage();
        });
    </script>
</body>
</html>

四、分布式场景:Redis Pub/Sub实现多节点消息同步

单节点能支撑几万连接,但生产环境肯定多节点部署。问题:用户A在Node1,用户B在Node2,怎么通信?

┌─────────────────────────────────────────────────────────────┐
│                      负载均衡器 (Nginx)                       │
└─────────────┬───────────────────────────────┬───────────────┘
              │                               │
    ┌─────────▼──────────┐        ┌──────────▼──────────┐
    │   WebSocket Node1  │◄──────►│   WebSocket Node2   │
    │   (用户A连接在此)   │        │   (用户B连接在此)    │
    └─────────┬──────────┘        └──────────┬──────────┘
              │                               │
              └──────────────┬────────────────┘
                             │
                    ┌────────▼────────┐
                    │   Redis Pub/Sub │
                    │   (消息总线)     │
                    └─────────────────┘

4.1 Redis配置

// RedisConfig.java
@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
    
    @Bean
    public RedisMessageListenerContainer redisContainer(
            RedisConnectionFactory factory,
            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(listenerAdapter, new PatternTopic("chat:channel:*"));
        return container;
    }
    
    @Bean
    public MessageListenerAdapter listenerAdapter(RedisMessageSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "onMessage");
    }
}

4.2 消息发布

// RedisMessagePublisher.java
@Component
public class RedisMessagePublisher {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String CHANNEL = "chat:channel:broadcast";
    
    public void publish(ChatMessage message) {
        String payload = JSON.toJSONString(message);
        redisTemplate.convertAndSend(CHANNEL, payload);
    }
}

4.3 消息订阅

// RedisMessageSubscriber.java
@Component
@Slf4j
public class RedisMessageSubscriber implements MessageListener {
    
    @Autowired
    private SessionManager sessionManager;
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String payload = new String(message.getBody());
        ChatMessage chatMessage = JSON.parseObject(payload, ChatMessage.class);
        
        // 只处理来自其他节点的消息(本地消息已直接发送)
        if (!isLocalMessage(chatMessage)) {
            log.debug("收到Redis广播消息: {}", payload);
            broadcastToLocalSessions(chatMessage);
        }
    }
    
    private void broadcastToLocalSessions(ChatMessage message) {
        String payload = JSON.toJSONString(message);
        sessionManager.getAllSessions().forEach(session -> {
            try {
                session.sendMessage(new TextMessage(payload));
            } catch (IOException e) {
                log.error("发送消息失败", e);
            }
        });
    }
}

五、性能优化:支撑10万并发的秘密

5.1 连接优化

// 调整Tomcat线程池
server.tomcat.max-threads=200
server.tomcat.min-spare-threads=50

// WebSocket容器配置
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
    ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
    container.setMaxTextMessageBufferSize(8192);
    container.setMaxBinaryMessageBufferSize(8192);
    container.setMaxSessionIdleTimeout(600000L);
    // 关键:异步发送超时
    container.setAsyncSendTimeout(5000L);
    return container;
}

5.2 消息压缩

// 启用WebSocket消息压缩
@Configuration
public class WebSocketCompressionConfig {
    
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // 启用压缩
        container.setMaxTextMessageBufferSize(8192);
        return container;
    }
}

5.3 限流保护

// RateLimiter.java
@Component
public class RateLimiter {
    
    private final LoadingCache<String, AtomicInteger> requestCounts = CacheBuilder.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build(new CacheLoader<String, AtomicInteger>() {
            @Override
            public AtomicInteger load(String key) {
                return new AtomicInteger(0);
            }
        });
    
    private static final int MAX_REQUESTS_PER_MINUTE = 60;
    
    public boolean allowRequest(String userId) {
        try {
            int count = requestCounts.get(userId).incrementAndGet();
            return count <= MAX_REQUESTS_PER_MINUTE;
        } catch (ExecutionException e) {
            return false;
        }
    }
}

5.4 JVM调优

# 启动参数
java -Xms4g -Xmx4g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:+UseStringDeduplication \
     -jar chat-server.jar

5.5 系统级优化

# 修改文件描述符限制(Linux)
ulimit -n 65535

# 修改TCP参数
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.netfilter.nf_conntrack_max=1000000

六、源码获取与思考题

源码获取

完整项目代码已开源,包含:

  • Spring Boot WebSocket服务端
  • Redis分布式消息同步
  • 前端HTML客户端
  • Docker部署配置

GitHub地址: https://github.com/example/websocket-chat

思考题

  1. WebSocket连接突然断开,如何保证消息不丢失? (提示:考虑消息确认机制、离线消息存储)

  2. 如果Redis挂了,分布式消息同步怎么降级? (提示:考虑本地缓存、直接RPC调用)

  3. 如何实现"已读回执"功能? (提示:考虑消息状态追踪、批量确认)

系列预告

下一篇我们将深入探讨:《Netty实现百万级WebSocket连接》,包括:

  • Netty线程模型详解
  • 内存池优化
  • 心跳与断线重连
  • 集群架构设计

敬请期待!


总结

本文从WebSocket协议原理出发,对比了三种实时通信方案,完整演示了Spring Boot + WebSocket构建聊天室的全过程,并给出了分布式架构和性能优化的实战经验。

核心要点回顾:

  • WebSocket一次握手,全双工通信
  • 连接管理用ConcurrentHashMap,线程安全
  • 分布式用Redis Pub/Sub做消息总线
  • 10万并发需要JVM、系统、代码三层优化

标签: WebSocket, 实时通信, Spring Boot, Redis, 高并发, 后端开发, 架构设计


如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题欢迎在评论区留言,我会一一回复。

于2024年4月-2025年9月期间,研究团队在贵州习水国家级自然保护区制定39条样线,涵盖灌木林、常绿阔叶林、针叶林、常绿落叶阔叶混交林、针阔混交林等不同植被类型,每条样线分春夏秋冬4个季节采集样品,用真菌采集软件记录经纬度、海拔、采集地点、时间、生境等信息,使用佳能相机(R6 mark Ⅱ)对大型真菌进行拍照,并采集标本,标本存放于贵州省生物研究所大型真菌标本馆(HGAMF)。 通过形态学初步鉴定,结合分子生物学最终鉴定,参考已]报道的中国毒蘑菇名录开展毒蘑菇的认定。 调查到保护区内有毒真菌7目25科64种,导致中毒的主要类型有急性肾衰竭型、神经精神型和胃肠炎型。最终形成贵州习水国家级自然保护区大型有毒真菌图片数据集,它由以下2个部分组成。 (1)附件1包含78张原始照片(.JPG),照片名字包括了大型有毒真菌的拉丁名和中文名,若无中文名的直接用拉丁名。 (2)附件2是一个压缩文件,包含了2张工作表,其中一张表是大型有毒真菌39条样线的信息,另一张表是大型有毒真菌的中毒类型。 照片采用佳能相机R6 mark Ⅱ拍摄,物种鉴定通过种文献核实,并经两位以上专家鉴定确认。该数据集可为研究地及周边的普通人识别有毒大型真菌提供参考,通过及时的图片对比,能有效避免误采误食大型有毒真菌,同时为因误食大型真菌可能引发的身体损伤进行了总结,能为患者及时治疗提供参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weitingfu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值