消息实体类


public class WsMessage {/** 消息id */private String id;/** 消息发送类型 */private Integer code;/** 发送人用户id */private String sendUserId;/** 发送人用户名 */private String username;/** 接收人用户id,多个逗号分隔 */private String receiverUserId;/** 发送时间 */private Date sendTime;/** 消息类型 */private Integer type;/** 消息内容 */private String msg;/** 消息扩展内容 */private Map<String, Object> ext;@Overridepublic String toString() {return "WsMessage [id=" + id + ", code=" + code + ", sendUserId=" + sendUserId + ", username=" + username+ ", receiverUserId=" + receiverUserId + ", sendTime=" + sendTime + ", type=" + type + ", msg=" + msg+ ", ext=" + ext + "]";}public String getId() {return id;}public void setId(String id) {this.id = id;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getSendUserId() {return sendUserId;}public void setSendUserId(String sendUserId) {this.sendUserId = sendUserId;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getReceiverUserId() {return receiverUserId;}public void setReceiverUserId(String receiverUserId) {this.receiverUserId = receiverUserId;}public Date getSendTime() {return sendTime;}public void setSendTime(Date sendTime) {this.sendTime = sendTime;}public Integer getType() {return type;}public void setType(Integer type) {this.type = type;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Map<String, Object> getExt() {return ext;}public void setExt(Map<String, Object> ext) {this.ext = ext;}}

消息常量类

public class MessageCodeConstant {/*** 私聊*/public static final int PRIVATE_CHAT_CODE = 1;/*** 群聊*/public static final int GROUP_CHAT_CODE = 2;/*** pong 信息*/public static final int PONG_CHAT_CODE = 4;/*** ping 信息*/public static final int PING_MESSAGE_CODE = 3;/*** 系统消息*/public static final int SYSTEM_MESSAGE_CODE = 5;}

/*** 消息类型* @author Administrator**/
public class MessageTypeConstant {/*** 普通系统消息*/public static final int NORMAL_SYSTEM_MESSGAE = 1;/*** 更新在线用户数*/public static final int UPDATE_USERCOUNT_SYSTEM_MESSGAE = 2;/*** 更新在线用户列表*/public static final int UPDATE_USERLIST_SYSTEM_MESSGAE = 3;/*** 个人系统消息*/public static final int PERSONAL_SYSTEM_MESSGAE = 4;}
public class WebSocketConstant {public static final int WEB_SOCKET_PORT = 7979;public static final String WEB_SOCKET_IP = "127.0.0.1";public static final String WEB_SOCKET_URL = "ws://" + WEB_SOCKET_IP + ":"+ WEB_SOCKET_PORT +"/websocket";}

会话管理

public class SessionHolder {/*** 存储每个客户端接入进来时的 channel 对象* 主要用于使用 writeAndFlush 方法广播信息* GlobalEventExecutor:单线程单例EventExecutor。它自动启动线程,并在1秒内任务队列中没有挂起的任务时停止线程。请注意,将大量任务调度到此执行程序是不可扩展的;使用专用的执行程序。*/public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);/*** 用于客户端和服务端握手时存储用户id和netty Channel对应关系*/public static Map<String, Channel> channelMap = new ConcurrentHashMap<String, Channel>(); }

工具类


import io.netty.channel.Channel;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;public class NettyAttrUtil {private static final AttributeKey<String> ATTR_KEY_LAST_HEARTBEAT_TIME = AttributeKey.valueOf("lastHeartBeatTime");private static final AttributeKey<String> ATTR_KEY_USER_ID = AttributeKey.valueOf("userId");/*** 刷新心跳时间* @param channel*/public static void refreshLastHeartBeatTime(Channel channel) {Long now = System.currentTimeMillis();channel.attr(ATTR_KEY_LAST_HEARTBEAT_TIME).set(now.toString());}/*** 获取最后一次心跳时间* @param channel* @return*/public static Long getLastHeartBeatTime(Channel channel) {String value = getAttribute(channel, ATTR_KEY_LAST_HEARTBEAT_TIME);if (value != null) {return Long.valueOf(value);}return null;}public static void setUserId(Channel channel, String value) {channel.attr(ATTR_KEY_USER_ID).set(value);}public static String getUserId(Channel channel) {String value = getAttribute(channel, ATTR_KEY_USER_ID);return value;}private static String getAttribute(Channel channel, AttributeKey<String> key) {Attribute<String> attr = channel.attr(key);return attr.get();}
}

处理请求参数工具类:


public class RequestParamUtil {/*** 解析出url参数中的键值对* 如 "index.jsp?Action=del&id=123",解析出Action:del,id:123存入map中* @param URL  url地址* @return  url请求参数部分* @author lzf*/public static Map<String, String> urlSplit(String URL){Map<String, String> mapRequest = new HashMap<String, String>();String[] arrSplit = null;String strUrlParam = truncateUrlPage(URL);if(strUrlParam == null){return mapRequest;}arrSplit = strUrlParam.split("[&]");for(String strSplit : arrSplit){String[] arrSplitEqual = null;arrSplitEqual = strSplit.split("[=]");//解析出键值if(arrSplitEqual.length > 1){//正确解析mapRequest.put(arrSplitEqual[0], arrSplitEqual[1]);}else{if(arrSplitEqual[0] != ""){//只有参数没有值,不加入mapRequest.put(arrSplitEqual[0], "");}}}return mapRequest;}/*** 去掉url中的路径,留下请求参数部分* @param strURL url地址* @return url请求参数部分* @author lzf*/private static String truncateUrlPage(String strURL){String strAllParam=null;String[] arrSplit=null;
//        strURL=strURL.trim().toLowerCase();strURL=strURL.trim();arrSplit=strURL.split("[?]");if(strURL.length()>1){if(arrSplit.length>1){for (int i=1;i<arrSplit.length;i++){strAllParam = arrSplit[i];}}}return strAllParam;}
}

服务类

import com.alibaba.fastjson.JSONObject;
import com.cola.chat_server.constant.MessageCodeConstant;
import com.cola.chat_server.model.WsMessage;
import com.cola.chat_server.util.NettyAttrUtil;
import com.cola.chat_server.util.SessionHolder;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.Map;
public class WebSocketInfoService {private static final Logger logger = LoggerFactory.getLogger(WebSocketInfoService.class);/*** 清除会话信息* @param channel*/public void clearSession(Channel channel) {String userId = NettyAttrUtil.getUserId(channel);// 清除会话信息SessionHolder.channelGroup.remove(channel);SessionHolder.channelMap.remove(userId);}/*** 广播 ping 信息*/public void sendPing() {WsMessage webSocketMessage = new WsMessage();webSocketMessage.setCode(MessageCodeConstant.PING_MESSAGE_CODE);String message = JSONObject.toJSONString(webSocketMessage);TextWebSocketFrame tws = new TextWebSocketFrame(message);SessionHolder.channelGroup.writeAndFlush(tws);}/*** 从缓存中移除Channel,并且关闭Channel*/public void scanNotActiveChannel() {Map<String, Channel> channelMap = SessionHolder.channelMap;// 如果这个直播下已经没有连接中的用户会话了,删除频道if (channelMap.size() == 0) {return;}for (Channel channel : channelMap.values()) {long lastHeartBeatTime = NettyAttrUtil.getLastHeartBeatTime(channel);long intervalMillis = (System.currentTimeMillis() - lastHeartBeatTime);if (!channel.isOpen()|| !channel.isActive()|| intervalMillis > 90000L) {channelMap.remove(channel);SessionHolder.channelGroup.remove(channel);if (channel.isOpen() || channel.isActive()) {channel.close();}}}}}

ChannelHandler处理器类


import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import com.alibaba.fastjson.JSONObject;
import com.cola.chat_server.constant.MessageCodeConstant;
import com.cola.chat_server.constant.MessageTypeConstant;
import com.cola.chat_server.constant.WebSocketConstant;
import com.cola.chat_server.model.WsMessage;
import com.cola.chat_server.service.WebSocketInfoService;
import com.cola.chat_server.util.DateUtils;
import com.cola.chat_server.util.NettyAttrUtil;
import com.cola.chat_server.util.RequestParamUtil;
import com.cola.chat_server.util.SessionHolder;import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;/*** Netty ChannelHandler,用来处理客户端和服务端的会话生命周期事件(握手、建立连接、断开连接、收消息等)* @Author * @Description 接收请求,接收 WebSocket 信息的控制类*/
public class WebSocketSimpleChannelInboundHandler extends SimpleChannelInboundHandler<Object> {private static final Logger logger = LoggerFactory.getLogger(WebSocketSimpleChannelInboundHandler.class);// WebSocket 握手工厂类private WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(WebSocketConstant.WEB_SOCKET_URL, null, false);private WebSocketServerHandshaker handshaker;private WebSocketInfoService websocketInfoService = new WebSocketInfoService();/*** 处理客户端与服务端之间的 websocket 业务*/private void handWebsocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {//判断是否是关闭 websocket 的指令if (frame instanceof CloseWebSocketFrame) {//关闭握手handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());websocketInfoService.clearSession(ctx.channel());return;}//判断是否是ping消息if (frame instanceof PingWebSocketFrame) {ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));return;}// 判断是否Pong消息if (frame instanceof PongWebSocketFrame) {ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));return;}//判断是否是二进制消息,如果是二进制消息,抛出异常if (!(frame instanceof TextWebSocketFrame)) {System.out.println("目前我们不支持二进制消息");ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));throw new RuntimeException("【" + this.getClass().getName() + "】不支持消息");}// 获取并解析客户端向服务端发送的 json 消息String message = ((TextWebSocketFrame) frame).text();logger.info("消息:{}", message);JSONObject json = JSONObject.parseObject(message);try {String uuid = UUID.randomUUID().toString();String time = DateUtils.date2String(new Date(), "yyyy-MM-dd HH:mm:ss");json.put("id", uuid);json.put("sendTime", time);int code = json.getIntValue("code");switch (code) {//群聊case MessageCodeConstant.GROUP_CHAT_CODE://向连接上来的客户端广播消息SessionHolder.channelGroup.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(json)));break;//私聊case MessageCodeConstant.PRIVATE_CHAT_CODE://接收人idString receiveUserId = json.getString("receiverUserId");String sendUserId = json.getString("sendUserId");String msg = JSONObject.toJSONString(json);// 点对点挨个给接收人发送消息for (Map.Entry<String, Channel> entry : SessionHolder.channelMap.entrySet()) {String userId = entry.getKey();Channel channel = entry.getValue();if (receiveUserId.equals(userId)) {channel.writeAndFlush(new TextWebSocketFrame(msg));}}// 如果发给别人,给自己也发一条if (!receiveUserId.equals(sendUserId)) {SessionHolder.channelMap.get(sendUserId).writeAndFlush(new TextWebSocketFrame(msg));}break;case MessageCodeConstant.SYSTEM_MESSAGE_CODE://向连接上来的客户端广播消息SessionHolder.channelGroup.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(json)));break;//pongcase MessageCodeConstant.PONG_CHAT_CODE:Channel channel = ctx.channel();// 更新心跳时间NettyAttrUtil.refreshLastHeartBeatTime(channel);default:}} catch(Exception e) {logger.error("转发消息异常:", e);e.printStackTrace();}}/*** 客户端与服务端创建连接的时候调用*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//创建新的 WebSocket 连接,保存当前 channellogger.info("————客户端与服务端连接开启————");
//        // 设置高水位
//        ctx.channel().config().setWriteBufferHighWaterMark();
//        // 设置低水位
//        ctx.channel().config().setWriteBufferLowWaterMark();}/*** 客户端与服务端断开连接的时候调用*/@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {logger.info("————客户端与服务端连接断开————");websocketInfoService.clearSession(ctx.channel());}/*** 服务端接收客户端发送过来的数据结束之后调用*/@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.flush();}/*** 工程出现异常的时候调用*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {logger.error("异常:", cause);ctx.close();}/*** 服务端处理客户端websocket请求的核心方法*/@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {if (o instanceof FullHttpRequest) {//处理客户端向服务端发起 http 请求的业务handHttpRequest(channelHandlerContext, (FullHttpRequest) o);} else if (o instanceof WebSocketFrame) {//处理客户端与服务端之间的 websocket 业务handWebsocketFrame(channelHandlerContext, (WebSocketFrame) o);}}/*** 处理客户端向服务端发起 http 握手请求的业务* WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。** WebSocket 连接过程:* 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;* 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;* 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。*/private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {// 如果请求失败或者该请求不是客户端向服务端发起的 http 请求,则响应错误信息if (!request.decoderResult().isSuccess()|| !("websocket".equals(request.headers().get("Upgrade")))) {// code :400sendHttpResponse(ctx, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));return;}//新建一个握手handshaker = factory.newHandshaker(request);if (handshaker == null) {//如果为空,返回响应:不受支持的 websocket 版本WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());} else {//否则,执行握手Map<String, String> params = RequestParamUtil.urlSplit(request.uri());String userId = params.get("userId");Channel channel = ctx.channel();NettyAttrUtil.setUserId(channel, userId);NettyAttrUtil.refreshLastHeartBeatTime(channel);handshaker.handshake(ctx.channel(), request);SessionHolder.channelGroup.add(ctx.channel());SessionHolder.channelMap.put(userId, ctx.channel());logger.info("握手成功,客户端请求uri:{}", request.uri());// 推送用户上线消息,更新客户端在线用户列表Set<String> userList = SessionHolder.channelMap.keySet();WsMessage msg = new WsMessage();Map<String, Object> ext = new HashMap<String, Object>();ext.put("userList", userList);msg.setExt(ext);msg.setCode(MessageCodeConstant.SYSTEM_MESSAGE_CODE);msg.setType(MessageTypeConstant.UPDATE_USERLIST_SYSTEM_MESSGAE);SessionHolder.channelGroup.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(msg)));}}/*** 服务端向客户端响应消息*/private void sendHttpResponse(ChannelHandlerContext ctx, DefaultFullHttpResponse response) {if (response.status().code() != 200) {//创建源缓冲区ByteBuf byteBuf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);//将源缓冲区的数据传送到此缓冲区response.content().writeBytes(byteBuf);//释放源缓冲区byteBuf.release();}//写入请求,服务端向客户端发送数据ChannelFuture channelFuture = ctx.channel().writeAndFlush(response);if (response.status().code() != 200) {/*** 如果请求失败,关闭 ChannelFuture* ChannelFutureListener.CLOSE 源码:future.channel().close();*/channelFuture.addListener(ChannelFutureListener.CLOSE);}}
}

channel初始化

public class WebSocketChanneInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {//请求解码器 HttpServerCodec是netty针对http编解码的处理类,但是这些只能处理像httpget的请求// ,也就是数据带在url问号后面的http请求socketChannel.pipeline().addLast("http-codec", new HttpServerCodec());//将多个消息转换成单一的消息对象//当我们用POST方式请求服务器的时候,对应的参数信息是保存在message body中的,// 如果只是单纯的用HttpServerCodec是无法完全的解析Http POST请求的,// 因为HttpServerCodec只能获取uri中参数,所以需要加上HttpObjectAggregator,HttpObjectAggregator这个netty的处理器就是为了解决这个问题而来的.它把HttpMessage和HttpContent聚合成为一个FullHttpRquest或者FullHttpRsponsesocketChannel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));//支持异步发送大的码流,一般用于发送文件流socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());//处理 websocket 和处理消息的发送socketChannel.pipeline().addLast("handler", new WebSocketSimpleChannelInboundHandler());}
}

服务启动类

/*** 服务启动类* @author Administrator**/
public class ApplicationMain
{public static void main( String[] args ){startNettyMsgServer();}private static void startNettyMsgServer() {// 使用多Reactor多线程模型,EventLoopGroup相当于线程池,内部维护一个或多个线程(EventLoop),每个EventLoop可处理多个Channel(单线程处理多个IO任务)// 创建主线程组EventLoopGroup,专门负责建立连接EventLoopGroup bossGroup = new NioEventLoopGroup(1);// 创建子线程组,专门负责IO任务的处理EventLoopGroup workGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workGroup);b.channel(NioServerSocketChannel.class);b.childHandler(new WebSocketChanneInitializer());System.out.println("服务端开启等待客户端连接....");Channel ch = b.bind(WebSocketConstant.WEB_SOCKET_PORT).sync().channel();//创建一个定长线程池,支持定时及周期性任务执行ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);WebSocketInfoService webSocketInfoService = new WebSocketInfoService();//定时任务:扫描所有的Channel,关闭失效的ChannelexecutorService.scheduleAtFixedRate(webSocketInfoService::scanNotActiveChannel,3, 60, TimeUnit.SECONDS);//定时任务:向所有客户端发送Ping消息executorService.scheduleAtFixedRate(webSocketInfoService::sendPing,3, 50, TimeUnit.SECONDS);ch.closeFuture().sync();} catch (Exception e) {e.printStackTrace();} finally {//            //退出程序bossGroup.shutdownGracefully();workGroup.shutdownGracefully();}}
}

netty自定义handler分别支持群聊和单聊相关推荐

