Netty案例 手动实现Nginx和聊天室支持单聊和群聊,心跳检测,强制下线

手动实现一个Nginx加聊天室的案例

闲来无事?🔥快来拿个小案例练手

简单易懂快速上手
案例不长快快 上车!
上车 🚗GO! GO! GO! 🚗

相信通过这个案例能让你学到更多东西

关注收藏是我最大动力

代码全部粘贴下面了,如果想要一个完整的包可以私信我,我也会把打包的链接留在后面

来介绍一下这个小案例:

1.服务器

  • 实现一个资源通过http映射类似Nginx
    我们的案例都在我们自己实现的服务器上面运行,没有使用Tomcat Nginx Spring等第三方

2.聊天业务

  • 实现单聊
  • 实现群聊
  • 房间人数实时检查
  • 实现离线消息
  • 实现账号冲突强制下线
  • 实现心跳在线检测

在这里插入图片描述


案例简介📄
Netty是Java的一个通信框架,是基于NIO来实现的,有许多的java中间件都是Netty来开发的,Netty的高性能是大家有目共睹,大家都认可的

本案例都是通过Netty来编写的,如果有netty基础可能更好理解一些,如果没有不要慌,如有需要私信我,马上更新Netty基础

这个案例分为两个阶段:

  • 服务器开发
  • 聊天室开发

话不多说开始编写代码


服务器开发

环境准备

先导入pom的配置
在这里插入图片描述
Netty的包,复制下来放到pom文件里就行了

 <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.75.Final</version>
        </dependency>

环境准备就这么多,没错就这么多!都手写的还要这么多框架干嘛


代码编写

服务器后端代码

给大家简单介绍一下原理,非常简单

是通过Netty实现Http协议,然后根据浏览器发来需要的文件地址,我们通过IO流把文件字节码读取出来,然后再通过Http协议发送到浏览器

下面是详细介绍,最后有完整代码

在这里插入图片描述
先写一个想要映射的文件夹的路径
在这里插入图片描述
我想要映射这个文件夹

创建Netty的启动类,并绑定端口

在这里插入图片描述
handler处理器操作http并进行io操作把数据返回给浏览器
在这里插入图片描述

这样我们的服务器后端代码就结束了

来测试
在浏览器里输入我们的地址 localhost:8081
在这里插入图片描述
回车

OHHHHHHHHHHHHHHHHHH!

OHHHHHHHHHHHHHHHHHH!
在这里插入图片描述
我们的页面加载出来了,我写的路径为 / 默认加载index.html

服务器后端的全部代码,简短精悍

    public static void main(String[] args) {

        String baseUrl="C:\\Users\\Alie\\Desktop\\demo";
        //初始化Netty
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        //对http进行解码操作
                        nioSocketChannel.pipeline().addLast(new HttpServerCodec());
                        //对管道添加处理器指定类型为http
                        nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception {
                                System.out.println(httpRequest.uri());
                                //获取请求的url
                                String url=httpRequest.uri();
                                //如果url为/则自动映射index.html文件
                                url=url.equals("/")?"/index.html":url;
                                //添加响应状态码200
                                DefaultFullHttpResponse defaultHttpResponse=new DefaultFullHttpResponse(httpRequest.protocolVersion(), HttpResponseStatus.OK);
                                //创建输入流从文件夹里去读取文件
                                FileInputStream fileInputStream=new FileInputStream(new File(baseUrl+url));
                                //字节数组暂存文件
                                byte[] bytes=new byte[1024];
                                int len=0;
                                //io操作将数据写回浏览器
                                while (-1!=(len=fileInputStream.read(bytes))) {

                                    defaultHttpResponse.content().writeBytes(bytes,0,len);
                                }
                                //关闭资源
                                fileInputStream.close();
                                channelHandlerContext.writeAndFlush(defaultHttpResponse);
                                channelHandlerContext.close();
                            }
                        });
                    }
                    //绑定端口
                }).bind(8081);

    }

聊天室前端代码

