DotNetty系列六:将服务端和客户端改为Winform窗口,使用Redis做为缓存,实现用户登录,好友,群组上下线显示。

这次改动挺大的。

1.服务端和客户端改为Winform窗口。好多细节未处理,只是实现了功能。

DotNetty系列六:将服务端和客户端改为Winform窗口,使用Redis做为缓存,实现用户登录,好友,群组上下线显示。    DotNetty系列六:将服务端和客户端改为Winform窗口,使用Redis做为缓存,实现用户登录,好友,群组上下线显示。

2.使用Redis做为缓存,版本redis-3.0.1,和RedisDesktopManager做管理。增加二个类库,一个用于Redis数据实体,一个Redis操作和测试。

数据实体部份:

用户:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MessagePack;

namespace CommonModel
{
    [MessagePackObject]
    public class Users
    {
        public static string cacheKey = "Users";

        [Key(0)]
        public int ID { get; set; }

        [Key(1)]
        public string name { get; set; }
        [Key(2)]
        public string password { get; set; }

        private DateTime? registered;

        [Key(3)]
        public DateTime? createtime
        {
            get
            {
                if (registered == null)
                {
                    registered = DateTime.Now;
                }
                return registered.Value;

            }
            set { registered = value; }
        }

        public List<Users> ListDatas()
        {
            List<Users> users = new List<Users>
            {
                new Users{ ID=0,name="zs",password="zs" },
                new Users{ ID=1,name="ls",password="ls" },
                new Users{ ID=2,name="ww",password="ww" },
                new Users{ ID=3,name="zl",password="zl" },
                new Users{ ID=4,name="wb",password="wb" }
            };
            return users;
        }
    }

    public class userFriends
    {
        /// <summary>
        /// 好友ID
        /// </summary>
        public int friend_id { set; get; }
        /// <summary>
        /// 好友名称
        /// </summary>
        public string name { set; get; }
        /// <summary>
        /// 是否在线
        /// </summary>
        public bool isOnline { set; get; }
    }

    public class userGroups
    {
        public string group_id { set; get; }
        public string group_name { set; get; }
        public int user_id { set; get; }
        public string user_name { set; get; }
        /// <summary>
        /// 是否在线
        /// </summary>
        public bool isOnline { set; get; }
    }
}


好友:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CommonModel
{
    public class Friends
    {
        public static string cacheKey = "Friends";

        public int ID { get; set; }

        public int user_id { get; set; }
        public int friend_id { get; set; }

        private DateTime? registered;
        public DateTime? createtime
        {
            get
            {
                if (registered == null)
                {
                    registered = DateTime.Now;
                }
                return registered.Value;

            }
            set { registered = value; }
        }

        public List<Friends> ListDatas()
        {
            List<Friends> friends = new List<Friends>
            {
                new Friends{ ID=0,user_id=0,friend_id=2 },
                new Friends{ ID=1,user_id=0,friend_id=4 },

                new Friends{ ID=3,user_id=1,friend_id=3 },
                new Friends{ ID=4,user_id=1,friend_id=4 },

                new Friends{ ID=5,user_id=2,friend_id=3 },
                new Friends{ ID=6,user_id=2,friend_id=4 },
                new Friends{ ID=7,user_id=2,friend_id=0 },

                new Friends{ ID=8,user_id=3,friend_id=1 },
                new Friends{ ID=9,user_id=3,friend_id=2 },
                new Friends{ ID=10,user_id=3,friend_id=4 },

                new Friends{ ID=11,user_id=4,friend_id=0 },
                new Friends{ ID=12,user_id=4,friend_id=1 },
                new Friends{ ID=13,user_id=4,friend_id=2 },
                new Friends{ ID=14,user_id=4,friend_id=3 }
            };
            return friends;
        }
    }
}

群组:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CommonModel
{
    public class Groups
    {
        public static string cacheKey = "Groups";

        public int ID { get; set; }

        public string group_id { get; set; }
        public string group_name { get; set; }
        public List<int> lUser { set; get; }

        private DateTime? registered;
        public DateTime? createtime
        {
            get
            {
                if (registered == null)
                {
                    registered = DateTime.Now;
                }
                return registered.Value;

            }
            set { registered = value; }
        }

        public List<Groups> ListDatas()
        {
            List<Groups> groups = new List<Groups>
            {
                new Groups{ ID=0, group_id="gOne", group_name="一组", lUser=new List<int>{0,1,2 } },
                new Groups{ ID=0, group_id="gTwo", group_name="二组", lUser=new List<int>{4,1,2 } },
                new Groups{ ID=1, group_id="gThree", group_name="三组", lUser=new List<int>{3,4 } }
            };
            return groups;
        }
    }
}