  1. Flask+geventwebsocket实现群聊与单聊功能

    Flask+WebSocket 实现群聊与单聊功能 群聊 py文件 from flask import Flask ,request,render_template from geventwebsoc ...

  2. websocket 群聊和单聊实现简单在线客服

    根据菜鸟教程上的解释: WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间 ...

  3. 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

    本文原题为"一套高可用群聊消息系统实现",由作者"于雨氏"授权即时通讯网整理和发布,内容有些许改动,作者博客地址:alexstocks.github.io.应作 ...

  4. 利用websocket实现群聊以及单聊

    利用websocket实现群聊以及单聊 项目结构 实现代码 运行截图 在这里提供一下思路,正常情况下我们登陆进去之后就应该打开一个ws连接,以便和服务器进行通信,将打开的管道用一个set容器进行存储, ...

  5. 记录一下对接腾讯云IM的部分接口(群聊、单聊)

    对接腾讯IM接口 不得不说腾讯IM的接口文档写的不尽人意,没对接过的就是个坑他那个UserId是给我这种没对接过的小白留个大坑; 上面是腾讯IM文档上的导入账号(坑) Identifier 就是我们自 ...

  6. im即时通讯开发:高可用、易伸缩、高并发的IM群聊、单聊架构方案设计

    要实现一整套能用于大用户量.高并发场景下的IM群聊,技术难度远超IM系统中的其它功能,原因在于:IM群聊消息的实时写扩散特性带来了一系列技术难题. 举个例子:如一个2000人群里,一条普通消息的发出问 ...