由于前端代码比较简单就直接给大家,直接用就好了

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<title>图片提示</title>
	<style type="text/css">
		body {
			margin: 0;
			padding: 40px;
		}
		li {
			list-style: none;
			float: left;
			display: inline;
			margin-right: 10px;
			border: 1px solid #AAAAAA;
		}

		.room {
			float: left;
			width: 100%;
			margin-top: 30px;
			height: 400px;
			border:1px solid black;
		}

		.room span{
			font-size: 14px;
		}

		/* tooltip */
		#tooltip {
			position: absolute;
			border: 1px solid #ccc;
			background: #333;
			padding: 2px;
			display: none;
			color: #fff;
		}
	
</head>

<body>
	<h3>图片提示效果</h3>
	
	
	<div class="room">
		<div class="top">
			定义用户名:<input type="text" id="userid"><input type="button" id="connt" value="立即连接">
		</div>

		<br>

		接收用户:<input type="text" id="toargetid"> <input type="text" id="msgContent">
		<input type="button" id="send" value="发送消息">
		<hr>
		接收消息:
		<div id="receiveMsg"></div>
	</div>
	<div class="room">
		<div class="top">
			房间号:<input type="text" id="roomid"><input type="button" id="jionroom" value="加入房间">
		</div>

		<br>
		<input type="text" id="roommsgContent"><input type="button" id="roomsend" value="发送消息">
		<hr>
		接收消息:
		<div id="roomreceiveMsg"></div>
	</div>
	<div class="video">


		
	</div>
</body>
<script>

	//消息模板
	messageObj = {
		userid: "",
		toTarget: "",
		msg: "",
		msgType: 0,
		singe: "",
	}
	//状态指令代码
	var ChatType = {
		CHANNEL_INIT: 0,
		CHAT_MSG: 1,
		CHAT_ROOM_MSG: 2,
		SYSTEM_MSG: 3,
		JOIN_ROOM: 4,
		PING:5
	}

	//websocket初始化
	function initWS() {
		mes = messageObj
		mes.msgType = ChatType.CHANNEL_INIT
		return JSON.stringify(mes)
	}
	//发送消息函数
	function sendMsg(target, msg) {
		mes = messageObj
		mes.toTarget=target
		mes.msgType=ChatType.CHAT_MSG
		mes.msg=msg
	
		return JSON.stringify(mes)

	}
	//聊天室发送消息函数
	function sendMsgTORoom(target, msg) {
		mes = messageObj
		mes.toTarget=target
		mes.msgType=ChatType.CHAT_ROOM_MSG
		mes.msg=msg
		return JSON.stringify(mes)
	}
	//加入聊天室函数
	function addRoom(target) {
		mes = messageObj
		mes.toTarget=target
		mes.msgType=ChatType.JOIN_ROOM
		return JSON.stringify(mes)
	}
	//心跳检测函数
	function ping() {
		mes = messageObj
		mes.msgType=ChatType.PING
		return JSON.stringify(mes)
	}
	//消息处理函数
	function dealMsg(data) {
		var type=data.msgType
		var rece=document.getElementById("receiveMsg")
		var roomrece=document.getElementById("roomreceiveMsg")
		if(type==ChatType.SYSTEM_MSG){
	
			rece.innerHTML+="<span style='color: rgb(255, 118, 14);' >[系统通知]"+data.msg+"</span><br>"
		}else if(type==ChatType.CHANNEL_INIT){
			roomrece.innerHTML+="<span>["+data.userid+"]:"+data.msg+"</span><br>"
		}else if(type==ChatType.CHAT_ROOM_MSG){
			roomrece.innerHTML+="<span>["+data.userid+"]:"+data.msg+"</span><br>"
		}else if(type==ChatType.CHAT_MSG){
			rece.innerHTML+="<span>["+data.userid+"]:"+data.msg+"</span><br>"
		}else if(type==ChatType.PING){
			websocket.send(ping())
		}
		
	}


	document.getElementById("send").onclick = function () {
		var str = document.getElementById("msgContent").value
		var target=document.getElementById("toargetid").value
		var tr=sendMsg(target,str)
		console.log(tr);
		websocket.send(tr)
	}
	
	document.getElementById("connt").onclick = function () {
		var userid = document.getElementById("userid").value
		messageObj.userid = userid
		InitWebSocket()

	}

	
	document.getElementById("jionroom").onclick = function () {
		var roomid=document.getElementById("roomid").value
		websocket.send(addRoom(roomid))

	}

	document.getElementById("roomsend").onclick = function () {
		var str = document.getElementById("roommsgContent").value
		var target=document.getElementById("roomid").value
		var tr=sendMsgTORoom(target,str)
		websocket.send(tr)
	}

	//websocket
	function InitWebSocket() {
		websocket = new WebSocket("ws://192.168.43.237:8080/ws")
		websocket.onopen = function () {
			websocket.send(initWS())
		}
		websocket.onclose = function () {
			console.log("连接断开");
		}
		websocket.onerror = function () {
			console.log("连接出错");
		}
		websocket.onmessage = function (e) {
			var data=JSON.parse(e.data)
			dealMsg(data)
			console.log(e);
			console.log(data);
		}
	}

