Netty是基于异步非阻塞I/O, 并使用事件驱动, 具有管道-过滤器架构风格的网络编程SDK.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 java.net.InetSocketAddress;
/
* 简单的Netty服务器示例
*/
public class EchoServer {
// 端口
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
new EchoServer(9998).start();
}
/
* 服务器启动
*/
public void start() throws Exception {
// 服务器响应处理器
final EchoServerHandler serverHandler = new EchoServerHandler();
// 事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap boot = new ServerBootstrap();
boot.group(group)
// 使用NIO的服务Socket通道
.channel(NioServerSocketChannel.class)
// 本地地址和端口
.localAddress(new InetSocketAddress(port))
// 服务的子处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 通道绑定响应处理器
sc.pipeline().addLast(serverHandler);
}
});
// 绑定服务器,使用同步sync方法等待到绑定完成
ChannelFuture f = boot.bind().sync();
// 服务器关闭同步
f.channel().closeFuture().sync();
} finally {
// 释放所有资源
group.shutdownGracefully().sync();
}
}
}
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/
* 简单的Netty服务器示例
*/
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
// 读取Channel的数据
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
// 并进行回写,但不Flush
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 读完成之后才进行Flush,并关闭Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 发生异常,关闭Channel
ctx.close();
}
}
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 java.net.InetSocketAddress;
/
* 简单的Netty客户端示例
*/
public class EchoClient {
// 服务器主机IP
private final String host;
// 端口
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception {
new EchoClient("127.0.0.1", 9998).start();
}
/
* 客户端启动
*/
public void start() throws Exception {
final EchoClientHandler clientHandler = new EchoClientHandler();
// 事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
// 客户端启动
Bootstrap boot = new Bootstrap();
boot.group(group)
// 客户端使用的是不带Server的SocketChannel
.channel(NioSocketChannel.class)
// 连接远程服务器的地址和端口
.remoteAddress(new InetSocketAddress(host, port))
// 设置处理器
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(clientHandler);
}
});
// 客户端同步连接到服务器
ChannelFuture f = boot.connect().sync();
// 同步关闭Channel
f.channel().closeFuture().sync();
} finally {
// 同步关闭并释放所有资源
group.shutdownGracefully().sync();
}
}
}
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/
* 简单的Netty客户端处理器示例
*/
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// Channel激活时,发生一条消息给服务器
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty Rocks!", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
// 接受服务端返回的消息
System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 发生异常时,打印堆栈并关闭Channel
cause.printStackTrace();
ctx.close();
}
}
组件名称 | 功能 | 说明 |
---|---|---|
Channel | Socket | 套接字 |
EventLoop | 控制流, 多线程处理, 并发 | 处理连接的生命周期中的事件 |
ChannelFuture | 异步通知 | 事件回调 |
Channel是Java NIO的一个基本构造, 它代表一个到实体(如一个硬件设备, 一个文件, 一个网络套接字, 或者一个能够执行一个或多个不同I/O操作的程序组件)的开放连接, 如读操作和写操作.
可以把Channel看作是传入(入站)或者传出(出站)数据的载体, 因此它可以被打开或者被关闭, 连接或断开连接.
Netty在内部使用回调来处理事件, 当回调被触发时, 相关的事件可以被一个实现接口ChannelHandler的对象进行处理.
Future提供了在操作完成时通知应用程序的方式, 这个对象可看作是一个异步操作结果的占位符, 它将在未来的某个时刻完成, 并提供对其结果的访问.
Netty提供了ChannelFuture, 其提供了几种额外的方法, 使得能够注册一个或多个ChannelFutureListener实例. 监听器的回调方法operationComplete()将会在对应的操作完成时被调用.
每个Netty的出站I/O操作都将返回一个ChannelFuture, 因此不会阻塞.
Netty使用不同的事件来通知我们状态的改变或者是操作的状态, 使得能够基于已经发生的事件来触发适当的动作. 事件分为两大类: 入站事件和出站事件.
Netty通过触发事件将Selector从应用程序中抽象处理, 消除了所有本来需要手动编写的派发代码. 在内部将会为每个Channel分配一个EventLoop, 用以处理所有事件.
EventLop本身只是由一个线程驱动, 其处理了一个Channel的所有I/O事件, 并且在该EventLoop的整个生命周期中都不会改变. 这个设计消除了可能的所有的在ChannelHandler实现中需要进行同步的任何顾虑.
Channel, EventLoop, Thread以及EventLoopGroop之间的关系:
Netty中的线程模型, 通过在同一个线程中处理某个给定的EventLoop中所产生的所有事件, 解决了线程上下文切换的问题, 并消除了在多个ChannelHandler中进行同步的需要.
Netty线程模型的性能取决于对于当前执行的Thread的身份的确定, 即确定Thread是否是分配给当前Channel以及它的EventLoop的那一个线程. 如果当前调用线程正是支撑EventLoop的线程, 那么提交的代码块将会被直接执行, 否则EventLoop将调度该任务以便稍后执行, 并将它放入到内部队列中. 当EventLoop下次处理它的事件时, 会执行队列中的那些任务/事件.
根据Netty的线程模型, 则需要特别注意永远不要将一个长时间运行的任务放入到执行队列中, 因为它将阻塞需要在同一线程上执行的其他任务. 如果必须要进行阻塞调用或执行长时间运行的惹我你, 建议使用专门的EventExecutor.
Netty线程模型中的异步传输实现只使用了少量的EventLoop以及所关联的Thread, 而且在当前的线程模型中, EventLoop可能会被多个Channel所共享. 这使得可以通过尽量少的Thread来支撑大量的Channel, 而不是每个Channel分配一个Thread. EventLoopGroup负责为每个新创建的Channel分配一个EventLoop, 一旦一个Channel被分配给一个EventLoop, 在Channel的整个生命周期中都使用这个EventLoop.
ChannelHandler充当了所有处理入站和出站数据的应用程序逻辑的容器.
ChannelPipeline为ChannelHandler链提供了容器, 并定义了用于在该链上传播入站和出站事件流的API. 当Channel被创建时, Channel会被自动分配到专属的ChannelPipeline.
ChannelHandler在应用程序初始化或者引导阶段被安装到ChannelPipeline. ChannelHandler对象接收事件, 执行处理逻辑, 并将数据传递给链中的下一个ChannelHandler. ChannelHandler的执行顺序是由它们被添加的顺序所决定的. 但ChannelHandler在处理事件过程中, 可自行在ChannelHandlerContext中进行修改ChannelPipeline, 如添加/删除相应的ChannelHandler.
Netty能够区分入站ChannelInboundHandler和出站ChannelOutboundHandler, 并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递.
当ChannelHandler被添加到ChannelPipeline时, ChannelHandler会被分配一个ChannelHanderContext, Context代表了ChannelHander和ChannelPipeline之间的绑定.
在Netty中, 有两种发送消息的方式:
ChannelHandlerContext主要功能是管理它所关联的ChannelHandler和在同一个ChannelPipeline中的其他ChannelHandler之间的交互. 一个ChannelHandler对应一个ChannelHandlerContext.
注意: 如果调用Channel和ChannelPipeline上的方法, 它们将沿着整个ChannelPipeline进行传播; 而调用ChannelHandlerContext上的相同方法, 则将从当前所关联的ChannelHandler开始, 并且只会传播给位于该ChannelPipeline中的下一个能够处理该事件的ChannelHandler.
如果需要将ChannelHandler绑定到多个ChannelPipeline中, 那么需要使用@Sharable注解进行标注, 否则会触发异常. 显然, 这样的ChannelHandler必须保证是线程安全的.
通过Netty发送或接收一个消息的时候, 将会发送一次数据转换, 入站二进制字节消息被解码为应用程序需要的个数, 出站消息被编码为二进制字节.
所有由Netty提供的编码器/解码器适配器类都实现了入站ChannelInboundHandler接口或出站ChannelOutboundHandler接口.
Netty的引导类为应用程序的网络层配置提供了容器, 涉及将一个进程绑定到某个指定的端口(服务端ServerBootStrap), 或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程(客户端BootStrap).
引导客户端只需要一个EventLoopGroup, 但是一个ServerBootStrap则需要两个(可以是同一个示例). 因为服务器需要两组不同的Channel, 第一组只包含一个ServerChannel, 代表服务器自身的已绑定到某个本地端口的正在监听的套接字, 而第二组将包含所有已创建的用来处理传入客户端连接的Channel.
有时需要从Channel中引导一个客户端子Channel去请求第三方系统, 更好的解决方案是: 通过Channel的EventLoop传递给客户端子Channel的BootStrap的group()方法来共享EventLoop, 这样避免了额外的线程创建以及相关的上下文切换.
ByteBuf是Netty的数据容器. ByteBuf实现了零拷贝, 方法的链式调用, 支持引用计数, 支持池化.
ByteBuf维护了两个不同的索引, 一个用于读取, 一个用于写入. 当从ByteBuf读取时, readerIndex将会被递增已经被读取的字节数. 当写入ByteBuf时, writerIndex会被递增. 名称以read或write开头的ByteBuf方法会推进对应的索引, 而名称以set或者get开头的操作则不会.
0 --可丢弃字节--> readerIndex --可读字节(content)--> writerIndex --可写字节--> capacity
ByteBuf的copy()方法返回的ByteBuf拥有独立的数据副本, 而duplicate()和slice()方法返回的ByteBuf是和源ByteBuf享有相同的数据副本, 只不过有自己的读写索引和标记索引.
Netty通过ByteBufAllocator接口来分配任意类型的ByteBuf实例. 可通过Channel的alloc()方法获得, 或者可通过ChannelHandlerContext的alloc()方法来获得. Netty默认使用池化的ByteBuf.
Netty通过引用计数的方式管理池化的ByteBuf的内存释放, 引用计数大于0, 保证对象不会被释放, 当减少到0时, 对应实例会被释放. 通过release()方法将引用计数设置为0, 从而一次性地使所有的活动引用都失效.
下面的代码是通过Netty框架编写的简单WebSocket协议实现.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.ImmediateEventExecutor;
import java.net.InetSocketAddress;
/
* 聊天服务器
*/
public class ChatServer {
// 创建ChannelGroup将保存所有已连接的WebSocketChannel
private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel;
/
* 服务器开启
*
* @param address 地址
*/
public ChannelFuture start(InetSocketAddress address) {
ServerBootstrap boot = new ServerBootstrap();
boot.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(createInitializer(channelGroup));
ChannelFuture future = boot.bind(address);
future.syncUninterruptibly();
this.channel = future.channel();
return future;
}
/
* 创建Channel初始化
*/
protected ChannelInitializer<Channel> createInitializer(ChannelGroup channelGroup) {
return new ChatServerInitializer(channelGroup);
}
public void destroy() {
if (channel != null) {
channel.close();
}
channelGroup.close();
group.shutdownGracefully();
}
public static void main(String[] args) {
ChatServer endpoint = new ChatServer();
ChannelFuture future = endpoint.start(new InetSocketAddress(9969));
// 关闭JVM时进行销毁
Runtime.getRuntime().addShutdownHook(new Thread(endpoint::destroy));
future.channel().closeFuture().syncUninterruptibly();
}
}
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/
* 初始化聊天服务器
*/
public class ChatServerInitializer extends ChannelInitializer<Channel> {
private final ChannelGroup group;
public ChatServerInitializer(ChannelGroup group) {
this.group = group;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// Http消息编解码
pipeline.addLast(new HttpServerCodec());
// 使用块进行文件写入
pipeline.addLast(new ChunkedWriteHandler());
// 聚合Http消息
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// 处理FullHttpRequest
pipeline.addLast(new HttpRequestHandler("/ws"));
// 处理WebSocket升级握手
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 处理TextWebSocketFrame和握手完成事件
pipeline.addLast(new TextWebSocketFrameHandler(group));
}
}
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;
/
* Http请求处理器,转发为URI中/ws的请求
*/
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String wsUri;
private static final File INDEX;
static {
try {
URL location = HttpRequestHandler.class
.getProtectionDomain().getCodeSource().getLocation();
String path = location.toURI() + "index.html";
path = !path.contains("file:") ? path : path.substring(5);
INDEX = new File(path);
} catch (URISyntaxException e) {
throw new IllegalStateException("Unable to locate index.html", e);
}
}
public HttpRequestHandler(String wsUri) {
this.wsUri = wsUri;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 如果请求了WebSocket协议生计,则增加引用计数器,并传递给下一个ChannelInBoundHandler
if (wsUri.equalsIgnoreCase(request.uri())) {
ctx.fireChannelRead(request.retain());
} else {
// 处理响应码为100的Continue请求
if (HttpUtil.is100ContinueExpected(request)) {
send100Continue(ctx);
}
// 读取Index.html
RandomAccessFile file = new RandomAccessFile(INDEX, "r");
// Http响应
HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
boolean keepAlive = HttpUtil.isKeepAlive(request);
// 如果请求了Keep-Alive添加相应的请求头信息
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
// 如果没有SSL证书,则使用零拷贝技术
if (ctx.pipeline().get(SslHandler.class) == null) {
ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
} else {
// 有SSL证书,则使用块
ctx.write(new ChunkedNioFile(file.getChannel()));
}
// 添加最后的Http消息内容
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive) {
// 写操作完成后关闭Channel
future.addListener(ChannelFutureListener.CLOSE);
file.close();
}
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
/
* 处理文本形式的WebSocket数据帧
*/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final ChannelGroup group;
public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 如果握手成功,则从Pipeline中删除HttpRequestHandler,因为不会再处理Http请求了
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
ctx.pipeline().remove(HttpRequestHandler.class);
// 通知所有已连接的WebSocket客户端的新的客户端已经连接上
group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
// 将新的WebSocketChannel添加到ChannelGroup中,以便接受到所有的WebSocket消息
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 添加消息的引用计数,并将它写到ChannelGroup中所有已经连接的客户端
group.writeAndFlush(msg.retain());
}
}
如果要实现携带SSL证书的安全访问, 则使用下面的代码:
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;
import java.security.cert.CertificateException;
/
* 有SSL加密的聊天服务器
*/
public class SecureChatServer extends ChatServer {
// SSL加密
private final SslContext context;
public SecureChatServer(SslContext context) {
this.context = context;
}
@Override
protected ChannelInitializer<Channel> createInitializer(ChannelGroup channelGroup) {
return new SecureChatServerInitializer(channelGroup, context);
}
public static void main(String[] args) {
try {
SelfSignedCertificate cert = new SelfSignedCertificate();
SslContext context = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()).build();
final SecureChatServer endpoint = new SecureChatServer(context);
ChannelFuture future = endpoint.start(new InetSocketAddress(9968));
Runtime.getRuntime().addShutdownHook(new Thread(endpoint::destroy));
future.channel().closeFuture().syncUninterruptibly();
} catch (CertificateException | SSLException e) {
throw new RuntimeException(e);
}
}
}
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
/
* 有SSL加密的聊天服务
*/
public class SecureChatServerInitializer extends ChatServerInitializer {
// SSL加密
private final SslContext context;
public SecureChatServerInitializer(ChannelGroup group, SslContext context) {
super(group);
this.context = context;
}
@Override
protected void initChannel(Channel ch) throws Exception {
// 先调用父类的初始化Channel方法
super.initChannel(ch);
SSLEngine engine = context.newEngine(ch.alloc());
engine.setUseClientMode(false);
// 将SSL添加到Pipeline中,注意是添加到头部
ch.pipeline().addFirst(new SslHandler(engine));
}
}
测试WebSocket的网页index.html:
<!doctype html>
<head>
<meta charset="utf-8">
<!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
Remove this if you use the .htaccess -->
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>WebSocket ChatServer</title>
<style type="text/css">
#ui * {
width: 100%;
}
#ui textarea {
height: 15em;
}
</style>
<script src="https://cdn.bootcss.com/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
function log(msg) {
var log = $('#log')
log.append(msg + " \n").scrollTop(log[0].scrollHeight - log.height());
}
$(function () {
$('#url').val((location.protocol.indexOf('https') == -1 ? 'ws://' : 'wss://') + location.host + '/ws')
if (!WebSocket) {
$('#not-supported').show()
} else {
var ws = null
$('#connect').click(function () {
if (ws == null || ws.readyState != 1) {
ws = new WebSocket($('#url').val())
ws.onerror = function (e) {
log('Error : ' + e.message)
}
ws.onopen = function () {
log('connected')
}
ws.onclose = function () {
log('disconnected')
}
ws.onmessage = function (d) {
log('Response : ' + d.data)
}
$('#send').click(function () {
var msg = $('#msg').val()
$('#msg').val('')
if (ws.send(msg)) {
log('Message sent')
} else {
log('Message not sent')
}
})
} else {
log('closing connection')
ws.close()
}
})
}
})
</script>
</head>
<body>
<div id="not-supported" style="float: left; width: 600px; margin-left: 10px; display: none">
<p>Uh-oh, the browser you're using doesn't have native support for WebSocket. That means you can't run this
demo.</p>
<p>The following link lists the browsers that support WebSocket:</p>
<p><a href="http://caniuse.com/#feat=websockets">http://caniuse.com/#feat=websockets</a></p>
</div>
<table>
<tr>
<td>
<div>Enter a message below to send</div>
<input type="text" id="msg"/>
<input type="submit" value="Send" id="send"/>
</td>
<td id="ui">
<input type="text" id="url"/>
<textarea id="log" disabled></textarea>
<input type="submit" id="connect" value="Connect"/>
</td>
</tr>
</table>
<div style="float: left; width: 600px; margin-left: 10px;">
<p><br><strong>Instructions:</strong></p>
<table class="instructions" cellspacing="0px" cellpadding="2px">
<tr>
<td valign="top" nowrap>Step 1: </td>
<td valign="top">Press the <strong>Connect</strong> button.</td>
</tr>
<tr>
<td valign="top" nowrap>Step 2: </td>
<td valign="top">Once connected, enter a message and press the <strong>Send</strong> button. The server's
response will
appear in the <strong>Log</strong> section. You can send as many messages as you like
</td>
</tr>
</table>
</div>
</body>
</html>