上一篇 netty(1)

一、TCP/IP 流式传输

  在上文演示了2进制流式传输引起的TCP拆包问题,这里继续演示文本型的传输问题,文本型的可以有以下几种策略

 

1.1 以特殊字符表示结尾

HTTP协议中以\r\n\r\n表示请求首部结束,这里也以\r\n\r\n表示特殊字符,非常容易理解,没有碰到\r\n\r\n就继续写入缓冲,碰到了表明是一个完整的逻辑数据,可以处理了。

Server端代码

public class Server {

    public static final String SERVER_DELIMITER = "\r\n\r\n";

    public static void main(String[] args) throws Exception {
//1 创建2个线程,一个是负责接收客户端的连接。一个是负责进行数据传输的
EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup();
//2 创建服务器辅助类
ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_SNDBUF, 32 * 1024)
.option(ChannelOption.SO_RCVBUF, 32 * 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//设置特殊分隔符
ByteBuf buf = Unpooled.copiedBuffer(SERVER_DELIMITER.getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
//设置字符串形式的解码
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ServerHandler());
}
}); //4 绑定连接
ChannelFuture cf = b.bind(8765).sync(); //等待服务器监听端口关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
} }
public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(" server channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String request = (String)msg;
System.out.println("Server :" + msg);
String response = "服务器响应:" + msg + Server.SERVER_DELIMITER;
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { } @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) throws Exception {
ctx.close();
}
}

客户端代码:

public class Client {

    public static final String CLIENT_DELIMITER = "\r\n\r\n";

    public static void main(String[] args) throws Exception {

        EventLoopGroup group = new NioEventLoopGroup();

        Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//
ByteBuf buf = Unpooled.copiedBuffer(CLIENT_DELIMITER.getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
}); ChannelFuture cf = b.connect("127.0.0.1", 8765).sync();
StringBuffer sb = new StringBuffer("GET " + "/index.jsp" + " HTTP/1.1\r\n");
sb.append("Host: www.javathinker.org\r\n");
sb.append("Accept: */*\r\n");
sb.append("Accept-Language: zh-cn\r\n");
sb.append("Accept-Encoding: gzip, deflate\r\n");
sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n");
sb.append("Connection: Keep-Alive\r\n\r\n");
cf.channel().writeAndFlush(Unpooled.wrappedBuffer(sb.toString().getBytes()));
// Thread.sleep(1000);
// cf.addListener(ChannelFutureListener.CLOSE); //等待客户端端口关闭
cf.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client channel active... ");
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
String response = (String)msg;
System.out.println("Client: " + response);
} finally {
ReferenceCountUtil.release(msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

1.2 以FixedLength的方式

跟上述代码类似,只需要修改下Decoder即可

b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});

二、使用Java对象传输

官网Object demo:http://netty.io/4.1/xref/io/netty/example/objectecho/package-summary.html

使用Java对象传输,只需要配置对应的encoder: Object->byte[] 和对应的decoder: byte[]->Object即可

2.1 使用java searilizable

先写个工具类,提供GZIP压缩功能,以及发送请求和处理请求的通用方法