  7. 群聊比单聊,为什么复杂这么多?

    群聊是多人社交的基本诉求,一个群友在群内发了一条消息,期望做到: (1)在线的群友能第一时间收到消息: (2)离线的群友能在登陆后收到消息: 群消息的实时性.可达性.离线消息的复杂度,要远高于单对单消 ...

  8. C++ 实现聊天室(群聊、单聊、文件传送)

    文章目录 前言 一.前置知识点 1.相关文章 2.WTL的基本使用流程 二.项目下载与文件介绍 三.服务器代码讲解 1.NetPacket类 2.dealCli函数 3.NoticeOtherUser ...

  9. Android 融云单聊与群聊消息免打扰功能设置与消息删除功能实现

    一.设置群聊与单聊消息免打扰功能: 1.下面直接进入逻辑代码: 实现监听事件: /*** 设置会话列表界面操作的监听器.*/RongIM.setConversationListBehaviorList ...

最新文章

  1. Linux下编译安装Boost1.66
  2. Ubuntu 进入、退出命令行的快捷键
  3. 天问一号火星探测器已飞离地球800多万公里 多个载荷完成自检
  4. python爬取音乐神器_Python爬虫提取神器,正则表达式(re模块),全程干货!
  5. 中南大学计算机学院羽毛球赛,“羽你同行”交通院第六届师生羽毛球赛圆满举办...
  6. (企业案例)使用Nacos持久化规则,改造sentinel-dashboard
  7. 为什么大公司都不用mfc和qt_百度竞价推广效果下降,为什么有的老板还是只愿意做百度推广?...
  8. 安装varnish-dashboard
  9. centos下 安装jdk
  10. MongoDB学习day10--数据库导入导出
  11. Javascript第六章prototype原型向构造器中添加属性,实现构造器共享,节约空间第五课
  12. Liferay的架构:缓存(第一部分)
  13. vue中面包屑的实现方法
  14. 入门必备小游戏之炸金花
  15. c语言进程伪装,易语言程序伪装软件
  16. 基于TI AM5728 + Artix-7 FPGA开发板(DSP+ARM) 5G通信测试手册
  17. 如何同时登陆多个微信账号
  18. 推荐几款好用的思维导图软件
  19. VOC2012数据集的探索性数据分析(EDA)
  20. rust建造一键升级_rust一键升级指令 | 手游网游页游攻略大全

热门文章

  1. pixhawk(认真的坎坷...人生路)
  2. 微信小程序上线了一款“小协议”的区块链应用小程序
  3. QQ2013协议分析(二)TCPF
  4. 在亚马逊正确合理的跟卖
  5. NAS与EOS只差了半个李笑来
  6. 使用freemarker模板生成带图片的word--html格式
  7. 基于Junit的HttpUnit测试
  8. Echart常见渐变和圆角用法(曲线、柱状图)
  9. 爱奇艺新财报:转机频频
  10. YOLOv4目标检测实战:中国交通标志识别