Redis测试项目:用于将数据写入缓存,直接读取使用,直接运行测试,就将数据写入。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Cache.Redis;
using CommonModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RedisUnitTest
{
    [TestClass]
    public class RedisTest
    {
        /// <summary>
        /// 写入用户数据
        /// </summary>
        [TestMethod]
        public void TestWriteUsersObject()
        {
            ICache cache = CacheFactory.Cache();
            List<Users> u = new Users().ListDatas();
            cache.Write(Users.cacheKey, u);
            List<Users> temp = cache.Read<List<Users>>(Users.cacheKey);
            var us = temp.Find(t => t.name.Equals("张三")&&t.password.Equals("zs"));
        }

        /// <summary>
        /// 写入好友数据
        /// </summary>
        [TestMethod]
        public void TestWriteFriendsObject()
        {
            ICache cache = CacheFactory.Cache();
            List<Friends> u = new Friends().ListDatas();
            cache.Write(Friends.cacheKey, u);
            List<Friends> temp = cache.Read<List<Friends>>(Friends.cacheKey);
            var us = temp.FindAll(t => t.user_id.Equals(4));
        }

        /// <summary>
        /// 写入群组数据
        /// </summary>
        [TestMethod]
        public void TestWriteGroupsObject()
        {
            ICache cache = CacheFactory.Cache();
            List<Groups> u = new Groups().ListDatas();
            cache.Write(Groups.cacheKey, u);
            List<Groups> temp = cache.Read<List<Groups>>(Groups.cacheKey);
            var us = temp.FindAll(t => t.ID.Equals(0));
        }

        /// <summary>
        /// 测试删除数据
        /// </summary>
        [TestMethod]
        public void TestRemove()
        {
            ICache cache = CacheFactory.Cache();
            cache.Remove(Groups.cacheKey);
            cache.RemoveAll();
        }
    }
}

。。。。Redis类库,代码太多,这里省略。

3.用户好友,群组,直接写入缓存读取使用。

using Cache.Redis;
using CommonModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace WinServer.UserInfo
{
    public class GetUser
    {
        ICache cache = CacheFactory.Cache();

        public Users GetUserById(int id) => cache.Read<List<Users>>(Users.cacheKey).Find(t => t.ID == id);

        public Users UserLogin(Users temp)
        {
            List<Users> users = cache.Read<List<Users>>(Users.cacheKey);
            Users self = users.Find(t => t.name.Equals(temp.name) && t.password.Equals(temp.password));
            return self;
        }

        public IEnumerable<userFriends> GetFriendsByID(int id)
        {
            List<Users> users = cache.Read<List<Users>>(Users.cacheKey);
            var lf = cache.Read<List<Friends>>(Friends.cacheKey).FindAll(t => t.user_id.Equals(id)).Select(
                s => new userFriends
                {
                    friend_id = s.friend_id,
                    name = users.Find(u => u.ID.Equals(s.friend_id)).name
                });

            return lf;
        }

        public IEnumerable<userGroups> GetGroupsByID(int id)
        {
            List<Users> users = cache.Read<List<Users>>(Users.cacheKey);

            List<Groups> gtem = cache.Read<List<Groups>>(Groups.cacheKey).FindAll(t => t.lUser.Contains(id));
            List<userGroups> ldGroup = new List<userGroups>();
            gtem.ForEach(o =>
            {
                o.lUser.ForEach(oo =>
                {
                    var user = users.FindAll(b => b.ID.Equals(oo)).Select(r =>
                        new userGroups
                        {
                            group_id = o.group_id,
                            group_name = o.group_name,
                            user_id = r.ID,
                            user_name = r.name
                        });
                    ldGroup.AddRange(user);
                });
            });
            return ldGroup;
        }

        /// <summary>
        /// 根据用户ID得到用户群组内用户列表
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public List<int> GetGroupMembersByUserId(int id) 
        {
            List<Groups> gtem = cache.Read<List<Groups>>(Groups.cacheKey).FindAll(t => t.lUser.Contains(id));

            List<int> lGUs = new List<int>();

            if (gtem.Count > 0)
            {
                gtem.ForEach(s => lGUs.AddRange(s.lUser));
                lGUs = lGUs.Distinct().ToList();
                lGUs.RemoveAll(j => j.Equals(id));
            }
            return lGUs;            
        }

    }
}

4.好友,群组内组员,服务端广播,客户端上下线变红变灰。

