ServerMain
public class ChatServerMain {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
ChannelFuture channelFuture = serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChatServerInitialize()).bind(8989).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
无fuck
说,没新东西。
ServerInitializer
public class ChatServerInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new ChatServerHandler());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
这里用到了新的解码器。
之前的
LengthFieldBasedFrameDecoder
针对的是底层数据流的帧
,也就是Frame
。这次的
DelimiterBasedFrameDecoder
完全针对的就是字符串了。其中的
lineDelimiter
也就是以\n
为分隔符,接下来你会很深刻的感触到。关于编解码器,后续专注研究。
ServerHandler
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
public static final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
Channel channel = ctx.channel();
String user = channel.remoteAddress().toString();
channelGroup.forEach(ch->{
if(ch == channel){
ch.writeAndFlush("[myself]:"+msg+"\n");
return;
}
ch.writeAndFlush("["+user+"]:" + msg+"\n");
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
channelGroup.forEach(ch -> {
ch.writeAndFlush("用户[" + channel.remoteAddress() + "]加入,当前在线"+(channelGroup.size()+1)+"人\n");
});
channelGroup.add(channel);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("用户["+ctx.channel().remoteAddress()+"]上线");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("用户["+ctx.channel().remoteAddress()+"]下线");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
channelGroup.forEach(ch->{
ch.writeAndFlush("用户["+ ctx.channel().remoteAddress()+"]离开,当前在线"+channelGroup.size()+"人\n");
});
}
}
粘包
先简单了解概念吧,刚好有事例进行对比。
如果一个包
1024
,你要发的数据是256
,然后怎么去发最方便呢,要发几个包呢?我们
256
就是一个整体,所以按道理说,应该发四个
包。不过呢,
TCP
自己的管理,当然是只发一个包更节省资源啦,当然了,你还得间隔比较短的时候并发。也就是说,当连续并发,且间隔较短的时候,
一个包
里面有两个包
的数据。不一定是全体的数据,但一个包的确要拆成两个或以上的部分,才能辨识其中的内容。
前面提到的编解码,如果不仔细拆分,混杂在一起的信息不能被解码,被丢弃的可能大大滴。
本来好好地数据,就被这样丢弃简直暴殄天物,但是粘包是无法避免的啊。
就和现在看到的诡异一样:每句传输的字符串,后面都跟上了
\n
。之前可没有这么麻烦的。
a\nb\nc
这个当几行呢,直接一看就是一行,但是\n
转义之后,算作的是三行。本来是一个包,我们需要的就是找个标准,然后进行
拆包
。粘包对应的解决办法,就是如此。想到了,于是对比一下。
lineDelimiter
呢,就是只认\n
然后进行拆分并解析的,如果你不加\n
的话,根本出发不了操作方法。
channelGroup
)说好的聊天呢,还是群聊的情况,会话管理是必须的,因为一个人发送的消息你必须把它发送给其他人。
我们可以使用Set
进行每个channel
的存储,添加或移除对应的时候进行挂你即可。
需要把消息进行广播的时候,慢慢遍历发送即可。
不过呢,netty
提供了channelGroup
这么一个管理工具,还有其他便捷方法,那我们就懒得去自己编写了。
// 创建channelGroup
public static final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 添加
channelGroup.add(channel);
// 大小
channelGroup.size();
// 移除
channelGroup.remove(channel);
代码中并没有使用remove
,因为channelGroup
中使用的是GlobalEventExecutor
,全局的,自动移除。
当会话建立,就添加进去进行管理,便于后续广播操作,会话注销就移除。
当然了,如果检测到@user
信息,你可以找出channel
然后进行单点发送啊,甚至私密信息也可以。
之前说过的生命周期
没有问题,现在添加两个:handlerAdded
和handlerRemoved
。
这个更类似于class
和object
的关系,同一个类,可以实例化多个实体。
所谓的add
就是来了一个链接,同一个handler
被实例化了一个新的对象,remove
就是销毁了这个对象。
之前介绍的其他方法,好比对象的方法,你可以更清晰的区使用和感知。
而这两个东西,更像是类创建
的实例化过程,更加的底层的感觉,比前面的更早或更晚发生。
也就是创建对象和内存回收一样,一般都是JVM
来做,而不是我们去调用的感觉。
handlerAdded
和handlerRemoved
更像是handler
的实例计数的东西,每次创建和销毁都会触发。
毕竟只有有
了,才有所谓的激活,是吧。
上线
和下线
的判断,在这里面,会更加的精确
一些。
电话呼叫中了,但是接通之前挂掉了,也算作打过电话了不是。
add
ClientMain
public class ChatClientMain {
public static void main(String[] args) throws IOException {
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
Channel channel = bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).handler(new ChatClientInitialize()).connect(new InetSocketAddress("localhost",8989)).channel();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while(true){
channel.writeAndFlush(reader.readLine()+ "\n");
}
}finally {
eventLoopGroup.shutdownGracefully();
}
}
}
外部获取
channel
这的确解决了我之前的一大疑惑:
handler
外如何获取channel
呵呵,怪自己吧,现在慢慢学,好好记。
这就是
用
和学
的区别,只有学全面了,用的时候才不会迷茫和无助。
外部循环获取控制台输入,发送时别忘记\n
。
ClientInitializer
public class ChatClientInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new ChatClientHandler());
}
}
同服务器,不再复述
Clienthandler
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println( msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
输入逻辑已在外部完成,handler
仅仅输出服务端数据即可,更多数据格式在服务端进行定制。
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
if("many".equals(msg)){
ctx.writeAndFlush("当前在线人数:"+channelGroup.size() + "人\n");
return;
}
Channel channel = ctx.channel();
String user = channel.remoteAddress().toString();
channelGroup.forEach(ch->{
if(ch == channel){
ch.writeAndFlush("[myself]:"+msg+"\n");
return;
}
ch.writeAndFlush("["+user+"]:" + msg+"\n");
});
}
当然了,写在方法内部不太合适。
你可以更加的规范一下。
至于私信?点对点发送?可以自己尝试一下。
聊天室后台如此,有精力可以写一个GUI
,区分一下输入和输出,自定义一下用户名。。。
人靠衣装嘛,有了外貌会更有个模样。
垃圾代码在此;