一、客户端如何建立连接并监听与发送数据
下面的代码例子教你如何建立与服务器的连接并发送与接收数据。
1.服务器代码。我这里的服务器代码是基于原生的serverSocket,每建立一个客户端连接会启动一个线程去读取与发送数据。
import java.io.*;
import java.net.*;
public class SocketServer {
public static final int PORT = 8031;//监听的端口号
public static Boolean Open = true;
public static void main(String[] args) {
System.out.println("服务器启动...\n");
SocketServer server = new SocketServer();
server.init();
}
public void init() {
try {
ServerSocket serverSocket = new ServerSocket(PORT);
while (Open) {
// 一旦有堵塞, 则表示服务器与客户端获得了连接
Socket client = serverSocket.accept();
// 处理这次连接
new HandlerThread(client);
}
serverSocket.close();
} catch (Exception e) {
System.out.println("服务器异常: " + e.getMessage());
}
}
private class HandlerThread implements Runnable {
private Socket socket;
public HandlerThread(Socket client) {
socket = client;
new Thread(this).start();
}
public void run() {
System.out.println("服务器与客户端获得了连接 " );
try {
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while(Open){
int length = input.available();
if(length>0){
byte[] bs = new byte[length];
input.read(bs); // 读取客户端数据,这里要注意和客户端输出流的写方法对应,客户端发送的是字节,读取的应该也是字节,同时编码方式要一致
String clientInputStr = new String(bs,"utf-8");//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException或者读取不到数据
System.out.println("客户端发过来的内容:" + clientInputStr);// 处理客户端数据
String s = "Data from server:"+clientInputStr+"\n";
//如果客户端发送的是close,执行关闭连接
if(clientInputStr.equals("close")){
Open = false;
s = "close\n";
}
// 向客户端回复信息 ,
out.write(s.getBytes("utf-8"));
out.flush();
System.out.println("发送给客户端数据:"+s);
}
}
out.close();
input.close();
} catch (Exception e) {
Open = false;
System.out.println("服务器 run 异常: " + e.getMessage());
}
}
}
}
2.客户端代码。作用是建立一个与客户端的连接,监听键盘输入的数据发送到服务器去,并监听打印服务器返回的数据,不多说,注释写的很清楚了。
public static Channel channel;
public static boolean open = true;
public static EventLoopGroup workerGroup;
public static void init() {
String host = "localhost";
int port = 8031;
workerGroup = new NioEventLoopGroup();//工作组,用于处理连接通信
try {
Bootstrap b = new Bootstrap(); // (1)引导程序,用于设置参数
b.group(workerGroup); // (2)添加工作组
b.channel(NioSocketChannel.class); // (3)添加通道,通道用于接收进来的连接
b.option(ChannelOption.TCP_NODELAY, true); // (4)设置通道连接参数
b.handler(new ChannelInitializer<SocketChannel>() {//ChannelInitializer是用于配置通道
@Override
public void initChannel(SocketChannel ch) throws Exception {
//主要是用于通过添加handlers 配置ChannelPipeline来实现应用网络逻辑
//在这里实现添加handlers到指定位置实现数据逻辑处理而已
ch.pipeline().addLast(new ClientDataHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
channel = channelFuture.channel();
new Thread(new Runnable() {
@Override
public void run() {
//启动线程进行数据输入发送,必须是新的线程,否则将会阻塞显示导致无法接收数据
getData();
}
}).start();
}
private void getData() {
while (open) {
try {
System.out.print("请输入: \t");
//获取键盘输入的数据发送给服务器
String str = new BufferedReader(new InputStreamReader(System.in)).readLine();
if (channel != null) {
channel.writeAndFlush(Unpooled.copiedBuffer(str.getBytes("utf-8")));
}
//如果输入为close,就关闭连接
if (str.equals("close")) {
closed();
}
Thread.sleep(1000);
} catch (Exception e) {
open = false;
System.out.println("客户端异常:" + e.getMessage());
} finally {
}
}
}
});
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} catch (Exception e) {
closed();
System.out.println("Exception:" + e.toString());
}
}
public static void closed() {
System.out.println("closed" );
open = false;
workerGroup.shutdownGracefully();
}
3.数据监听。在ClientDataHandler进行监听:
public class ClientDataHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte [] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String message = new String(req,"UTF-8");
System.out.println("来自服务器的数据:"+ message);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//释放资源
ctx.close();
}
}
4.打印结果:
客户端:
请输入: hello
来自服务器的数据:Data from server:hello
请输入: i am bifan
来自服务器的数据:Data from server:i am bifan
请输入:
服务器端:
服务器启动...
服务器与客户端获得了连接
客户端发过来的内容:hello
发送给客户端数据:Data from server:hello
客户端发过来的内容:i am bifan
发送给客户端数据:Data from server:i am bifan
5.添加编码器与解码器。上面我们发送与接收的是字节数据,但是我们处理的是字符串,所以对发送与接收的字符串进行了编码与解码,如果添加了编码器与解码器,那么就不用字节处理了。
添加发送编码器与接收解码器:
@Override
public void initChannel(SocketChannel ch) throws Exception {
//主要是用于通过添加handlers 配置ChannelPipeline来实现应用网络逻辑
//在这里实现添加handlers到指定位置实现数据逻辑处理而已
//添加发送数据编码器
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
//添加接收数据解码器
ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
ch.pipeline().addLast(new ClientDataHandler());
}
发送数据时就可以直接发送字符串了:
//获取键盘输入的数据发送给服务器
String str = new BufferedReader(new InputStreamReader(System.in)).readLine();
if (channel != null) {
channel.writeAndFlush(str);//直接发送字符串数据
}
接收数据处理也可以直接处理字符串:
public class ClientDataHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println("来自服务器的数据:"+ message);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//释放资源
ctx.close();
}
}
也可以使用自己自定义的编码器与解码器,只需要继承MessageToMessageDecoder与MessageToMessageEncoder就可以,可以实现发送与接收对象等自定义的格式。
二、使用IdleStateHandler发送心跳包
IdleStateHandler是空闲状态处理,顾明思意就是在不跟服务交流的时间,如果处于某一状态,可以做相应的处理,那就可以利用这个去发送心跳包。
添加方式一样:
ch.pipeline().addLast(new IdleStateHandler(0,0,5));
三个参数:
int readerIdleTimeSeconds:读取空闲,超过这个时间没有读取来自服务器的数据,将触发
int writerIdleTimeSeconds:写出空闲,超过这个时间没有发送数据,将触发
int allIdleTimeSeconds:整体空闲将触发
触发将在userEventTriggered中触发。
下面我们先创建一个包括处理心跳包的HeartBeatHandler,这个是集成了SimpleChannelInboundHandler,因为上面我们的例子最终处理的是字符串,具体看注释,挺详细的了:
public class HeartBeatHandler extends SimpleChannelInboundHandler<String> {
public static final String HearBeatSendToServer = "HearBeatSendToServer";//发送的心跳消息
public static final String HearBeatSendFromServer = "HearBeatSendFromServer";//服务器心跳的回复
/**
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//在这个方法里,如果空闲触发,我们将发送心跳包
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
switch (e.state()) {
case READER_IDLE://读取空闲
handleReaderIdle(ctx);
break;
case WRITER_IDLE://写出空闲
handleWriterIdle(ctx);
break;
case ALL_IDLE://整体空闲
handleAllIdle(ctx);
break;
default:
break;
}
}
}
/**
* @param ctx 读取超时处理
*/
protected void handleReaderIdle(ChannelHandlerContext ctx) {
}
/**
* @param ctx 写超时处理
*/
protected void handleWriterIdle(ChannelHandlerContext ctx) {
}
/**
* @param ctx 整体超时处理
*/
protected void handleAllIdle(ChannelHandlerContext ctx) {
//超时了,发送一个心跳
sendHearBeatToServer(ctx);
}
/**
* @param channelHandlerContext
* @param s
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
if (s.equals(HearBeatSendToServer)) {//这个是发送心跳的消息,直接发送心跳包
sendHearBeatToServer(channelHandlerContext);
} else if (s.equals(HearBeatSendFromServer)) {//这个说明收到了服务器对心跳包的响应
onReceiveHearBeatResponseFromServer(channelHandlerContext);
} else {//这个是收到了其他数据
handleData(channelHandlerContext, s);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//释放资源
ctx.close();
}
/**
* 消息业务逻辑处理
*
* @param channelHandlerContext
* @param s
*/
protected void handleData(ChannelHandlerContext channelHandlerContext, String data) {
}
/**
* @param channelHandlerContext 收到来自服务器的心跳响应
*/
protected void onReceiveHearBeatResponseFromServer(ChannelHandlerContext channelHandlerContext) {
System.out.println("收到来自服务器的心跳响应:" + HearBeatSendFromServer);
}
/**
* @param channelHandlerContext 发送心跳到服务器
*/
protected void sendHearBeatToServer(ChannelHandlerContext channelHandlerContext) {
channelHandlerContext.writeAndFlush(HearBeatSendToServer);
}
}
我们的ClientDataHandler只需要继承HeartBeatHandler处理我们的业务数据即可:
ublic class ClientDataHandler extends HeartBeatHandler{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
@Override
protected void handleData(ChannelHandlerContext channelHandlerContext, String s) {
super.handleData(channelHandlerContext, s);
System.out.println("收到来自服务器数据:"+s);
}
}
最终,我们的客户端带心跳处理的完整代码如下:
public static Channel channel;
public static boolean open = true;
public static EventLoopGroup workerGroup;
public static void init() {
String host = "localhost";
int port = 8032;
workerGroup = new NioEventLoopGroup();//工作组,用于处理连接通信
try {
Bootstrap b = new Bootstrap(); // (1)引导程序,用于设置参数
b.group(workerGroup); // (2)添加工作组
b.channel(NioSocketChannel.class); // (3)添加通道,通道用于接收进来的连接
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)设置通道连接参数
b.handler(new ChannelInitializer<SocketChannel>() {//ChannelInitializer是用于配置通道
@Override
public void initChannel(SocketChannel ch) throws Exception {
//主要是用于通过添加handlers 配置ChannelPipeline来实现应用网络逻辑
//在这里实现添加handlers到指定位置实现数据逻辑处理而已
//添加发送数据编码器
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
//添加接收数据解码器
ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
//添加空闲处理
ch.pipeline().addLast(new IdleStateHandler(0,0,5));
//添加到空闲发送心跳包的数据处理Handler
ch.pipeline().addLast(new ClientDataHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
channel = channelFuture.channel();
new Thread(new Runnable() {
@Override
public void run() {
//启动线程进行数据输入发送,必须是新的线程,否则将会阻塞显示导致无法接收数据
getData();
}
}).start();
}
private void getData() {
while (open) {
try {
System.out.print("请输入: \t");
//获取键盘输入的数据发送给服务器
String str = new BufferedReader(new InputStreamReader(System.in)).readLine();
if (channel != null) {
channel.writeAndFlush(str);
}
//如果输入为close,就关闭连接
if (str.equals("close")) {
closed();
}
Thread.sleep(1000);
} catch (Exception e) {
open = false;
System.out.println("客户端异常:" + e.getMessage());
} finally {
}
}
}
});
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} catch (Exception e) {
closed();
System.out.println("Exception:" + e.toString());
}
}
public static void closed() {
System.out.println("closed" );
open = false;
workerGroup.shutdownGracefully();
}
服务器也需要修改下,需要响应客户端发送的心跳包,整体代码如下:
import java.io.*;
import java.net.*;
public class SocketServer {
public static final int PORT = 8032;//监听的端口号
public static Boolean Open = true;
public static void main(String[] args) {
System.out.println("服务器启动...\n");
SocketServer server = new SocketServer();
server.init();
}
public void init() {
try {
ServerSocket serverSocket = new ServerSocket(PORT);
while (Open) {
// 一旦有堵塞, 则表示服务器与客户端获得了连接
Socket client = serverSocket.accept();
// 处理这次连接
new HandlerThread(client);
}
serverSocket.close();
} catch (Exception e) {
System.out.println("服务器异常: " + e.getMessage());
}
}
private class HandlerThread implements Runnable {
private Socket socket;
public HandlerThread(Socket client) {
socket = client;
new Thread(this).start();
}
public void run() {
System.out.println("服务器与客户端获得了连接 " );
try {
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while(Open){
int length = input.available();
if(length>0){
byte[] bs = new byte[length];
input.read(bs); // 读取客户端数据,这里要注意和客户端输出流的写方法对应,客户端发送的是字节,读取的应该也是字节,同时编码方式要一致
String clientInputStr = new String(bs,"utf-8");//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException或者读取不到数据
System.out.println("客户端发过来的内容:" + clientInputStr);// 处理客户端数据
String s = "Data from server:"+clientInputStr+"\n";
//如果客户端发送的是close,执行关闭连接
if(clientInputStr.equals("close")){
Open = false;
s = "close\n";
}
if(clientInputStr.equals("HearBeatSendToServer")){
s = "HearBeatSendFromServer";//如果是客户端的心跳消息,发送响应
}
// 向客户端回复信息 ,
out.write(s.getBytes("utf-8"));
out.flush();
System.out.println("发送给客户端数据:"+s);
}
}
out.close();
input.close();
} catch (Exception e) {
Open = false;
System.out.println("服务器 run 异常: " + e.getMessage());
}
}
}
}
运行代码,效果如下:
客户端:
请输入: 收到来自服务器的心跳响应:HearBeatSendFromServer
gg收到来自服务器的心跳响应:HearBeatSendFromServer
收到来自服务器数据:Data from server:gg
请输入: dd
收到来自服务器数据:Data from server:dd
请输入: gg
收到来自服务器数据:Data from server:gg
//空闲后会发送心跳包并收到响应
请输入: 收到来自服务器的心跳响应:HearBeatSendFromServer
服务器端:
服务器与客户端获得了连接
客户端发过来的内容:HearBeatSendToServer
发送给客户端数据:HearBeatSendFromServer
客户端发过来的内容:HearBeatSendToServer
发送给客户端数据:HearBeatSendFromServer
客户端发过来的内容:gg
发送给客户端数据:Data from server:gg
客户端发过来的内容:dd
发送给客户端数据:Data from server:dd
客户端发过来的内容:gg
发送给客户端数据:Data from server:gg
客户端发过来的内容:HearBeatSendToServer
如果断线了,如何自动重连?在ClientDataHandler的channelInactive会触发断线事件:
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
NettyClient.doReConnect();//执行重连
}
public static void doReConnect(){
if (channel != null && channel.isActive()) {
return;
}
ChannelFuture future = b.connect(host, port);//b是Bootstrap
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture futureListener) throws Exception {
if (futureListener.isSuccess()) {
channel = futureListener.channel();//重连成功
} else {
futureListener.channel().eventLoop().schedule(new Runnable() {//十秒后重新连接
@Override
public void run() {
doReConnet();
}
}, 10, TimeUnit.SECONDS);
}
}
});
}
三、一些心得笔记
Bootstrap:用于指导客户端使用channel的引导程序,TCP连接可以使用提供的connect执行连接。
EventLoopGroup: 工作组,用于调度对应的处理channel到对应的事件去。
1.感谢https://segmentfault.com/a/1190000006931568#articleHeader6,学习了不少。
2.api文档很完善,http://netty.io/4.1/api/index.html
3.转载注明:https://blog.csdn.net/u014614038/article/details/80263713