服务端:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using CommonLib;
using CommonModel;
using DotNetty.Handlers.Timeout;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Groups;

namespace WinServer
{
    public class ServerHandler : ChannelHandlerAdapter, IDisposable
    {
        public event CommandReceiveEvent MessageReceived;
        public event MessageSendEvent MessageSend;
        public event MessageGroupSendEvent MessageGroupSend;
        public event MessageOfflineEvent MessageOffline;

        public IChannelHandlerContext _Socket { get; set; }

        private void OnMessageReceive(Message msg) => MessageReceived?.Invoke(this, new MessageEventArgs(msg));
        private void OnMessageSend(Message msg) => MessageSend?.Invoke(this, new MessageEventArgs(msg));
        private void OnMessageGroupSend(Message msg) => MessageGroupSend?.Invoke(this, new MessageEventArgs(msg));
        private void OnMessageOffline(int Uid) => MessageOffline?.Invoke(this, Uid);

        public void AddUser(Users temp)
        {
            AllClients.AddOrUpdate(temp.ID, _Socket.Channel, (k, v) => v);
        }

        /// <summary>
        /// 得到该用户所有好友在线状态
        /// </summary>
        /// <param name="fs"></param>
        /// <returns></returns>
        public List<userFriends> GetAllFriends(List<userFriends> fs)
        {
            Action<userFriends> IsOnline = s =>
            {
                if (AllClients.ContainsKey(s.friend_id))
                {
                    s.isOnline = true;
                }
            };
            fs.ForEach(IsOnline);

            return fs;
        }

        /// <summary>
        /// 得到该用户所在群组内的用户在线状态
        /// </summary>
        /// <param name="fs"></param>
        /// <returns></returns>
        public List<userGroups> GetGroupsUsers(List<userGroups> fs)
        {
            Action<userGroups> IsOnline = s =>
            {
                if (AllClients.ContainsKey(s.user_id))
                {
                    s.isOnline = true;
                }
            };
            fs.ForEach(IsOnline);

            return fs;
        }