package netty;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import netty.marshalling.Req;
import netty.marshalling.Resp; import java.io.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; /**
* Created by carl.yu on 2016/11/7.
*/
public class Utils {
public static byte[] gzip(byte[] data) throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(bos);
gzip.write(data);
gzip.finish();
gzip.close();
byte[] ret = bos.toByteArray();
bos.close();
return ret;
} public static byte[] ungzip(byte[] data) throws Exception{
ByteArrayInputStream bis = new ByteArrayInputStream(data);
GZIPInputStream gzip = new GZIPInputStream(bis);
byte[] buf = new byte[1024];
int num = -1;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((num = gzip.read(buf, 0 , buf.length)) != -1 ){
bos.write(buf, 0, num);
}
gzip.close();
bis.close();
byte[] ret = bos.toByteArray();
bos.flush();
bos.close();
return ret;
} public static void send(ChannelFuture cf) throws Exception{
for (int i = 1; i <= 2; i++) {
Req req = new Req();
req.setId("" + i);
req.setName("pro" + i);
req.setRequestMessage("数据信息" + i);
String path = "G:\\projects-helloworld\\lucene\\src\\main\\resources\\in\\" + i + ".jpg";
String fileName = i + ".jpg";
req.setFileName(fileName);
File file = new File(path);
FileInputStream in = new FileInputStream(file);
byte[] data = new byte[in.available()]; in.read(data);
in.close();
req.setAttachment(Utils.gzip(data));
cf.channel().writeAndFlush(req);
}
} public static void recv(ChannelHandlerContext ctx, Object msg) throws Exception{
Req req = (Req) msg;
System.out.println("req:"+req);
System.out.println("Server : " + req.getId() + ", " + req.getName() + ", " + req.getRequestMessage());
byte[] attachment = Utils.ungzip(req.getAttachment());
String fileName = req.getFileName();
String path = "G:\\projects-helloworld\\lucene\\src\\main\\resources\\out\\" + fileName;
FileOutputStream fos = new FileOutputStream(path);
fos.write(attachment);
fos.close(); Resp resp = new Resp();
resp.setId(req.getId());
resp.setName("resp" + req.getId());
resp.setResponseMessage("响应内容" + req.getId());
ctx.writeAndFlush(resp);//.addListener(ChannelFutureListener.CLOSE);
} public static void main(String[] args) throws Exception{ //读取文件
String readPath = System.getProperty("user.dir") + File.separatorChar + "sources" + File.separatorChar + "006.jpg";
File file = new File(readPath);
FileInputStream in = new FileInputStream(file);
byte[] data = new byte[in.available()];
in.read(data);
in.close(); System.out.println("文件原始大小:" + data.length);
//测试压缩 byte[] ret1 = gzip(data);
System.out.println("压缩之后大小:" + ret1.length); byte[] ret2 = ungzip(ret1);
System.out.println("还原之后大小:" + ret2.length); //写出文件
String writePath = System.getProperty("user.dir") + File.separatorChar + "receive" + File.separatorChar + "006.jpg";
FileOutputStream fos = new FileOutputStream(writePath);
fos.write(ret2);
fos.close(); }
}

Req:

import java.io.Serializable;

public class Req implements Serializable {

    private static final long SerialVersionUID = 1L;

    private String id;
private String name;
private String requestMessage;
private byte[] attachment;
private String fileName; public String getId() {
return id;
} public void setId(String id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getRequestMessage() {
return requestMessage;
} public void setRequestMessage(String requestMessage) {
this.requestMessage = requestMessage;
} public byte[] getAttachment() {
return attachment;
} public void setAttachment(byte[] attachment) {
this.attachment = attachment;
} public String getFileName() {
return fileName;
} public void setFileName(String fileName) {
this.fileName = fileName;
} @Override
public String toString() {
return "Req{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", requestMessage='" + requestMessage + '\'' +
", fileName='" + fileName + '\'' +
'}';
}
}

Resp:

import java.io.Serializable;

public class Resp implements Serializable{

    private static final long serialVersionUID = 1L;

    private String id = "1";
private String name ="aaa";
private String responseMessage ="this is demo"; public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
} @Override
public String toString() {
return "Resp{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", responseMessage='" + responseMessage + '\'' +
'}';
}
}

Server端代码:

public class ObjectEchoServer {
static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslCtx = null;
} EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
p.addLast(
new ObjectEncoder(),
new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
new ObjectEchoServerHandler());
}
}); // Bind and start to accept incoming connections.
b.bind(PORT).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class ObjectEchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// Echo back the received object to the client.
Utils.recv(ctx, msg);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

client端代码:

public final class ObjectEchoClient {

