netty對多協議進行編解碼

来源:https://www.cnblogs.com/quliang/archive/2023/02/04/17087419.html
-Advertisement-
Play Games

1、netty如何解析多協議 前提: 項目地址:https://gitee.com/q529075990qqcom/NB-IOT.git 我們需要一個創建mavne項目,這個項目是我已經寫好的項目,項目結構圖如下: 創建公共模塊 創建子模塊,準備好依賴Netty4.1版本 <dependencies ...


1、netty如何解析多協議

前提:

項目地址:https://gitee.com/q529075990qqcom/NB-IOT.git

我們需要一個創建mavne項目,這個項目是我已經寫好的項目,項目結構圖如下:

 

 

 

創建公共模塊

創建子模塊,準備好依賴Netty4.1版本

<dependencies>
    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.72.Final</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.28</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.28</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.esotericsoftware</groupId>
      <artifactId>kryo</artifactId>
      <version>5.3.0</version>
    </dependency>
  </dependencies>
maven依賴

 

序列化的定義是:將一個對象編碼成一個位元組流(I/O);而與之相反的操作被稱為反序列化。

package serializer;

/**
 * @description:
 * @author: quliang
 * @create: 2022-10-20 15:16
 **/
public interface Serializer {
    /**
     * 序列化
     *
     * @param obj
     * @return
     * @throws Exception
     */
    byte[] serialize(Object obj) throws Exception;

    /**
     * 反序列化
     *
     * @param bytes
     * @param clazz
     * @param <T>
     * @return
     * @throws Exception
     */
    <T> T deserialize(byte[] bytes, Class<T> clazz) throws Exception;

}
自定義序列化介面
package serializer;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * @description:
 * @author: quliang
 * @create: 2022-10-20 15:18
 **/

public class KryoSerializer implements Serializer {
    private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.setReferences(true);
        kryo.setRegistrationRequired(false);
        ((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
                .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
        return kryo;
    });

    @Override
    public byte[] serialize(Object obj) throws Exception {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            Output output = new Output(baos);
            Kryo kryo = kryoThreadLocal.get();
            kryo.writeObject(output, obj);
            kryoThreadLocal.remove();
            return output.toBytes();
        } catch (IOException e) {
            throw new Exception("序列化失敗", e);
        }
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) throws Exception {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) {
            Input input = new Input(bais);
            Kryo kryo = kryoThreadLocal.get();
            Object obj = kryo.readObject(input, clazz);
            kryoThreadLocal.remove();
            return clazz.cast(obj);
        } catch (IOException e) {
            throw new Exception("反序化失敗");
        }
    }
}
Kryo實現序列化介面

 

我們需要解析兩種協議,那我們就要提前定義好兩種協議,分別是消息協議、登錄協議

 

消息協議相關

package protocol.msg;

import lombok.Data;
import lombok.Getter;

/**
 * @description: 消息協議: |magic|version|data|
 * @author: quliang
 * @create: 2022-12-10 20:46
 **/
@Data
public class MsgProtocol {
    @Getter
    private byte magic=0;
    @Getter
    private byte version=1;
}
消息協議基類
package protocol.msg.request;

import lombok.Data;
import protocol.msg.MsgProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:58
 **/
@Data
public class MsgRequest extends MsgProtocol {
    private String msg;
}
消息請求子類
package protocol.msg.response;

import lombok.Data;
import protocol.msg.MsgProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:41
 **/
@Data
public class MsgResponse extends MsgProtocol {
    private int statCode;
}
消息響應子類
package encoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import protocol.msg.MsgProtocol;
import serializer.KryoSerializer;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:53
 **/

public class MsgEncoder extends MessageToByteEncoder<MsgProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, MsgProtocol msgProtocol, ByteBuf in) throws Exception {
        in.writeByte(msgProtocol.getMagic());
//        in.writeByte(msgProtocol.code());
        in.writeByte(msgProtocol.getVersion());
        byte[] data = new KryoSerializer().serialize(msgProtocol);
        in.writeShort(data.length);
        in.writeBytes(data);
    }
}
消息協議編碼
package decoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.MsgProtocol;
import serializer.KryoSerializer;

import java.util.List;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-10 20:52
 **/
@Slf4j
public class MsgDecoder extends ByteToMessageDecoder {

    private Class<MsgProtocol> msgClass;

    public MsgDecoder(Class clazz) {
        this.msgClass = clazz;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        try {
            byte magic = in.readByte();
            byte version = in.readByte();

            short dataSize = in.readShort();
            byte[] data = new byte[dataSize];
            in.readBytes(data);

            MsgProtocol baseProtocol = new KryoSerializer().deserialize(data, msgClass);
            out.add(baseProtocol);
        } catch (Exception e) {
            //如果解碼錯誤,將數據傳遞到下一個解碼器中
            log.error("msg decoder {}",e.getMessage());
            // 重置讀取位元組索引,因為上邊已經讀了(readBytes),不加這個會導致數據為空
            in.resetReaderIndex();
            // 這裡是複製流,複製一份,防止skipBytes跳過,導致傳遞的消息變成空;
            ByteBuf buff = in.retainedDuplicate();
            //原因是netty不允許有位元組內容不讀的情況發生,所以採用下邊的方法解決。
            in.skipBytes(in.readableBytes());
            //繼續傳遞到下一個解碼器中
            out.add(buff);
        }
    }
}
消息協議解碼

 