        /// <summary>
        /// 给在线好友广播发放此用户上线,下线
        /// </summary>
        /// <param name="uf"></param>
        /// <param name="ms"></param>
        public void SendOnlineNotifyToFriends<T>(IEnumerable<userFriends> uf, T obj)
        {
            try
            {
                uf.ToList().ForEach(async s =>
                {
                    if(AllClients.ContainsKey(s.friend_id))
                    {
                        await AllClients[s.friend_id].WriteAndFlushAsync(obj);
                    }
                });
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        /// 给用户所在所有群组内在线成员发送,上线通知
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="lgus"></param>
        /// <param name="obj"></param>
        public void SendOnlineNotifyToGroupsUsers<T>(List<int> lgus,T obj)
        {
            try
            {
                lgus.ForEach(async s =>
                {
                    if (AllClients.ContainsKey(s))
                    {
                        await AllClients[s].WriteAndFlushAsync(obj);
                    }
                });
            }
            catch (Exception ex) { }
        }

        /// <summary>
        /// 单发数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        public async void SendData<T>(T obj)
        {
            try
            {
                await _Socket.WriteAndFlushAsync(obj);
            }
            catch (Exception ex) { }
        }

        /// <summary>
        /// 群发数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        public void GroupSendData<T>(T obj)
        {
            try
            {
                AllClients.Values.ToList().ForEach(async s => await s.WriteAndFlushAsync(obj));
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        private int lossConnectCount = 0;
        public override async void UserEventTriggered(IChannelHandlerContext context, object evt)
        {
            await Task.Run(() =>
            {
                //已经15秒未收到客户端的消息了!
                if (evt is IdleStateEvent eventState)
                {
                    if (eventState.State == IdleState.ReaderIdle)
                    {
                        lossConnectCount++;
                        if (lossConnectCount > 2)
                        {
                            //("关闭这个不活跃通道!");
                            context.CloseAsync();
                        }
                    }
                }
                else
                {
                    base.UserEventTriggered(context, evt);
                }
            });
        }

        public override bool IsSharable => true;//标注一个channel handler可以被多个channel安全地共享。

        //  重写基类的方法,当消息到达时触发,这里收到消息后,在控制台输出收到的内容,并原样返回了客户端
        public override async void ChannelRead(IChannelHandlerContext context, object message)
        {
            await Task.Run(() =>
            {
                if (message is Message oo)
                {
                    OnMessageReceive(oo);
                }
            });
        }

        static volatile ConcurrentDictionary<int, IChannel> AllClients = new ConcurrentDictionary<int, IChannel>();

        //客户端连接进来时
        public override async void HandlerAdded(IChannelHandlerContext context)
        {
            await Task.Run(() =>
            {
                //Console.WriteLine($"客户端{context}上线.");
                base.HandlerAdded(context);

                //AllClients.AddOrUpdate((context.Channel.RemoteAddress as IPEndPoint).Port, context.Channel, (k, v) => v);

            });
        }

        //客户端下线断线时
        public override async void HandlerRemoved(IChannelHandlerContext context)
        {
            await Task.Run(() =>
            {
                //Console.WriteLine($"客户端{context}下线.");
                base.HandlerRemoved(context);

                var key = AllClients.Where(q => q.Value == context.Channel).SingleOrDefault();//.Select(q => q.Key);  //get all keys
                AllClients.TryRemove(key.Key, out IChannel temp);

                //给好友广播下线通知
                OnMessageOffline(key.Key);


                //AllClients.TryRemove((context.Channel.RemoteAddress as IPEndPoint).Port, out IChannel temp);
                //Message ms = new Message { Command = COMMAND.Message, Content = $"恭送{context.Channel.RemoteAddress}离开." };
                //OnMessageGroupSend(ms);
            });
        }

        //服务器监听到客户端活动时
        public override async void ChannelActive(IChannelHandlerContext context)
        {
            _Socket = context;//赋值
            await Task.Run(() =>
            {
                //Console.WriteLine($"客户端{context.Channel.RemoteAddress}在线.");
                base.ChannelActive(context);
            });
        }

        //服务器监听到客户端不活动时
        public override async void ChannelInactive(IChannelHandlerContext context)
        {
            await Task.Run(() =>
            {
                //Console.WriteLine($"客户端{context.Channel.RemoteAddress}离线了.");
                base.ChannelInactive(context);
            });
        }

        // 输出到客户端,也可以在上面的方法中直接调用WriteAndFlushAsync方法直接输出
        public override async void ChannelReadComplete(IChannelHandlerContext context) => await Task.Run(() => { context.Flush(); });

        //捕获 异常,并输出到控制台后断开链接,提示:客户端意外断开链接,也会触发
        public override async void ExceptionCaught(IChannelHandlerContext context, Exception exception)
        {
            await Task.Run(() =>
            {
                //Console.WriteLine("异常: " + exception);
                context.CloseAsync();
            });
        }

        public async void Dispose()
        {
            await _Socket.DisconnectAsync();
            await _Socket.CloseAsync();
        }
    }
}

服务端页面:

using Cache.Redis;
using CommonLib;
using CommonModel;
using DotNetty.Codecs;
using DotNetty.Handlers.Timeout;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using WinServer.UserInfo;

namespace WinServer
{
    public partial class ChatServer : Form
    {
        IChannel boundChannel;
        // 主工作线程组,设置为1个线程
        MultithreadEventLoopGroup bossGroup = new MultithreadEventLoopGroup(1);
        // 工作线程组,默认为内核数*2的线程数
        MultithreadEventLoopGroup workerGroup = new MultithreadEventLoopGroup();

        ServerBootstrap bootstrap;

        static ServerHandler serverHandler;

        List<Users> us = new List<Users>();

        public ChatServer()
        {
            InitializeComponent();

        }

        private async void btn_ConnectServer_Click(object sender, EventArgs e)
        {
            if (boundChannel == null)
            {
                await RunServerAsync();
            }
        }

        private async void btn_DisConnectServer_ClickAsync(object sender, EventArgs e)
        {
            //关闭服务
            if (boundChannel != null)
            {
                await boundChannel.CloseAsync();
                await Task.WhenAll(
                       bossGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)),
                       workerGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)));

                boundChannel = null;
                bootstrap = null;
                //serverHandler.Dispose();
                //bootstrap = null;
            }
        }

