一、IO 流的分类
IO流可以操作磁盘数据文件的读写,读写处理效率的高低直接影响到服务响应请求的性能。
IO分为BIO (Blocking I/O),NIO(New I/O), AIO(Asynchronous I/O)
BIO:同步阻塞I/O模式,数据的读取及写入阻塞在一个线程内等待其完成,一请求一应答
NIO:同步非阻塞I/O模式,包含以下几个核心组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
AIO:也即NIO2,异步非阻塞I/O模式,尚未广泛应用。
二、基于JDK NIO的Socket 编程
Socket网络连接通信,有Server端,Client端。
一般地,服务端会创建2个Selector,一个用于阻塞监听客户端的连接,一个用于调度处理客户的请求任务
打开Selector:
Selector serverSelector = Selector.open(); // 监听客户端连接
Selector clientSelector = Selector.open(); // 调度处理客户端请求任务
启动服务端:
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8888));
listenerChannel.configureBlocking(false); // true-阻塞,同BIO, false-非阻塞,使用NIO
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
轮询监听客户端的连接,将serverSelector中的连接记录转交注册到clientSelector中:
// 轮询监听客户端新连接
while (true) {
// 阻塞1000 ms
if (serverSelector.select(1000) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// 客户端新建连接,无需创建新线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
轮询调度处理clientSeletor中接收的客户端请求任务:
// 轮询调度处理Selector接收的客户端请求任务
while (true) {
// 阻塞 1000 ms
if (clientSelector.select(1000) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
clientChannel.read(byteBuffer);
byteBuffer.flip();
String response = Charset.defaultCharset().newDecoder().decode(byteBuffer).toString();
log.info();
// todo something with response
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
以上代码放到一个main方法中,两段死循环分别开启两个线程,运行后打开cmd命令窗口,执行telnet localhost 8888, 任意输入后,可以看到服务端控制台能够成功接收到客户端的请求信息(当然了telnet命令窗口的输入是不可见的,只管输入好了)。
由上可见,NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。只是多加了一层channel,这样可以双向读写,并且读写时都是通过Buffer来操作channel的。不过,JDK的NIO实现Socket编程太过繁琐,下面看下Netty的使用。
三、基于 Netty NIO的Socket 编程
以下基于SpringBoot工程:
yml配置:
#netty
netty:
port: 8888
boss:
thread:
count: 2
worker:
thread:
count: 100
keepAlive: true
backlog: 2048
公用常量:
public interface NettyAttribute {
/** netty消息体 拆包粘包分隔符 **/
String DELIMITER = "_#_";
/** Netty 连接绑定对象key **/
AttributeKey<UserPoint> ATTACHMENT_KEY = AttributeKey.valueOf("ATTACHMENT_KEY");
}
Netty Server 配置:
@Configuration
public class NettyServerConfig {
@Value("${netty.boss.thread.count}")
private int bossCount;
@Value("${netty.worker.thread.count}")
private int workerCount;
@Value("${netty.keepAlive}")
private boolean keepAlive;
@Value("${netty.backlog}")
private int backlog;
@Resource(name = "nettyChannelInitializer")
private NettyChannelInitializer channelInitializer;
@Bean(name = "serverBootstrap")
public ServerBootstrap serverBootstrap() {
// Netty Server 启动类配置
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup(), workerGroup()) // 创建selector
.channel(NioServerSocketChannel.class) // 创建channel
.childHandler(channelInitializer); // channelHandler初始化channel
// 追加 Netty Server 启动类其他配置项:日志输出、keepAlive
Map<ChannelOption<?>, Object> tcpChannelOptions = this.tcpChannelOptions();
Set<ChannelOption<?>> keySet = tcpChannelOptions.keySet();
for (@SuppressWarnings("rawtypes")ChannelOption option : keySet) {
serverBootstrap.option(option, tcpChannelOptions.get(option));
}
return serverBootstrap;
}
@Bean(name = "bossGroup", destroyMethod = "shutdownGracefully")
public NioEventLoopGroup bossGroup() {
return new NioEventLoopGroup(bossCount);
}
@Bean(name = "workerGroup", destroyMethod = "shutdownGracefully")
public NioEventLoopGroup workerGroup() {
return new NioEventLoopGroup(workerCount);
}
@Bean(name = "tcpChannelOptions")
public Map<ChannelOption<?>, Object> tcpChannelOptions() {
Map<ChannelOption<?>, Object> options = new HashMap<ChannelOption<?>, Object>();
options.put(ChannelOption.SO_KEEPALIVE, keepAlive);
options.put(ChannelOption.SO_BACKLOG, backlog);
return options;
}
}
Netty Server 启动类配置:
@Slf4j
@Component
public class NettyServerStarter {
@Resource(name = "serverBootstrap")
private ServerBootstrap serverBootstrap;
@Value("${netty.port}")
private int tcpPort;
private List<ChannelFuture> serverChannelFuture;
@PostConstruct
public void start() throws Exception {
log.info("netty server starting at {}", tcpPort);
// 将多个ServerSocket放到集合容器中便于销毁管理
serverChannelFuture = new ArrayList<>();
serverChannelFuture.add(serverBootstrap.bind(new InetSocketAddress(tcpPort)).sync()); // 启动一个线程,创建ServerSocket并绑定监听指定端口
// 一台服务器上可以绑定多个服务端口
/*serverChannelFuture.add(bootstrap.bind(8192).sync());
serverChannelFuture.add(bootstrap.bind(8193).sync());*/
}
@PreDestroy
public void stop() throws Exception {
if (serverChannelFuture != null) {
for (ChannelFuture channelFuture : serverChannelFuture) {
channelFuture.channel().closeFuture().sync();
}
}
}
}
Netty Channel 初始化配置:
@Component("nettyChannelInitializer")
public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private ServerHandler serverHandler;
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 心跳检测,300秒没有读操作,自动断开连接。
pipeline.addLast("idle", new IdleStateHandler(300, 0, 0));
// 协议解码处理器,判断是什么协议(WebSocket还是TcpSocket),然后动态修改编解码器
pipeline.addLast("protocolDecoder", new ProtocolDecoder());
// 针对socket的解码器(WebSocket基于http,TcpSocket基于tcp, 底层都是基于tcp,以下为通用解码配置)
ByteBuf delimiter = Unpooled.copiedBuffer(NettyAttribute.DELIMITER.getBytes());
pipeline.addLast("delimiter", new DelimiterBasedFrameDecoder(4096, delimiter));
pipeline.addLast("stringDecoder", new StringDecoder());
pipeline.addLast("stringEncoder", new StringEncoder());
// 服务器逻辑
pipeline.addLast("handler", serverHandler);
}
}
协议(WebSocket / TcpSocket)解码处理器配置:
@Slf4j
public class ProtocolDecoder extends ByteToMessageDecoder {
/**
* 请求行信息的长度,ws为:GET /ws HTTP/1.1, http为:GET / HTTP/1.1
*/
private static final int PROTOCOL_LENGTH = 16;
/**
* WebSocket握手协议的前缀, 在访问ws的时候,请求地址需要为如下格式 ws://ip:port/ws
*/
private static final String WEBSOCKET_PREFIX = "GET /ws";
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
String protocol = this.getBufStart(in);
ChannelPipeline pipeline = ctx.channel().pipeline();
log.info("ProtocolHandler protocol:" + protocol);
if (protocol.startsWith(WEBSOCKET_PREFIX)) {
// HttpServerCodec:将请求和应答消息解码为HTTP消息
pipeline.addBefore("handler", "http-codec", new HttpServerCodec());
// HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
pipeline.addBefore("handler", "aggregator", new HttpObjectAggregator(65535));
// ChunkedWriteHandler:向客户端发送HTML5文件,文件过大会将内存撑爆
pipeline.addBefore("handler", "http-chunked", new ChunkedWriteHandler());
pipeline.addBefore("handler", "WebSocketAggregator", new WebSocketFrameAggregator(65535));
// 用于处理websocket, /ws为访问websocket时的uri
pipeline.addBefore("handler", "ProtocolHandler", new WebSocketServerProtocolHandler("/ws"));
// 移除可能缓存的TcpSocket解码器
pipeline.remove(DelimiterBasedFrameDecoder.class);
pipeline.remove(StringDecoder.class);
}
// 重置index标记位
in.resetReaderIndex();
// 移除该协议处理器,该channel后续的处理由对应协议安排好的编解码器处理
pipeline.remove(this.getClass());
}
/**
* 获取buffer中指定长度的信息
* @param in
* @return
*/
private String getBufStart(ByteBuf in) {
int length = in.readableBytes();
if (length > PROTOCOL_LENGTH) {
length = PROTOCOL_LENGTH;
}
// 标记读取位置
in.markReaderIndex();
byte[] content = new byte[length];
in.readBytes(content);
return new String(content);
}
}
Netty 消息处理控制器:
@Slf4j
@Component
@ChannelHandler.Sharable
public class ServerHandler extends SimpleChannelInboundHandler<Object> {
@Autowired
private KafkaProducer kafkaProducer;
private AtomicInteger connectNum = new AtomicInteger(0);
/**
* 客户端连接到服务端后执行
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info(String.format("客户端 id=%s 连接到服务端成功", ctx.channel().id()));
super.channelActive(ctx);
int connections = connectNum.incrementAndGet();
log.info("connections = {}", connections);
}
/**
* 断线移除会话
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
User user = ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).get();
if (user != null && StringUtils.isNotEmpty(user.getId())) {
log.info("channelInactive 用户断开连接 {}", JSON.toJSONString(user));
NettyChannelManager.removeChannel(user.getId(), user.getAppId());
// todo 用户断开连接移除用户在线状态
}
log.info(String.format("客户端 id=%s 断线成功", ctx.channel().id()));
super.channelInactive(ctx);
log.info("[channelInactive]channel.isActive() = {}", ctx.channel().isActive());
connectNum.decrementAndGet();
}
/**
* 异常处理
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("异常" + ctx.channel());
User user = ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).get();
super.exceptionCaught(ctx, cause);
log.info("[exceptionCaught]channel.isActive() = {}", ctx.channel().isActive());
ctx.close();
}
/**
* 心跳超时处理
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
log.info("userEventTriggered事件触发 idelState = " + ((IdleStateEvent) evt).state());
IdleStateEvent event = (IdleStateEvent) evt;
// 超时
if(Arrays.asList(IdleState.READER_IDLE, IdleState.WRITER_IDLE, IdleState.ALL_IDLE).contains(event.state())){
log.info("很长时间没有接收到客户端消息了,自动关闭channel");
// 关闭channel
final ChannelFuture writeAndFlush = ctx.channel().writeAndFlush("Time Out,you will closed!");
writeAndFlush.addListener(future -> {
User user = ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).get();
if (user != null) {
// todo 用户心跳检测超时移除用户在线状态
}
ctx.channel().close();
});
}
} else {
super.userEventTriggered(ctx, evt);
}
}
/**
* 接收消息
* @param ctx
* @param object
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object object){
boolean isWebsocket = object instanceof TextWebSocketFrame;
String msg = "";
if(isWebsocket){
// websocket
msg = ((TextWebSocketFrame) object).text().replaceAll(NettyAttribute.DELIMITER, "");
}else if(object instanceof String){
// netty-tcp socket
msg = (String) object;
}else{
log.info("收到未知类型的消息:" + msg);
return;
}
log.info(String.format("Netty Server receive client[id=%s] msg============== %s ", ctx.channel().id(), msg));
NettyMsg<?> nettyMsg = JSON.parseObject(msg, NettyMsg.class);
// 根据不同消息类型处理相关业务
this.dealWithNettyMsg(ctx, msg, nettyMsg, isWebsocket);
}
/**
* 根据不同消息类型处理相关业务
* @param ctx
* @param msg
* @param nettyMsg
* @param isWebsocket
*/
public void dealWithNettyMsg(ChannelHandlerContext ctx, String msg, NettyMsg<?> nettyMsg, boolean isWebsocket){
switch (nettyMsg.getMsgType()) {
// 握手成功
case NettyMsgType.RESPONSE_SUCCESS:
this.handshake(ctx, msg, nettyMsg, isWebsocket, kafkaProducer);
break;
// 心跳检测
case NettyMsgType.HEART_BEAT :
// 聊天消息
case NettyMsgType.CHAT_MSG :
// todo IM 消息处理
break;
}
}
/**
* 发送消息
* @param ctx
* @param msg
* @param isWebsocket
*/
public void writeAndFlush(ChannelHandlerContext ctx, String msg, boolean isWebsocket){
msg += NettyAttribute.DELIMITER;
ctx.writeAndFlush(isWebsocket ? new TextWebSocketFrame(msg) : msg);
}
/**
* 握手成功
* @param ctx
* @param msg
* @param nettyMsg
* @param isWebsocket
*/
public void handshake(ChannelHandlerContext ctx, String msg, NettyMsg<?> nettyMsg,
boolean isWebsocket, KafkaProducer kafkaProducer){
// 第一次连接,握手成功
User user = new User();
user.setId(nettyMsg.getUserId().toString());
user.setAppId(nettyMsg.getAppId());
ctx.channel().attr(NettyAttribute.ATTACHMENT_KEY).set(user);
// 接收客户端的连接channel
NettyChannelManager.addChannel(nettyMsg.getUserId(), nettyMsg.getAppId(), ctx);
NettyMsgManager.writeAndFlush(ctx, msg, isWebsocket);
log.info("用户:" + JSON.toJSONString(userPoint) + "握手连接 / 心跳检测 成功");
// todo 更新用户在线信息
}
}
Netty session channel 管理:
由于ChannelHandlerContext没有实现序列化,不能通过Redis等进行分布式存储共享。这里只能保存到各个服务节点的JVM内存中(见下面NettyChannelManager处理方法),然后通过kafka分发到各个服务节点。服务端由于负载均衡,且未配置IP hash,客户端每次连接的可能是不同的服务节点,但只会连接到其中一个服务节点(不会重复消费消息),因此kafka的各个服务节点的consumer group需配置不同,即确保每个节点都消费消息
// java -jar /usr/local/jar/xx-im/xx-im.jar -Xms1024m -Xmx1024m -Xmn256m
// --spring.profiles.active=pro --spring.kafka.consumer.group-id=topic_xx_im_01
// --spring.profiles.active=pro --spring.kafka.consumer.group-id=topic_xx_im_02
@Slf4j
public class NettyChannelManager {
/**
* 在线channels
*/
private static Map<String, ChannelHandlerContext> channelMap = new ConcurrentHashMap<>();
/**
* 获取用户Netty会话的key
* @param userId
* @param appId
* @return
*/
private static String getKey(Integer userId, Integer appId){
return "userId:" + userId + ":" + appId;
}
/**
* 加入
* @param userId
* @param appId
* @param channel
*/
public static void addChannel(Integer userId, Integer appId, ChannelHandlerContext channel) {
channelMap.put(getKey(userId, appId), channel);
}
/**
* 移除channel
* @param userId
* @param appId
*/
public static void removeChannel(Integer userId, Integer appId){
String key = getKey(userId, appId);
ChannelHandlerContext channel = channelMap.get(key);
// 心跳检测,空闲会话channel会自动关闭
if (channel != null ) {
channel.close();
}
channelMap.remove(key);
}
/**
* 获取指定channel
* @param userId
* @param appId
* @return
*/
public static ChannelHandlerContext getChannel(Integer userId, Integer appId) {
return channelMap.get(getKey(userId, appId));
}
}
以上是基于Netty+Kafka实现的Socket通信(IM系统),不同业务类型的消息只需要在ServerHandler中分别处理即可。