.NET Core和SignalR实现一个简单版聊天系统——服务端1

最近公司给了一个需求,因为业务服务是部署在linux系统内的,linux无法连接打印机,所以需要写一个winform作为客户端放到用户的windows去获取用户电脑的打印机列表。于是就用到了双工通信。

一开始想用websocket,写了一大堆代码,最后却有跨域和无法连接服务端等问题,可是我把服务放到自己的服务器却又可以,由于急着交付,也没想着找bug了,赶忙又换成了SignalR,也罢websocket代码量多,还不好管理。弄完以后,就突然想写一个winform的聊天系统,于是就有了这篇文章。

一、搭建环境

准备一个.net core的WebApi项目和一个EntityFrameWork 4.7的 winform项目

 二、服务端配置数据库

先建个数据库吧,这边用的EF Core的Code First代码先行。

既然是简单版的那就三个表UserInfo用户表,Buddy好友表,ChatRecord聊天记录

 代码如下:非常简单,用户表就不用说了,好友表就是自己的userid和好友的userid

public class UserInfo:BaseModel
    {
        public string UserName { get; set; }
        public string NickName { get; set; }
        public string PassWord { get; set; }
        public DateTime LastLoginTime { get; set; }=DateTime.Now;
        public bool IsDelete { get; set; }
    }

public class Buddy:BaseModel
    {
        public long UserId { get; set; }
        public long FriendId { get; set; }//好友的userid
    }
public class ChatRecord
    {
        public long Id { get; set; }  
        /// <summary>
        /// 发送者ID
        /// </summary>
        public long SenderId { get; set; }
        /// <summary>
        /// 接收者ID
        /// </summary>
        public long RecipientId { get; set; }
        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }

        public DateTime SendTime { get; set; }
    }
 public class BaseModel
    {
        public long Id { get; set; }
        public DateTime CreateTime { get; set; } = DateTime.Now;
    }

然后建立数据库上下文ChatRoomContext。

注意要下载两个包,版本不要过高,可能不适配。我这边是5.0.11 

public class ChatRoomContext : DbContext
    {
        public ChatRoomContext(DbContextOptions<ChatRoomContext> options) : base(options)
        {

        }

        public DbSet<UserInfo> UserInfos { get; set; }
        public DbSet<Buddy> Buddys { get; set; }
        public DbSet<ChatRecord> ChatRecords { get; set; }
    }

在startup类中的ConfigureServices方法注入数据库服务,连接字符串写在配置文件appsetting.json中

public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            
            services.AddDbContext<ChatRoomContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("Default"));
            });
        }

 用过EF Core的肯定知道下面要怎么做了,那就是生成迁移文件,并生成数据库

启动项目选服务端,打开程序包管理器执行以下两句指令

add-migration init

update-database

 执行完后就可以去数据库看看没有成功生成数据库了。

三、数据库操作的服务

先在根目录建一个service文件夹,一个接口一个实现 ,里面写了一些对数据库的CRUD

 