    static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
} EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
}
p.addLast(
new ObjectEncoder(),
new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
new ObjectEchoClientHandler());
}
}); // Start the connection attempt.
ChannelFuture cf = b.connect(HOST, PORT).sync();
Utils.send(cf);
cf.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class ObjectEchoClientHandler extends ChannelInboundHandlerAdapter {

    private final List<Integer> firstMessage;

    /**
* Creates a client-side handler.
*/
public ObjectEchoClientHandler() {
firstMessage = new ArrayList<Integer>(ObjectEchoClient.SIZE);
for (int i = 0; i < ObjectEchoClient.SIZE; i++) {
firstMessage.add(Integer.valueOf(i));
}
} @Override
public void channelActive(ChannelHandlerContext ctx) {
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
Resp resp = (Resp)msg;
System.out.println("Client : " + resp.getId() + ", " + resp.getName() + ", " + resp.getResponseMessage());
} finally {
ReferenceCountUtil.release(msg);
}
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

2.2 使用jboss marshalling序列化

        <dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling-serial</artifactId>
<version>2.0.0.Beta2</version>
</dependency>

jboss marshalling自动支持netty的codec

public final class MarshallingCodeCFactory {

    /**
* 创建Jboss Marshalling解码器MarshallingDecoder
* @return MarshallingDecoder
*/
public static MarshallingDecoder buildMarshallingDecoder() {
//首先通过Marshalling工具类的精通方法获取Marshalling实例对象 参数serial标识创建的是java序列化工厂对象。
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
//创建了MarshallingConfiguration对象,配置了版本号为5
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
//根据marshallerFactory和configuration创建provider
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
//构建Netty的MarshallingDecoder对象,俩个参数分别为provider和单个消息序列化后的最大长度
MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024 * 1024 * 1);
return decoder;
} /**
* 创建Jboss Marshalling编码器MarshallingEncoder
* @return MarshallingEncoder
*/
public static MarshallingEncoder buildMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
//构建Netty的MarshallingEncoder对象,MarshallingEncoder用于实现序列化接口的POJO对象序列化为二进制数组
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}

直接在server中配置codec即可,其他都类似

b.group(pGroup, cGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
//设置日志
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ServerHandler());
}
});
        b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
sc.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
sc.pipeline().addLast(new ClientHandler());
}
});

2.3 其他序列化框架

其他序列化框架都类似,只要有对应的encoder和decoder,比如Kryo和Google protocol Buf

三、其他

3.1 read_time_out和write_time_out

  在用netty进行socket通信时,通常也会遇到read time out和write time out的设置问题,netty也是通过handler来实现的,netty默认提供了2个类

// The connection is closed when there is no inbound traffic
// for 30 seconds. public class MyChannelInitializer extends ChannelInitializer<Channel> {
public void initChannel(Channel channel) {
channel.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(30);
channel.pipeline().addLast("myHandler", new MyHandler());
}
} // Handler should handle the ReadTimeoutException.
public class MyHandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
if (cause instanceof ReadTimeoutException) {
// do something
} else {
super.exceptionCaught(ctx, cause);
}
}
} ServerBootstrap bootstrap = ...;
...
bootstrap.childHandler(new MyChannelInitializer());
...

当读超时,可以捕获该异常,也可以丢弃此连接,防止占用服务器资源。

同样的

   // The connection is closed when a write operation cannot finish in 30 seconds.

   public class MyChannelInitializer extends ChannelInitializer<Channel> {
public void initChannel(Channel channel) {
channel.pipeline().addLast("writeTimeoutHandler", new WriteTimeoutHandler(30);
channel.pipeline().addLast("myHandler", new MyHandler());
}
} // Handler should handle the WriteTimeoutException.
public class MyHandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
if (cause instanceof WriteTimeoutException) {
// do something
} else {
super.exceptionCaught(ctx, cause);
}
}
} ServerBootstrap bootstrap = ...;
...
bootstrap.childHandler(new MyChannelInitializer());
...

