netty基础知识

1.websocket 和netty的关系

websocket是一个通信协议,而netty是一个Java网络编程框架。我们可以利用netty实现websocket通信,也可以用其他的。

2.HTTP是单工的还是双工的还是半双工的

先来区分三者的含义

1.单工: 数据传输只允许在一个方向上的传输,只能一方来发送数据,另一方来接收数据并发送。例如:对讲机

2.半双工:数据传输允许两个方向上的传输,但是同一时间内,只可以有一方发送或接受消息。例如:打电话

3.全双工:同时可进行双向传输。例如:websocket

3.http协议是什么工作模式呢

1.http1.0:单工。因为是短连接,客户端发起请求之后,服务端处理完请求并收到客户端的响应后即断开连接。

2.http1.1:半双工。默认开启长连接keep-alive,开启一个连接可发送多个请求。

3.http2.0:全双工,允许服务端主动向客户端发送数据。

4.WebSocket和HTTP什么关系?

WebSocket借助http协议进行握手,三次握手成功后,就会变身为TCP通道,从此与http不再相见。

在WebSocket API中,浏览器和服务器只需要完成一次握手(不是指建立TCP连接的那个三次握手,是指在建立TCP连接后传输一次握手数据),两者之间就直接可以创建持久性的连接,并进行双向数据传输

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

5.Netty到底是什么

Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。
在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。
Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的高性能并发模型

6.为什么选择Netty

如果不用netty,使用原生JDK的话,有如下问题:
1、API复杂
2、对多线程很熟悉:因为NIO涉及到Reactor模式
3、高可用的话:需要出路断连重连、半包读写、失败缓存等问题
4、JDK NIO的bug
而Netty来说,他的api简单、性能高而且社区活跃(dubbo、rocketmq等都使用了它)

7.netty实战实现聊天系统

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.33.Final</version>
        </dependency>
@Component
public class NettyServer {
	
	private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
	
	/**
	 *  netty端口
	 */
	private static int port = 8080;
	
	private static class SingletionWSServer {
		static final NettyServer instance = new NettyServer();
	}
	
	public static NettyServer getInstance() {
		return SingletionWSServer.instance;
	}
	
	private EventLoopGroup mainGroup;
	private EventLoopGroup subGroup;
	private ServerBootstrap server;
	private ChannelFuture future;
	
	public NettyServer() {
		mainGroup = new NioEventLoopGroup();
		subGroup = new NioEventLoopGroup();
		server = new ServerBootstrap();
		server.group(mainGroup, subGroup)
			.channel(NioServerSocketChannel.class)
			.childHandler(new NettyChannelInitializer());
	}
	
	public void start() {
		this.future = server.bind(port);
		log.info("netty server server 启动完毕... port = "+port);
	}
	
}

管道初始化

public class NettyChannelInitializer extends ChannelInitializer<SocketChannel>{

	private static final Logger log = LoggerFactory.getLogger(NettyChannelInitializer.class);
	
	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
		log.info(" 管道初始化...... ");
		ChannelPipeline pipeline = ch.pipeline();
		// websocket 基于http协议,所以要有http编解码器
		pipeline.addLast("HttpServerCodec",new HttpServerCodec());
			
		// 对写大数据流的支持 	
		pipeline.addLast(new ChunkedWriteHandler());
		
		// 对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse
		// 几乎在netty中的编程,都会使用到此hanler
		pipeline.addLast(new HttpObjectAggregator(1024*64));
		
		
		// 增加心跳支持 start
		// 针对客户端,如果在1分钟时没有向服务端发送读写心跳(ALL),则主动断开
		// 如果是读空闲或者写空闲,不处理
		pipeline.addLast(new IdleStateHandler(8, 10, 12));
		// 自定义的空闲状态检测
		pipeline.addLast(new NettyWsChannelInboundHandler());
		
		// 以下是支持httpWebsocket
		/**
		 * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
		 * 本handler会帮你处理一些繁重的复杂的事
		 * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
		 * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
		 */
		pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
			
		// 自定义的wshandler
		pipeline.addLast(new NettyWsChannelInboundHandler());
			
		// 自定义 http
		pipeline.addLast(new NettyHttpChannelInboundHandler());
	}

}

处理消息的handler

