上篇文章我介绍了netty聊天室的实现效果与实现需求,这篇文章我会给大家展示代码
注:部分代码直接使用netty权威指南中的示例代码
netty权威指南附源代码下载地址:https://download.csdn.net/download/qq_37316272/10872031
netty聊天室在线演示地址:https://blog.csdn.net/qq_37316272/article/details/85130365
主启动类代码
package com.ning.netty;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WebSocketServer {
public static ConcurrentMap<Object, Object> channels = new ConcurrentHashMap<>();
public void run(int port) throws Exception{
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipline = ch.pipeline();
pipline.addLast("http-codec",new HttpServerCodec());
pipline.addLast("aggregator",new HttpObjectAggregator(165536));
ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
pipline.addLast("handler",new WebSocketServerHandler());
}
});
Channel ch = b.bind(port).sync().channel();
System.out.println("Web socket 启动完成,端口号:"+port);
System.out.println("http://localhost:"+port+"/");
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new WebSocketServer().run(8099);
}
}
注意这里我定义了一个 ConcurrentMap用来存放用户和对应的channel连接。
主要看handler代码实现
首先定义静态代码存放位置,主要用于返回页面的html代码:
private String basePath;
{
String os = System.getProperty("os.name");
if(os.toLowerCase().startsWith("win")){
basePath = "G:/myeclipsework/nettychat/src/main/resources/static/";
}else{
basePath = "/opt/nettychat/static/";
}
}
如果接受到的请求是http请求,则去到定义的静态目录下寻找资源:
@Override
protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
如果请求是http请求,请求地址是网站根路径,直接返回index.html,否则就按照请求的路径寻找资源。如果是websocket请求则创建握手处理器类,注意因为消息中我需要包含base64编码的图片,是上万长度的字符串所以这里创建握手处理器类讲传输长度设置为65536*5,处理websocket连接。
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
if (!req.getDecoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
String uri = req.getUri();
File file;
if(uri.equals("/")){
file = new File(basePath+"/index.html");
}else{
file = new File(basePath+uri);
}
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
} catch (FileNotFoundException fnfe) {
return;
}
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
setContentLength(response, fileLength);
setContentTypeHeader(response, file);
if (isKeepAlive(req)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192),
ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
if (total < 0) { // total unknown
System.err.println("Transfer progress: " + progress);
} else {
System.err.println("Transfer progress: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!isKeepAlive(req)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
return;
}
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://localhost:8099/websocket", null, false,65536*5);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
先看一下网页代码实现吧:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Netty聊天室-信息时代-技术博客</title>
<style>
*{padding: 0;margin: 0;list-style: none;}
html,body{width: 100%;height: 100%;}
#useUl li{
list-style: none;
cursor: pointer;
height: 50px;
width: 250px;
border-bottom: 1px solid #ccc;
}/* #ff784d */
#useUl li img{
width: 50px;
height: 50px;
border-radius: 50%;
vertical-align: middle;
margin-right: 20px;
}
#loginDiv{
padding: 25px;
width: 260px;
height: 400px;
border: 1px solid #ccc;
margin: 100px auto;
}
#headImg{
width: 100px;
height: 100px;
border-radius: 50%;
margin: auto;
line-height: 100px;
text-align: center;
cursor: pointer;
}
#username{
width: 95%;
display: block;
height: 30px;
border: 1px solid #ccc;
padding-left: 5%;
margin-top: 30px;
}
#login{
height: 30px;
margin-top: 30px;
width: 100%;
background: #ff784d;
border: none;
cursor: pointer;
outline: none;
color: #fff;
}
#login:hover{
background: #ff521b;
}
#chatDiv{
width: 100%;
height: 100%;
display: none;
}
#userDiv{
width: 250px;
min-height: 400px;
border: 1px solid #ccc;
float: right;
position: absolute;
top: 0;
right: 0;
padding: 20px;
}
#mesDiv{
width: 600px;
height: 500px;
border: 1px solid #ccc;
position: absolute;
left: 30%;
top: 50%;
margin-top: -250px;
}
#sendBtn{
width: 60px;
height: 25px;
background: #ff784d;
border: none;
cursor: pointer;
outline: none;
color: #fff;
}
#sendBtn:hover{
background: #ff521b;
}
.msgDiv{
padding: 0 10px;
margin: 5px 0;
}
.msgDiv .headImg{
width: 40px;
height: 40px;
border-radius: 50%;
border:1px solid #ccc;
margin-right:10px;
vertical-align: top;
}
.msgDiv .msgWrap{
display: inline-block;
max-width: 70%;
}
.msgDiv .userN{
color: #999;
}
.langMsg{
background: #e2e2e2;
padding: 10px;
border-radius: 5px;
color: #333;
}
.rightMes .headImg{
margin-left: 10px;
margin-right: 0;
}
.rightMes{
text-align: right;
}
.rightMes .langMsg{
background: #5FB878;
color:#fff
}
#mes{
overflow-y: scroll;
}
#mes::-webkit-scrollbar {
width: 4px;
height: 1px;
}
#mes::-webkit-scrollbar-thumb {
background: #fff;
}
#mes::-webkit-scrollbar-track {
background: #fff;
}
#mes:hover::-webkit-scrollbar-thumb {
background: #ff3e00;
}
#mes:hover::-webkit-scrollbar-track {
background: rgba(128, 128, 128, 0.5);
}
</style>
</head>
<body>
<canvas id="canvas" style="display: none">
</canvas>
<div id="loginDiv">
<div id="headImg" style="padding: 10px;border:1px solid #ccc">点击上传头像</div><input type="file" id="fileInp" style="display: none"><input type="text" id="username" placeholder="请输入用户名"><input type="button" id="login" value="登陆">
<input type="hidden" id="headInp">
</div>
<div id="chatDiv">
<div id="mesDiv" style="">
<div id="topUser" style="border-bottom: 1px solid #ccc;height: 45px;line-height: 30px;padding: 10px;">
<img src="/favicon.ico" style="vertical-align: middle;border: 1px solid #ccc;margin-right: 20px;border-radius: 50%;">Netty总群
</div>
<div id="mes" style="height: 280px;border-bottom: 1px solid #ccc;vertical-align: top;">
</div>
<div id="mesText" style="height: 130px;padding: 10px;">
<p><img src="/image.png" alt="" style="width: 20px;cursor: pointer;" id="chatImg"></p>
<textarea id="sendInp" style="font-size: 16px;border: none;overflow: hidden;resize: none;outline: none;width: 100%;height: 80px;"></textarea>
<p style="text-align: right;"><input type="button" id="sendBtn" value="发送"></p>
</div>
</div>
<input type="hidden" id="chatInp">
<input type="file" id="chatFileInp" style="display: none">
<div id="userDiv">
<div style="margin: auto;width: 100px;height: 100px;border-radius: 50%;overflow: hidden;">
<img src="" id="headSrc" style="width:100px;height: 100px;">
</div>
<p style="padding: 20px 0;text-align: center;border-bottom: 1px solid #ccc;"><span style="color: #ff521b;" id="us">admin</span></p>
<ul id="useUl" style=";vertical-align: top;">
</ul>
</div>
</div>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
var socket;
if(!window.WebSocket){
window.WebSokcet = window.MozWebSocket;
}
if(window.WebSocket){
socket = new WebSocket("ws://192.168.1.9:8099/websocket");
socket.onmessage = function(event){
var res = JSON.parse(event.data);
console.log(res);
if(res.type == "user"){
$("#mes").append('<div class="leftMes msgDiv"><img src="/favicon.ico" class="headImg"><div class="msgWrap"><p class="userN">管理员</p><p class="langMsg">'+res.username+'已登录</p></div></div>');
$("#useUl").append("<li flag='0'><img src='/headImg/"+res.headImg+"'>"+res.username+"</li>")
}
if(res.type == "msg"){
$("#mes").append('<div class="leftMes msgDiv"><img src="/headImg/'+res.headImg+'" class="headImg"><div class="msgWrap"><p class="userN">'+res.username+'</p><p class="langMsg">'+res.msg+'</p></div></div>');
}
if(res.type == "logout"){
$("#mes").append('<div class="leftMes msgDiv"><img src="/headImg/'+res.headImg+'" class="headImg"><div class="msgWrap"><p class="userN">管理员</p><p class="langMsg">'+res.username+'已退出</p></div></div>');
$("#useUl li").each(function(){
if($(this).text() == res.username){
$(this).remove();
}
});
}
if(res.type == "img"){
$("#mes").append('<div class="leftMes msgDiv"><img src="/headImg/'+res.headImg+'" class="headImg"><div class="msgWrap"><p class="userN">'+res.username+'</p><p class="langMsg"><img src="/chatimg/'+res.chatImg+'" onload="scrollBottom()" style="width:100px"></p></div></div>');
}
$("#mes").scrollTop($("#mes").prop("scrollHeight"),200);
}
socket.onopen = function(event){
console.log("打开WebSocket服务正常,浏览器支持WebSoket");
}
socket.onclose = function(evnet){
console.log("WebSocket关闭!");
}
}
$("#headImg").click(function(){
$("#fileInp").click();
});
$("#chatImg").click(function(){
$("#chatFileInp").click();
});
$("#fileInp").change(function(){
var f = $(this)[0].files[0];
var reader = new FileReader();
if(f){
reader.readAsDataURL(f);
reader.onload = function(e){
var res = reader.result;
$("#headImg").html("<img id='HEADIMG' style='width:100px;border-radius:50%;height:100px' src='"+res+"'>");
/* base64转canvas */
var _img = new Image();
_img.src = res;
_img.onload = function(){
var can = document.getElementById("canvas");
var w = $("#HEADIMG").width();
var h = $("#HEADIMG").height();
var ctx = can.getContext("2d");
$(can).attr("width",w);
$(can).attr("height",h);
$(can).width(w);
$(can).height(h)
ctx.clearRect(0,0,$(can).width(),$(can).height())
ctx.drawImage(_img, 0, 0,100,100);
var base64 = can.toDataURL("image/jpeg");
$("#headInp").val(base64);
}
/* */
}
}
});
function scrollBottom(){
$("#mes").scrollTop($("#mes").prop("scrollHeight"),200);
}
$("#chatFileInp").change(function(){
var f = $(this)[0].files[0];
var reader = new FileReader();
if(f){
reader.readAsDataURL(f);
reader.onload = function(e){
var res = reader.result;
$("#mes").append('<div class="rightMes msgDiv"><div class="msgWrap"><p class="userN">我</p><p class="langMsg"><img src="'+res+'" style="width:100px"></p></div><img src="'+$("#headSrc").attr("src")+'" onload="scrollBottom()" class="headImg"></div>');
var _img = new Image();
_img.src = res;
_img.onload = function(){
var can = document.getElementById("canvas");
var w = _img.width;
var h = _img.height;
if(w > 700){
w = 400;
h = 300;
}
$(can).attr("width",w);
$(can).attr("height",h);
$(can).width(w);
$(can).height(h)
var ctx = can.getContext("2d");
ctx.drawImage(_img, 0, 0,w,h);
var base64 = can.toDataURL("image/jpeg");
$("#chatInp").val(base64);
var list = $("#useUl li[flag=1]");
var len = list.length;
var str = "";
if(len > 0){
for(var i = 0;i < len;i++){
if(i != len -1){
str += list.eq(i).text()+",";
}else{
str += list.eq(i).text()
}
}
}
var isSelect = str == "" ? 0 : 1;
var param = {
"isSelect":isSelect,
"type":"img",
"user":str,
"username":$("#username").val().trim(),
"chatImg":$("#chatInp").val()
}
socket.send(JSON.stringify(param));
}
}
}
});
$("#sendBtn").click(function(){
var list = $("#useUl li[flag=1]");
var len = list.length;
var str = "";
if(len > 0){
for(var i = 0;i < len;i++){
if(i != len -1){
str += list.eq(i).text()+",";
}else{
str += list.eq(i).text()
}
}
}
var isSelect = str == "" ? 0 : 1;
var param = {
"isSelect":isSelect,
"type":"msg",
"user":str,
"username":$("#username").val().trim(),
"msg":$("#sendInp").val().trim()
}
$("#mes").append('<div class="rightMes msgDiv"><div class="msgWrap"><p class="userN">我</p><p class="langMsg">'+$("#sendInp").val().trim()+'</p></div><img src="'+$("#headSrc").attr("src")+'" class="headImg"></div>');
$("#mes").scrollTop($("#mes").prop("scrollHeight"),200);
socket.send(JSON.stringify(param));
});
$(document).on("click","#useUl li",function(){
if($(this).attr("flag") == "0"){
$(this).attr("flag",1)
$(this).css("color","red");
}else{
$(this).css("color","black")
$(this).attr("flag",0)
}
});
$("#login").click(function(){
var user = $("#username").val().trim();
if(user == "" || $("#headInp").val() == ""){
alert("请输入用户名,并上传头像");
return false;
}
socket.send(JSON.stringify({"type":"user","username":user,"headImg":$("#headInp").val()}));
$("#loginDiv").hide();
$("#chatDiv").show();
$("#mes").append('<div class="leftMes msgDiv"><img src="/favicon.ico" class="headImg"><div class="msgWrap"><p class="userN">管理员</p><p class="langMsg">欢迎登陆:'+user+'</p></div></div>');
$("#us").text(user);
$("#headSrc").attr("src",$("#headInp").val());
$("#mes").scrollTop($("#mes").prop("scrollHeight"),200);
});
</script>
</html>
网页一加载完成就会用js进行websocket连接,这时,服务器与浏览器已经建立了连接,但我要实现的是用户带头像登陆功能,所以暂时不能存储用户的信息和channel。当用户选择图片之后,首先将图片进行base64绘制到canvas上,目的是将图片转化成统一的jpg格式,方便服务器统一解析。发送的数据格式为json数据字符串。请注意是将json对象字符串化。JSON.stringify()方法。下面是服务器的接受消息的方法。
接受的消息首先是字符串,需要把字符串转化为对象,所以用fastjson转化成Map对象,每个消息上我都定义了消息类型,如果是user类型,则是用户登录,如果是logout类型则是用户退出,其他都是消息,消息分类普通消息和图片消息
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception{
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(
String.format("%s frame types not supported", frame.getClass().getName()));
}
String request = ((TextWebSocketFrame) frame).text();
System.out.println(request);
Map<String, Object> map = (Map<String, Object>) JSON.parse(request);
if (map.get("type").equals("user")) {
String uuid = saveImg(map,"headimg","headImg");
map.put("headImg", uuid+".jpg");
for (Map.Entry<Object, Object> c : WebSocketServer.channels.entrySet()) {
Map<String,Object> key = (Map<String, Object>) c.getKey();
Map<String, Object> maps = new HashMap<String, Object>();
maps.put("type", "user");
maps.put("username", key.get("username"));
maps.put("headImg", key.get("headImg"));
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(maps)));
}
request = JSON.toJSONString(map);
sendAllUser(request);
WebSocketServer.channels.put(map, ctx.channel());
} else {
if(map.get("type").equals("img")){
String uuid = saveImg(map,"chatimg","chatImg");
map.put("chatImg", uuid+".jpg");
}
Map<String, Object> myInfo = getMyInfo(ctx);
map.put("headImg", myInfo.get("headImg"));
request = JSON.toJSONString(map);
if (map.get("isSelect").equals(0)) {
sendAllNotMe(ctx.channel(), request);
} else {
sendSelectUser(map.get("user").toString(), request);
}
}
}
当用户登录的时候发送得是用户名,头像键值对字符串,所以把用户头像的base64字符串转化成图片存在本地,并把用户头像属性换成本地存放的文件名称,并且把当前登陆的用户信息发送给其他已登录的用户,提示新用户登陆,并且将所有已登录的用户发送给这个新登录的用户,提示当前所有已登录的用户。给当前所有登陆用户发送完消息后,在将自己的信息放入到map中,这样再有新用户登陆,就会遍历这个map发送消息了。
如果是普通的消息,则看用户是否选中其他用户,如果选中了其他用户,则为私聊,否则群发所有登陆用户。
如果服务器收到图片消息,则首先将base64图片解码成图片,存放于服务器目录下,然后将文件名和发送给其他用户,其他用户展示消息会访问服务器的图片进行展示。
下面是保存图片代码
private String saveImg(Map<String, Object> map,String path,String param) throws IOException, FileNotFoundException {
String headImg = map.get(param).toString();
headImg = headImg.split(",")[1];
Decoder decoder = Base64.getDecoder();
byte[] b = decoder.decode(headImg);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {// 调整异常数据
b[i] += 256;
}
}
String uuid = UUID.randomUUID().toString();
File dir = new File(basePath+path);
if(!dir.exists()){
dir.mkdirs();
}
File file = new File(dir+File.separator+uuid+".jpg");
if(!file.exists()){
file.createNewFile();
}
FileChannel chan = new FileOutputStream(file).getChannel();
ByteBuffer buf = ByteBuffer.allocate(b.length);
buf.put(b);
buf.flip();
chan.write(buf);
chan.close();
return uuid;
}
这是发送给选中用户的代码
private static void sendSelectUser(String userStr, String msg) {
String[] split = userStr.split(",");
List<String> list = Arrays.asList(split);
for(Map.Entry<Object, Object> en :WebSocketServer.channels.entrySet()){
Map<String,Object> key = (Map<String, Object>) en.getKey();
if(list.contains(key.get("username"))){
((Channel)en.getValue()).writeAndFlush(new TextWebSocketFrame(msg));
}
}
}
发送给所有用户,但不包括自己,因为自己的不用接收自己的消息,当点击发送按钮的时候直接操作dom进行展示即可
private static void sendAllNotMe(Channel ch, String msg) {
for (Map.Entry<Object, Object> c : WebSocketServer.channels.entrySet()) {
if (!c.getValue().equals(ch)) {
((Channel) c.getValue()).writeAndFlush(new TextWebSocketFrame(msg));
}
}
}
当用户刷新页面,或者用户异常,则channel关闭,则视为用户退出,执行下面的代码,页面会提示用户已退出:
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
for (Map.Entry<Object, Object> ent : WebSocketServer.channels.entrySet()) {
if (ent.getValue().equals(ctx.channel())) {
Map<String,Object> user = (Map<String,Object>) ent.getKey();
String username = user.get("username").toString();
WebSocketServer.channels.remove(user);
for (Map.Entry<Object, Object> s : WebSocketServer.channels.entrySet()) {
Map<String, Object> maps = new HashMap<String, Object>();
maps.put("type", "logout");
maps.put("username", username);
Channel channel = (Channel) s.getValue();
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(maps)));
}
break;
}
}
ctx.close();
}
源码下载地址:https://download.csdn.net/download/qq_37316272/10872047
netty权威指南下载地址:https://download.csdn.net/download/qq_37316272/10872031
欢迎大家下载,多多学习交流,我这个程序还有许多需要改进的地方,欢迎大家不吝赐教哦!