        private async Task RunServerAsync()
        {
            try
            {

                //声明一个服务端Bootstrap,每个Netty服务端程序,都由ServerBootstrap控制,
                //通过链式的方式组装需要的参数
                bootstrap = new ServerBootstrap();
                bootstrap
                    .Group(bossGroup, workerGroup) // 设置主和工作线程组
                    .Channel<TcpServerSocketChannel>() // 设置通道模式为TcpSocket
                    .Option(ChannelOption.SoBacklog, 100) // 设置网络IO参数等,这里可以设置很多参数,当然你对网络调优和参数设置非常了解的话,你可以设置,或者就用默认参数吧
                    .Option(ChannelOption.SoKeepalive, true)//保持连接
                    .Option(ChannelOption.RcvbufAllocator, new AdaptiveRecvByteBufAllocator(1024, 1024, 65536))
                    .ChildHandler(new ActionChannelInitializer<ISocketChannel>(channel =>
                    {
                        //工作线程连接器 是设置了一个管道,服务端主线程所有接收到的信息都会通过这个管道一层层往下传输
                        //同时所有出栈的消息 也要这个管道的所有处理器进行一步步处理
                        IChannelPipeline pipeline = channel.Pipeline;

                        //实体类编码器
                        pipeline.AddLast(new CommonEncoder<CommonLib.Message>());
                        pipeline.AddLast(new CommonDecoder());

                        pipeline.AddLast("framing-enc", new LengthFieldPrepender(4, false));
                        pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 4, 0, 4));

                        // IdleStateHandler 心跳
                        //服务端为读IDLE
                        pipeline.AddLast(new IdleStateHandler(180, 0, 0));//第一个参数为读,第二个为写,第三个为读写全部

                        //业务handler ,这里是实际处理业务的Handler
                        serverHandler = new ServerHandler
                        {
                            _Socket = pipeline.FirstContext()
                        };
                        serverHandler.MessageReceived += ServerHandler_MessageReceived;
                        serverHandler.MessageSend += ServerHandler_MessageSend;
                        serverHandler.MessageGroupSend += ServerHandler_MessageGroupSend;
                        serverHandler.MessageOffline += ServerHandler_MessageOffline; ;

                        pipeline.AddLast(serverHandler);

                    }));

