Netty 学习(一):服务端启动 & 客户端启动

作者: Grey

原文地址:

博客园:Netty 学习(一):服务端启动 & 客户端启动

CSDN:Netty 学习(一):服务端启动 & 客户端启动

说明

Netty 封装了 Java NIO 的很多功能,大大简化了 Java 网络编程的难度,同时 Netty 也支持多种协议,Netty 架构图如下

注:上图来自 Netty 官网

BIO 模型

传统的Java BIO模型代码如下

客户端代码

import java.net.Socket;
import java.util.Date; /**
* 传统 BIO 的客户端实现
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since 1.1
*/
public class IOClient {
public static void main(String[] args) {
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 8000);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (Exception e) { }
}).start();
}
}

服务端代码

package bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket; /**
* 传统 BIO 的 服务端实现
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since 1.1
*/
public class IOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
new Thread(() -> {
while (true) {
try {
// 阻塞
Socket socket = serverSocket.accept();
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按照字节流的方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) { }
}).start();
} catch (IOException e) { }
}
}).start();
}
}

上述代码比较直白,缺点也很明显

每个连接创建成功后都需要由一个线程来维护,同一时刻有大量线程处于阻塞状态,此外,线程数量太多,也会导致操作系统频繁进行线程切换,使得应用性能下降。

NIO 模型

为了解决 BIO 的问题,引入了 NIO,即:一个新的连接来了以后,不会创建一个while 死循环取监听有数据可读,而是直接把这条连接注册到 Selector 上。然后,通过检查这个 Selector,就可以批量监测出有数据可读的连接,进而读取数据。

BIO读写是面向流的,一次性只能从流中读取一个字节或者多字节,并且读完之后流无法再读取,需要自己缓存数据。而 NIO 的读写是面向 Buffer 的,可以随意读取里面任何字节的数据,不需要自己缓存数据,只需要移动读写指针即可。

但是 Java 原生的 NIO 代码编程非常繁琐,一个简单的服务端代码,使用 NIO 模型,代码如下

package nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set; /**
* NIO 实现服务端
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since 1.4
*/
public class NIOServer {
public static void main(String[] args) throws Exception {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (Exception e) { }
}).start();
new Thread(() -> {
try {
while (true) {
if (clientSelector.select(1) > 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();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (Exception e) { }
}).start();
}
}

Netty 客户端和服务端

Netty 解决了 NIO 编程繁琐的痛点,封装了很多友好的 API,

同样实现服务端和客户端,如果使用 Netty,就简单很多

使用 Netty 实现一个最简单的服务端(每个组件使用见注释)

package netty.v1;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder; /**
* 使用 Netty 实现服务端
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since
*/
public class NettyServer {
public static void main(String[] args) {
// 引导服务端的启动
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 用于监听端口,接收新连接的线程组
NioEventLoopGroup boss = new NioEventLoopGroup();
// 表示处理每一个连接的数据读写的线程组
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap.group(boss, worker)
// 指定IO模型为NIO
.channel(NioServerSocketChannel.class)
// 定义后面每一个连接的数据读写
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
System.out.println("服务启动中......");
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
// 本地绑定一个8000端口启动服务端
.bind(8000);
}
}

使用 Netty 实现一个最简单的客户端(每个组件说明见注释)

package netty.v1;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder; import java.util.Date; /**
* Netty 实现客户端
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect("localhost", 8000).channel();
while (true) {
channel.writeAndFlush(new Date() + ": hello world!");
Thread.sleep(2000);
}
}
}

注:运行上述代码需要引入 Netty 依赖包

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

更复杂一点的场景

在 Netty 简单客户端和服务端基础上,增加一些更复杂的场景,比如:

服务端支持端口检测,即:针对已经被占用的端口,可以调整端口配置并自动绑定到一个空闲端口

客户端支持重连,即:设置一个最大重连次数,客户端允许多次重新连接服务端直到达到最大重连次数。

服务端代码如下(增加的配置参数见注释说明)

package netty.v2;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.AttributeKey; /**
* Netty 自动绑定递增端口
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since
*/
public class NettyServerBindDynamicPort { public static void main(String[] args) {
// 引导服务端的启动
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 用于监听端口,接收新连接的线程组
NioEventLoopGroup boss = new NioEventLoopGroup();
// 表示处理每一个连接的数据读写的线程组
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap.group(boss, worker)
// 指定IO模型为NIO
.channel(NioServerSocketChannel.class)
// 可以给服务端的Channel指定一些属性,非必须
.attr(AttributeKey.newInstance("serverName"), "nettyServer")
// 可以给每一个连接都指定自定义属性,非必须
.childAttr(AttributeKey.newInstance("clientKey"), "clientValue")
// 使用option方法可以定义服务端的一些TCP参数
// 这个设置表示系统用于临时存放已经完成三次握手的请求的队列的最大长度,
// 如果连接建立频繁,服务器创建新的连接比较慢,则可以适当调大这个参数
.option(ChannelOption.SO_BACKLOG, 1024)
// 以下两个配置用于设置每个连接的TCP参数
// SO_KEEPALIVE: 表示是否开启TCP底层心跳机制,true表示开启
.childOption(ChannelOption.SO_KEEPALIVE, true)
// TCP_NODELAY:表示是否开启Nagle算法,true表示关闭,false表示开启
// 如果要求高实时性,有数据发送时就马上发送,就设置为关闭;
// 如果需要减少发送次数,减少网络交互,就设置为开启。
.childOption(ChannelOption.TCP_NODELAY, true)
// 定义后面每一个连接的数据读写
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
System.out.println("服务启动中......");
}
});
// 本地绑定一个8000端口启动服务
bind(serverBootstrap, 8000);
} public static void bind(final ServerBootstrap serverBootstrap, final int port) {
serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("端口[" + port + "]绑定成功");
} else {
System.err.println("端口[" + port + "]绑定失败");
bind(serverBootstrap, port + 1);
}
});
}
}