netty初探(2)的更多相关文章

  1. netty初探(1)

    参考目录: 1. user-guide : http://netty.io/wiki/user-guide-for-4.x.html 2. demo: http://netty.io/wiki/ 3. ...

  2. Netty初探

    匠心零度 转载请注明原创出处,谢谢! 说在前面 为什么我们需要学习netty?谈谈自己的看法,由于本人水平有限,如果有那里不对,希望各位大佬积极指出,欢迎在留言区进行评论交流.探讨. 由于移动互联网的 ...

  3. netty系列之:netty初探

    目录 简介 netty介绍 netty的第一个服务器 netty的第一个客户端 运行服务器和客户端 总结 简介 我们常用浏览器来访问web页面得到相关的信息,通常来说使用的都是HTTP或者HTTPS协 ...

  4. netty中级篇(2)

    上一篇 netty入门篇(1) 一.编码解码技术 如何评价一个编解码技术: 是否支持跨语言,或者说支持的语言是否丰富 编码码流大小,影响传输速度 编码和解码的性能,即时间 类库是否精致,API是否方便 ...

  5. Netty(一):入门篇

    匠心零度 转载请注明原创出处,谢谢! 说在前面 上篇文章对Netty进行了初探:Netty初探,主要介绍了下我们为什么需要学习netty.netty介绍等:本篇文章接着上篇文章的内容.本篇为了方便大家 ...

  6. 图灵,咕泡,鲁班学院--Java高级架构师-互联网企业级实战VIP课程(价值6380)

    课程介绍:        讲课内容涉及Java互联网技术工程框架.应用框架.        性能调优 (Tomcat Nginx JVM)         分布式框架(并发编程 Zookeeper N ...

  7. Netty学习(2):IO模型之NIO初探

    NIO 概述 前面说到 BIO 有着创建线程多,阻塞 CPU 等问题,因此为解决 BIO 的问题,NIO 作为同步非阻塞 IO模型,随 JDK1.4 而出生了. 在前面我们反复说过4个概念:同步.异步 ...

  8. 基于netty框架的Socket传输

    一.Netty框架介绍 什么是netty?先看下百度百科的解释:         Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开 ...

  9. 响应式Spring Cloud初探

    响应式Spring Cloud初探 分类:工程原文链接:The Road to Reactive Spring Cloud作者:  JOSH LONG译者: helloworldtang日期:JUNE ...

随机推荐

  1. T-SQL 随机返回特定行数据和分页查询

    T-SQL 随机返回特定行数据和分页查询 T-SQL 语言相较于标准SQL添加了很多特性,为了提高SQL Server的表现,是有必要深入了解的,面试时一般也会包含这两个小问题. 首先,是在一个Adv ...

  2. javascript eval和JSON之间的关系

    eval函数的工作原理 eval函数会评估一个给定的含有javascript代码的字符串,并且视图去执行包含在字符串里的表达式或者 一系列的合法的javascript语句.eval函数将把最后一个表达 ...

  3. 将 Servlet (HTTP POST/GET)请求发布到OSB

    转载地址:http://user.qzone.qq.com/32221895/blog/1406960844 ------------------------------------- 发帖要200字 ...

  4. bat编程基本知识

    1 声明变量 ::注意=前后不要留空格.随便说一下,在bat中,连续两个冒号表示注释 set var1=test 如果要引用这个变量的话,可以这样写:%var1% 2 echo off/on echo ...

  5. c++字符串连接方法大观

    c++字符串连接方法大观 抛砖引玉: springf(config_itemID[i],"ItemID%s_%d",i,i); 大家说说自己都用什么方法,个有什么利弊呢?

  6. 模拟Vue之数据驱动4

    一.前言 在"模拟Vue之数据驱动3"中,我们实现了为每个对象扩展一个$set方法,用于新增属性使用,这样就可以监听新增的属性了. 当然,数组也是对象,也可以通过$set方法实现新 ...

  7. ASP.NET MVC with Entity Framework and CSS一书翻译系列文章之第五章:排序、分页和路由

    本章的重点是对产品信息增加排序和分页的功能,以及使用ASP.NET Routing特性添加更加友好的URL支持. 注意:如果你想按照本章的代码编写示例,你必须完成第四章或者直接从www.apress. ...

  8. 【Spring】基于注解的实现SpringMVC+MySQL

    目录结构: // contents structure [-] SprinigMVC是什么 SpringMVC工作原理 @Controller和@RequestMapping注解 @Controlle ...

  9. codewars-random(4)

    问题简介:算多长时间用本金能得到想要的利息,基础题. 代码:function calculateYears(principal, interest, tax, desired) {    // you ...

  10. grunt--自动化任务快速上手

    这篇文章将带领你用Grunt来提速和优化网站开发的流程.首先我们会简短介绍Grunt的功能,然后我们直接上手,介绍如何用Grunt的不同插件来替你完成网站项目开发中的很多繁冗工作. 接着我们会创建一个 ...