说说 IO流 及 Socket编程

一、IO 流的分类

IO流可以操作磁盘数据文件的读写,读写处理效率的高低直接影响到服务响应请求的性能。

IO分为BIO (Blocking I/O),NIO(New I/O), AIO(Asynchronous I/O)

BIO:同步阻塞I/O模式,数据的读取及写入阻塞在一个线程内等待其完成,一请求一应答

NIO:同步非阻塞I/O模式,包含以下几个核心组件:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

AIO:也即NIO2,异步非阻塞I/O模式,尚未广泛应用。

  

二、基于JDK NIO的Socket 编程

Socket网络连接通信,有Server端,Client端。

一般地,服务端会创建2个Selector,一个用于阻塞监听客户端的连接,一个用于调度处理客户的请求任务

打开Selector:

Selector serverSelector = Selector.open(); // 监听客户端连接
Selector clientSelector = Selector.open();   // 调度处理客户端请求任务

启动服务端:

ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8888));
listenerChannel.configureBlocking(false);  // true-阻塞,同BIO, false-非阻塞,使用NIO
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

轮询监听客户端的连接,将serverSelector中的连接记录转交注册到clientSelector中:

// 轮询监听客户端新连接
while (true) {
    // 阻塞1000 ms
    if (serverSelector.select(1000) > 0) {
        Set<SelectionKey> set = serverSelector.selectedKeys();
        Iterator<SelectionKey> keyIterator = set.iterator();
        while (keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();
            if (key.isAcceptable()) {
                try {
                    // 客户端新建连接,无需创建新线程,而是直接注册到clientSelector
                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                } finally {
                    keyIterator.remove();
                }
            }
        }
    }
}

轮询调度处理clientSeletor中接收的客户端请求任务:

// 轮询调度处理Selector接收的客户端请求任务
while (true) {
   // 阻塞 1000 ms
    if (clientSelector.select(1000) > 0) {
        Set<SelectionKey> set = clientSelector.selectedKeys();
        Iterator<SelectionKey> keyIterator = set.iterator();
        while (keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();
            if (key.isReadable()) {
                try {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    clientChannel.read(byteBuffer);
                    byteBuffer.flip();
                    String response = Charset.defaultCharset().newDecoder().decode(byteBuffer).toString();
                    log.info();
                     // todo something with response
                } finally {
                    keyIterator.remove();
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
        }
    }
}

以上代码放到一个main方法中,两段死循环分别开启两个线程,运行后打开cmd命令窗口,执行telnet localhost 8888, 任意输入后,可以看到服务端控制台能够成功接收到客户端的请求信息(当然了telnet命令窗口的输入是不可见的,只管输入好了)。

由上可见,NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。只是多加了一层channel,这样可以双向读写,并且读写时都是通过Buffer来操作channel的。不过,JDK的NIO实现Socket编程太过繁琐,下面看下Netty的使用。

三、基于 Netty NIO的Socket 编程

以下基于SpringBoot工程:

yml配置:

#netty
netty:
  port: 8888
  boss:
    thread:
      count: 2
  worker:
    thread:
      count: 100
  keepAlive: true
  backlog: 2048

公用常量:

public interface NettyAttribute {

    /** netty消息体 拆包粘包分隔符 **/
    String DELIMITER = "_#_";

    /** Netty 连接绑定对象key **/
    AttributeKey<UserPoint> ATTACHMENT_KEY = AttributeKey.valueOf("ATTACHMENT_KEY");

}

Netty Server 配置:

@Configuration
public class NettyServerConfig {

    @Value("${netty.boss.thread.count}")
    private int bossCount;

    @Value("${netty.worker.thread.count}")
    private int workerCount;

    @Value("${netty.keepAlive}")
    private boolean keepAlive;

    @Value("${netty.backlog}")
    private int backlog;

    @Resource(name = "nettyChannelInitializer")
    private NettyChannelInitializer channelInitializer;

    @Bean(name = "serverBootstrap")
    public ServerBootstrap serverBootstrap() {
        // Netty Server 启动类配置
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup(), workerGroup()) // 创建selector
                .channel(NioServerSocketChannel.class) // 创建channel
                .childHandler(channelInitializer);  // channelHandler初始化channel

        // 追加 Netty Server 启动类其他配置项:日志输出、keepAlive 
        Map<ChannelOption<?>, Object> tcpChannelOptions = this.tcpChannelOptions();
        Set<ChannelOption<?>> keySet = tcpChannelOptions.keySet();
        for (@SuppressWarnings("rawtypes")ChannelOption option : keySet) {
            serverBootstrap.option(option, tcpChannelOptions.get(option));
        }

        return serverBootstrap;
    }

    @Bean(name = "bossGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(bossCount);
    }

    @Bean(name = "workerGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workerCount);
    }

    @Bean(name = "tcpChannelOptions")
    public Map<ChannelOption<?>, Object> tcpChannelOptions() {
        Map<ChannelOption<?>, Object> options = new HashMap<ChannelOption<?>, Object>();
        options.put(ChannelOption.SO_KEEPALIVE, keepAlive);
        options.put(ChannelOption.SO_BACKLOG, backlog);
        return options;
    }

}

Netty Server 启动类配置:

@Slf4j
@Component
public class NettyServerStarter {

    @Resource(name = "serverBootstrap")
    private ServerBootstrap serverBootstrap;

    @Value("${netty.port}")
    private int tcpPort;

    private List<ChannelFuture> serverChannelFuture;

    @PostConstruct
    public void start() throws Exception {
        log.info("netty server starting at {}", tcpPort);
    
        // 将多个ServerSocket放到集合容器中便于销毁管理
        serverChannelFuture = new ArrayList<>();
        serverChannelFuture.add(serverBootstrap.bind(new InetSocketAddress(tcpPort)).sync()); // 启动一个线程,创建ServerSocket并绑定监听指定端口

        // 一台服务器上可以绑定多个服务端口
        /*serverChannelFuture.add(bootstrap.bind(8192).sync());
        serverChannelFuture.add(bootstrap.bind(8193).sync());*/
    }

    @PreDestroy
    public void stop() throws Exception {
        if (serverChannelFuture != null) {
            for (ChannelFuture channelFuture : serverChannelFuture) {
                channelFuture.channel().closeFuture().sync();
            }
        }
    }

}

Netty Channel 初始化配置:

@Component("nettyChannelInitializer")
public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Autowired
    private ServerHandler serverHandler;

    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();

        // 心跳检测,300秒没有读操作,自动断开连接。
        pipeline.addLast("idle", new IdleStateHandler(300, 0, 0));

        // 协议解码处理器,判断是什么协议(WebSocket还是TcpSocket),然后动态修改编解码器
        pipeline.addLast("protocolDecoder", new ProtocolDecoder());

        // 针对socket的解码器(WebSocket基于http,TcpSocket基于tcp, 底层都是基于tcp,以下为通用解码配置)
        ByteBuf delimiter = Unpooled.copiedBuffer(NettyAttribute.DELIMITER.getBytes());
        pipeline.addLast("delimiter", new DelimiterBasedFrameDecoder(4096, delimiter));
        pipeline.addLast("stringDecoder", new StringDecoder());
        pipeline.addLast("stringEncoder", new StringEncoder());

        // 服务器逻辑
        pipeline.addLast("handler", serverHandler);
    }

}

协议(WebSocket / TcpSocket)解码处理器配置:

@Slf4j
public class ProtocolDecoder extends ByteToMessageDecoder {

    /**
     * 请求行信息的长度,ws为:GET /ws HTTP/1.1, http为:GET / HTTP/1.1
     */
    private static final int PROTOCOL_LENGTH = 16;

    /**
     * WebSocket握手协议的前缀, 在访问ws的时候,请求地址需要为如下格式 ws://ip:port/ws
     */
    private static final String WEBSOCKET_PREFIX = "GET /ws";

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        String protocol = this.getBufStart(in);
        ChannelPipeline pipeline = ctx.channel().pipeline();
        log.info("ProtocolHandler protocol:" + protocol);
        if (protocol.startsWith(WEBSOCKET_PREFIX)) {
            // HttpServerCodec:将请求和应答消息解码为HTTP消息
            pipeline.addBefore("handler", "http-codec", new HttpServerCodec());
            // HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
            pipeline.addBefore("handler", "aggregator", new HttpObjectAggregator(65535));
            // ChunkedWriteHandler:向客户端发送HTML5文件,文件过大会将内存撑爆
            pipeline.addBefore("handler", "http-chunked", new ChunkedWriteHandler());
            pipeline.addBefore("handler", "WebSocketAggregator", new WebSocketFrameAggregator(65535));
            // 用于处理websocket, /ws为访问websocket时的uri
            pipeline.addBefore("handler", "ProtocolHandler", new WebSocketServerProtocolHandler("/ws"));

            // 移除可能缓存的TcpSocket解码器
            pipeline.remove(DelimiterBasedFrameDecoder.class);
            pipeline.remove(StringDecoder.class);
        }