其中bind方法是递归函数,即每次尝试失败的时候,端口号加1,直到端口绑定成功为止。

客户端代码如下(增加的配置参数见注释说明)

package netty.v2;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.AttributeKey; import java.util.Date;
import java.util.concurrent.TimeUnit; /**
* Netty 实现可自动重连的客户端
*
* @author <a href="mailto:410486047@qq.com">Grey</a>
* @date 2022/9/12
* @since
*/
public class NettyClientRetry {
static final int MAX_RETRY = 6; public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap
// 指定线程模型
.group(group)
// 指定IO类型为NIO
.channel(NioSocketChannel.class)
// attr可以为客户端Channel绑定自定义属性
.attr(AttributeKey.newInstance("clientName"), "nettyClient")
// 连接的超时时间,如果超过这个时间,仍未连接到服务端,则表示连接失败
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
// 表示是否开启TCP底层心跳机制,true表示开启
.option(ChannelOption.SO_KEEPALIVE, true)
// 是否开启Nagle算法,如果要求高实时性,有数据就马上发送,则为true
// 如果需要减少发送次数,减少网络交互,就设置为false
.option(ChannelOption.TCP_NODELAY, true)
// IO处理逻辑
.handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) { }
});
connect(bootstrap, "localhost", 8000, MAX_RETRY);
} private static void connect(final Bootstrap bootstrap, final String host, final int port, int retry) {
bootstrap.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else if (retry == 0) {
System.err.println("重试次数已经使用完毕");
} else {
// 第几次重试
int order = (MAX_RETRY - retry) + 1;
// 本次的重试间隔
int delay = 1 << order;
System.out.println(new Date() + ": 连接失败,第" + order + "次重连...");
bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS);
}
});
} }

其中connect也是递归方法,每次尝试连接失败的时候,retry参数减1,直到为0。但是在通常情况下,连接失败不会立即重连,而是通过一个指数退避的方式,即:delay 参数的配置,每隔 2 的幂次时间来建立连接。

参考资料

User guide for 4.x

跟闪电侠学 Netty:Netty 即时聊天实战与底层原理

深度解析Netty源码

