使用netty建立websocket通道
1、引入maven依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.60.Final</version>
</dependency>
2、编写通道处理
@Slf4j
@NoArgsConstructor
public class LcfcSebsocketChannelHandler extends SimpleChannelInboundHandler<Object> {
private static final String TOKEN="token";
private WebsocketConfig socketConfig;
private WebSocketServerHandshaker handshaker;
private WebsocketConnectLogService logService;
public LcfcSebsocketChannelHandler(WebsocketConfig config,WebsocketConnectLogService logService){
this.socketConfig = config;
this.logService = logService;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("收到消息:"+msg);
if (msg instanceof FullHttpRequest){
//以http请求形式接入,但是走的是websocket
handleHttpRequest(ctx, (FullHttpRequest) msg);
}else if (msg instanceof WebSocketFrame){
//处理websocket客户端的消息
handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//添加连接
// String userId = ctx.channel().attr(WebsocketUtils.ATTRIBUTE_KEY_USER_ID).get();
WebsocketUtils.addChannel(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("链接发生错误=",cause.getMessage());
String userId = ctx.channel().attr(WebsocketUtils.ATTRIBUTE_KEY_USER_ID).get();
saveConnectLog(ctx,userId,cause.getMessage(),MessageConstants.exceptionStatus.exception);
WebsocketUtils.removeChannel(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//断开连接
log.debug("客户端断开连接:"+ctx.channel());
WebsocketUtils.removeChannel(ctx.channel());
saveConnectLog(ctx,null,"客户端断开链接");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){
// 判断是否关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
// 本例程仅支持文本消息,不支持二进制消息
if (!(frame instanceof TextWebSocketFrame)) {
log.debug("本例程仅支持文本消息,不支持二进制消息");
throw new UnsupportedOperationException(String.format(
"%s frame types not supported", frame.getClass().getName()));
}
// 返回应答消息
String request = ((TextWebSocketFrame) frame).text();
log.debug("服务端收到:" + request);
TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()
+ ctx.channel().id() + ":" + request);
// 群发
// log.info("发送消息");
// WebsocketUtils.send2All(tws);
// 返回【谁发的发给谁】
// ctx.channel().writeAndFlush(tws);
}
/**
* 唯一的一次http请求,用于创建websocket
* */
private void handleHttpRequest(ChannelHandlerContext ctx,
FullHttpRequest req) {
//要求Upgrade为websocket,过滤掉get/Post
if (!req.decoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
//若不是websocket方式,则创建BAD_REQUEST的req,返回给客户端
saveConnectLog(ctx,null,"要求Upgrade为websocket",MessageConstants.exceptionStatus.exception);
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST),"非websocket请求");
return;
}
String uri = req.uri();
ConcurrentMap<String, String> paramMap = getUrlParams(uri);
log.debug("获取uri中参数={}",paramMap);
if (paramMap.get(TOKEN)==null){
saveConnectLog(ctx,null,"请求参数中没有token",
MessageConstants.exceptionStatus.exception);
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST),"token不合法");
return;
}
try {
String userId = PlatfromComponent.getInstants(socketConfig.getCheckUrl()).checkToken(paramMap.get(TOKEN));
paramMap.put(WebsocketUtils.KEY_USER_ID,userId);
}catch (Exception e){
String msg = "根据token获取用户信息出错";
log.error(msg);
saveConnectLog(ctx,null,msg,
MessageConstants.exceptionStatus.exception);
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.BAD_REQUEST),msg);
return;
}
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://"+socketConfig.getRemoteUrl()+":"+socketConfig.getPort()+"/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory
.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
WebsocketUtils.setChannelParam(ctx.channel(),paramMap.get(WebsocketUtils.KEY_USER_ID));
String ip = req.headers().get("X-real-ip");
if (ip!=null){
saveConnectLog(ctx,null,"链接建立成功",MessageConstants.exceptionStatus.normal,ip);
}else {
saveConnectLog(ctx,null,"链接建立成功");
}
}
}
/**
* 拒绝不合法的请求,并返回错误信息
* */
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req, DefaultFullHttpResponse res,String msg) {
// 返回应答给客户端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(msg,
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
ChannelFuture f = ctx.channel().writeAndFlush(res);
// 如果是非Keep-Alive,关闭连接
if (!isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
private static ConcurrentMap<String, String> getUrlParams(String url) {
ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
url = url.replace("?", ";");
if (!url.contains(";")) {
return map;
}
if (url.split(";").length > 0) {
String[] arr = url.split(";")[1].split("&");
for (String s : arr) {
String key = s.split("=")[0];
String value = s.split("=")[1];
map.put(key, value);
}
return map;
} else {
return map;
}
}
private void saveConnectLog(ChannelHandlerContext ctx,String userId,String msg){
saveConnectLog(ctx,userId,msg,MessageConstants.exceptionStatus.normal,null);
}
private void saveConnectLog(ChannelHandlerContext ctx,String userId,String msg,Integer status){
saveConnectLog(ctx,userId,msg,status,null);
}
/**
*
* @param ctx
* @param userId 用户id
* @param msg 链接描述
* @param status 链接状态
*/
private void saveConnectLog(ChannelHandlerContext ctx,String userId,String msg,Integer status,String clientIP){
if (userId==null){
userId = ctx.channel().attr(WebsocketUtils.ATTRIBUTE_KEY_USER_ID).get();
}
if (clientIP==null){
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
clientIP = insocket.getAddress().getHostAddress();
}
WebsocketConnectLog log = WebsocketConnectLog.builder()
.host(clientIP)
.msg(msg)
.userId(userId)
.status(status)
.time(new Date())
.build();
logService.save(log);
}
}
3、配置通道初始化
@NoArgsConstructor
@AllArgsConstructor
public class LcfcWebsocketChannelInitializer extends ChannelInitializer<SocketChannel> {
private WebsocketConfig config;
private WebsocketConnectLogService logService;
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("http-codec", new HttpServerCodec());
socketChannel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
socketChannel.pipeline().addLast("handler", new LcfcSebsocketChannelHandler(config,logService));
}
}
4、启动websocket服务端
@Slf4j
@Component
public class LcfcWebsockerServerStart implements ApplicationRunner {
@Autowired
WebsocketConfig config;
@Autowired
WebsocketConnectLogService logService;
@Override
public void run(ApplicationArguments args) throws Exception {
new Thread(()->{init();}).start();
// init();
}
private void init(){
log.info("正在启动websocket服务器");
NioEventLoopGroup boss=new NioEventLoopGroup();
NioEventLoopGroup work=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap=new ServerBootstrap();
bootstrap.group(boss,work);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new LcfcWebsocketChannelInitializer(config,logService));
Channel channel = bootstrap.bind(config.getPort()).sync().channel();
log.info("webSocket服务器启动成功:"+channel);
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
log.info("运行出错:"+e);
}finally {
boss.shutdownGracefully();
work.shutdownGracefully();
log.info("websocket服务器已关闭");
}
}
}
启动后如果看到 17:24:38.692 [Thread-32] INFO c.l.w.s.i.LcfcWebsockerServerStart - [init,41] - webSocket服务器启动成功:[id: 0x4dbe0472, L:/0:0:0:0:0:0:0:0:19999],说明启动成功
5、websocket客户端编写
5.1 编写通道处理
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;
}
@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;
}
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"Unexpected FullHttpResponse (getStatus=" + response.getStatus() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
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();
}
}
5.2 启动客户端
public class WebsocketClient {
static final String URL = System.getProperty("url", "ws://127.0.0.1:19999/websocket?userId=456");
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;
if (uri.getPort() == -1) {
if ("ws".equalsIgnoreCase(scheme)) {
port = 80;
} else if ("wss".equalsIgnoreCase(scheme)) {
port = 443;
} else {
port = -1;
}
} else {
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, false, 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(
new HttpClientCodec(),
new HttpObjectAggregator(8192),
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();
}
}
}
help文档
config
@Data
@Configuration
@ConfigurationProperties(prefix = "websocket")
public class WebsocketConfig {
private Integer port;
private String remoteUrl;
private String checkUrl;
}
// 配置文件
websocket:
port: 19999 # 占用端口
remote-url: 127.0.0.1 远程ip
check-url: http://10.0.0.0:19200/api/auth/oauth/check_token 验证用户token地址
websocket 工具类
public class WebsocketUtils {
public static final String KEY_USER_ID = "userId";
public static final AttributeKey<String> ATTRIBUTE_KEY_USER_ID = AttributeKey.valueOf(KEY_USER_ID);
private static ChannelGroup GlobalGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private static ConcurrentMap<String, ChannelId> ChannelMap=new ConcurrentHashMap();
public static void setChannelParam(Channel channel, String userId){
channel.attr(ATTRIBUTE_KEY_USER_ID).setIfAbsent(userId);
}
public static void addChannel(Channel channel){
GlobalGroup.add(channel);
}
public static void addChannel(Channel channel,String userId){
setChannelParam(channel,userId);
GlobalGroup.add(channel);
// ChannelMap.put(userId,channel);
}
public static void removeChannel(Channel channel){
GlobalGroup.remove(channel);
// ChannelMap.remove(channel.id().asShortText());
}
public static Channel findChannel(String id){
return GlobalGroup.find(ChannelMap.get(id));
}
public static Boolean sendToOne(final String userId, TextWebSocketFrame tws){
Boolean send = false;
for (Iterator<Channel> it = GlobalGroup.stream().iterator(); it.hasNext(); ) {
Channel x = it.next();
String itemUserId = x.attr(ATTRIBUTE_KEY_USER_ID).get();
if (userId.equals(itemUserId)){
x.writeAndFlush(tws);
send = true;
}
}
return send;
}
public static void send2All(TextWebSocketFrame tws){
GlobalGroup.writeAndFlush(tws);
}
}