                // bootstrap绑定到指定端口的行为 就是服务端启动服务,同样的Serverbootstrap可以bind到多个端口
                boundChannel = await bootstrap.BindAsync(3399);
                //Console.WriteLine("服务启动");

            }
            catch (Exception ex)
            {
                string me = ex.Message;
            }
            finally
            {
                //释放工作组线程
                //await Task.WhenAll(
                //    bossGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)),
                //    workerGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)));
            }
        }

        private async void ServerHandler_MessageOffline(object sender, int e)
        {
            try
            {
                var client = sender as ServerHandler;
                await Task.Run(() =>
                {
                    GetUser gu = new GetUser();
                    //用户好友列表
                    List<userFriends> fs = gu.GetFriendsByID(e).ToList();
                    //得到好友在线状态
                    fs = client.GetAllFriends(fs);
                    //给在线好友广播发放此用户上线
                    var online = fs.Where(s => s.isOnline);
                    if (online.Count() > 0)
                    {
                        var ms = new CommonLib.Message
                        {
                            Command = COMMAND.ServerToClient,
                            EchatMode = ChatMode.GetInfo,
                            Eoperation = Operation.OfflineNotify,
                            strJson = JsonHelper.SerializeObject(e)
                        };
                        client.SendOnlineNotifyToFriends(online, ms);
                    }

                    UpdateUI.SetServerOffline(lb_UserList, e);

                    us.RemoveAll(j => j.ID.Equals(e));

                    //给用户群组广播发送此用户下线
                    //在线群组
                    var lgus = gu.GetGroupMembersByUserId(e);
                    //给群组内成员发送上线通知
                    var gms = new CommonLib.Message
                    {
                        Command = COMMAND.ServerToClient,
                        EchatMode = ChatMode.GetInfo,
                        Eoperation = Operation.GroupsUserOfflineNotify,
                        strJson = JsonHelper.SerializeObject(e)
                    };
                    client.SendOnlineNotifyToGroupsUsers(lgus, gms);
                });
            }
            catch (Exception ex) { }
        }

        private async void ServerHandler_MessageGroupSend(object sender, MessageEventArgs e)
        {
            try
            {
                var clients = sender as ServerHandler;
                await Task.Run(() => { clients.GroupSendData(e.CMD); });
            }
            catch (Exception ex) { }
        }

        private async void ServerHandler_MessageSend(object sender, MessageEventArgs e)
        {
            try
            {
                var client = sender as ServerHandler;
                await Task.Run(() => { client.SendData(e.CMD); });
            }
            catch (Exception ex) { }
        }

        private async void ServerHandler_MessageReceived(object sender, MessageEventArgs e)
        {
            try
            {
                var client = sender as ServerHandler;
                await Task.Run(() =>
                {
                switch (e.CMD.Command)
                {
                    case COMMAND.ClientToServer:
                            switch ((e.CMD as CommonLib.Message).Eoperation)
                            {
                                case Operation.UserLogin:
                                    Users temp = JsonHelper.DeserializeJsonToObject<Users>((e.CMD as CommonLib.Message).strJson);
                                    GetUser gu = new GetUser();

                                    var self = gu.UserLogin(temp);
                                    if (self is null)
                                    {
                                        ServerHandler_MessageSend(sender, new MessageEventArgs(new CommonLib.Message
                                        {
                                            Command = COMMAND.ServerToClient,
                                            EchatMode = ChatMode.Error,
                                            Eoperation = Operation.UserLogin,
                                            Content = "无此用户或者用户密码错误"
                                        }));
                                        Thread.Sleep(1000);
                                        client._Socket.CloseAsync();
                                    }
                                    else
                                    {
                                        //添加用户信息
                                        client.AddUser(self);
                                        //服务端列表显示该用户
                                        us.Add(self);
                                        UpdateUI.SetText(lb_UserList, us);
                                        //Application.DoEvents();

                                        //用户信息
                                        ServerHandler_MessageSend(sender, new MessageEventArgs(new CommonLib.Message
                                        {
                                            Command = COMMAND.ServerToClient,
                                            EchatMode = ChatMode.GetInfo,
                                            Eoperation = Operation.UserLogin,
                                            Content = "登录成功",
                                            strJson = JsonHelper.SerializeObject(self)
                                        }));
                                        //用户好友列表
                                        List<userFriends> fs = gu.GetFriendsByID(self.ID).ToList();
                                        //得到好友在线状态
                                        fs = client.GetAllFriends(fs);

                                        Thread.Sleep(1000);

                                        ServerHandler_MessageSend(sender, new MessageEventArgs(new CommonLib.Message
                                        {
                                            Command = COMMAND.ServerToClient,
                                            EchatMode = ChatMode.GetInfo,
                                            Eoperation = Operation.GetFriends,
                                            strJson = JsonHelper.SerializeObject(fs)
                                        }));

                                        //给在线好友广播发放此用户上线
                                        var online = fs.Where(s => s.isOnline);
                                        if (online.Count() > 0)
                                        {
                                            var ms = new CommonLib.Message
                                            {
                                                Command = COMMAND.ServerToClient,
                                                EchatMode = ChatMode.GetInfo,
                                                Eoperation = Operation.OnlineNotify,
                                                strJson = JsonHelper.SerializeObject(self.ID)
                                            };
                                            client.SendOnlineNotifyToFriends(online, ms);
                                        }

                                        Thread.Sleep(1000);

                                        //用户群组内组员列表
                                        var gs = gu.GetGroupsByID(self.ID).ToList();
                                        gs = client.GetGroupsUsers(gs);

                                        ServerHandler_MessageSend(sender, new MessageEventArgs(new CommonLib.Message
                                        {
                                            Command = COMMAND.ServerToClient,
                                            EchatMode = ChatMode.GetInfo,
                                            Eoperation = Operation.GetGroups,
                                            strJson = JsonHelper.SerializeObject(gs)
                                        }));

                                        //在线群组
                                        var lgus = gu.GetGroupMembersByUserId(self.ID);
                                        //给群组内成员发送上线通知
                                        var gms = new CommonLib.Message
                                        {
                                            Command = COMMAND.ServerToClient,
                                            EchatMode = ChatMode.GetInfo,
                                            Eoperation = Operation.GroupsUserOnlineNotify,
                                            strJson = JsonHelper.SerializeObject(self.ID)
                                        };
                                        client.SendOnlineNotifyToGroupsUsers(lgus, gms);
                                    }
                                    return;
                                default:
                                    return;
                            }
                        case COMMAND.Message:
                            return;
                        default:
                            return;
                    }
                });
            }
            catch (Exception ex) { }
        }

    }

}

客户端只改动页面部份:

using CommonLib;
using CommonModel;
using DotNetty.Handlers.Timeout;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using DotNetty.Codecs;

namespace WinClient
{
    public partial class ChatForm : Form
    {
        private WaitForm wfDlg = new WaitForm();
        MultithreadEventLoopGroup group = new MultithreadEventLoopGroup();
        IChannel clientChannel;

        Users onlineUser;

        string userName = string.Empty, Password = string.Empty;

        public ChatForm()
        {
            InitializeComponent();
        }

        private void InterChatMenuItem_Click(object sender, EventArgs e)
        {
            UpdateUI.SetText(lb_conection, "登录");

            LoginForm loginForm = new LoginForm();
            if(loginForm.ShowDialog()==DialogResult.OK)
            {
                userName = loginForm.txtUserName.Text;
                Password = loginForm.txtPassword.Text;
                loginForm.Close();

                Task.Run(() => RunClientAsync());
            }
            txtChatContent.Focus();
            Application.DoEvents();
            
            //wfDlg.ShowDialog();
        }