</script>

</html>

聊天室后端代码

配置环境文件

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.75.Final</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

我先把代码和结构发出来按照这个结构还原后就可直接运行
在这里插入图片描述
server类


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class Server {
    public static void main(String[] args) throws InterruptedException {
    	//两个任务组
        NioEventLoopGroup group1 = new NioEventLoopGroup();
        NioEventLoopGroup group2 = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .group(group1, group2)
                    .channel(NioServerSocketChannel.class)
                    //初始传入我们自己的定义的ConnInit类
                    .childHandler(new ConnInit());
             //绑定端口为8080
            ChannelFuture channel = serverBootstrap.bind(8080).sync();
            channel.channel().closeFuture().sync();
            
        }finally {
        //优雅关闭server
            group1.shutdownGracefully();
            group2.shutdownGracefully();
        }
    }
}

Conninit类
连接初始化

package myChatRoom;


import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import myChatRoom.pojo.MyMessage;

public class ConnInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        //心跳机制检测假死 10s未读到数据触发 5s未发送触发
        pipeline.addLast(new IdleStateHandler(10,5,0));
        //为心跳机制 到达10s 和 5s 时编写触发函数
        pipeline.addLast(new ChannelDuplexHandler(){
            @Override
            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                IdleStateEvent event=(IdleStateEvent) evt;
                //当5s未写写数据会触发,这时我们可以主动发送数据给前端然后让前端返回数据给我们
                if(event.state()==IdleState.WRITER_IDLE){
                    MyMessage myMessage = new MyMessage();
                    myMessage.setMsgType(ChatType.PING);
                    ctx.channel().writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
                    System.out.println("ping");
                }
                //当10s未读到数据时会触发,当5s后发送给前端,到10s检测没读到数据说明前端没有给我们返回数据,前端可能已经断开,这时可以断开连接
                if(event.state()==IdleState.READER_IDLE){
                    System.out.println("10s未读到数据");
                }
                super.userEventTriggered(ctx, evt);
            }
        });



        //http编解码
        pipeline.addLast(new HttpServerCodec());
        //数据流写支持
        pipeline.addLast(new ChunkedWriteHandler());
        //聚合操作 解决粘包拆包
        pipeline.addLast(new HttpObjectAggregator(1024*64));

        //实现websocket
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        //处理消息加入我们自己的处理器
        pipeline.addLast(new MyChatHandler());

    }
}


ChatType类
指定消息类型

public  class  ChatType{

      public static final int CHANNEL_INIT=0;
      public static final int CHAT_MSG=1;
      public static final int CHAT_ROOM_MSG=2;
      public static final int SYSTEM_MSG=3;
      public static final int JOIN_ROOM=4;
      public static final int PING=5;
}

MyMessage
消息实体类


import com.alibaba.fastjson.JSON;
import lombok.Data;

@Data
public class MyMessage {
	//发送者id
    String userid;
    //接收者
    String toTarget;
    //内容
    String msg;
    //类型
    int msgType;
    
    int singe;

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

MyChatHandler类
我们自己的聊天消息处理类

package myChatRoom;

import com.alibaba.fastjson.JSON;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.server.Impl.ChatRoomHandlerImpl;
import myChatRoom.server.Impl.ConnectHandlerImpl;
import myChatRoom.pojo.MyMessage;
import myChatRoom.server.Impl.MsgHandlerImpl;

public class MyChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    ConnectHandlerImpl connectHandler = new ConnectHandlerImpl();
    ChatRoomHandlerImpl chatRoomHandler=new ChatRoomHandlerImpl();
    MsgHandlerImpl msgHandler=new MsgHandlerImpl();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
    	//获取消息文本
        String text = frame.text();
        System.out.println(text);
        //把文本消息转换为我们的消息类
        MyMessage myMessage = JSON.parseObject(text, MyMessage.class);
        int msgType = myMessage.getMsgType();
		
