一、服务端
1、添加Maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2、application.yml
server:
port: 8001
3、启动类
@SpringBootApplication
public class NettyServerApplication implements CommandLineRunner {
@Autowired
private NettyServerBootStrap serverBootStrap;
public static void main(String[] args) {
SpringApplication.run(NettyServerApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
serverBootStrap.start();
}
}
4、NettyServerBootStrap
@Component
@Slf4j
public class NettyServerBootStrap {
@Autowired
private NettyServerHandler nettyServerHandler;
public void start() throws InterruptedException {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
// 使消息立即发出去,不用等待到一定的数据量才发出去
.option(ChannelOption.TCP_NODELAY, true)
// 保持长连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
p.addLast(new StringDecoder(CharsetUtil.UTF_8));
p.addLast(new StringEncoder(CharsetUtil.UTF_8));
p.addLast(nettyServerHandler);
}
});
// 绑定端口,同步等待成功
ChannelFuture f = bootstrap.bind(5678).sync();
if (f.isSuccess()) {
log.info("Netty Start successful");
} else {
log.error("Netty Start failed");
}
// 等待服务监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 退出,释放线程资源
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
5、NettyServerHandler
@Component
@ChannelHandler.Sharable
@Slf4j
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
/**
* @Description 客户端断开连接时执行,将客户端信息从Map中移除
* @param ctx
* @Date 2019/8/28 14:22
* @Author wuyong
* @return
**/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端断开连接:{}", getClientIp(ctx.channel()));
NettyChannelMap.remove((SocketChannel) ctx.channel());
}
/**
* @Description 客户端连接时执行,将客户端信息保存到Map中
* @param ctx
* @Date 2019/8/28 14:22
* @Author wuyong
* @return
**/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("有新的客户端连接:{}", getClientIp(ctx.channel()));
String clientIp = getClientIp(ctx.channel());
NettyClient client = new NettyClient((SocketChannel) ctx.channel(), getClientIp(ctx.channel()));
NettyChannelMap.add(clientIp, client);
}
/**
* @Description 收到消息时执行,根据消息类型做不同的处理
* @param ctx
* @param msg
* @Date 2019/8/28 14:33
* @Author wuyong
* @return
**/
@Override
public void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
log.info("收到客户端消息:" + msg);
// 这个消息一般是结构化的数据,比如JSON字符串,解析这个JSON字符串,做相应的逻辑处理
JSONObject msgObj = JSON.parseObject(msg);
String msgType = msgObj.getString("msgType");
switch (msgType) {
// 回复客户端请求
case "req":
doReply(ctx);
break;
default:
break;
}
}
/**
* @description: TODO
* @param ctx
* @param cause
* @Author: wuyong
* @Date: 2019/08/30 13:41:51
* @return: void
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("抛出异常执行,包括客户端断开连接时,会抛出IO异常");
}
/**
* @description: 当收到客户端的消息后,进行处理
* @param ctx
* @Author: wuyong
* @Date: 2019/08/30 14:10:59
* @return: void
*/
private void doReply(ChannelHandlerContext ctx) {
String reply = "{\"msgType\":\"reply\",\"data\":\"回复的数据\"}";
ctx.channel().writeAndFlush(reply);
}
/**
* @Description 获取客户端IP
* @param channel
* @Date 2019/8/28 14:32
* @Author wuyong
* @return
**/
private String getClientIp(Channel channel) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) channel.remoteAddress();
String clientIP = inetSocketAddress.getAddress().getHostAddress();
return clientIP;
}
/**
* @Description 当有新的客户端连接的时候,用于保存客户端信息
* @Date 2019/8/28 14:20
* @Author wuyong
* @return
**/
public static class NettyChannelMap {
public static Map<String, NettyClient> map = new ConcurrentHashMap<>();
public static void add(String clientId, NettyClient client) {
map.put(clientId, client);
}
public static NettyClient get(String clientId) {
return map.get(clientId);
}
public static void remove(SocketChannel socketChannel) {
for (Map.Entry entry : map.entrySet()) {
if (((NettyClient) entry.getValue()).getChannel() == socketChannel) {
map.remove(entry.getKey());
}
}
}
}
/**
* @Description 封装客户端的信息
* @Date 2019/8/28 14:21
* @Author wuyong
* @return
**/
@Data
public static class NettyClient {
/**客户端与服务器的连接*/
private SocketChannel channel;
/**ip地址*/
private String clientIp;
// ......
public NettyClient(SocketChannel channel, String clientIp) {
this.channel = channel;
this.clientIp = clientIp;
}
}
}
至此,一个简单的Netty服务端就完成了。接下来写一个Controller,用于获取当前在线的客户端列表:
6、NettyServerController
@RestController
@RequestMapping("/server")
public class NettyServerController {
@GetMapping("/clientList")
public Map<String, NettyServerHandler.NettyClient> clientList() {
return NettyServerHandler.NettyChannelMap.map;
}
}
项目结构如下:
二、客户端
客户端添加的依赖、配置文件以及启动类和服务端类似。
1、NettyClientBootStrap
@Component
@Slf4j
public class NettyClientBootStrap {
private static final String HOST = "localhost";
private static final int PORT = 5678;
private static SocketChannel socketChannel = null;
public void start() throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.group(eventLoopGroup);
bootstrap.remoteAddress(HOST, PORT);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new IdleStateHandler(20, 10, 0));
socketChannel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
ChannelFuture future = bootstrap.connect(HOST, PORT).sync();
if (future.isSuccess()) {
socketChannel = (SocketChannel) future.channel();
log.info("connect server success");
}
}
public static SocketChannel getSocketChannel() {
return socketChannel;
}
}
2、NettyClientHandler
@Component
@Slf4j
@ChannelHandler.Sharable
public class NettyClientHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("断开连接执行");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("连接成功执行");
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
log.info("收到消息执行:" + msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("抛出异常执行");
}
}
3、编写一个Controller,用于向服务器发送消息:
@RestController
@RequestMapping("/client")
public class NettyClientController {
/**
* @description: 模拟向服务器发送消息
* @param
* @Author: wuyong
* @Date: 2019/08/30 14:10:09
* @return: java.lang.String
*/
@RequestMapping("/req")
public String req() {
String msg = "{\"msgType\":\"req\",\"clientId\":\"请求数据\"}";
NettyClientBootStrap.getSocketChannel().writeAndFlush(msg);
return "success";
}
}
客户端的结构如下:
三、测试
首先启动服务端:
然后启动客户端,启动成功后可以看到如下输出:
然后查看服务端的控制台:
在浏览器中访问http://localhost:8001/server/clientList,可以看到确实注册成功了:
接下来使用客户端向服务器发消息,在浏览器中执行http://localhost:8002/client/req,在服务端的控制台输出如下信息:
客户端的控制台输出了如下信息:
接下来断开客户端的连接,在服务端的控制台输出了如下信息:
查看客户端列表,也为空了:
四、使用Socket连接服务器
也可以直接使用Socket连接Netty服务器,编写一个简单的Demo如下:
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("localhost", 5678);
// 向服务器发消息
OutputStream outputStream = socket.getOutputStream();
String msg = "{\"msgType\":\"req\",\"clientId\":\"请求数据\"}";
outputStream.write(msg.getBytes(CharsetUtil.UTF_8));
outputStream.flush();
Thread.sleep(1000);
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
char[] ch = new char[65536];
int len = -1;
while ((len = br.read(ch)) != -1) {
String result = new String(ch, 0, len);
System.out.println("服务器返回数据:" + result);
}
}
}
运行main方法,可以看到控制台输出:
服务器返回数据:{"msgType":"reply","data":"回复的数据"}
五、粘包/拆包
正常情况下,一条消息是一个整体,一次接收到的消息只会是一条。但是实际情况下,收到的消息可能是多条消息粘在一起,或者一条消息被拆分成了多条。这就是所谓的粘包/拆包。产生粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。
对于粘包和拆包问题,常见的解决方案有四种(https://my.oschina.net/zhangxufeng/blog/3023794):
1、客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度(FixedLengthFrameDecoder解码器)。
2、客户端在每个包的末尾使用固定的分隔符,例如\r\n,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包(LineBasedFrameDecoder或DelimiterBasedFrameDecoder解码器)。
3、将消息分为消息头和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息(LengthFieldBasedFrameDecoder和LengthFieldPrepender)。
4、通过自定义协议进行粘包和拆包的处理(通过继承LengthFieldBasedFrameDecoder
和LengthFieldPrepender
来实现粘包和拆包的处理)。
首先,模拟一下粘包和拆包的问题。
修改NettyClientController,连续向服务器发送20条消息:
private void doReply(ChannelHandlerContext ctx) {
String reply = "{\"msgType\":\"reply\",\"data\":\"回复的数据\"}";
for (int i = 0; i < 20; i++) {
ctx.channel().writeAndFlush(reply);
}
}
查看服务端的控制台,输出如下:
2019-08-30 15:35:52.330 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.331 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.331 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.331 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
2019-08-30 15:35:52.331 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.331 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
2019-08-30 15:35:52.331 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:35:52.332 INFO 5772 --- [ntLoopGroup-1-0] com.wuychn.server.NettyServerHandler : 抛出异常执行,包括客户端断开连接时,会抛出IO异常
可知第三条消息就发生了粘包的问题。我这里采用在末尾加特定的分隔符(\r\n)的方式来解决。
修改NettyServerBootstrap,添加一个LineBasedFrameDecoder解码器:
@Component
@Slf4j
public class NettyServerBootStrap {
@Autowired
private NettyServerHandler nettyServerHandler;
public void start() throws InterruptedException {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
// 使消息立即发出去,不用等待到一定的数据量才发出去
.option(ChannelOption.TCP_NODELAY, true)
// 保持长连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
p.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
p.addLast(new StringDecoder(CharsetUtil.UTF_8));
p.addLast(new StringEncoder(CharsetUtil.UTF_8));
p.addLast(nettyServerHandler);
}
});
// 绑定端口,同步等待成功
ChannelFuture f = bootstrap.bind(5678).sync();
if (f.isSuccess()) {
log.info("Netty Start successful");
} else {
log.error("Netty Start failed");
}
// 等待服务监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 退出,释放线程资源
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
修改NettyClientController,在消息末尾增加\r\n:
@RestController
@RequestMapping("/client")
public class NettyClientController {
/**
* @description: 模拟向服务器发送消息
* @param
* @Author: wuyong
* @Date: 2019/08/30 14:10:09
* @return: java.lang.String
*/
@RequestMapping("/req")
public String req() {
String msg = "{\"msgType\":\"req\",\"clientId\":\"请求数据\"}\r\n";
for (int i = 0; i < 20; i++) {
NettyClientBootStrap.getSocketChannel().writeAndFlush(msg);
}
return "success";
}
}
再次测试,服务器的控制台输出如下:
2019-08-30 15:47:05.959 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.050 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.051 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.051 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.051 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.051 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.051 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.051 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.052 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.052 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.052 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.052 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.052 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.052 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.053 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.053 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.053 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.053 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.053 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
2019-08-30 15:47:06.053 INFO 216 --- [ntLoopGroup-1-1] com.wuychn.server.NettyServerHandler : 收到客户端消息:{"msgType":"req","clientId":"请求数据"}
可见一切正常。
同样的道理,服务器发送给客户端的消息也会出现粘包/拆包的问题,客户端也可以加上LineBasedFrameDecoder解码器,然后服务器发送的消息使用\r\n结尾即可解决。
如果客户端使用的Socket,可以做如下处理:
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("localhost", 5678);
// 向服务器发消息
OutputStream outputStream = socket.getOutputStream();
String msg = "{\"msgType\":\"req\",\"clientId\":\"请求数据\"}\r\n";
outputStream.write(msg.getBytes(CharsetUtil.UTF_8));
outputStream.flush();
// 模拟收到服务端粘包消息:服务器收到客户端消息后,循环回复20条消息
// 为了让小郭更明显,这里让客户端暂停1秒
Thread.sleep(1000);
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
char[] ch = new char[65536];
int len = -1;
while ((len = br.read(ch)) != -1) {
String result = new String(ch, 0, len);
System.out.println("原始消息:" + result); // 获取到的原始消息,是有粘包或者拆包的
String[] results = result.split("\r\n"); // 按照\r\n拆分,这里没有处理拆包的问题
for (String str : results) {
System.out.println("接收到消息:" + str);
}
}
}
}