        private async void OutInterChatMenuItem_ClickAsync(object sender, EventArgs e)
        {
            if (clientChannel != null)
            {
                await clientChannel.CloseAsync();
                await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));

                UpdateUI.SetText(lb_conection, "就绪");
            }
        }

        private async Task RunClientAsync()
        {
            try
            {
                var bootstrap = new Bootstrap();
                bootstrap
                    .Group(group)
                    .Channel<TcpSocketChannel>()
                    .Option(ChannelOption.TcpNodelay, true)
                    .Option(ChannelOption.RcvbufAllocator,new AdaptiveRecvByteBufAllocator(1024,1024,65536))
                    .Handler(new ActionChannelInitializer<ISocketChannel>(c =>
                    {
                        IChannelPipeline pipeline = c.Pipeline;

                        //实体类编码器
                        pipeline.AddLast(new CommonEncoder<CommonLib.Message>());
                        pipeline.AddLast(new CommonDecoder());

                        pipeline.AddLast("framing-enc", new LengthFieldPrepender(4, false));
                        pipeline.AddLast("framing-dec", new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 4, 0, 4));

                        // IdleStateHandler 心跳
                        //客户端为写IDLE
                        pipeline.AddLast(new IdleStateHandler(0, 180, 0));//第一个参数为读,第二个为写,第三个为读写全部

                        ClientHandler clientHandler = new ClientHandler
                        {
                            _Socket = pipeline.FirstContext()
                        };
                        clientHandler.MessageReceived += ClientHandler_MessageReceived;
                        clientHandler.MessageSend += ClientHandler_MessageSend;
                        pipeline.AddLast(clientHandler);

                    }));

                clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3399));

                UpdateUI.SetText(lb_conection, "连接至服务器");

            }
            catch (Exception ex)
            {
                UpdateUI.SetText(lb_conection, "无法连接服务器");
            }
            finally
            {
                //await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1));
            }
        }


        private async void ClientHandler_MessageSend(object sender, MessageEventArgs e)
        {
            try
            {
                var client = sender as ClientHandler;
                await Task.Run(() =>
                {
                    switch (e.CMD.Command)
                    {
                        case COMMAND.ClientToServer:
                            CommonLib.Message ms = e.CMD as CommonLib.Message;
                            ms.strJson = JsonHelper.SerializeObject(new Users { name = userName, password = Password });

                            client.SendData(ms);
                            break;
                        default:
                            client.SendData(e.CMD);
                            break;
                    }
                    
                });
            }
            catch (Exception ex) { }
        }

        private async void ClientHandler_MessageReceived(object sender, MessageEventArgs e)
        {
            try
            {
                await Task.Run(() =>
                {
                    var client = sender as ClientHandler;
                    var ms = e.CMD as CommonLib.Message;
                    switch (ms.Command)
                    {
                        case COMMAND.ServerToClient:
                            switch (ms.Eoperation)
                            {
                                case Operation.UserLogin:
                                    if(ms.EchatMode.Equals(ChatMode.Error))
                                    {
                                        UpdateUI.SetText(lb_conection, $"{ms.Content}");
                                        //Application.DoEvents();
                                    }
                                    else
                                    {
                                        onlineUser = JsonHelper.DeserializeJsonToObject<Users>(ms.strJson);
                                        UpdateUI.SetText(lb_conection, $"{onlineUser.name}");
                                        //Application.DoEvents();
                                    }
                                    return;
                                case Operation.GetFriends:
                                    List<userFriends> us = JsonHelper.DeserializeJsonToObject<List<userFriends>>(ms.strJson).OrderByDescending(s => s.isOnline).ToList();
                                    UpdateUI.SetText(lbUserFriends, us);
                                    //Application.DoEvents();
                                    return;
                                case Operation.GetGroups:
                                    List<userGroups> gs = JsonHelper.DeserializeJsonToObject<List<userGroups>>(ms.strJson).OrderByDescending(s => s.isOnline).ToList();
                                    UpdateUI.SetTreeView(tv_groups, gs);
                                    //Application.DoEvents();
                                    return;
                                case Operation.OnlineNotify:
                                    string uId = JsonHelper.DeserializeJsonToObject<string>(ms.strJson);
                                    UpdateUI.SetOnlineOffline(lbUserFriends,ms.Eoperation, uId);
                                    //Application.DoEvents();
                                    return;
                                case Operation.OfflineNotify:
                                    string uuId = JsonHelper.DeserializeJsonToObject<string>(ms.strJson);
                                    UpdateUI.SetOnlineOffline(lbUserFriends, ms.Eoperation, uuId);
                                    //Application.DoEvents();
                                    return;
                                case Operation.GroupsUserOnlineNotify:
                                    string guId = JsonHelper.DeserializeJsonToObject<string>(ms.strJson);
                                    UpdateUI.SetGroupsUsersOnlineOffline(tv_groups, ms.Eoperation, guId);
                                    //Application.DoEvents();
                                    return;
                                case Operation.GroupsUserOfflineNotify:
                                    string guoId = JsonHelper.DeserializeJsonToObject<string>(ms.strJson);
                                    UpdateUI.SetGroupsUsersOnlineOffline(tv_groups, ms.Eoperation, guoId);
                                    //Application.DoEvents();
                                    return;
                                default:
                                    return;
                            }
                        default:
                            return;
                    }

                });
            }
            catch (Exception ex) { }
        }

        private void lbUserFriends_DrawItem(object sender, DrawItemEventArgs e)
        {
            if (e.Index >= 0)
            {
                var tem = (lbUserFriends.Items[e.Index] as userFriends);
                e.DrawBackground();
                Brush mybsh = Brushes.Black;
                // 判断是什么类型的标签
                if (tem.isOnline)
                {
                    mybsh = Brushes.Red;
                }
                else 
                {
                    mybsh = Brushes.Gray;
                }
                // 焦点框
                //e.DrawFocusRectangle();
                //文本 
                e.Graphics.DrawString(tem.name, e.Font, mybsh, e.Bounds, StringFormat.GenericDefault);
            }

        }

        private void bt_send_Click(object sender, EventArgs e)
        {
            var to = lbUserFriends.SelectedItem as userFriends;
            if (to != null&& to.isOnline)
            {
                var receiver = to as userFriends;
            }
            else
            {
                MessageBox.Show("此用户不在线");
            }
        }
    }
}

