现在网上有很多netty实现的websocket服务端,但是客户端实现的不多,或者说是写的比较散,现写下。
另外,源码可以参考github:weboscket客户端以及服务端实现
首先,构建一个抽象类,定义一下对外的接口等:
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.io.Closeable;
/**
* 客户端抽象
*
* @author lukou
* @date 2023/05/18
*/
public abstract class AbstractWebsocketClient implements Closeable {
/**
* 发送消息.
*
* @param message 发送文本
*/
public void send(String message) throws MyException {
Channel channel = getChannel();
if (channel != null) {
channel.writeAndFlush(new TextWebSocketFrame(message));
return;
}
throw new MyException("连接已经关闭");
}
/**
* 连接并发送消息.
*/
public void connect() throws MyException {
try {
doOpen();
doConnect();
} catch (Exception e) {
throw new MyException("连接没有成功打开,原因是:{}" + e.getMessage(), e);
}
}
/**
* 初始化连接.
*/
protected abstract void doOpen();
/**
* 建立连接.
*/
protected abstract void doConnect() throws MyException;
/**
* 获取本次连接channel.
*
* @return {@link Channel}
*/
protected abstract Channel getChannel();
/**
* 关闭连接.
*/
@Override
public abstract void close();
}
AbstractWebsocketClient 这个抽象类主要定义了一些必要的接口,属性connectionTimeout主要用于连接时间的设置,属性WebsocketContext相当于上下文。
下面是编写子类继承AbstractWebsocketClient:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.URISyntaxException;
/**
* websocket客户端
*
* @author lukou
* @date 2023/05/18
*/
public class WebsocketClient extends AbstractWebsocketClient {
private static final Logger log = LoggerFactory.getLogger(WebsocketClient.class);
private static final NioEventLoopGroup NIO_GROUP = new NioEventLoopGroup();
private final URI uri;
private final int port;
private Bootstrap bootstrap;
private WebsocketClientHandler handler;
private Channel channel;
public WebsocketClient(String url) throws URISyntaxException, MyException {
super();
this.uri = new URI(url);
this.port = getPort();
}
/**
* Extract the specified port
*
* @return the specified port or the default port for the specific scheme
*/
private int getPort() throws MyException {
int port = uri.getPort();
if (port == -1) {
String scheme = uri.getScheme();
if ("wss".equals(scheme)) {
return 443;
} else if ("ws".equals(scheme)) {
return 80;
} else {
throw new MyException("unknown scheme: " + scheme);
}
}
return port;
}
@Override
protected void doOpen() {
// websocket客户端握手实现的基类
WebSocketClientHandshaker webSocketClientHandshaker = WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders());
// 业务处理类
handler = new WebsocketClientHandler(webSocketClientHandshaker);
// client端,引导client channel启动
bootstrap = new Bootstrap();
// 添加管道 绑定端口 添加作用域等
bootstrap.group(NIO_GROUP).channel(NioSocketChannel.class).handler(new WebsocketChannelInitializer(handler));
}
@Override
protected void doConnect() {
try {
// 启动连接
channel = bootstrap.connect(uri.getHost(), port).sync().channel();
// 等待握手响应
handler.handshakeFuture().sync();
} catch (InterruptedException e) {
log.error("websocket连接发生异常", e);
Thread.currentThread().interrupt();
}
}
@Override
protected Channel getChannel() {
return channel;
}
@Override
public void close() {
if (channel != null) {
channel.close();
}
}
public boolean isOpen() {
return channel.isOpen();
}
}
WebsocketClient主要是实现了父类方法。
接下来,需要添加处理器到客户端:
public class WebsocketChannelInitializer extends ChannelInitializer {
private final WebsocketClientHandler handler;
public WebsocketChannelInitializer(WebsocketClientHandler handler) {
this.handler = handler;
}
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpClientCodec());
p.addLast(new HttpObjectAggregator(8192));
p.addLast(WebSocketClientCompressionHandler.INSTANCE);
p.addLast(handler);
}
}
WebsocketChannelInitializer主要还是用于管道中添加处理器,当然包括自定义处理器。
接下实现自定义处理器:
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* websocket客户端处理程序
*
* @author lukou
* @date 2023/05/18
*/
public class WebsocketClientHandler extends SimpleChannelInboundHandler
剩下的相关的类也贴一下吧
上下文WebsocketContext,可以根据需求修改:
public class WebsocketContext {
/**
* 计数器(用于监听是否返回结果)
*/
private CountDownLatch countDownLatch;
/**
* 最终结果
*/
private String result;
public WebsocketContext(CountDownLatch countDownLatch) {
this.countDownLatch= countDownLatch;
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch= countDownLatch;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
异常类MyException:
public class MyException extends Exception {
public MyException () {
super();
}
public MyException (String message) {
super(message);
}
/**
* 用指定的详细信息和原因构造一个新的异常.
*
* @param message
* @param cause
* @return:
*/
public MyException (String message, Throwable cause) {
super(message, cause);
}
}
测试:
import lombok.extern.slf4j.Slf4j;
import java.net.URISyntaxException;
@Slf4j
public class Test {
public static void main(String[] args) {
try (WebsocketClient websocketClient = new WebsocketClient("ws://localhost:8081/example4/ws")) {
// 连接
websocketClient.connect();
// 发送消息
websocketClient.send("xxxxxxxxxxxxxxxxx");
// 阻塞一下,否则这里客户端会调用close方法
Thread.sleep(10);
} catch (URISyntaxException | MyException | InterruptedException e) {
log.error("发生异常,原因:{}", e.getMessage(), e);
}
}
}
至此,整个客户端封装的差不多了,当然如果用于生产还是要根据实际情况修改的。
如果有不足的地方也请指出,大家一起探讨,感谢。