public class NettyWsChannelInboundHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
	
	private static final Logger log = LoggerFactory.getLogger(NettyWsChannelInboundHandler.class);
	
	/**
	 * 用于记录和管理所有客户端的channle
	 */
	public static ChannelGroup users = 
			new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
	
	/**
	 * 用户信息列表
	 */
	public static List<FriendInfo> friendList = new ArrayList<FriendInfo>();
	
	/**
	 * 从channel缓冲区读数据
	 */
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) 
			throws Exception {
		// 获得   channel
		Channel currentChannel = ctx.channel();
		// 获取客户端传输过来的消息
		String content = msg.text();
			
		// 1. 获取客户端发来的消息
		DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
		Integer action = dataContent.getAction();
		
		// 2. 判断消息类型,根据不同的类型来处理不同的业务
		if (action == MsgActionEnum.CONNECT.type) {
			// 	2.1  当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来
			String sendId = dataContent.getChatMsg().getSendId();
			UserChannelRel.put(sendId, currentChannel);
			UserChannelRel.output();
			
		} else if (action == MsgActionEnum.CHAT.type) {
			//  2.2  聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]
			ImChatMsgLogsService imChatMsgLogsService = (ImChatMsgLogsService)SpringBeanUtil.getBean("imChatMsgLogsServiceImpl");
			ChatMsg chatMsg = dataContent.getChatMsg();
			String sendId = chatMsg.getSendId();
			String receiveId = chatMsg.getReceiveId();
			String msgContent = chatMsg.getMsgContent();
			Integer mainUserId = chatMsg.getMainUserId();
			Integer userId = chatMsg.getUserId();
				
			// 保存消息到数据库,并且标记为 未签收
			ImChatMsgLogs logs = new ImChatMsgLogs();
			logs.setMainUserId(mainUserId);
			logs.setUserId(userId);
			logs.setSendId(sendId);
			logs.setReceiveId(receiveId);
			logs.setMsgContent(msgContent);
			logs.setToType(1);
			Integer msgId = imChatMsgLogsService.saveWebMsgLogs(logs);
			chatMsg.setMsgId(msgId.toString());
			// 消息发送时间
			chatMsg.setSendTime(new Date());
			DataContent dataContentMsg = new DataContent();
			dataContentMsg.setChatMsg(chatMsg);
			// 给自己发送成功消息
			Channel sendIdChannel = UserChannelRel.get(sendId);
			sendIdChannel.writeAndFlush(
					new TextWebSocketFrame(
							JsonUtils.objectToJson(dataContent)));
			
			// 发送消息 从全局用户Channel关系中获取接受方的channel
			Channel receiverChannel = UserChannelRel.get(receiveId);
			
			if (receiverChannel == null) {
				// TODO channel为空代表用户离线,推送消息(JPush,个推,小米推送)   添加离线消息记录
				log.info(" 用户离线1 ... receiverChannel 是  null");
				imChatMsgLogsService.updateOfflineStatusTwo(msgId);
			} else {
				// 当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在
				Channel findChannel = users.find(receiverChannel.id());
				if (findChannel != null) {
					// 用户在线		
					receiverChannel.writeAndFlush(
							new TextWebSocketFrame(
									JsonUtils.objectToJson(dataContent)));
				} else {
					// 用户离线 TODO 推送消息	 添加离线消息记录
					log.info(" 用户离线2 ... findChannel 是  null");
					imChatMsgLogsService.updateOfflineStatusTwo(msgId);
				}
			}	
		} else if (action == MsgActionEnum.SIGNED.type) {
			log.info(" 消息通知.....  ");
			
			// 扩展字段在signed类型的消息中,代表需要去签收的消息id,逗号间隔
			String msgIdsStr = dataContent.getExtand();
			String msgIds[] = msgIdsStr.split(",");
			
			List<String> msgIdList = new ArrayList<>();
			for (String mid : msgIds) {
				if (StringUtils.isNotBlank(mid)) {
					msgIdList.add(mid);
				}
			}	
			if (msgIdList != null && !msgIdList.isEmpty() && msgIdList.size() > 0) {
				//  2.3  签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收]
				// 批量签收
				ImChatMsgLogsService imChatMsgLogsService = (ImChatMsgLogsService)SpringBeanUtil.getBean("imChatMsgLogsServiceImpl");
				imChatMsgLogsService.updateMsgReadStatusOne(msgIdList);
			}
			
		} else if (action == MsgActionEnum.KEEPALIVE.type) {
			//  2.4  心跳类型的消息		
		    log.info("收到来自channel为[" + currentChannel + "]的心跳包...");
		}else if (action == MsgActionEnum.FRIEND_REQUEST.type) {
			// 好友申请
			ChatMsg chatMsg = dataContent.getChatMsg();
			String sendId = chatMsg.getSendId();
			String receiveId = chatMsg.getReceiveId();
			log.info("sendId = "+sendId +".... 好友申请.... receiveId =" +receiveId);
		}else if (action == MsgActionEnum.GROUP_MSG.type) {
			// 群消息发送
			ImChatMsgLogsService imChatMsgLogsService = (ImChatMsgLogsService)SpringBeanUtil.getBean("imChatMsgLogsServiceImpl");
			ChatMsg chatMsg = dataContent.getChatMsg();
			String sendId = chatMsg.getSendId();
			//String receiveId = chatMsg.getReceiveId();
			String msgContent = chatMsg.getMsgContent();
			Integer mainUserId = chatMsg.getMainUserId();
			Integer userId = chatMsg.getUserId();
				
			// 保存消息到数据库,并且标记为 未签收
			ImChatMsgLogs logs = new ImChatMsgLogs();
			logs.setMainUserId(mainUserId);
			logs.setUserId(userId);
			logs.setSendId(sendId);
			// 1 是测试群	
			logs.setGroupInfoId(1);
			//logs.setReceiveId(receiveId);
			logs.setMsgContent(msgContent);
			logs.setToType(2);
			Integer msgId = imChatMsgLogsService.saveWebMsgLogs(logs);
			chatMsg.setMsgId(msgId.toString());
			// 消息发送时间	
			chatMsg.setSendTime(new Date());
			DataContent dataContentMsg = new DataContent();
			dataContentMsg.setChatMsg(chatMsg);
			// 给所有在线的 im用户 发送信息
			for (Channel c : users) {
				c.writeAndFlush(	
						new TextWebSocketFrame(
								JsonUtils.objectToJson(dataContent)));
			}	
			// 更新消息状态为已读	
			log.info(" 群消息发送... users.size = "+users.size());
		}
	}
	
	/**
	 * 当客户端连接服务端之后(打开连接)
	 * 获取客户端的channle,并且放到ChannelGroup中去进行管理
	 */
	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		users.add(ctx.channel());
		log.info(" netty 获得连接.....	");
	}

	@Override
	public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
		String channelId = ctx.channel().id().asShortText();
		// 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
		users.remove(ctx.channel());
		log.info("客户端被移除,channelId为:" + channelId);
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		// 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除
		ctx.channel().close();
		users.remove(ctx.channel());
		log.info(" netty 异常了...... ");
	}

}