public interface IUserService
    {
        /// <summary>
        /// 获取该用户的所有好友
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public Task<List<UserInfo>> GetBuddyId(long userId);
        /// <summary>
        /// 获取用户信息
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public Task<UserInfo> GetUserInfoById(long userId);
        /// <summary>
        /// 添加用户
        /// </summary>
        /// <param name="userInfo"></param>
        /// <returns></returns>
        public Task<UserInfo> CreateUserInfo(UserInfo userInfo);
        /// <summary>
        /// 添加好友
        /// </summary>
        /// <param name="buddy"></param>
        /// <returns></returns>
        public Task<string> AddBuddy(Buddy buddy);
        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="pwd"></param>
        /// <returns></returns>
        public Task<UserInfo> Login(string userName, string pwd);
        /// <summary>
        /// 用户名是否存在
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public Task<bool> Exits(string userName);
        /// <summary>
        /// 昵称是否存在
        /// </summary>
        /// <param name="nickName"></param>
        /// <returns></returns>
        public Task<bool> ExitsByNickName(string nickName);
    }

 为了方便和偷懒,有些地方的返回值就直接用string了,因为好做winform的MessageBox提示

 public class UserService: IUserService
    {
        private readonly ChatRoomContext _db;

        public UserService(ChatRoomContext db)
        {
            _db = db;
        }
        public async Task<List<UserInfo>> GetBuddyId(long userId)
        {
            //我添加的
            var youBuddy = await _db.Buddys.Where(n => n.UserId == userId).
                Select(n => n.FriendId).ToListAsync();
            //他人添加的
            var buddys = await _db.Buddys.Where(n => n.FriendId == userId).
                Select(x => x.UserId).ToListAsync();
            youBuddy.AddRange(buddys);
            List<UserInfo> result = new List<UserInfo>();
            foreach (var item in youBuddy)
            {
                var userInfo= await GetUserInfoById(item);
                result.Add(userInfo);
            }
            return result;
        }

        public async Task<UserInfo> GetUserInfoById(long userId)
        {
            return await _db.UserInfos.FindAsync(userId);
        }

        public async Task<UserInfo> CreateUserInfo(UserInfo userInfo)
        { 
            _db.UserInfos.Add(userInfo);
            await _db.SaveChangesAsync();
            return userInfo;
        }

        public async Task<string> AddBuddy(Buddy buddy)
        {
            if (buddy.FriendId==buddy.UserId)
            {
                return "不能自己添加自己";
            }
            var youBuddy=await _db.Buddys.
                FirstOrDefaultAsync(n => n.UserId == buddy.UserId && n.FriendId == buddy.FriendId);
            var buddys =
                await _db.Buddys.FirstOrDefaultAsync(n => n.UserId == buddy.FriendId && n.FriendId == buddy.UserId);
            if (youBuddy==null&&buddys==null)
            {
                await _db.Buddys.AddAsync(buddy);
                await _db.SaveChangesAsync();
                return "添加成功";
            }

            return "改好友已存在";
        }

        public async Task<UserInfo> Login(string userName,string pwd)
        {
            var userInfo=await _db.UserInfos
                .FirstOrDefaultAsync(n => n.UserName == userName && n.PassWord == pwd);
            return userInfo;
        }

        public async Task<bool> Exits(string userName)
        {
            var count=await _db.UserInfos.Where(n => n.UserName == userName).CountAsync();
            if (count>0)
            {
                return true;
            }

            return false;
        }

        public async Task<bool> ExitsByNickName(string nickName)
        {
            var count = await _db.UserInfos.Where(n => n.NickName == nickName).CountAsync();
            if (count>0)
            {
                return true;
            }

            return false;
        }
    }

为了使用依赖注入,将其在startup中注册服务

 public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            
            services.AddScoped<IUserService, UserService>();
            services.AddDbContext<ChatRoomContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("Default"));
            });
        }

 四、完成注册的Api

新建一个UserController,完成注册的api

