最近公司给了一个需求,因为业务服务是部署在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; }
///
/// 发送者ID
///
public long SenderId { get; set; }
///
/// 接收者ID
///
public long RecipientId { get; set; }
///
/// 内容
///
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 options) : base(options)
{
}
public DbSet UserInfos { get; set; }
public DbSet Buddys { get; set; }
public DbSet ChatRecords { get; set; }
}
在startup类中的ConfigureServices方法注入数据库服务,连接字符串写在配置文件appsetting.json中
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("Default"));
});
}
用过EF Core的肯定知道下面要怎么做了,那就是生成迁移文件,并生成数据库
启动项目选服务端,打开程序包管理器执行以下两句指令
add-migration init
update-database
先在根目录建一个service文件夹,一个接口一个实现 ,里面写了一些对数据库的CRUD
public interface IUserService
{
///
/// 获取该用户的所有好友
///
///
///
public Task> GetBuddyId(long userId);
///
/// 获取用户信息
///
///
///
public Task GetUserInfoById(long userId);
///
/// 添加用户
///
///
///
public Task CreateUserInfo(UserInfo userInfo);
///
/// 添加好友
///
///
///
public Task AddBuddy(Buddy buddy);
///
/// 登录
///
///
///
///
public Task Login(string userName, string pwd);
///
/// 用户名是否存在
///
///
///
public Task Exits(string userName);
///
/// 昵称是否存在
///
///
///
public Task ExitsByNickName(string nickName);
}
为了方便和偷懒,有些地方的返回值就直接用string了,因为好做winform的MessageBox提示
public class UserService: IUserService
{
private readonly ChatRoomContext _db;
public UserService(ChatRoomContext db)
{
_db = db;
}
public async Task> 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 result = new List();
foreach (var item in youBuddy)
{
var userInfo= await GetUserInfoById(item);
result.Add(userInfo);
}
return result;
}
public async Task GetUserInfoById(long userId)
{
return await _db.UserInfos.FindAsync(userId);
}
public async Task CreateUserInfo(UserInfo userInfo)
{
_db.UserInfos.Add(userInfo);
await _db.SaveChangesAsync();
return userInfo;
}
public async Task 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 Login(string userName,string pwd)
{
var userInfo=await _db.UserInfos
.FirstOrDefaultAsync(n => n.UserName == userName && n.PassWord == pwd);
return userInfo;
}
public async Task Exits(string userName)
{
var count=await _db.UserInfos.Where(n => n.UserName == userName).CountAsync();
if (count>0)
{
return true;
}
return false;
}
public async Task 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();
services.AddDbContext(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("Default"));
});
}
新建一个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 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}";
}
}
新建两个文件夹,分别是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类的接口
///
/// 客户端需要监听的方法
///
public interface IChatClient
{
///
/// 用户登录时发送
///
///
///
public Task LoginResult(bool result,long? uid);
///
/// 好友上线通知
///
///
///
public Task BuddysOnline(OnlineToUserMessage onlineToUserMessage);
///
/// 接收消息
///
///
public Task ReceiveMessage(string msg,DateTime sendTime);
///
/// 接收好友列表
///
///
public Task ReceiveBuddyList(List
ChatHub这个类我们慢慢说,我们先将CRUD操作的服务注入进来,再创建一个字典用于存用户的id和该用户的连接id(connectionId),这里我们需要记住一个点,每个客户端连接成功后都会有一个唯一的连接id,之后我们会利用连接id来给指定的用户发送信息
public class ChatHub:Hub
{
///
/// key用户id,value连接id
///
private static Dictionary map = new Dictionary();
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
{
///
/// key用户id,value连接id
///
private static Dictionary map = new Dictionary();
private readonly IUserService _userService;
public ChatHub(IUserService userService)
{
this._userService = userService;
}
///
/// 连接关闭
///
///
///
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);
}
///
/// 新连接
///
///
public override async Task OnConnectedAsync()
{
//此时可以得到连接id,但无法知道是哪个用户
await base.OnConnectedAsync();
}
///
/// 登录
///
///
///
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();
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,在群里@群主即可