前端发起websocket

window.IM_CHAT = {
		socket: null,
		init: function() {
			if (window.WebSocket) {		
				IM_CHAT.socket = new WebSocket(wsWebSocketUrl);
				IM_CHAT.socket.onopen = function() {
					$(".im-connect-status").text("im连接成功.."+imChatUser.user.nickname);
					IM_CHAT.chatConnect();	
				},	
				IM_CHAT.socket.onclose = function() {
					$(".im-connect-status").text("连接关闭.."+imChatUser.user.nickname);
				},		
				IM_CHAT.socket.onerror = function() {
					$(".im-connect-status").text("发生错误.."+imChatUser.user.nickname);
				},
				IM_CHAT.socket.onmessage = function(e) {
					console.log("接受到消息:" + e.data);		
					var dataContent = JSON.parse(e.data);
					var action = dataContent.action;
					var chatMsg = dataContent.chatMsg;
					var msgContent = chatMsg.msgContent;
					// 发送者id
					var sendId = chatMsg.sendId;
					var sendTime = chatMsg.sendTime;
					// 接受者id
					var receiveId = chatMsg.receiveId;
					
					var html = "";
					var sendNickname = "";
					if(imChatUser.user.id==sendId){	
						// 自己发的	
						html+='<li class="layim-chat-mine"> ';
						html+='<div class="layim-chat-user">';
						html+='<img src="'+imChatUser.user.pictureUrl+'">';
						html+='<cite><i>'+commonDateFormat(sendTime,"yyyy-MM-dd hh:mm:ss")+'</i>'+imChatUser.user.nickname+'</cite>';
						html+='</div>';	
						html+='<div class="layim-chat-text" style="background-color: #00c1de;color: #efeff4;">'+msgContent+'</div>';
						html+='</li>';	
					}else{
						for (var i = 0; i < all_user_list_cache.length; i++) {
							var obj = all_user_list_cache[i];	
							if(obj.id==sendId){
								// 收到别人发的		
								html+='<li>';
								html+='<div class="layim-chat-user">';	
								html+='<img src="'+obj.pictureUrl+'">';
								html+='<cite>'+obj.nickname+'<i>'+commonDateFormat(sendTime,"yyyy-MM-dd hh:mm:ss")+'</i></cite>';
								html+='</div>';
								html+='<div class="layim-chat-text">'+msgContent+'</div>';
								html+='</li>';	
								sendNickname = obj.nickname;
								break;
							}
						}
					}
					// 2, "聊天消息"	7, "群消息"
					if(action==2){		
						if(imChatUser.user.id==sendId){	
							$(".im-chat-msg-logs-to-receive-friend-user-id-"+sendId+receiveId).append(html);	
							$(".layim-chat-logs-main.friend-user-id-"+receiveId).scrollTop($(".layim-chat-logs-main.friend-user-id-"+receiveId)[0].scrollHeight);
							$(".sed-msg-friend-user-id-"+receiveId).val("");		
						}else{	
							$(".im-chat-msg-logs-to-receive-friend-user-id-"+receiveId+sendId).append(html);		
							$(".layim-chat-logs-main.friend-user-id-"+sendId).scrollTop($(".layim-chat-logs-main.friend-user-id-"+sendId)[0].scrollHeight);
							//$(".sed-msg-friend-user-id-"+receiveId).val("");
							voicePlaying("好友消息: "+sendNickname+" 发送了, 内容为: "+msgContent)
						}		
					}else if(action==7){
						$(".im-chat-msg-logs-to-group").append(html);			
						$(".layui-form-item.layim-chat-logs-main").scrollTop($(".layui-form-item.layim-chat-logs-main")[0].scrollHeight);
						if(imChatUser.user.id==sendId){							
							$("textarea[name='sedGroupMsg']").val("")
						}else{	
							// 群消息  	
							voicePlaying("群消息: "+sendNickname+" 发送了, 内容为: "+msgContent)
						}
					}
				}
			} else {
				alert("浏览器不支持websocket协议...");
			}
		},
		chatConnect: function() {
			if (!imChatUser) {	
				layer.msg("请先登录,获取用户失败");
				return false;
			}
			var chatMsgObj = {
				sendId:imChatUser.user.id,	// 发送者的用户id	
				receiveId:"",				// 接受者的用户id
				msgContent:"",				// 聊天内容
				msgId:""					// 用于消息的签收
			}
			var msgObj = {
				action:1,			// CONNECT(1, "第一次(或重连)初始化连接"), CHAT(2, "聊天消息"), SIGNED(3, "消息签收"), 
									// KEEPALIVE(4, "客户端保持心跳"), PULL_FRIEND(5, "拉取好友");
				chatMsg:chatMsgObj,	// 用户的聊天内容entity
				extand:""			// 扩展字段
			}			
			var msgObjJson = JSON.stringify(msgObj);
			IM_CHAT.socket.send(msgObjJson);
				
			// 查询 群信息列表
			selectGroupLogsList(1);
		},
		chatSendMsg: function(msg,rId) {
			if (!imChatUser) {
				layer.msg("请先登录,获取用户失败");
				return false;
			}
			if (!msg) {	
				layer.msg("消息不能为空");
				return false;
			}
			var chatMsgObj = {
				sendId:imChatUser.user.id,		// 发送者的用户id	
				receiveId:rId,					// 接受者的用户id
				msgContent:msg,					// 聊天内容
				msgId:""						// 用于消息的签收
			}
			var msgObj = {
				action:2,						// CONNECT(1, "第一次(或重连)初始化连接"), CHAT(2, "聊天消息"), SIGNED(3, "消息签收"), 
												// KEEPALIVE(4, "客户端保持心跳"), PULL_FRIEND(5, "拉取好友");
				chatMsg:chatMsgObj,				// 用户的聊天内容entity
				extand:""						// 扩展字段
			}		
			var msgObjJson = JSON.stringify(msgObj);
			IM_CHAT.socket.send(msgObjJson);
		},
		chatSendGroupMsg: function(msg) {
			if (!imChatUser) {
				layer.msg("请先登录,获取用户失败");
				return false;
			}
			if (!msg) {	
				layer.msg("消息不能为空");
				return false;
			}
			var chatMsgObj = {
				sendId:imChatUser.user.id,		// 发送者的用户id	
				receiveId:"",					// 接受者的用户id
				msgContent:msg,					// 聊天内容
				msgId:""						// 用于消息的签收
			}
			var msgObj = {
				action:7,			// CONNECT(1, "第一次(或重连)初始化连接"), CHAT(2, "聊天消息"), SIGNED(3, "消息签收"), 
									// KEEPALIVE(4, "客户端保持心跳"), PULL_FRIEND(5, "拉取好友");
				chatMsg:chatMsgObj,	// 用户的聊天内容entity
				extand:""			// 扩展字段
			}			
			var msgObjJson = JSON.stringify(msgObj);
			IM_CHAT.socket.send(msgObjJson);
		},
	};		
  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值