5.改动自定义协议。

/// <summary>
    /// 基本通信协议
    /// </summary>
    public enum COMMAND
    {
        /// <summary>
        /// 心跳
        /// </summary>
        HeartBeat = 1000,
        /// <summary>
        /// 消息
        /// </summary>
        Message = 1,

        /// <summary>
        /// 空消息
        /// </summary>
        NULL = 0,
        /// <summary>
        /// 客户端到服务器
        /// </summary>
        ClientToServer = 7,
        /// <summary>
        /// 客户端到客户端
        /// </summary>
        ClientToClient = 8,
        /// <summary>
        /// 服务器到客户端
        /// </summary>
        ServerToClient = 9,
        /// <summary>
        /// 异常捕获
        /// </summary>
        ExceptionCaught = 6,

    }

    /// <summary>
    /// 协议里的操作内容
    /// </summary>
    public enum Operation
    {
        /// <summary>
        /// 用户登陆
        /// </summary>
        UserLogin = 10,

        /// <summary>
        /// 得到帐户信息
        /// </summary>
        GetUser =11,
        /// <summary>
        /// 得到好友列表
        /// </summary>
        GetFriends = 0,
        /// <summary>
        /// 得到群组列表
        /// </summary>
        GetGroups = 1,
        /// <summary>
        /// 普通消息
        /// </summary>
        NormalMessage=2,
        /// <summary>
        /// 上线通知
        /// </summary>
        OnlineNotify = 1001,
        /// <summary>
        /// 下线通知
        /// </summary>
        OfflineNotify = 1002,
        /// <summary>
        /// 群组成员上线通知
        /// </summary>
        GroupsUserOnlineNotify = 1003,
        /// <summary>
        /// 群组成员下线通知
        /// </summary>
        GroupsUserOfflineNotify = 1004,
    }

    /// <summary>
    /// 聊天模式
    /// </summary>
    public enum ChatMode
    {
        /// <summary>
        /// 单聊
        /// </summary>
        SingleChat=0,
        /// <summary>
        /// 群聊
        /// </summary>
        GroupChat = 1,
        /// <summary>
        /// 私聊
        /// </summary>
        PrivateChat = 2,

        /// <summary>
        /// 得到信息
        /// </summary>
        GetInfo = 3,        
        /// <summary>
        /// 发生错误
        /// </summary>
        Error = 5000,
    }

6.还有杂七杂八的好多东西。代码太多,下载源码看吧。

7.完成后的效果。

DotNetty系列六:将服务端和客户端改为Winform窗口,使用Redis做为缓存,实现用户登录,好友,群组上下线显示。

redis下载

项目源码