关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
上一节介绍了WebSocket的服务端的相关内容,这一节我们将继续分享WebSocket客户端,稍后我们将手搓两种客户端分别连接我们的WebSocket服务端。
02 Netty客户端
2.1 代码示例
java
@Slf4j
public class WebMockClient {
@Getter
private SocketChannel socketChannel;
private String url;
public WebMockClient(String url) {
this.url = url;
}
public void connect() throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(eventLoopGroup);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(65535));
pipeline.addLast(new WebSocketClientHandler(url));
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9090).sync();
this.socketChannel = (SocketChannel) channelFuture.channel();
}
}
2.2 参数说明
socketChannel:用来发送消息的客户端,和之前介绍TCP的客户端一致
url:用来连接WebSocket的地址
引导类和TCP的客户端一致:
java
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(eventLoopGroup);
2.3 编解码
java
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(65535));
pipeline.addLast(new WebSocketClientHandler(url));
}
});
这里需要说明的是客户端的HTTP请求编解码同样有属于自己的编解码HttpClientCodec,同样是一个组合的编解码:
io.netty.handler.codec.http.HttpRequestEncoderio.netty.handler.codec.http.HttpResponseDecoder
这里编解码正好和服务端的编解码相反:给HttpRequest编码,给HttpResponse解码
WebSocketClientHandler为自定义的处理器。
2.4 自定义处理器
java
@Slf4j
public class WebSocketClientHandler extends SimpleChannelInboundHandler {
private String url;
private WebSocketClientHandshaker handshaker;
public WebSocketClientHandler(String url) {
this.url = url;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 建立客户端
Channel channel = ctx.channel();
log.info(">>>>Socket客户端建立连接:channelId={}", channel.id());
handshaker = WebSocketClientHandshakerFactory.newHandshaker(
new URI(url), WebSocketVersion.V13, null, true, new DefaultHttpHeaders()
);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
this.handshaker.handshake(channel);
log.info(">>>>WebSocket Client connected! >>{}", channel.id());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 断开链接
Channel channel = ctx.channel();
log.info(">>>>Socket客户端断开连接:channelId={}", channel.id());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接受消息
Channel channel = ctx.channel();
log.info(">>>>Socket收到来自通道channelId[{}]发送的消息:{}", channel.id(), msg);
log.info(">>>>msg:{}", msg.getClass());
if (msg instanceof FullHttpResponse) {
// 处理HTTP响应(握手阶段)
FullHttpResponse response = (FullHttpResponse) msg;
if (!handshaker.isHandshakeComplete()) {
handshaker.finishHandshake(ctx.channel(), response);
log.info(">>>>WebSocket Handshake completed!");
}
return;
}
if (msg instanceof WebSocketFrame) {
// 处理文本消息
TextWebSocketFrame textFrame = (TextWebSocketFrame) msg;
log.info(">>>>msg -> textFrame:{}", textFrame.text());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info(">>>>异常:", cause);
}
}
这里自定义的处理器和之前的很多不同,需要额外处理WebSocket请求的握手操作
握手操作的主要类:
io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker
握手的时机
在使用客户端发送数据之前需要完成三次握手。
handlerAdded建立连接之后创建握手的引导类:
java
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 建立客户端
Channel channel = ctx.channel();
log.info(">>>>Socket客户端建立连接:channelId={}", channel.id());
handshaker = WebSocketClientHandshakerFactory.newHandshaker(
new URI(url), WebSocketVersion.V13, null, true, new DefaultHttpHeaders()
);
}
channelActive连接被激活时和通道建立握手关系:
java
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
this.handshaker.handshake(channel);
log.info(">>>>WebSocket Client connected! >>{}", channel.id());
}
这两个其实可以合成一个,放在channelActive里面。当然需要实际的场景。
握手的处理
握手的处理也就是客户端和服务端相互发送消息,需要在channelRead0()里面处理。握手的传输是基于HTTP协议的,所以返回的消息的类型是:
io.netty.handler.codec.http.FullHttpResponse
我们需要处理的是,如果握手没有完成,需要我们手动完成握手。
java
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接受消息
Channel channel = ctx.channel();
log.info(">>>>Socket收到来自通道channelId[{}]发送的消息:{}", channel.id(), msg);
log.info(">>>>msg:{}", msg.getClass());
if (msg instanceof FullHttpResponse) {
// 处理HTTP响应(握手阶段)
FullHttpResponse response = (FullHttpResponse) msg;
if (!handshaker.isHandshakeComplete()) {
// 完成握手
handshaker.finishHandshake(ctx.channel(), response);
log.info(">>>>WebSocket Handshake completed!");
}
return;
}
// 后续处理
}
正常消息的接收
java
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// ......
if (msg instanceof WebSocketFrame) {
// 处理文本消息
TextWebSocketFrame textFrame = (TextWebSocketFrame) msg;
log.info(">>>>msg -> textFrame:{}", textFrame.text());
}
}
握手之后的传输协议就会升级成webSocket协议:

返回的消息类型是:
io.netty.handler.codec.http.websocketx.TextWebSocketFrame
2.5 注意事项
使用客户端的时候,必选在握手完成之后发送消息才会被正确接收,否则就会出现消息丢失的问题。
2.6 测试
java
@Test
void testWebClient() throws Exception {
WebMockClient webClient = new WebMockClient("ws://127.0.0.1:9090/testWs");
webClient.connect();
SocketChannel socketChannel = webClient.getSocketChannel();
System.out.println("睡眠开始,等待握手结束......");
Thread.sleep(3000);
System.out.println("睡眠结束,等待握手结束......");
socketChannel.writeAndFlush(new TextWebSocketFrame("测试WebSocket客户端......"));
}
这里的睡眠就是为了等待握手操作接收。真正生产中使用的话需要增加标志位来判断是否握手成功,或者直接使用handshaker.isHandshakeComplete()
测试效果


03 Web客户端
Web客户端就简单了,主流的浏览器均支持。我们之前测试在线WebSocket测试就是基于浏览器的。

我们也手搓一个。
3.1 页面展示

页面源代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket测试</title>
</head>
<body>
<div style="width: 700px; margin: 0 auto;">
<div>
<h1>WebSocket测试</h1>
<div id="message" style="height: 300px; overflow: auto; border: 1px solid #ccc;"></div>
</div>
<div style="margin-top: 10px;">
<input type="text" id="messageInput">
<button onclick="sendMessage()">发送信息</button>
</div>
</div>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
let socket;
$(function(){
if ("WebSocket" in window){
console.log('浏览器支持 WebSocket');
socket = new WebSocket("ws://localhost:9090/testWs");
// 打来链接
socket.onopen = function(event) {
console.log("WebSocket is open now.");
};
// 处理消息
socket.onmessage = function(event) {
let message = event.data;
console.log("Received message: " + message);
$("#message").append('<p>收到消息:'+ message + '</p>');
};
// 关闭链接
socket.onclose = function(event) {
console.log("WebSocket is closed now.");
};
}else {
console.log('浏览器不支持 WebSocket');
}
});
// 发送消息
function sendMessage() {
let message = $("#messageInput").val();
socket.send(message);
$("#messageInput").val("");
$("#message").append('<p>发送消息:'+ message + '</p>');
}
</script>
</html>
3.2 效果展示

3.3 番外
为了兼容更多的浏览器,可以直接引入对应的js。如Github上star较多的https://github.com/gimite/web-socket-js项目。
小编这里就不测试了,有兴趣的可以去试试。
04 小结
两种手搓的客户端就已经完成了。WebSocket的传输有没有拆包粘包的问题呢?框架自身又是怎么解决的呢?我们下期讲。