第一个Netty程序
其实就是netty中example中的代码。代码结构为:EchoClient、EchoClientHandler、EchoServer和EchoServerHandler
代码如下,EchoServer:
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
int port = 10086;
EchoServer echoServer = new EchoServer(port);
System.out.println("服务器即将启动");
echoServer.start();
System.out.println("服务器关闭");
}
public void start() throws InterruptedException {
final EchoServerHandler serverHandler = new EchoServerHandler();
// 线程组
EventLoopGroup group = new NioEventLoopGroup(1);
try {
// 服务端启动必须
ServerBootstrap b = new ServerBootstrap();
// 将线程组传入
b.group(group)
// 指定使用NIO进行网络传输
.channel(NioServerSocketChannel.class)
// 指定服务器监听端口
.localAddress(new InetSocketAddress(port))
// 服务端每接收到一个连接请求,就会新启一个socket通信,也就是channel,所以下面这段代码的作用就是为这个子channel增加handle
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 添加到该子channel的pipeline的尾部
ch.pipeline().addLast(serverHandler);
}
});
// 异步绑定到服务器,sync()会阻塞直到完成
ChannelFuture f = b.bind().sync();
// 阻塞直到服务器的channel关闭
f.channel().closeFuture().sync();
} finally {
// 优雅关闭线程组
group.shutdownGracefully().sync();
}
}
}
EchoServerHandler:
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
// 客户端读到数据以后,就会执行
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server accept:" + in.toString(CharsetUtil.UTF_8));
ctx.write(in);
}
/*** 服务端读取完成网络数据后的处理*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
/*** 发生异常后的处理*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
EchoClient
public class EchoClient {
private final int port;
private final String host;
public EchoClient(int port, String host) {
this.port = port;
this.host = host;
}
public void start() throws InterruptedException {
// 线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 客户端启动必备
Bootstrap b = new Bootstrap();
// 把线程组传入
b.group(group)
// 指定使用NIO进行网络传输
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new EchoClientHandler());
// 连接到远程节点,阻塞直到连接完成
ChannelFuture f = b.connect().sync();
// 阻塞程序,直到Channel发生了关闭
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient(10086, "127.0.0.1").start();
}
}
EchoClientHandler:
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
// 客户端读到数据以后,就会执行
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("client accept: " + msg.toString(CharsetUtil.UTF_8));
}
// 连接建立以后
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty", CharsetUtil.UTF_8));
// ctx.fireChannelActive();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
}
}
重要的类、方法解析
EventLoop
EventLoop可以简单理解为一个线程、EventLoopGroup则可以理解为线程组。
EventLoopGroup group = new NioEventLoopGroup();
服务端使用的是:
ServerBootstrap b = new ServerBootstrap();
客户端使用的是:
Bootstrap b = new Bootstrap();
ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap则是由想要连接到远程节点的客户端应用程序所使用的。
引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个,因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。
Channel是Java NIO的一个基本构造。它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作。
目前,可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
事件和ChannelHandler、ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(serverHandler);
}
});
Netty使用不同的事件来通知我们状态的改变或者是操作的状态,这使得能够基于已经发生的事件来触发适当的动作。
可能由入站数据或者相关的状态更改而触发的事件包括:
- 连接已被激活或者连接失活;
- 数据读取;
- 用户事件;
- 错误事件。
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
- 打开或者关闭到远程节点的连接;
- 将数据写到或者冲刷到套接字。
每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。
Netty 提供了大量预定义的可以开箱即用的ChannelHandler实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。
ChannelFuture
Netty 中所有的I/O 操作都是异步的。
JDK预置了java.util.concurrent.Future接口,Future提供了一种在操作完成时通知应用程序的方式。它将在未来的某个时刻完成,并提供对其结果的访问。但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以Netty提供了它自己的实现ChannelFuture,用于在执行异步操作的时候使用。每个Netty的出站I/O操作都将返回一个ChannelFuture。
Netty为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相当繁琐的事务上本来会花费的时间与精力。下面会接着介绍http、udp和ws这几种方式。
实现HTTP
代码如下:
public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
private static final byte[] CONTENT = "hello world".getBytes();
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg;
FullHttpResponse response =
new DefaultFullHttpResponse(req.protocolVersion(), OK, Unpooled.wrappedBuffer(CONTENT));
response.headers()
.set(CONTENT_TYPE, TEXT_PLAIN)
.setInt(CONTENT_LENGTH, response.content().readableBytes());
ChannelFuture f = ctx.write(response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
public final class HttpHelloWorldServer {
public static void main(String[] args) throws Exception {
// 主从多线程Reactor模式
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) {
ChannelPipeline p = ch.pipeline();
// netty针对http编解码的处理类
p.addLast(new HttpServerCodec());
// netty针对http编解码的处理类
p.addLast(new HttpServerExpectContinueHandler());
// 自己的业务处理逻辑
p.addLast(new HttpHelloWorldServerHandler());
}
});
Channel ch = b.bind(10086).sync().channel();
System.err.println("Open your web browser and navigate to " + "http://127.0.0.1:10086");
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
然后浏览器访问http://127.0.0.1:10086,返回结果 hello world。
实现UDP
UDP协议
- 面向无连接的通讯协议;
- 通讯时不需要接收方确认,属于不可靠的传输;
- 因为不需要建立连接,所以传输速度快,但是容易丢失数据。
报文组成
- 源端口:源端口号,在需要对方回信时选用,不需要时可用全0。
- 目的端口:目的端口号,这在终点交付报文时必须要使用到。
- 长度:UDP用户数据包的长度,其最小值是8(仅有首部)。
- 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃。
- 数据
UDP是面向无连接的通讯协议,UDP报头由4个域组成,其中每个域各占用2个字节,其中包括目的端口号和源端口号信息,数据报的长度域是指包括报头和数据部分在内的总字节数,校验值域来保证数据的安全。由于通讯不需要连接,所以可以实现广播发送。
UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。
UDP与TCP位于同一层,但它不管数据包的顺序、错误或重发。因此,UDP不被应用于那些使用虚电路的面向连接的服务,UDP主要用于那些面向查询、应答的服务。
实现UDP单播
单播:定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。面向连接的协议和无连接协议都支持这种模式。代码如下:
public class UdpAnswerSide {
public final static String ANSWER = "here we go: ";
public void run(int port) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
// 和tcp的不同,udp没有接受连接的说法,所以即使是接收端,也使用Bootstrap
Bootstrap b = new Bootstrap();
// 由于我们用的是UDP协议,所以要用NioDatagramChannel来创建
b.group(group)
.channel(NioDatagramChannel.class)
.handler(new AnswerHandler());
// 没有接受客户端连接的过程,监听本地端口即可
ChannelFuture f = b.bind(port).sync();
System.out.println("answer service start.");
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 10086;
new UdpAnswerSide().run(port);
}
}
public class AnswerHandler extends SimpleChannelInboundHandler<DatagramPacket> {
// 应答的具体内容从常量字符串数组中取得,由nextQuote方法随机获取
private static final String[] DICTIONARY = {
"若不披上这件衣裳,众生又怎知我尘缘已断,金海尽干。",
"只要心中还有放不下的偶像,终有一天,它将化为修行路上的无解业障。",
"忘了,是新添的垢;记得,是老套的旧。",
"祸乱人心,倒果为因,师兄如此执着于输赢,可笑,可悲!",
"人也,兽也,佛也,妖也,众生自有根器,持优劣为次第,可乱来不得。你说,对吗?孙悟空"
};
private static final Random r = new Random();
private String nextQuote() {
return DICTIONARY[r.nextInt(DICTIONARY.length - 1)];
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
// 获得请求
String req = packet.content().toString(CharsetUtil.UTF_8);
if (UdpQuestionSide.QUESTION.equals(req)) {
String answer = UdpAnswerSide.ANSWER + nextQuote();
System.out.println("receive message: " + req);
// 重新 new 一个DatagramPacket对象,我们通过packet.sender()来获取发送者的消息。重新发送出去!
ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(answer, CharsetUtil.UTF_8), packet.sender()));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
cause.printStackTrace();
}
}
public class UdpQuestionSide {
public final static String QUESTION = "give me more.";
public void run(int port) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
// 由于我们用的是UDP协议,所以要用NioDatagramChannel来创建
.channel(NioDatagramChannel.class)
.handler(new QuestoinHandler());
// 不需要建立连接
Channel ch = b.bind(0).sync().channel();
// 将UDP请求的报文以DatagramPacket打包发送给接受端
ch.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(QUESTION, CharsetUtil.UTF_8),
new InetSocketAddress("127.0.0.1", port)))
.sync();
// 不知道接收端能否收到报文,也不知道能否收到接收端的应答报文
// 所以等待15秒后,不再等待,关闭通信
if (!ch.closeFuture().await(15000)) {
System.out.println("timeout.");
}
} catch (Exception e) {
group.shutdownGracefully();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int answerPort = 10086;
new UdpQuestionSide().run(answerPort);
}
}
public class QuestoinHandler extends SimpleChannelInboundHandler<DatagramPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg)
throws Exception {
// 获得应答,DatagramPacket提供了content()方法取得报文的实际内容
String response = msg.content().toString(CharsetUtil.UTF_8);
if (response.startsWith(UdpAnswerSide.ANSWER)) {
System.out.println(response);
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
answer日志输出:
answer service start.
receive message: give me more.
receive message: give me more.
receive message: give me more.
question日志输出:
here we go: 若不披上这件衣裳,众生又怎知我尘缘已断,金海尽干。
实现UDP广播
广播:传输到网络(或者子网)上的所有主机。
这里不做具体代码演示,因为和单播区别不大,相对重要的就是ip需要改为255.255.255.255。
实现WebSocket
代码如下:
Server:
public final class WebSocketServer {
// 创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上
private final static ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
static final boolean SSL = false;
// 通过ssl访问端口为8443,否则为8080
static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080"));
public static void main(String[] args) throws Exception {
// SSL配置
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslCtx = null;
}
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(sslCtx, channelGroup));
Channel ch = b.bind(PORT).sync().channel();
System.out.println("打开浏览器访问: " + (SSL ? "https" : "http") + "://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
private final ChannelGroup group;
/*websocket访问路径*/
private static final String WEBSOCKET_PATH = "/websocket";
private final SslContext sslCtx;
public WebSocketServerInitializer(SslContext sslCtx, ChannelGroup group) {
this.sslCtx = sslCtx;
this.group = group;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
// 增加对http的支持
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
// Netty提供,支持WebSocket应答数据压缩传输
pipeline.addLast(new WebSocketServerCompressionHandler());
// Netty提供,对整个websocket的通信进行了初始化(发现http报文中有升级为websocket的请求),包括握手,以及以后的一些通信控制
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));
// 浏览器访问时展示index页面
pipeline.addLast(new ProcessWsIndexPageHandler(WEBSOCKET_PATH));
// 对websocket的数据进行处理
pipeline.addLast(new ProcessWsFrameHandler(group));
}
}
public class ProcessWsFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ChannelGroup group;
public ProcessWsFrameHandler(ChannelGroup group) {
this.group = group;
}
private static final Logger logger
= LoggerFactory.getLogger(ProcessWsFrameHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx,
WebSocketFrame frame) throws Exception {
// 判断是否为文本帧,目前只处理文本帧
if (frame instanceof TextWebSocketFrame) {
// Send the uppercase string back.
String request = ((TextWebSocketFrame) frame).text();
logger.info("{} received {}", ctx.channel(), request);
ctx.channel().writeAndFlush(
new TextWebSocketFrame(request.toUpperCase(Locale.CHINA)));
// 群发实现:一对一道理一样
group.writeAndFlush(new TextWebSocketFrame(
"Client " + ctx.channel() + " say:" + request.toUpperCase(Locale.CHINA)));
} else {
String message = "unsupported frame type: " + frame.getClass().getName();
throw new UnsupportedOperationException(message);
}
}
// 重写 userEventTriggered()方法以处理自定义事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 检测事件,如果是握手成功事件,做点业务处理
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
// 通知所有已经连接的WebSocket 客户端新的客户端已经连接上了
group.writeAndFlush(new TextWebSocketFrame(
"Client " + ctx.channel() + " joined"));
// 将新的 WebSocket Channel 添加到 ChannelGroup 中,
// 以便它可以接收到所有的消息
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
}
public class ProcessWsIndexPageHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String websocketPath;
public ProcessWsIndexPageHandler(String websocketPath) {
this.websocketPath = websocketPath;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
// 处理错误或者无法解析的http请求
if (!req.decoderResult().isSuccess()) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
}
// 只允许Get请求
if (req.method() != GET) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
return;
}
// 发送index页面的内容
if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {
//生成WebSocket的访问地址,写入index页面中
String webSocketLocation = getWebSocketLocation(ctx.pipeline(), req, websocketPath);
System.out.println("WebSocketLocation:[" + webSocketLocation + "]");
//生成index页面的具体内容,并送往浏览器
ByteBuf content = MakeIndexPage.getContent(webSocketLocation);
FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, req, res);
} else {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
// 发送应答
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
// 错误的请求进行处理 (code<>200).
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 发送应答.
ChannelFuture f = ctx.channel().writeAndFlush(res);
//对于不是长连接或者错误的请求直接关闭连接
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
/*根据用户的访问,告诉用户的浏览器,WebSocket的访问地址*/
private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {
String protocol = "ws";
if (cp.get(SslHandler.class) != null) {
protocol = "wss";
}
return protocol + "://" + req.headers().get(HttpHeaderNames.HOST) + path;
}
}
public final class MakeIndexPage {
private static final String NEWLINE = "\r\n";
public static ByteBuf getContent(String webSocketLocation) {
return Unpooled.copiedBuffer(
"<html><head><title>Web Socket Test</title></head>"
+ NEWLINE +
"<body>" + NEWLINE +
"<script type=\"text/javascript\">" + NEWLINE +
"var socket;" + NEWLINE +
"if (!window.WebSocket) {" + NEWLINE +
" window.WebSocket = window.MozWebSocket;" + NEWLINE +
'}' + NEWLINE +
"if (window.WebSocket) {" + NEWLINE +
" socket = new WebSocket(\"" + webSocketLocation + "\");"
+ NEWLINE +
" socket.onmessage = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + '\\n' + event.data" + NEWLINE +
" };" + NEWLINE +
" socket.onopen = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = \"Web Socket opened!\";" + NEWLINE +
" };" + NEWLINE +
" socket.onclose = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + \"Web Socket closed\"; "
+ NEWLINE +
" };" + NEWLINE +
"} else {" + NEWLINE +
" alert(\"Your browser does not support Web Socket.\");"
+ NEWLINE +
'}' + NEWLINE +
NEWLINE +
"function send(message) {" + NEWLINE +
" if (!window.WebSocket) { return; }" + NEWLINE +
" if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
" socket.send(message);" + NEWLINE +
" } else {" + NEWLINE +
" alert(\"The socket is not open.\");" + NEWLINE +
" }" + NEWLINE +
'}' + NEWLINE +
"</script>" + NEWLINE +
"<form onsubmit=\"return false;\">" + NEWLINE +
"<input type=\"text\" name=\"message\" " +
"value=\"Hello, World!\"/>" +
"<input type=\"button\" value=\"Send Web Socket Data\""
+ NEWLINE +
" onclick=\"send(this.form.message.value)\" />"
+ NEWLINE +
"<h3>Output</h3>" + NEWLINE +
"<textarea id=\"responseText\" " +
"style=\"width:500px;height:300px;\"></textarea>"
+ NEWLINE +
"</form>" + NEWLINE +
"</body>" + NEWLINE +
"</html>" + NEWLINE, CharsetUtil.US_ASCII);
}
}
client:
public final class WebSocketClient {
static final String URL = System.getProperty("url", "ws://127.0.0.1:8080/websocket");
static final String SURL = System.getProperty("url", "wss://127.0.0.1:8443/websocket");
public static void main(String[] args) throws Exception {
URI uri = new URI(URL);
String scheme = uri.getScheme() == null ? "ws" : uri.getScheme();
final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
final int port = uri.getPort();
if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) {
System.err.println("Only WS(S) is supported.");
return;
}
final boolean ssl = "wss".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
EventLoopGroup group = new NioEventLoopGroup();
try {
// Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
// If you change it to V00, ping is not supported and remember to change
// HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
final WebSocketClientHandler handler =
new WebSocketClientHandler(
WebSocketClientHandshakerFactory
.newHandshaker(
uri, WebSocketVersion.V13,
null,
true,
new DefaultHttpHeaders()));
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(),
host, port));
}
p.addLast(
//http协议为握手必须
new HttpClientCodec(),
new HttpObjectAggregator(8192),
//支持WebSocket数据压缩
WebSocketClientCompressionHandler.INSTANCE,
handler);
}
});
// 连接服务器
Channel ch = b.connect(uri.getHost(), port).sync().channel();
// 等待握手完成
handler.handshakeFuture().sync();
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String msg = console.readLine();
if (msg == null) {
break;
} else if ("bye".equals(msg.toLowerCase())) {
ch.writeAndFlush(new CloseWebSocketFrame());
ch.closeFuture().sync();
break;
} else if ("ping".equals(msg.toLowerCase())) {
WebSocketFrame frame = new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[]{8, 1, 8, 1}));
ch.writeAndFlush(frame);
} else {
WebSocketFrame frame = new TextWebSocketFrame(msg);
ch.writeAndFlush(frame);
}
}
} finally {
group.shutdownGracefully();
}
}
}
public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
// 负责和服务器进行握手
private final WebSocketClientHandshaker handshaker;
// 握手的结果
private ChannelPromise handshakeFuture;
public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
// 当前Handler被添加到ChannelPipeline时,
// new出握手的结果的实例,以备将来使用
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
handshakeFuture = ctx.newPromise();
}
// 通道建立,进行握手
@Override
public void channelActive(ChannelHandlerContext ctx) {
handshaker.handshake(ctx.channel());
}
// 通道关闭
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("WebSocket Client disconnected!");
}
// 读取数据
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel ch = ctx.channel();
// 握手未完成,完成握手
if (!handshaker.isHandshakeComplete()) {
try {
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
System.out.println("WebSocket Client connected!");
handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException e) {
System.out.println("WebSocket Client failed to connect");
handshakeFuture.setFailure(e);
}
return;
}
// 握手已经完成,升级为了websocket,不应该再收到http报文
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"Unexpected FullHttpResponse (getStatus=" + response.status() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
// 处理websocket报文
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
System.out.println("WebSocket Client received message: " + textFrame.text());
} else if (frame instanceof PongWebSocketFrame) {
System.out.println("WebSocket Client received pong");
} else if (frame instanceof CloseWebSocketFrame) {
System.out.println("WebSocket Client received closing");
ch.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
if (!handshakeFuture.isDone()) {
handshakeFuture.setFailure(cause);
}
ctx.close();
}
}
客户端可以使用上面代码,或者网页访问http://127.0.0.1:8080/。
简单的日志如下:
WebSocket Client connected!
123
WebSocket Client received message: 123
WebSocket Client received message: Client [id: 0xc2015e3d, L:/127.0.0.1:8080 - R:/127.0.0.1:56790] say:123
WebSocket Client received message: Client [id: 0x65dee4ca, L:/127.0.0.1:8080 - R:/127.0.0.1:56912] joined
WebSocket Client received message: Client [id: 0x65dee4ca, L:/127.0.0.1:8080 - R:/127.0.0.1:56912] say:HELLO, WORLD!