netty实现socket服务器 处理websocket请求
最近有两个都用到netty做服务端的项目,第一个是c直接发起socket建立连接的请求,第二个是react框架的app,用websocket协议发起连接请求,netty处理稍有不同,记录一下。
netty高性能:https://www.infoq.cn/article/netty-high-performance
netty调优:https://blog.csdn.net/C_J33/article/details/80737053
#### 先来看第一个项目:
Springboot版本是1.5.10,点进去发现默认依赖没有netty,加入netty依赖。
maven依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.10.Final</version>
</dependency>
spring启动完成后,新建线程指定端口启动socket
private int port;
public SocketService(int port) {
this.port = port;
}
public SocketService() {
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public void startSocket() throws Exception{
// 接受socket链接循环器
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 处理业务逻辑循环器
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap bs = new ServerBootstrap();
bs.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
// 回车换行作为消息分隔符,消息最大长度设置1024
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder())
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new MyServerHandler());
}
})
// 请求处理线程满时,临时存放完成握手队列的大小,默认50
.option(ChannelOption.SO_BACKLOG, 1024);
// 是否启用心跳保活机制,若链接建立并2小时左右无数据传输,此机制才会被激活(tcp机制)。
//.childOption(ChannelOption.SO_KEEPALIVE, true);
// 同步等待socket链接结果,用户线程waite,直到连接完成被notify,继续执行用户代码
ChannelFuture future = bs.bind(port).sync();
future.channel().closeFuture().sync();
}finally {
// 优雅的释放资源
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
> netty使用很简单,只要加入相应处理handle即可。
>
> 传输层为了效率,tcp协议发送数据包时有可能合并发送,对接收方来说会产生粘包问题,需要在应用层解决拆包,收发数据时协商设计分割点,一般而言有四种分割收到包的方法:
>
> 1. 发送方在发送每段数据后拼接回车换行符,接收方读到“\r\n”则认为是一个独立的数据包。netty默认解析实现是LineBasedFrameDecoder,加入解码handle即可。
> 2. 其他自定义分割符号,如“#”。netty实现handle是DelimiterBasedFrameDecoder.
> 3. 无论数据大小,每次发送固定长度,如1024字节,不够的0补位,超出的截断。缺点是比较生硬,数据小的时候浪费带宽资源。netty实现的handle是FixedLengthFrameHandle.
> 4. 数据分为消息头,消息体,消息头定义消息体长度,接收端解析出长度后只读取指定的长度。需要自己实现decoder。
_上述DecoderHandle全部继承ByteToMessageDecoder,是netty封装的解析二进制数据的处理类,只要将相应handle添加到pipeline中即可,解析完成后传输给自定义的逻辑处理类MyServerHandler。此项目中与c端约定传输json字符串格式数据,每段数据手动增加换行分割符。_
#### 第二个项目(netty与websocket)
springboot版本2.0.6.RELEASE,点进去发现默认依赖<netty.version>4.1.29.Final</netty.version>。
maven依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
SocketService与上一个项目相同,只是把匿名内部类单独创建为ChildChannelInit类,具体实现为:
public class ChildChannelInit extends ChannelInitializer<SocketChannel> {
private Logger logger = LoggerFactory.getLogger(ChildChannelInit.class);
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// logger.debug("有客戶端鏈接新建一條chennel ......");
SSLEngine sslEngine = SslUtil.generateSSLContext().createSSLEngine();
sslEngine.setUseClientMode(false); //服务器端模式
sslEngine.setNeedClientAuth(false); //不需要验证客户端
ch.pipeline().addLast("ssl", new SslHandler(sslEngine));
ch.pipeline().addLast("http-codec", new HttpServerCodec());
// ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
// 把多个httpmessagge组装成一个的默认实现
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("ping", new IdleStateHandler(20,0,0, TimeUnit.SECONDS));
ch.pipeline().addLast("handler", new MyNettyHandler());
}
}
上面是几条是为了给socket加入ssl功能,SslUtil类的主要方法:
private static volatile SSLContext ssl_Context = null;
public static SSLContext generateSSLContext() {
if (null == ssl_Context){
synchronized (SslUtil.class){
if (null == ssl_Context){
try {
KeyStore ks = KeyStore.getInstance("JKS");
InputStream ksInputStream = new FileInputStream(APP_CONFIG.getKeyStorePath());
ks.load(ksInputStream, APP_CONFIG.getKeyStorePass().toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, APP_CONFIG.getKeyStoreKeyPass().toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
ssl_Context = sslContext;
}catch (Exception e){
logger.info("load ssl context failed, error:{}",e.getLocalizedMessage());
}
}
}
}
return ssl_Context;
}
下面的Handle方法则是因为websocket协议是通过http协议握手,然后切换(升级)到socket协议,主要是用来处理http协议的编解码添加的netty自定义实现的handle。
这里有个问题,也是本篇要记录的初衷,在接收消息的handle中,后期测试发现,客户端发来10000条数据,内容是json,每次解析出json中的cmd指令回复相应数据,总会少回6-7条,想到了是粘包导致的问题,但无论是加分割符的编解码还是自定义二进制decoder,pipeline中都不会加载,也就没有任何作用。
__后来查看netty源码,发现在http发送握手后,netty会自动添加及调整websocket的编解码。__
// handshake方法内的部分原码,
p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
> websocketDecoder继承WebSocketFrameDecoder,会处理编解码,并把二进制数据转换成binaryFrame或Textframe,其中frame有个isFinalFragment方法可以判断是否是一条数据的最后一段,如果不是,会通过ContinuationWebSocketFrame消息类型发送剩下的数据,自己在代码逻辑中可以拼接出完整的数据,避免了拆包不清的问题。
>
> 这里处理的是text消息类型,binary同理,用byte数组存就可以了。
自定义处理类:
@ChannelHandler.Sharable
public class MyNettyHandler extends ChannelInboundHandlerAdapter {
private Logger logger = LoggerFactory.getLogger(MyNettyHandler.class);
private WebSocketServerHandshaker handshaker;
private String appendStr = "";
private String currentUserId = "";
String wsFactroyUri = "";
/**客户端链接建立,即为活跃*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("new connect active !! channelId:{}",ctx.channel().id().asShortText());
}
/**客户端断开链接,通道不活跃*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (currentUserId != null && NettyManager.channelGroupMap.containsKey(currentUserId)){
NettyManager.channelGroupMap.get(currentUserId).remove(ctx.channel());
logger.debug("client disconnect!! channelId:{} map user size:{} current user connCount:{}",ctx.channel().id().asShortText(),NettyManager.channelGroupMap.size(), NettyManager.channelGroupMap.get(currentUserId).size());
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
logger.error("!!!!EXCEPTION:{}",cause.toString());
ctx.close();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.第一次握手请求消息由HTTP协议承载,所以它是一个HTTP消息,执行handleHttpRequest方法来处理WebSocket握手请求。
//2.客户端通过socket提交请求消息给服务端,WebSocketServerHandler接收到的是已经解码后的WebSocketFrame消息。
if (msg instanceof FullHttpRequest){
handleHttpRequest(ctx,(FullHttpRequest) msg);
}else if (msg instanceof WebSocketFrame){
handleSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state().equals(IdleState.READER_IDLE)){
logger.info("Can't get client msg or ping in idle time,channel will be closed, channelId:{} ", ctx.channel().id().asLongText());
ctx.channel().close();
}else {
super.userEventTriggered(ctx, evt);
}
}
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws UnsupportedEncodingException{
// 利用http协议完成握手后升级到webSocket
if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
WebSocketServerHandshakerFactory handShakerFac = new WebSocketServerHandshakerFactory( wsFactroyUri, null, false);
handshaker = handShakerFac.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
// 通过它构造握手响应消息返回给客户端
// 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码,
// 添加WebSocketEncoder和WebSocketDecoder之后,服务端就可以自动对WebSocket消息进行编解码了
handshaker.handshake(ctx.channel(), req);
}
}
private void handleSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){
// 判断是否为关闭链接
if (frame instanceof CloseWebSocketFrame){
logger.info("get close socket command");
handshaker.close(ctx.channel(),(CloseWebSocketFrame)frame.retain());
return;
}
// 判断是否ping消息
if (frame instanceof PingWebSocketFrame) {
logger.info("get ping socket command");
ctx.channel().write( new PongWebSocketFrame(frame.content().retain()));
return;
}
// 文本内容
if (frame instanceof TextWebSocketFrame){
String body = ((TextWebSocketFrame) frame).text();
if (!frame.isFinalFragment()){
appendStr += body;
}else {
handleMsg(ctx, body);
}
}else if (frame instanceof ContinuationWebSocketFrame){
String halfBody = ((ContinuationWebSocketFrame) frame).text();
appendStr += halfBody;
if (frame.isFinalFragment()){
handleMsg(ctx, appendStr);
appendStr = "";
}
}
}
private void handleMsg(ChannelHandlerContext ctx, String body){
JSONObject jsonObject ;
try {
jsonObject = new JSONObject(body);
}catch (Exception e){
logger.error("get json error :{}",body);
return;
}
String cmd = (String) jsonObject.get("command");
if (cmd.equals("auth")){
handleAuthLogic(ctx, jsonObject);
}else if (cmd.equals("client_ping")){
handleClientPingLogic(ctx, jsonObject);
}
}
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
// 返回应答给客户端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
private static boolean isKeepAlive(FullHttpRequest req) {
return false;
}
/** 客戶端登錄 */
private void handleAuthLogic(ChannelHandlerContext ctx, JSONObject jsonObject){
logger.debug("json:{}",jsonObject.toString());
String userId = (String)jsonObject.get("from");
Long clientTime = (Long)jsonObject.get("timestamp");
String uniqueId = (String) jsonObject.get("uniqueId");
Long currentTime = System.currentTimeMillis();
Long diff = currentTime - clientTime;
AuthRes authRes = new AuthRes();
authRes.setCommand("auth");
authRes.setFrom("sys");
authRes.setTo(userId);
authRes.setDiff_time(diff);
Service2Controller<UserProfile> s2c = NettyManager.USER_SERVICE.getUserById(userId);
UserProfile userProfile = s2c.getData();
// UserProfile userProfile = new UserProfile();
boolean shouldClose = false;
if (userProfile == null){
authRes.setResult("failed");
authRes.setResson("user_not_exist");
shouldClose = true;
}else {
authRes.setResult("ok");
authRes.setResson("success");
currentUserId = userId;
// 保存当前user的所有链接chennel
// NettyManager.addConnectMap(userId, ctx.channel());
if (!NettyManager.channelGroupMap.containsKey(userId)){
NettyManager.channelGroupMap.put(userId, new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));
}
NettyManager.channelGroupMap.get(userId).add(ctx.channel());
logger.info("connect map user size:{} connect count:{}",NettyManager.channelGroupMap.size(),NettyManager.channelGroupMap.get(userId).size());
}
authRes.setTimestamp(currentTime);
String resString = authRes.toString() + NettyManager.SEP;
ctx.channel().writeAndFlush(new TextWebSocketFrame(resString));
if (shouldClose){
ctx.close();
}
}
/** 客戶端ping */
private void handleClientPingLogic(ChannelHandlerContext ctx, JSONObject jsonObject){
Long clientTime = (Long)jsonObject.get("timestamp");
Long currentTime = System.currentTimeMillis();
long diffTime = Math.abs(currentTime - clientTime);
if (diffTime < 30 * 1000){
JsonObject object = new JsonObject();
object.addProperty("command","client_ping_receive");
String resString = object.toString() + NettyManager.SEP;
ctx.channel().writeAndFlush(new TextWebSocketFrame(resString));
logger.info("receive client ping command ,res:{}", resString);
}
}
}
代码中还有维持存储客户端连接的逻辑,一并记录,保存连接的容器结构是:
Map<String, ChannelGroup> channelGroupMap = new ConcurrentHashMap<>;
键为用户ID,值为当前用户的连接集合。在给某个用户发送数据,在相应地方调用channelGroupMap.get("userId").writeAndFlush()方法即可。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
使用netty时,如果链接异常关闭会打印对应的log信息,下面是别人的博客地址,记录一下。
终止一个连接的正常方式是发送FIN。 在发送缓冲区中 所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。 但我们有时也有可能发送一个RST报文段而不是F IN来中途关闭一个连接。这称为异常关闭 。 进程关闭socket的默认方式是正常关闭,如果需要异常关闭,利用 SO_LINGER选项来控制。 异常关闭一个连接对应用程序来说有两个优点: (1)丢弃任何待发的已经无意义的 数据,并立即发送RST报文段; (2)RST的接收方利用关闭方式来 区分另一端执行的是异常关闭还是正常关闭。 值得注意的是RST报文段不会导致另一端产生任何响应,另一端根本不进行确认。收到RST的一方将终止该连接。程序行为如下: 阻塞模型下,内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端RST。 非阻塞模型下,select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报错RST。 游戏测试过程中发现某些socket错误经常出现,以下是测试游戏服务器时通常考虑的case. 服务器端: 1. Case:客户端程序正常运行的情况下,拔掉网线,杀掉客户端程序 目的:模拟客户端死机、系统突然重启、网线松动或网络不通等情况 结论:这种情况下服务器程序没有检测到任何异常,并最后等待“超时”才断开TCP连接 2. Case:客户端程序发送很多数据包后正常关闭Socket并exit进程(或不退出进程) 目的:模拟客户端发送完消息后正常退出的情况 结论:这种情况下服务器程序能够成功接收完所有消息,并最后收到“对端关闭”(Recv返回零)消息 3. Case:客户端程序发送很多数据包后不关闭Socket直接exit进程 目的:模拟客户端程序退出而忘记关闭Socket的情况(比如通过Windows窗口的关闭图标退出进程,而没有捕获相应关闭事件做正常退出处理等) 结论:这种情况下服务器程序能够收到部分TCP消息,然后收到“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误 4. Case:客户端程序发送很多数据包的过程中直接Kill进程 目的:模拟客户端程序崩溃或非正常方式结束进程(比如Linux下”kill -9″或Windows的任务管理器杀死进程)的情况 结论:这种情况下服务器程序很快收到“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误 5. Case:客户端程序发送很多数据包后正常关闭Socket并exit进程(或不退出进程) 目的:模拟客户端正常关闭Socket后,服务器端在检查到TCP对端关闭前向客户端发送消息的情况 结论:这种情况下服务器程序接收和发送部分TCP消息后,在Send消息时产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误 总结: 当TCP连接的进程在忘记关闭Socket而退出、程序崩溃、或非正常方式结束进程的情况下(Windows客户端),会导致TCP连接的对端进程产生“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误 当TCP连接的进程机器发生死机、系统突然重启、网线松动或网络不通等情况下,连接的对端进程可能检测不到任何异常,并最后等待“超时”才断开TCP连接 当TCP连接的进程正常关闭Socket时,对端进程在检查到TCP关闭事件之前仍然向TCP发送消息,则在Send消息时会产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误 客户端 1. 服务器端已经close了Socket,客户端再发送数据 目的:测试在TCP对端进程已经关闭Socket时,本端进程还未检测到连接关闭的情况下继续向对端发送消息 结论:第一包可以发送成功,但第二包发送失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误 2. 服务器端发送数据到TCP后close了Socket,客户端再发送一包数据,然后接收消息 目的:测试在TCP对端进程发送数据后关闭Socket,本端进程还未检测到连接关闭的情况下发送一包消息,并接着接收消息 结论:客户端能够成功发送第一包数据(这会导致服务器端发送一个RST包 <已抓包验证>),客户端再去Recv时,对于Windows和Linux程序有如下不同的表现: Windows客户端程序:Recv失败,错误码为“10053: An established connection was aborted by the software in your host machine” Linux客户端程序:能正常接收完所有消息包,最后收到正常的对端关闭消息(这一点与Window下不一样) 3. 服务器端在TCP的接收缓冲区中还有未接收数据的情况下close了Socket,客户端再收包 目的:测试在TCP的接收缓冲区中还有未接收数据的情况下关闭Socket时,对端进程是否正常 结论:这种情况服务器端就会向对端发送RST包,而不是正常的FIN包(已经抓包证明),这就会导致客户端提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误 总结: 当TCP连接的对端进程已经关闭了Socket的情况下,本端进程再发送数据时,第一包可以发送成功(但会导致对端发送一个RST包过来): 之后如果再继续发送数据会失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误; 之后如果接收数据,则Windows下会报10053的错误,而Linux下则收到正常关闭消息 TCP连接的本端接收缓冲区中还有未接收数据的情况下close了Socket,则本端TCP会向对端发送RST包,而不是正常的FIN包,这就会导致对端进程提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误 --------------------- 作者:九嶷山 来源:CSDN 原文:https://blog.csdn.net/larry_zeng1/article/details/78982370 版权声明:本文为博主原创文章,转载请附上博文链接!