[Route("api/[controller]/[action]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        private readonly IUserService _userService;

        public UserController(IUserService userService)
        {
            this._userService = userService;
        }
        [HttpPost]
        public async Task<string> Register(UserInfo userInfo)
        {
            if (await _userService.Exits(userInfo.UserName))
            {
                return "用户名已存在";
            }

            if (await _userService.ExitsByNickName(userInfo.NickName))
            {
                return "昵称已存在";
            }

            var user = await _userService.CreateUserInfo(userInfo);
            return $"注册成功,您的id是{user.Id}";
        }
    }

 五、配置集线器ChatHub

新建两个文件夹,分别是HubDto和Hubs,在HubDto文件下放的是用来传输和接收数据的类

//好友上线通知
public class OnlineToUserMessage
    {
        public string NickName { get; set; }
        //要通知的连接id
        public string ConnectionId { get; set; }
        public string Message { get; set; }
    }
//发送信息
public class ToUserMessageDto
    {
        //接收者的连接id
        public string ToConnectionId { get; set; }
        public string Message { get; set; }
    }

先写一个用于扩展Hub类的接口

/// <summary>
    /// 客户端需要监听的方法
    /// </summary>
    public interface IChatClient
    {
        /// <summary>
        /// 用户登录时发送
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public Task LoginResult(bool result,long? uid);
        /// <summary>
        /// 好友上线通知
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public Task BuddysOnline(OnlineToUserMessage onlineToUserMessage);
        /// <summary>
        /// 接收消息
        /// </summary>
        /// <returns></returns>
        public Task ReceiveMessage(string msg,DateTime sendTime);
        /// <summary>
        /// 接收好友列表
        /// </summary>
        /// <returns></returns>
        public Task ReceiveBuddyList(List<object> map);
        /// <summary>
        /// 接收添加好友结果
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        public Task ReceiveAddBuddyResult(string result);
    }

ChatHub这个类我们慢慢说,我们先将CRUD操作的服务注入进来,再创建一个字典用于存用户的id和该用户的连接id(connectionId),这里我们需要记住一个点,每个客户端连接成功后都会有一个唯一的连接id,之后我们会利用连接id来给指定的用户发送信息

 public class ChatHub:Hub<IChatClient>
    {
        /// <summary>
        /// key用户id,value连接id
        /// </summary>
        private static Dictionary<string, string> map = new Dictionary<string, string>();
        private readonly IUserService _userService;
        public ChatHub(IUserService userService)
        {
            this._userService = userService;
        }
    }

之前我们已经在controller中写过了注册的方法,我们在这边写上登录的方法(为什么不把注册也放进来,是因为没有必要,而登录放在这里是为了在用户登录成功之后,得到他的连接id和用户id,当winform启动时就会与服务端连接起来,那时我们可以得到他的connectionId,但却不知道他是哪个用户,所以只能通过登录时去获取,而注册不需要)

了解几个知识:

1.在ChatHub类中的每一个方法都有Context和Clients属性,Context中可以获取到连接id和其他信息,Clients用来发送信息主动通知客户端。

2.Hub支持泛型接口,接口中的方法将可以在Clients属性中直接调用。Clients中有多种情况,入all全部,group分组等。

原生调用:Clients.Clients("连接id或连接id集合").SendAsync("方法名","参数1","参数2");

SendAsync方法,用于主动通知客户端,第一个参数必填,是方法名。

这个方法名是可以随意起名字的,它并不是你代码中某一个方法的名字,参数都是object类型的。方法名将用于你客户端的监听,客户端通过监听这个方法来接收参数以及处理业务。而使用泛型接口后,Clients中将无法使用SendAsync,而是只能使用接口中定义的方法,它的本质就是调用的SendAsync方法,然后将你接口中的方法名作为参数放入了SendAsync中的第一个参数。用接口的好处是他使代码更清楚,更强类型一些和规范些。

 这边登录成功之后我们调用了LoginResult方法,主动通知客户端你的登录结果,而客户端呢也需要监听这个方法。

await Clients.Clients(Context.ConnectionId).LoginResult(true,userInfo.Id);

等同于原生写法

await Clients.Clients(Context.ConnectionId).SendAsync("LoginResult",true,userInfo.Id);

这边画了个草图

public class ChatHub:Hub<IChatClient>
    {
        /// <summary>
        /// key用户id,value连接id
        /// </summary>
        private static Dictionary<string, string> map = new Dictionary<string, string>();
        private readonly IUserService _userService;
        public ChatHub(IUserService userService)
        {
            this._userService = userService;
        }
        
        /// <summary>
        /// 连接关闭
        /// </summary>
        /// <param name="exception"></param>
        /// <returns></returns>
        public override Task OnDisconnectedAsync(Exception? exception)
        {
            if(map.Count>0)
            {
                //连接关闭时从map中删除
                var remove = map.FirstOrDefault(n => n.Value == Context.ConnectionId);
                map.Remove(remove.Key);
            }
            return base.OnDisconnectedAsync(exception);
        }
        /// <summary>
        /// 新连接
        /// </summary>
        /// <returns></returns>
        public override async Task OnConnectedAsync()
        {
            //此时可以得到连接id,但无法知道是哪个用户
            await base.OnConnectedAsync();
        }
        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public async Task Login(string userName,string pwd)
        {
            //校验账户密码是否正确
            var userInfo = await _userService.Login(userName, pwd);
            if (userInfo==null)
            {
                //登录结果通知
                await Clients.Clients(Context.ConnectionId).LoginResult(false,0);
                return;
            }
            //正确,将用户id作为key,连接id作为value存入map中
            map.Add(userInfo.Id.ToString(),Context.ConnectionId);
            //通知客户端登录成功
            await Clients.Clients(Context.ConnectionId).LoginResult(true,userInfo.Id);
            //通知用户的在线的好友
            OnlineToUserMessage onlineToUserMessage = new OnlineToUserMessage
            {
                NickName = userInfo.NickName,
                ConnectionId = Context.ConnectionId,
            };
            //要通知的好友列表
            var youBuddy =await _userService.GetBuddyId(userInfo.Id);
            var sendList = new List<string>();
            foreach (var item in youBuddy)
            {
                //好友的id是否在map中,不在代表没上线
                var key = item.Id.ToString();
                if (map.ContainsKey(key))
                {
                    sendList.Add(map[key]);
                }
            }

            if (sendList.Any())
            {
                //通知好友
                await Clients.Clients(sendList).BuddysOnline(onlineToUserMessage);
            }
        }
    }

先写到这,困了困了,明天在写下一篇。代码其实写的很粗浅,因为懒,但这个教程我说的还是比较细致了。

.NET Core和SignalR实现一个简单版聊天系统——服务端2https://blog.csdn.net/hyx1229/article/details/121277634?spm=1001.2014.3001.5502https://blog.csdn.net/hyx1229/article/details/121277634?spm=1001.2014.3001.5502

 源码地址:https://download.csdn.net/download/hyx1229/42548815 

需要免费源码的可以加.net core学习交流群:831181779,在群里@群主即可 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不想只会CRUD的猿某人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值