        // 重置index标记位
        in.resetReaderIndex();

        // 移除该协议处理器,该channel后续的处理由对应协议安排好的编解码器处理
        pipeline.remove(this.getClass());
    }

    /**
     * 获取buffer中指定长度的信息
     * @param in
     * @return
     */
    private String getBufStart(ByteBuf in) {
        int length = in.readableBytes();
        if (length > PROTOCOL_LENGTH) {
            length = PROTOCOL_LENGTH;
        }
        // 标记读取位置
        in.markReaderIndex();
        byte[] content = new byte[length];
        in.readBytes(content);
        return new String(content);
    }

}

Netty 消息处理控制器:

@Slf4j
@Component
@ChannelHandler.Sharable
public class ServerHandler extends SimpleChannelInboundHandler<Object> {

    @Autowired
    private KafkaProducer kafkaProducer;

    private AtomicInteger connectNum = new AtomicInteger(0);

    /**
     * 客户端连接到服务端后执行
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info(String.format("客户端 id=%s 连接到服务端成功", ctx.channel().id()));
        super.channelActive(ctx);

        int connections = connectNum.incrementAndGet();
        log.info("connections = {}", connections);
    }

    /**
     * 断线移除会话
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        User user = ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).get();
        if (user != null && StringUtils.isNotEmpty(user.getId())) {
            log.info("channelInactive 用户断开连接 {}", JSON.toJSONString(user));
            NettyChannelManager.removeChannel(user.getId(), user.getAppId());
            // todo 用户断开连接移除用户在线状态
        }

        log.info(String.format("客户端 id=%s 断线成功", ctx.channel().id()));
        super.channelInactive(ctx);
        log.info("[channelInactive]channel.isActive() = {}", ctx.channel().isActive());

        connectNum.decrementAndGet();
    }

    /**
     * 异常处理
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("异常" + ctx.channel());
        User user = ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).get();

        super.exceptionCaught(ctx, cause);
        log.info("[exceptionCaught]channel.isActive() = {}", ctx.channel().isActive());
        ctx.close();
    }

    /**
     * 心跳超时处理
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            log.info("userEventTriggered事件触发 idelState = " + ((IdleStateEvent) evt).state());
            IdleStateEvent event = (IdleStateEvent) evt;
            // 超时
            if(Arrays.asList(IdleState.READER_IDLE, IdleState.WRITER_IDLE, IdleState.ALL_IDLE).contains(event.state())){
                log.info("很长时间没有接收到客户端消息了,自动关闭channel");
                // 关闭channel
                final ChannelFuture writeAndFlush = ctx.channel().writeAndFlush("Time Out,you will closed!");
                writeAndFlush.addListener(future -> {
                    User user = ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).get();
                    if (user != null) {
                        // todo 用户心跳检测超时移除用户在线状态
                    }
                    ctx.channel().close();
                });
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    /**
     * 接收消息
     * @param ctx
     * @param object
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object object){
        boolean isWebsocket = object instanceof TextWebSocketFrame;

        String msg = "";
        if(isWebsocket){
            // websocket
            msg = ((TextWebSocketFrame) object).text().replaceAll(NettyAttribute.DELIMITER, "");
        }else if(object instanceof String){
            // netty-tcp socket
            msg = (String) object;
        }else{
            log.info("收到未知类型的消息:" + msg);
            return;
        }

        log.info(String.format("Netty Server receive client[id=%s] msg============== %s ", ctx.channel().id(), msg));

        NettyMsg<?> nettyMsg = JSON.parseObject(msg, NettyMsg.class);

        // 根据不同消息类型处理相关业务
        this.dealWithNettyMsg(ctx, msg, nettyMsg, isWebsocket);
    }

    /**
     * 根据不同消息类型处理相关业务
     * @param ctx
     * @param msg
     * @param nettyMsg
     * @param isWebsocket
     */
    public void dealWithNettyMsg(ChannelHandlerContext ctx, String msg, NettyMsg<?> nettyMsg, boolean isWebsocket){
        switch (nettyMsg.getMsgType()) {
            // 握手成功
            case NettyMsgType.RESPONSE_SUCCESS:
                this.handshake(ctx, msg, nettyMsg, isWebsocket, kafkaProducer);
                break;

            // 心跳检测
            case NettyMsgType.HEART_BEAT :

            //  聊天消息
            case NettyMsgType.CHAT_MSG :
                // todo IM 消息处理
                break;
        }
    }