		//对不同的消息类型来分别进行处理
        if(msgType==ChatType.CHANNEL_INIT){
        	//连接建立初始化
            connectHandler.addUserConnect(myMessage.getUserid(), ctx.channel());
            msgHandler.checkMsg(myMessage.getUserid());
        }else if(msgType==ChatType.CHAT_MSG){
        	//个人消息发送
            msgHandler.send(myMessage);
        }else if (msgType==ChatType.CHAT_ROOM_MSG){
        	//聊天室消息发送
            chatRoomHandler.sendRoomMsg(myMessage.getUserid(), myMessage);
        }else if (msgType==ChatType.JOIN_ROOM){
        	//加入聊天室消息
            chatRoomHandler.addUserInRoom(myMessage.getUserid(), myMessage.getToTarget());
        }else if(msgType==ChatType.SYSTEM_MSG){
        	//系统通知
            System.out.println("系统通知");
        }
    }
	
	//连接断开处理
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
		//移除房间的人数
        chatRoomHandler.leafRoom(connectHandler.getIdbyChannel().get(ctx.channel()));
        //移除通道管理的通道
        connectHandler.removeConnect(ctx.channel());
        System.out.println("断开连接");
    }
}

server接口和实现类
1.ChatRoomHandler和ChatRoomHandlerImpl
聊天室消息处理类

package myChatRoom.server;

import myChatRoom.pojo.MyMessage;

public interface ChatRoomHandler {
    void addUserInRoom(String userid,String roomid);
    
    void sendRoomMsg(String userid, MyMessage message);
    
    void leafRoom(String userid);
    
}

ChatRoomHandlerImpl 实现

package myChatRoom.server.Impl;

import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.server.ChatRoomHandler;
import myChatRoom.pojo.MyMessage;

import java.util.*;

public class ChatRoomHandlerImpl implements ChatRoomHandler {
	//map里存储全部的聊天室 set为聊天室成员
    static Map<String, Set<String>> roomMap=new HashMap<>();
    
    ConnectHandlerImpl connectHandler=new ConnectHandlerImpl();
	//加入聊天室
    @Override
    public void addUserInRoom(String userid, String roomid) {
        if (roomMap.containsKey(roomid)) {
            roomMap.get(roomid).add(userid);
        }else {
            Set<String> list=new HashSet<>();
            list.add(userid);
            roomMap.put(roomid,list);
        }
        MyMessage myMessage = new MyMessage();
        myMessage.setUserid(userid);
        myMessage.setToTarget(roomid);
        myMessage.setMsg("[加入房间] 当前房间在线人数:"+roomMap.get(roomid).size());
        sendRoomMsg(userid,myMessage);
    }
	//发送聊天室消息
    @Override
    public void sendRoomMsg(String userid, MyMessage message) {

        String toTarget = message.getToTarget();
        Set<String> list = roomMap.get(toTarget);
        Map<String, Channel> connects = connectHandler.getConnects();
        for (String s : list) {
            Channel channel = connects.get(s);
            channel.writeAndFlush(new TextWebSocketFrame(message.toString()));
        }
    }
	//离开房间处理,通知聊天室其他人有人离开
    @Override
    public void leafRoom(String userid) {
        roomMap.forEach((k,v)->{
            if (v.contains(userid)) {
                v.remove(userid);
                MyMessage myMessage = new MyMessage();
                myMessage.setUserid(userid);
                myMessage.setToTarget(k);
                myMessage.setMsg(userid+"[离开房间]当前房间人数"+v.size());
                sendRoomMsg(userid,myMessage);
            }
        });
    }

}

2.ConnectionHandler
用户连接处理
ConnectionHandler

package myChatRoom.server;
import io.netty.channel.Channel;
import java.util.Map;
public interface ConnectHandler {

    /**
     * #Description
     * @param userid    用户id
     * @param channel   连接通道
     * @return void
     * @author shuyu
     * #Date 2022/4/14
     */
     void  addUserConnect(String userid, Channel channel);

    /**
     * #Description
     * @param channel   连接通道
     * @return void
     * @author shuyu
     * #Date 2022/4/14
     */
    void removeConnect(Channel channel);