Netty 学习(一):服务端启动 & 客户端启动的更多相关文章

  1. C#Winform窗体实现服务端和客户端通信例子(TCP/IP)

    Winform窗体实现服务端和客户端通信的例子,是参考这个地址 http://www.cnblogs.com/longwu/archive/2011/08/25/2153636.html 进行了一些异 ...

  2. Netty学习笔记(二) 实现服务端和客户端

    在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...

  3. Netty 学习(二):服务端与客户端通信

    Netty 学习(二):服务端与客户端通信 作者: Grey 原文地址: 博客园:Netty 学习(二):服务端与客户端通信 CSDN:Netty 学习(二):服务端与客户端通信 说明 Netty 中 ...

  4. Netty入门一:服务端应用搭建 & 启动过程源码分析

    最近周末也没啥事就学学Netty,同时打算写一些博客记录一下(写的过程理解更加深刻了) 本文主要从三个方法来呈现:Netty核心组件简介.Netty服务端创建.Netty启动过程源码分析 如果你对Ne ...

  5. Netty服务端与客户端(源码一)

    首先,整理NIO进行服务端开发的步骤: (1)创建ServerSocketChannel,配置它为非阻塞模式. (2)绑定监听,配置TCP参数,backlog的大小. (3)创建一个独立的I/O线程, ...

  6. Python3学习之路~8.3 socket 服务端与客户端

    通过8.2的实例1-6,我们可以总结出来,socket的服务端和客户端的一般建立步骤: 服务端 步骤:1创建实例,2绑定,3监听,4阻塞,5发送&接收数据,6关闭. #Author:Zheng ...

  7. 红帽学习笔记[RHCE]OpenLDAP 服务端与客户端配置

    目录 OpenLDAP 服务端与客户端配置 关于LDIF 一个LDIF基本结构一个条目 属性 Object的类型 服务端 安装 生成证书 生成默认数据 修改基本的配置 导入基础数据 关于ldif的格式 ...

  8. 【转】TCP/UDP简易通信框架源码,支持轻松管理多个TCP服务端(客户端)、UDP客户端

    [转]TCP/UDP简易通信框架源码,支持轻松管理多个TCP服务端(客户端).UDP客户端 目录 说明 TCP/UDP通信主要结构 管理多个Socket的解决方案 框架中TCP部分的使用 框架中UDP ...

  9. C# Socket服务端与客户端通信(包含大文件的断点传输)

    步骤: 一.服务端的建立 1.服务端的项目建立以及页面布局 2.各功能按键的事件代码 1)传输类型说明以及全局变量 2)Socket通信服务端具体步骤:   (1)建立一个Socket   (2)接收 ...

随机推荐

  1. 摸鱼人常备5个Python迷你项目,玩一整天不是问题(附源码)

    大家好鸭,我是小熊猫 在使用Python的过程中,我最喜欢的就是Python的各种第三方库,能够完成很多操作. 下面就给大家介绍5个通过Python构建的项目,以此来学习Python编程. 一.石头剪 ...

  2. .NET Core 实现后台任务(定时任务)IHostedService(一)

    原文链接:https://www.cnblogs.com/ysmc/p/16456787.html 最近有小伙伴问道,在 .Net Core 中,如何定时执行任务,而因为需要执行的任务比较简单,并不想 ...

  3. 常用Linux音译

    su:Swith user 切换用户,切换到root用户 cat: Concatenate 串联 uname: Unix name 系统名称 df: Disk free 空余硬盘 du: Disk u ...

  4. 乐观锁和悲观锁在kubernetes中的应用

    数据竞争和竞态条件 Go并发中有两个重要的概念:数据竞争(data race)和竞争条件(race condition).在并发程序中,竞争问题可能是程序面临的最难也是最不容易发现的错误之一. 当有两 ...

  5. 003 Jwt登录流程图

    用户\角色\权限 用户是一个基本的单位 用户和角色的关系是多对多,所以要有一张保存用户和角色关系的中间表 角色也不能直接决定这个用户能做什么操作,有哪些权限, 需要再关联权限表决定 角色和权限也是多对 ...

  6. Linux一些错误总结

    1.cannot verify <mydomainname> certificate, issued by '/C=US/O=Let's Encrypt/CN=R3': 解决1:wget ...

  7. Modbus转BACnet IP网关

    BACnet是楼宇自动化和控制网络数据通信协议的缩写.它是为楼宇自动化网络开发的数据通信协议   根据1999年底互联网上楼宇自动化网络的信息,全球已有数百家国际知名制造商支持BACnet,包括楼宇自 ...

  8. CentOS Docker安装 && docker 基础指令

    1 # 直接从官网下载docker的安装命令包(docker已经很贴心将安装shell脚本帮我们准备好了) 2 curl -fsSL get.docker.com -o get-docker.sh 3 ...

  9. 小白之Python基础(三)

    列表和元组 1.列表:最常用的 Python 数据类型(可变的数据类型) 1)列表是一个值,它包含多个值构成的序列: 2)通过[ ]或list()创建的有序元素的集合: 3)表项(列表中的值,也可以叫 ...

  10. Docker Compose安装部署Jenkins

    流水线可以让项目发布流程更加清晰,docker可以大大减少Jenkins配置. 1.前言 数据卷挂载到 /var 磁盘目录下,因为该磁盘空间较大,后面需要挂载容器数据卷,以防内存吃紧. 为了可以留存启 ...