   /**
     * 发送消息
     * @param ctx
     * @param msg
     * @param isWebsocket
     */
    public void writeAndFlush(ChannelHandlerContext ctx, String msg, boolean isWebsocket){
        msg += NettyAttribute.DELIMITER;
        ctx.writeAndFlush(isWebsocket ? new TextWebSocketFrame(msg) : msg);
    }

    /**
     * 握手成功
     * @param ctx
     * @param msg
     * @param nettyMsg
     * @param isWebsocket
     */
    public void handshake(ChannelHandlerContext ctx, String msg, NettyMsg<?> nettyMsg,
                                 boolean isWebsocket, KafkaProducer kafkaProducer){
        // 第一次连接,握手成功
        User user = new User();
        user.setId(nettyMsg.getUserId().toString());
        user.setAppId(nettyMsg.getAppId());
        ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).set(user);
        // 接收客户端的连接channel
        NettyChannelManager.addChannel(nettyMsg.getUserId(), nettyMsg.getAppId(), ctx);
        NettyMsgManager.writeAndFlush(ctx, msg, isWebsocket);

        log.info("用户:" + JSON.toJSONString(userPoint) + "握手连接 / 心跳检测 成功");

        // todo 更新用户在线信息
    }

}

Netty session channel 管理:

由于ChannelHandlerContext没有实现序列化,不能通过Redis等进行分布式存储共享。这里只能保存到各个服务节点的JVM内存中(见下面NettyChannelManager处理方法),然后通过kafka分发到各个服务节点。服务端由于负载均衡,且未配置IP hash,客户端每次连接的可能是不同的服务节点,但只会连接到其中一个服务节点(不会重复消费消息),因此kafka的各个服务节点的consumer group需配置不同,即确保每个节点都消费消息

// java -jar /usr/local/jar/xx-im/xx-im.jar -Xms1024m -Xmx1024m -Xmn256m

// --spring.profiles.active=pro --spring.kafka.consumer.group-id=topic_xx_im_01

// --spring.profiles.active=pro --spring.kafka.consumer.group-id=topic_xx_im_02

@Slf4j
public class NettyChannelManager {

    /**
     * 在线channels
     */
    private static Map<String, ChannelHandlerContext> channelMap = new ConcurrentHashMap<>();

    /**
     * 获取用户Netty会话的key
     * @param userId
     * @param appId
     * @return
     */
    private static String getKey(Integer userId, Integer appId){
        return "userId:" + userId + ":" + appId;
    }

    /**
     * 加入
     * @param userId
     * @param appId
     * @param channel
     */
    public static void addChannel(Integer userId, Integer appId, ChannelHandlerContext channel) {
        channelMap.put(getKey(userId, appId), channel);
    }

    /**
     * 移除channel
     * @param userId
     * @param appId
     */
    public static void removeChannel(Integer userId, Integer appId){
        String key = getKey(userId, appId);
        ChannelHandlerContext channel = channelMap.get(key);
        // 心跳检测,空闲会话channel会自动关闭
        if (channel != null ) {
            channel.close();
        }
        channelMap.remove(key);
    }

    /**
     * 获取指定channel
     * @param userId
     * @param appId
     * @return
     */
    public static ChannelHandlerContext getChannel(Integer userId, Integer appId) {
        return channelMap.get(getKey(userId, appId));
    }

}

以上是基于Netty+Kafka实现的Socket通信(IM系统),不同业务类型的消息只需要在ServerHandler中分别处理即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值