登錄協議相關

package protocol.system;



import lombok.Getter;

/**
 * @description: 登錄協議: |magic|version|code|data|
 * @author: quliang
 * @create: 2022-12-09 18:10
 **/

public class LoginProtocol {
    @Getter
    private byte magic=0;
    @Getter
    private byte version=1;
    @Getter
    public byte code;
}
登錄協議基類
package protocol.system.request;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import protocol.system.LoginProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 18:17
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest extends LoginProtocol {

    private String userId;

    private String userName;
}
登錄請求子類
package protocol.system.response;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import protocol.system.LoginProtocol;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 18:22
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse extends LoginProtocol {

    private String msg;

    private String data;

}
登錄響應子類
package encoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import protocol.system.LoginProtocol;
import serializer.KryoSerializer;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 22:11
 **/

public class LoginEncoder extends MessageToByteEncoder<LoginProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, LoginProtocol baseProtocol, ByteBuf in) throws Exception {
        in.writeByte(baseProtocol.getMagic());
        in.writeByte(baseProtocol.getCode());
        in.writeByte(baseProtocol.getVersion());
        byte[] data = new KryoSerializer().serialize(baseProtocol);
        in.writeShort(data.length);
        in.writeBytes(data);
    }
}
登錄協議編碼
package decoder;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import protocol.system.LoginProtocol;
import serializer.KryoSerializer;
import java.util.List;

/**
 * @description:
 * @author: quliang
 * @create: 2022-12-06 17:59
 **/
@Slf4j
public class LoginDecoder extends ByteToMessageDecoder {

    private Class<LoginProtocol> clazz;

    public LoginDecoder(Class clazz) {
        this.clazz = clazz;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        try {
            byte magic = in.readByte();
            byte code = in.readByte();
            byte version = in.readByte();

            short dataSize = in.readShort();
            byte[] data = new byte[dataSize];
            in.readBytes(data);

            LoginProtocol baseProtocol = new KryoSerializer().deserialize(data, clazz);
            out.add(baseProtocol);
        } catch (Exception e) {
            //如果解碼錯誤,將數據傳遞到下一個解碼器中
            log.error("login decoder {}", e.getMessage());
            // 重置讀取位元組索引,因為上邊已經讀了(readBytes),不加這個會導致數據為空
            in.resetReaderIndex();
            // 這裡是複製流,複製一份,防止skipBytes跳過,導致傳遞的消息變成空;
            ByteBuf buff = in.retainedDuplicate();
            //原因是netty不允許有位元組內容不讀的情況發生,所以採用下邊的方法解決。
            in.skipBytes(in.readableBytes());
            //繼續傳遞到下一個解碼器中
            out.add(buff);
        }
    }
}
登錄協議解碼

 這樣公共模塊就創建完成了

創建服務端

package com.ql;

import com.ql.handler.MsgHandler;
import decoder.LoginDecoder;
import decoder.MsgDecoder;
import com.ql.handler.LoginHandler;
import encoder.LoginEncoder;
import encoder.MsgEncoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.system.request.LoginRequest;
import protocol.msg.request.MsgRequest;

/**
 * @author quliang
 * @description 服務端
 * @date 2022-12-06 17:39:14
 */
@Slf4j
public class IotServer {

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap().group(
                    bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new LoggingHandler(LogLevel.INFO));
                            /**
                             * 心跳機制
                             */
                            //pipeline.addLast(new IdleStateHandler(5, 10, 5, TimeUnit.SECONDS));

                            /**
                             * 消息、登錄解碼器
                             */
                            pipeline.addLast(new LoginDecoder(LoginRequest.class));
                            pipeline.addLast(new MsgDecoder(MsgRequest.class));

                            /**
                             * 消息、登錄處理器
                             */
                            pipeline.addLast(new MsgHandler());
                            pipeline.addLast(new LoginHandler());

                            /**
                             * 消息、登錄編碼器
                             */
                            pipeline.addLast(new MsgEncoder());
                            pipeline.addLast(new LoginEncoder());

                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 1024);

            ChannelFuture cf = bootstrap.bind(8849).sync();
            log.info("socket服務端啟動成功 {}", cf.channel().localAddress().toString());

            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }

    }
}
服務端代碼
package com.ql.handler;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.request.MsgRequest;
import protocol.msg.response.MsgResponse;

/**
 * @description: 消息處理器
 * @author: quliang
 * @create: 2022-12-10 20:57
 **/
