需求描述: 现在有个websocket的客户端给我方服务端发数据 我方服务端收到数据以后 需要转发给另一个服务端
使用的框架:netty5 (知道这个废弃了,是后期才知道的换了有点麻烦不过他们实现都差不多)
启动类:
import java.util.Date;
import javax.servlet.http.HttpServlet;
public class InitThread extends HttpServlet{
private static final long serialVersionUID = -8358167536605440315L;
public static Client bootstrap=new Client("你请求的地址", 你请求的端口);
public void init(){
try {
System.out.println(new Date()+"开始创建转发客户端");
bootstrap.create();
SocketService service = new SocketService(8080);
System.out.println(new Date()+"开始创建Socket服务");
service.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端:
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.handler.codec.string.StringEncoder;
public class SocketService {
//程序启动时候就声明一个线程池
public static final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
private final static ArrayList<String> datas = new ArrayList<String>();
private final int port;
public SocketService(int port) {
this.port = port;
}
public void start(){
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
// 有数据立即发送
sb.option(ChannelOption.TCP_NODELAY, true);
// 保持连接
sb.childOption(ChannelOption.SO_KEEPALIVE, true);
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.print(new Date()+"报告信息:有一客户端链接到本服务端");
System.out.println("IP:" + ch.remoteAddress().getHostName()+"Port:" + ch.remoteAddress().getPort());
System.out.println("报告完毕");
ch.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
ch.pipeline().addLast(new SocketServiceThread()); // 客户端触发操作
ch.pipeline().addLast(new ByteArrayEncoder());
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(new Date()+":"+SocketService.class + "启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
} catch(Exception e){
System.out.println(new Date()+":服务端出现异常了:"+e.toString());
}finally {
try {
group.shutdownGracefully().sync();
bossGroup.shutdownGracefully().sync();// 释放线程池资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
SocketServiceThread
import java.util.ArrayList;
import java.util.Date;
import org.apache.commons.codec.binary.Hex;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
public class SocketServiceThread extends ChannelHandlerAdapter {
static Logger log = Logger.getLogger(SocketServiceThread.class.getName());
/*
* channelAction
*
* channel 通道 action 活跃的
*
* 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*
*/
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress().toString() + " 通道已激活!");
}
/*
* channelInactive
*
* channel 通道 Inactive 不活跃的
*
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*
*/
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress().toString() + "客户端已断开!");
// 关闭流
ctx.close();
}
/**
* 功能:读取服务器发送过来的信息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg){
try {
ByteBuf in = (ByteBuf) msg;
byte[] receiveMsgBytes = new byte[in.readableBytes()];
in.readBytes(receiveMsgBytes);
//转发
TranspondThread transpondThread=new TranspondThread();
transpondThread.setTranspondData(receiveMsgBytes);
SocketService.cachedThreadPool.execute(transpondThread);
String message = Hex.encodeHexString(receiveMsgBytes).toUpperCase();
log.info("接收数据:"+message);
//把数据存入缓存
SocketService.datas.add(message);
if(SocketService.datas.size()>99){
System.out.println("开始插入数据");
//写入自己的数据库
WriteThread writeThread=new WriteThread();
writeThread.setData((ArrayList<String>)SocketService.datas.clone());
SocketService.cachedThreadPool.execute(writeThread);
SocketService.datas.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
/**
* ByteBuf是一个引用计数对象,这个对象必须显示地调用release()方法来释放。
* 请记住处理器的职责是释放所有传递到处理器的引用计数对象。
*/
ReferenceCountUtil.release(msg);
}
}
/**
* 功能:读取完毕客户端发送过来的数据之后的操作
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 读取完客户数据以后反馈给客户端信息
String response = "0";
ByteBuf encoded = ctx.alloc().buffer(4 * response.length());
encoded.writeBytes(response.getBytes());
ctx.writeAndFlush(encoded);
log.info("服务端接收数据完毕..");
System.out.println(new Date()+":服务端接收数据完毕..");
}
/**
* 功能:服务端发生异常的操作
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(new Date()+"异常信息:" + cause.getMessage());
ctx.close();
}
}
channelRead 方法里面是主要处理类
这里面处理有两个操作 第一个转发
TranspondThread transpondThread=new TranspondThread();
transpondThread.setTranspondData(receiveMsgBytes);
SocketService.cachedThreadPool.execute(transpondThread);
第二个插入处理
//把数据存入缓存
SocketService.datas.add(message);
if(SocketService.datas.size()>99){
System.out.println("开始插入数据");
//写入自己的数据库
WriteThread writeThread=new WriteThread();
writeThread.setData((ArrayList<String>)SocketService.datas.clone());
SocketService.cachedThreadPool.execute(writeThread);
SocketService.datas.clear();
}
可以看到我把这两个处理分别放入了不同的线程里面这样可以提高服务端接收客户端的速率 这样转发速度和插入速度不会影响到接收速度
现在看下转发客户端
TranspondThread (这里面有个问题我想说一下就是 如果是单独做一个客户端的话断线重连问题://重连不能直接调用create方法不然会出现大量线程累积处于等待状态情况(差点被坑了))
import java.util.Date;
import org.apache.commons.codec.binary.Hex;
import org.apache.log4j.Logger;
public class TranspondThread implements Runnable {
private byte[] transpondData;
public byte[] getTranspondData() {
return transpondData;
}
public void setTranspondData(byte[] transpondData) {
this.transpondData = transpondData;
}
@Override
public void run() {
try {
int number=0;
do{
if(number>0){
log.info("开始第"+number+"次补发");
System.out.println(new Date()+":开始第"+number+"次补发");
}
if(Client.socketChannel.isActive()){
Client.socketChannel.writeAndFlush(transpondData);
InitThread.transpondCont++;
}else{
System.out.println("通道不活跃,重新连接");
do{
//重连不能直接调用create方法不然会出现大量线程累积处于等待状态情况
InitThread.bootstrap.connect();
number++;
}while(!Client.socketChannel.isActive()&&number<=3);
if(Client.socketChannel.isActive()){
Client.socketChannel.writeAndFlush(transpondData);
InitThread.transpondCont++;
}
}
number++;
}while(Client.result==null&&number<=3);
if(Client.result==null&&number>3){
System.out.println(new Date()+"已补发三次,丢弃数据:"+Hex.encodeHexString(this.transpondData).toUpperCase());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Client 这个客户端代码其实和服务端大同小异唯一区别就是我它拆分了
import java.nio.charset.Charset;
import java.util.Date;
import org.apache.log4j.Logger;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.codec.bytes.ByteArrayEncoder;
import io.netty.handler.codec.string.StringEncoder;
public class Client {
public static String result;
public static SocketChannel socketChannel;
private Bootstrap b = new Bootstrap();
private EventLoopGroup workerGroup = new NioEventLoopGroup();
private String host;
private int port;
public Client(String host,int port){
this.host=host;
this.port=port;
}
public synchronized void create(){
addGroup();
connect();
}
public synchronized void addGroup(){
try {
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE,true);
b.option(ChannelOption.TCP_NODELAY, true);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ClientHandler clientHandler=new ClientHandler();
ch.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
ch.pipeline().addLast(clientHandler);
ch.pipeline().addLast(new ByteArrayEncoder());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void connect(){
try {
ChannelFuture f = b.connect(this.host, this.port).sync();
if (!f.isSuccess()) {
System.out.println(new Date()+":连接失败!");
}else{
socketChannel = (SocketChannel)f.sync().channel();
System.out.println("connect server 成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.util.Date;
import org.apache.commons.codec.binary.Hex;
import org.apache.log4j.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class ClientHandler extends ChannelHandlerAdapter {
static Logger log = Logger.getLogger(ClientHandler.class.getName());
//读取服务端反馈的数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf result = (ByteBuf) msg;
try {
byte[] result1 = new byte[result.readableBytes()];
result.readBytes(result1);
Client.result=Hex.encodeHexString(result1);
Hex.encodeHexString(result1));
} finally {
result.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 当出现异常就关闭连接
System.out.println(new Date()+":出现异常");
ctx.close();
}
// 连接成功后,向server发送消息
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println(new Date()+":客户端发送数据完毕..");
}
}
拆包粘包问题解决方案
拆包粘包问题其实就是客户端那边发来的数据因为一些原因 一次数据被分成了两次或多次接收完 或者是一次接收到多条数据
这个问题的解决方法 是先把收到的数据缓存起来然后再按照约定的规则截取成自己想要的数据