    Map<String,Channel> getConnects();
    Map<Channel,String> getIdbyChannel();

}
package myChatRoom.server.Impl;

import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.pojo.MyMessage;
import myChatRoom.server.ConnectHandler;

import java.util.HashMap;
import java.util.Map;

public class ConnectHandlerImpl implements ConnectHandler {
  static   Map<String,Channel> userMap=new HashMap<>();
  //所有的连接都在此管理
  static   Map<Channel,String> channelMap=new HashMap<>();
  //用户连接添加新连接
    @Override
    public void addUserConnect(String userid, Channel channel) {
        MyMessage myMessage = new MyMessage();
        myMessage.setMsgType(3);
        //如果此用户已经登录了就把此用于的通道关闭强制下线,新用户接入连接
        if(userMap.containsKey(userid)){
            Channel channel1 = userMap.get(userid);
            myMessage.setMsg("账号异地登录强制下线!");

            channel1.writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
            channel1.close();
            channelMap.remove(channel1);
        }
        userMap.put(userid,channel);
        channelMap.put(channel,userid);
        myMessage.setMsg("欢迎用户:"+userid);
        channel.writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
    }

	//移除连接
    @Override
    public void removeConnect(Channel channel) {
        userMap.remove(channelMap.get(channel));
        channelMap.remove(channel);
    }

    @Override
    public Map<String,Channel> getConnects() {
        System.out.println(userMap);
        return userMap;
    }

    @Override
    public Map<Channel,String> getIdbyChannel() {
        return channelMap;
    }
}

3.MsgHandler
好友聊天消息处理
MsgHandler

package myChatRoom.server;

import myChatRoom.pojo.MyMessage;

public interface MsgHandler {
    /**
     * #Description 消息发送
     * @param message   消息体
     * @return void
     * @author shuyu
     * #Date 2022/4/14
     */
    void send(MyMessage message);
}

MsgHandlerImpl实现类

package myChatRoom.server.Impl;

import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import myChatRoom.server.MsgHandler;
import myChatRoom.pojo.MyMessage;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MsgHandlerImpl implements MsgHandler {
    ConnectHandlerImpl connectHandler=new ConnectHandlerImpl();
    Map<String, Channel> connects = connectHandler.getConnects();
    //如果有消息但是好友没有在线会在此暂存
   public static Map<String, List<MyMessage>> messageQeueu=new HashMap<>();
   
    @Override
    public void send(MyMessage message) {
        String toTarget = message.getToTarget();
        Channel channel = connects.get(toTarget);
        if (channel!=null){
            channel.writeAndFlush(new TextWebSocketFrame(message.toString()));
        }else {
            if (messageQeueu.containsKey(message.getToTarget())) {
                messageQeueu.get(message.getToTarget()).add(message);
            }else {
                List<MyMessage> list = new ArrayList<>();
                list.add(message);
                messageQeueu.put(message.getToTarget(),list);
            }
        }
        System.out.println(message.getToTarget()+":"+channel);
    }
	
	//检查函数如果发现map有消息并且好友在线发送消息
    public void checkMsg(String userid){
        List<MyMessage> myMessages = messageQeueu.get(userid);
        if(myMessages!=null){
            Channel channel = connects.get(userid);
            System.out.println(myMessages);
            System.out.println(channel);
            for (MyMessage myMessage : myMessages) {
                channel.writeAndFlush(new TextWebSocketFrame(myMessage.toString()));
            }
            messageQeueu.remove(userid);
        }

    }
}

至此我们聊天的代码也结束了

效果测试

开始测试
输入我们的地址 localhost:8081
1.连接两个用户
在这里插入图片描述
成功!
2. 1,2用户互发消息
在这里插入图片描述
成功!
3.账号顶替下线
在这里插入图片描述
成功!
4.离线消息
此时右边为离线状态,让左边发消息给1,然后再登录1
登录1后立即受到消息
在这里插入图片描述
成功!

5,聊天室聊天

在这里插入图片描述
成功!

6.心跳检测
在这里插入图片描述
这些全部都是心跳包
也是成功了

案例到此结束

希望可以帮助到你

如果有什么问题和疑问可以私信我,我一定尽力解答

如果感觉还不错欢迎 收藏 和 关注!
后面我会持续更新

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Alie鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值