@Slf4j
@ChannelHandler.Sharable
public class MsgHandler extends SimpleChannelInboundHandler<MsgRequest> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("上線{}", ctx.channel().remoteAddress().toString());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgRequest request) throws Exception {
        log.info("服務端讀取消息體數據為{}", request.toString());
        MsgResponse response = new MsgResponse();
        response.setStatCode(200);
        ctx.channel().writeAndFlush(response);
    }
}
服務端消息處理器
package com.ql.handler;

import io.netty.channel.*;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import protocol.system.request.LoginRequest;
import protocol.system.response.LoginResponse;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 登錄處理器
 * @author: quliang
 * @create: 2022-12-06 18:14
 **/
@Slf4j
@ChannelHandler.Sharable
public class LoginHandler extends SimpleChannelInboundHandler<LoginRequest>{
    private static AtomicInteger READER_COUNT = new AtomicInteger(0);

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("服務端:{} 通道開啟!", ctx.channel().localAddress().toString());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("服務端: {} 通道關閉!", ctx.channel().localAddress().toString());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequest loginRequest) throws Exception {
        log.info("讀取數據 {} ", loginRequest.toString());
        LoginResponse response= new LoginResponse("success", null);
        ctx.channel().writeAndFlush(response);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        log.info("...............數據接收-完畢...............");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        log.error("...............業務處理異常...............{}", cause);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            Channel channel = ctx.channel();
            switch (event.state()) {
                case READER_IDLE:
                    log.info("讀空閑");
                    READER_COUNT.addAndGet(1);
                    break;
                case WRITER_IDLE:
                    log.info("寫空閑");
                    break;
                default:
                    break;
            }
            ctx.disconnect();
            if (READER_COUNT.get() > 3) {
                log.info("close this channel {}", channel.remoteAddress().toString());
            }
        }
    }

}
服務端登錄處理器

 

服務端其實很多都是直接引用公共模塊的,代碼也並不複雜

創建消息客戶端

package com.ql;

import com.ql.handler.ClientMsgHandler;
import decoder.MsgDecoder;
import encoder.MsgEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import protocol.msg.response.MsgResponse;

import java.net.InetSocketAddress;

/**
 * @author quliang
 * @description 客戶端
 * @date 2022-12-06 17:37:56
 */
@Slf4j
public class IotCli

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 總結-舊生命周期 初始化階段: 由ReactDOM.render()觸發 初次渲染 constructor() componentWillMount() render() componentDidMount() > 常用 一般在這個鉤子中做一些初始化的事,例如:開啟定時器,發送網路請求,訂閱消息 更 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 本文用一個簡單的 demo 講解 App端 半屏連續掃碼 的實現方式,包括(條形碼、二維碼等各種各樣的碼)。 我會從實現思路講起,如果你比較急可以直接跳到 動手實現 章節獲取代碼。 開發和運行環境 開發工具:HBuilderX 前端框架: ...
  • 一篇文章帶你瞭解設計模式——行為型模式 在之前的文章我們已經介紹了設計模式中的創建者模式和結構型模式,下麵我們來介紹最後一部分行為型模式 行為型模式用於描述程式在運行時複雜的流程式控制制,即描述多個類或對象之間怎樣相互協作共同完成單個對象都無法單獨完成的任務 行為型模式分為類行為模式和對象行為模式,前者 ...
  • 開篇詞 | 四縱四橫,帶你透徹理解分散式技術 誰更好掌握了分散式技術,誰就更容易在新一輪技術浪潮中獲得主動。 很多有多年工作經驗的人,在分散式上面,也可能會有下麵的問題: 各種分散式概念、名詞學了一大堆,但經常張冠李戴,傻傻分不清楚。 做了多年技術,也參與了很多分散式技術實踐,卻無法回答工作中各種分 ...
  • 概述 數組是相同類型數據的有序集合 可以是任何類型 每一個數據被稱為該數組的一個數組元素,可以使用下標訪問每一個元素 下標從0開始,按順序遞增 數組長度是固定的,創建後不可改變 數組屬於引用類型 聲明、記憶體、初始化和使用 聲明 可以使用 Type[] arr;//常用 或者 Type arr[]; ...
  • 1.編寫一個計算減法的方法,當第一個數小於第二個數時,拋出“被減數不能小於減數"的異常。 class Sub(Exception): def __init__(self, x, y): self.x = x self.y = y try: a = int(input('請輸入被減數')) b = i ...
  • 文中代碼 smart_girl = {"name":"yuan wai", "age": 25,"address":"Beijing"} 第一種方式:pop()方法 註意:找不到對應的key,pop方法會拋出異常KeyError smart_girl.pop("name") #返回值是value # ...
  • 1.已知列表li_num1 = [4, 5, 2, 7]和li_num2 = [3, 6],請將這兩個列表合併為一個列表,並將合併後的列表中的元素按降序排列。 1 # 方法一 2 li_num1 = [4, 5, 2, 7, ] 3 li_num2 = [3, 6] 4 # extend() 函數用 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...